wip: agui integration
This commit is contained in:
250
src/renderer/components/AssistantSidebar/AssistantSidebar.css
Normal file
250
src/renderer/components/AssistantSidebar/AssistantSidebar.css
Normal file
@@ -0,0 +1,250 @@
|
||||
.assistant-sidebar {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
}
|
||||
|
||||
.assistant-sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-sidebar-header p {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.assistant-sidebar-context {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border));
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-editorWidget-background, rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.assistant-sidebar-context-label {
|
||||
font-size: 11px;
|
||||
opacity: 0.75;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.assistant-sidebar-context-value {
|
||||
font-size: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assistant-sidebar-prompt {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.assistant-sidebar-start-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.assistant-sidebar-error {
|
||||
margin: 0;
|
||||
color: var(--vscode-errorForeground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-panel-output {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-metric-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.assistant-sidebar-metric-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.assistant-sidebar-table th,
|
||||
.assistant-sidebar-table td {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
padding: 6px;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.assistant-sidebar-raw-message {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
padding-top: 8px;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.assistant-sidebar-widget-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-widget-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.assistant-sidebar-widget-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-type {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(48px, auto) 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-item progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.assistant-sidebar-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-form-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card h4,
|
||||
.assistant-sidebar-card p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assistant-sidebar-image {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-image img {
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.assistant-sidebar-image figcaption {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.assistant-sidebar-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-tab-strip {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assistant-sidebar-tab-button.active {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.assistant-sidebar-tab-panel {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
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;
|
||||
1
src/renderer/components/AssistantSidebar/index.ts
Normal file
1
src/renderer/components/AssistantSidebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AssistantSidebar } from './AssistantSidebar';
|
||||
Reference in New Issue
Block a user