wip: agui integration
This commit is contained in:
239
src/renderer/components/AssistantSidebar/AssistantSidebar.tsx
Normal file
239
src/renderer/components/AssistantSidebar/AssistantSidebar.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user