wip: agui integration

This commit is contained in:
2026-02-25 19:51:58 +01:00
parent 5efbcfe03a
commit fcdf869a7c
59 changed files with 3467 additions and 267 deletions

View File

@@ -0,0 +1,239 @@
import React, { useMemo, useState } from 'react';
import { useAppStore } from '../../store';
import { resolveAssistantEditorContext } from '../../navigation/assistantPromptContext';
import { planAssistantRequest } from '../../navigation/assistantConversation';
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
import { ensureConversationId } from '../../navigation/chatSession';
import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
import { useChatMessageSender } from '../../navigation/useChatMessageSender';
import { useChatSurfaceState } from '../../navigation/useChatSurfaceState';
import { ChatTranscript } from '../ChatSurface';
import { AssistantPanelControls } from '../AssistantPanelControls';
import { useI18n } from '../../i18n';
import '../../styles/chatSurface.css';
import './AssistantSidebar.css';
export const AssistantSidebar: React.FC = () => {
const { t: tr } = useI18n();
const surfaceMode = getChatSurfaceMode('sidebar');
const [prompt, setPrompt] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [conversationId, setConversationId] = useState<string | null>(null);
const [panelElements, setPanelElements] = useState<AssistantPanelElement[]>([]);
const [actionError, setActionError] = useState<string | null>(null);
const {
tabs,
activeTabId,
posts,
media,
setSelectedPost,
setSelectedMedia,
openTab,
setActiveView,
toggleSidebar,
togglePanel,
toggleAssistantSidebar,
} = useAppStore();
const { sendMessage: sendChatMessage } = useChatMessageSender({
chatService: window.electronAPI?.chat,
});
const {
messages,
isStreaming,
streamingContent,
toolEvents,
beginUserTurn,
finalizeAssistantTurn,
appendAssistantMessage,
stopStreaming,
} = useChatSurfaceState();
const activeTab = useMemo(() => tabs.find((tab) => tab.id === activeTabId) ?? null, [tabs, activeTabId]);
const editorContext = useMemo(
() => resolveAssistantEditorContext({ activeTab, posts, media }),
[activeTab, posts, media],
);
const contextSummary = useMemo(() => {
if (!editorContext) {
return tr('assistantSidebar.context.none');
}
const title = editorContext.title ? `${editorContext.title}` : '';
const id = editorContext.id ? ` (${editorContext.id})` : '';
return `${editorContext.tabType}${id}${title}`;
}, [editorContext, tr]);
const persistActionEvent = async (message: string) => {
if (!conversationId) {
return;
}
try {
await window.electronAPI?.chat.addSystemEvent(conversationId, message);
} catch (error) {
console.error('Failed to persist assistant action event:', error);
}
};
const handleStart = async () => {
const trimmed = prompt.trim();
if (!trimmed || isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const chatService = window.electronAPI?.chat;
if (!chatService) {
throw new Error('Chat service unavailable');
}
const resolvedConversationId = await ensureConversationId({
currentConversationId: conversationId,
createTitle: tr('assistantSidebar.conversationTitle'),
chatService,
});
if (!conversationId) {
setConversationId(resolvedConversationId);
}
const requestPlan = planAssistantRequest({
conversationId,
userPrompt: trimmed,
context: editorContext,
});
beginUserTurn(resolvedConversationId, trimmed);
const sendResult = await sendChatMessage({
conversationId: resolvedConversationId,
message: requestPlan.outboundMessage,
metadata: { surface: 'sidebar' },
});
if (!sendResult.success) {
appendAssistantMessage(
resolvedConversationId,
tr('chat.errorPrefix', { error: sendResult.error || tr('chat.errorNoResponse') }),
);
stopStreaming();
throw new Error(sendResult.error || 'Failed to send assistant message');
}
if (sendResult.message) {
const parsedResponse = extractAssistantResponseContent(sendResult.message);
finalizeAssistantTurn(resolvedConversationId, parsedResponse.displayText);
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
} else {
appendAssistantMessage(resolvedConversationId, tr('chat.errorEmptyResponse'));
stopStreaming();
}
setPrompt('');
} catch (error) {
console.error('Failed to start assistant conversation:', error);
setErrorMessage(tr('assistantSidebar.error.startFailed'));
} finally {
setIsSubmitting(false);
}
};
const handleAssistantAction = (action: string, payload?: Record<string, unknown>) => {
const result = dispatchAssistantAction(
{
action,
payload,
},
{
setSelectedPost,
setSelectedMedia,
openTab,
setActiveView,
toggleSidebar,
togglePanel,
toggleAssistantSidebar,
},
);
if (!result.handled) {
setActionError(result.error || tr('assistantSidebar.error.actionFailed'));
void persistActionEvent(
`Assistant action failed: ${action}${result.error ? ` (${result.error})` : ''}`,
);
return;
}
setActionError(null);
void persistActionEvent(
`Assistant action executed: ${action}${payload ? ` ${JSON.stringify(payload)}` : ''}`,
);
};
return (
<div className="assistant-sidebar chat-surface">
<div className="assistant-sidebar-header">
<h3>{tr('assistantSidebar.title')}</h3>
<p>{tr('assistantSidebar.description')}</p>
</div>
<div className="assistant-sidebar-context chat-surface-section">
<span className="assistant-sidebar-context-label">{tr('assistantSidebar.context.label')}</span>
<span className="assistant-sidebar-context-value">{contextSummary}</span>
</div>
<textarea
className="assistant-sidebar-prompt chat-surface-input"
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder={tr('assistantSidebar.prompt.placeholder')}
rows={6}
/>
<button
type="button"
className="assistant-sidebar-start-button"
disabled={isSubmitting}
onClick={() => void handleStart()}
>
{isSubmitting ? tr('assistantSidebar.button.starting') : tr('assistantSidebar.button.start')}
</button>
{errorMessage && <p className="assistant-sidebar-error chat-surface-error">{errorMessage}</p>}
{actionError && <p className="assistant-sidebar-error chat-surface-error">{actionError}</p>}
{surfaceMode.showWelcomeTips && messages.length === 0 && !isStreaming && (
<div className="assistant-sidebar-raw-message chat-surface-section">
{tr('chat.welcomeDescription')}
</div>
)}
{messages.length > 0 && (
<div className="assistant-sidebar-raw-message chat-surface-section">
<ChatTranscript
messages={messages}
isStreaming={isStreaming}
streamingContent={streamingContent}
toolEvents={toolEvents}
assistantRoleLabel={tr('chat.role.assistant')}
userRoleLabel={tr('chat.role.you')}
showToolMarkers={surfaceMode.showToolMarkers}
/>
</div>
)}
{panelElements.length > 0 && (
<AssistantPanelControls elements={panelElements} onAction={handleAssistantAction} />
)}
</div>
);
};
export default AssistantSidebar;