feat: finally a good working state
This commit is contained in:
@@ -8,7 +8,6 @@ import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
|
||||
import { useChatMessageSender } from '../../navigation/useChatMessageSender';
|
||||
import { useChatSurfaceState } from '../../navigation/useChatSurfaceState';
|
||||
import { useA2UISurface } from '../../a2ui/useA2UISurface';
|
||||
import { A2UIRenderer } from '../../a2ui/A2UIRenderer';
|
||||
import { ChatTranscript } from '../ChatSurface';
|
||||
import { useI18n } from '../../i18n';
|
||||
import '../../styles/chatSurface.css';
|
||||
@@ -55,7 +54,19 @@ export const AssistantSidebar: React.FC = () => {
|
||||
} = useChatSurfaceState();
|
||||
|
||||
// A2UI surface rendering
|
||||
const { surfaces, dispatchAction, updateLocalData } = useA2UISurface({ conversationId });
|
||||
const {
|
||||
surfacesByTurn,
|
||||
latestSurfaceId,
|
||||
dismissedSurfaceIds,
|
||||
dismissSurface,
|
||||
dispatchAction,
|
||||
updateLocalData,
|
||||
} = useA2UISurface({ conversationId });
|
||||
|
||||
// Current turn index for associating streaming surfaces
|
||||
const currentTurnIndex = useMemo(() => {
|
||||
return messages.filter(m => m.role === 'user').length - 1;
|
||||
}, [messages]);
|
||||
|
||||
const activeTab = useMemo(() => tabs.find((tab) => tab.id === activeTabId) ?? null, [tabs, activeTabId]);
|
||||
|
||||
@@ -266,19 +277,16 @@ export const AssistantSidebar: React.FC = () => {
|
||||
assistantRoleLabel={tr('chat.role.assistant')}
|
||||
userRoleLabel={tr('chat.role.you')}
|
||||
showToolMarkers={surfaceMode.showToolMarkers}
|
||||
surfacesByTurn={surfacesByTurn}
|
||||
latestSurfaceId={latestSurfaceId}
|
||||
dismissedSurfaceIds={dismissedSurfaceIds}
|
||||
onSurfaceDismiss={dismissSurface}
|
||||
onSurfaceAction={dispatchAction}
|
||||
onSurfaceDataChange={updateLocalData}
|
||||
currentTurnIndex={currentTurnIndex}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{surfaces.map((surface) => (
|
||||
<A2UIRenderer
|
||||
key={surface.surfaceId}
|
||||
surfaceId={surface.surfaceId}
|
||||
tree={surface.tree}
|
||||
onAction={dispatchAction}
|
||||
onDataChange={updateLocalData}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import type { ChatConversation, ChatModel } from '../../types/electron';
|
||||
import { useChatMessageSender } from '../../navigation/useChatMessageSender';
|
||||
import { useChatSurfaceState } from '../../navigation/useChatSurfaceState';
|
||||
import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
|
||||
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
|
||||
import { useA2UISurface } from '../../a2ui/useA2UISurface';
|
||||
import { A2UIRenderer } from '../../a2ui/A2UIRenderer';
|
||||
import { useAppStore } from '../../store';
|
||||
import { ChatTranscript } from '../ChatSurface';
|
||||
import { useI18n } from '../../i18n';
|
||||
@@ -60,7 +59,19 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
} = useChatSurfaceState();
|
||||
|
||||
// A2UI surface rendering
|
||||
const { surfaces, dispatchAction, updateLocalData } = useA2UISurface({ conversationId });
|
||||
const {
|
||||
surfacesByTurn,
|
||||
latestSurfaceId,
|
||||
dismissedSurfaceIds,
|
||||
dismissSurface,
|
||||
dispatchAction,
|
||||
updateLocalData,
|
||||
} = useA2UISurface({ conversationId });
|
||||
|
||||
// Current turn index for associating streaming surfaces
|
||||
const currentTurnIndex = useMemo(() => {
|
||||
return messages.filter(m => m.role === 'user').length - 1;
|
||||
}, [messages]);
|
||||
|
||||
// Scroll to bottom when messages change
|
||||
const scrollToBottom = useCallback(() => {
|
||||
@@ -368,18 +379,15 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
userRoleLabel={tr('chat.role.you')}
|
||||
showToolMarkers={surfaceMode.showToolMarkers}
|
||||
endRef={messagesEndRef}
|
||||
surfacesByTurn={surfacesByTurn}
|
||||
latestSurfaceId={latestSurfaceId}
|
||||
dismissedSurfaceIds={dismissedSurfaceIds}
|
||||
onSurfaceDismiss={dismissSurface}
|
||||
onSurfaceAction={dispatchAction}
|
||||
onSurfaceDataChange={updateLocalData}
|
||||
currentTurnIndex={currentTurnIndex}
|
||||
/>
|
||||
|
||||
{surfaces.map((surface) => (
|
||||
<A2UIRenderer
|
||||
key={surface.surfaceId}
|
||||
surfaceId={surface.surfaceId}
|
||||
tree={surface.tree}
|
||||
onAction={dispatchAction}
|
||||
onDataChange={updateLocalData}
|
||||
/>
|
||||
))}
|
||||
|
||||
{actionError && <p className="chat-surface-error">{actionError}</p>}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ import React from 'react';
|
||||
import Markdown from 'marked-react';
|
||||
import type { ChatMessage } from '../../types/electron';
|
||||
import type { ChatToolEvent } from '../../navigation/useChatSurfaceState';
|
||||
import type { A2UIResolvedComponent, A2UIClientAction } from '../../../main/a2ui/types';
|
||||
import { InlineSurface } from '../../a2ui/InlineSurface';
|
||||
import type { SurfaceEntry } from '../../a2ui/useA2UISurface';
|
||||
import { computeTurnIndex } from '../../a2ui/surfaceAssociation';
|
||||
|
||||
interface ChatTranscriptProps {
|
||||
messages: ChatMessage[];
|
||||
@@ -12,6 +16,20 @@ interface ChatTranscriptProps {
|
||||
userRoleLabel: string;
|
||||
showToolMarkers?: boolean;
|
||||
endRef?: React.RefObject<HTMLDivElement | null>;
|
||||
/** Surfaces grouped by the turn index that created them */
|
||||
surfacesByTurn?: Map<number, SurfaceEntry[]>;
|
||||
/** The surfaceId of the most recently created surface */
|
||||
latestSurfaceId?: string | null;
|
||||
/** Set of surface IDs the user has dismissed */
|
||||
dismissedSurfaceIds?: Set<string>;
|
||||
/** Callback to dismiss a surface */
|
||||
onSurfaceDismiss?: (surfaceId: string) => void;
|
||||
/** Callback for surface actions */
|
||||
onSurfaceAction?: (action: A2UIClientAction) => void;
|
||||
/** Callback for surface data changes */
|
||||
onSurfaceDataChange?: (surfaceId: string, path: string, value: unknown) => void;
|
||||
/** The current streaming turn index */
|
||||
currentTurnIndex?: number;
|
||||
}
|
||||
|
||||
export const ChatTranscript: React.FC<ChatTranscriptProps> = ({
|
||||
@@ -23,6 +41,13 @@ export const ChatTranscript: React.FC<ChatTranscriptProps> = ({
|
||||
userRoleLabel,
|
||||
showToolMarkers = true,
|
||||
endRef,
|
||||
surfacesByTurn,
|
||||
latestSurfaceId,
|
||||
dismissedSurfaceIds,
|
||||
onSurfaceDismiss,
|
||||
onSurfaceAction,
|
||||
onSurfaceDataChange,
|
||||
currentTurnIndex,
|
||||
}) => {
|
||||
const renderToolMarkers = (events: ChatToolEvent[]) => {
|
||||
if (events.length === 0) {
|
||||
@@ -65,7 +90,34 @@ export const ChatTranscript: React.FC<ChatTranscriptProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderMessage = (message: ChatMessage) => {
|
||||
const renderInlineSurfaces = (turnIndex: number) => {
|
||||
if (!surfacesByTurn?.has(turnIndex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const turnSurfaces = surfacesByTurn.get(turnIndex)!;
|
||||
const visibleSurfaces = turnSurfaces.filter(
|
||||
(s) => !dismissedSurfaceIds?.has(s.surfaceId),
|
||||
);
|
||||
|
||||
if (visibleSurfaces.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return visibleSurfaces.map((surface) => (
|
||||
<InlineSurface
|
||||
key={surface.surfaceId}
|
||||
surfaceId={surface.surfaceId}
|
||||
tree={surface.tree}
|
||||
isExpanded={surface.surfaceId === latestSurfaceId}
|
||||
onDismiss={onSurfaceDismiss}
|
||||
onAction={onSurfaceAction}
|
||||
onDataChange={onSurfaceDataChange}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const renderMessage = (message: ChatMessage, messageIndex: number) => {
|
||||
if (message.role === 'system' || message.role === 'tool') {
|
||||
return null;
|
||||
}
|
||||
@@ -80,7 +132,7 @@ export const ChatTranscript: React.FC<ChatTranscriptProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
const messageEl = (
|
||||
<div key={message.id} className={`chat-message ${message.role}`}>
|
||||
<div className="chat-message-avatar">
|
||||
{message.role === 'user' ? '\u{1F464}' : '\u{1F916}'}
|
||||
@@ -113,28 +165,47 @@ export const ChatTranscript: React.FC<ChatTranscriptProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// After an assistant message, render any inline surfaces for this turn
|
||||
if (message.role === 'assistant' && surfacesByTurn) {
|
||||
const turnIndex = computeTurnIndex(messages, messageIndex);
|
||||
const inlineSurfaces = renderInlineSurfaces(turnIndex);
|
||||
if (inlineSurfaces) {
|
||||
return (
|
||||
<React.Fragment key={message.id}>
|
||||
{messageEl}
|
||||
{inlineSurfaces}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return messageEl;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{messages.map(renderMessage)}
|
||||
{messages.map((message, index) => renderMessage(message, index))}
|
||||
|
||||
{isStreaming && (streamingContent || (showToolMarkers && toolEvents.length > 0)) && (
|
||||
<div className="chat-message assistant streaming">
|
||||
<div className="chat-message-avatar">{'\u{1F916}'}</div>
|
||||
<div className="chat-message-content">
|
||||
<div className="chat-message-header">
|
||||
<span className="chat-message-role">{assistantRoleLabel}</span>
|
||||
<span className="streaming-indicator">{'\u25CF'}</span>
|
||||
</div>
|
||||
{showToolMarkers ? renderToolMarkers(toolEvents) : null}
|
||||
{streamingContent && (
|
||||
<div className="chat-message-text">
|
||||
<Markdown gfm>{streamingContent}</Markdown>
|
||||
<>
|
||||
<div className="chat-message assistant streaming">
|
||||
<div className="chat-message-avatar">{'\u{1F916}'}</div>
|
||||
<div className="chat-message-content">
|
||||
<div className="chat-message-header">
|
||||
<span className="chat-message-role">{assistantRoleLabel}</span>
|
||||
<span className="streaming-indicator">{'\u25CF'}</span>
|
||||
</div>
|
||||
)}
|
||||
{showToolMarkers ? renderToolMarkers(toolEvents) : null}
|
||||
{streamingContent && (
|
||||
<div className="chat-message-text">
|
||||
<Markdown gfm>{streamingContent}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentTurnIndex !== undefined && renderInlineSurfaces(currentTurnIndex)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isStreaming && !streamingContent && (!showToolMarkers || toolEvents.length === 0) && (
|
||||
|
||||
Reference in New Issue
Block a user