feat: finally a good working state
This commit is contained in:
69
src/renderer/a2ui/InlineSurface.css
Normal file
69
src/renderer/a2ui/InlineSurface.css
Normal file
@@ -0,0 +1,69 @@
|
||||
.inline-surface {
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vscode-editorGroup-border, var(--vscode-panel-border));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inline-surface.collapsed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
background: var(--vscode-textBlockQuote-background, rgba(127, 127, 127, 0.1));
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.inline-surface.collapsed:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.inline-surface-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inline-surface-title {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inline-surface-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--vscode-editorGroup-border, var(--vscode-panel-border));
|
||||
background: var(--vscode-textBlockQuote-background, rgba(127, 127, 127, 0.1));
|
||||
}
|
||||
|
||||
.inline-surface-expand,
|
||||
.inline-surface-collapse,
|
||||
.inline-surface-dismiss {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.inline-surface-expand:hover,
|
||||
.inline-surface-collapse:hover,
|
||||
.inline-surface-dismiss:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.inline-surface.expanded > .a2ui-surface {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
153
src/renderer/a2ui/InlineSurface.tsx
Normal file
153
src/renderer/a2ui/InlineSurface.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* InlineSurface component.
|
||||
*
|
||||
* Wraps A2UIRenderer with expand/collapse and dismiss controls.
|
||||
* Renders inline within the chat transcript, anchored to the
|
||||
* assistant message turn that created the surface.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { A2UIResolvedComponent, A2UIClientAction } from '../../main/a2ui/types';
|
||||
import { A2UIRenderer } from './A2UIRenderer';
|
||||
import './InlineSurface.css';
|
||||
|
||||
interface InlineSurfaceProps {
|
||||
surfaceId: string;
|
||||
tree: A2UIResolvedComponent[];
|
||||
isExpanded: boolean;
|
||||
onDismiss?: (surfaceId: string) => void;
|
||||
onAction?: (action: A2UIClientAction) => void;
|
||||
onDataChange?: (surfaceId: string, path: string, value: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a display title from the surface's component tree.
|
||||
* Tries the root component's `title` or `label` property,
|
||||
* then falls back to the capitalized component type.
|
||||
*/
|
||||
export function deriveSurfaceTitle(tree: A2UIResolvedComponent[]): string {
|
||||
if (tree.length === 0) {
|
||||
return 'Surface';
|
||||
}
|
||||
const root = tree[0];
|
||||
const title = root.properties?.title as string | undefined;
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
const label = root.properties?.label as string | undefined;
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
return root.type.charAt(0).toUpperCase() + root.type.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an icon character for the surface based on the root component type.
|
||||
*/
|
||||
export function getSurfaceIcon(tree: A2UIResolvedComponent[]): string {
|
||||
if (tree.length === 0) {
|
||||
return '\u25A0';
|
||||
}
|
||||
const type = tree[0].type;
|
||||
const icons: Record<string, string> = {
|
||||
chart: '\u{1F4CA}',
|
||||
table: '\u{1F4CB}',
|
||||
form: '\u{1F4DD}',
|
||||
card: '\u{1F4C4}',
|
||||
metric: '\u{1F4CF}',
|
||||
list: '\u{1F4CB}',
|
||||
tabs: '\u{1F4C2}',
|
||||
};
|
||||
return icons[type] || '\u25A0';
|
||||
}
|
||||
|
||||
export const InlineSurface: React.FC<InlineSurfaceProps> = ({
|
||||
surfaceId,
|
||||
tree,
|
||||
isExpanded: defaultExpanded,
|
||||
onDismiss,
|
||||
onAction,
|
||||
onDataChange,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
// Auto-collapse/expand when the parent changes which surface is latest
|
||||
useEffect(() => {
|
||||
setExpanded(defaultExpanded);
|
||||
}, [defaultExpanded]);
|
||||
|
||||
const surfaceTitle = deriveSurfaceTitle(tree);
|
||||
const surfaceIcon = getSurfaceIcon(tree);
|
||||
|
||||
if (!expanded) {
|
||||
return (
|
||||
<div
|
||||
className="inline-surface collapsed"
|
||||
onClick={() => setExpanded(true)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
setExpanded(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="inline-surface-icon">{surfaceIcon}</span>
|
||||
<span className="inline-surface-title">{surfaceTitle}</span>
|
||||
<button
|
||||
className="inline-surface-expand"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded(true);
|
||||
}}
|
||||
aria-label="Expand surface"
|
||||
>
|
||||
{'\u25B6'}
|
||||
</button>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="inline-surface-dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss(surfaceId);
|
||||
}}
|
||||
aria-label="Dismiss surface"
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-surface expanded">
|
||||
<div className="inline-surface-header">
|
||||
<span className="inline-surface-icon">{surfaceIcon}</span>
|
||||
<span className="inline-surface-title">{surfaceTitle}</span>
|
||||
<button
|
||||
className="inline-surface-collapse"
|
||||
onClick={() => setExpanded(false)}
|
||||
aria-label="Collapse surface"
|
||||
>
|
||||
{'\u25BC'}
|
||||
</button>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="inline-surface-dismiss"
|
||||
onClick={() => onDismiss(surfaceId)}
|
||||
aria-label="Dismiss surface"
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<A2UIRenderer
|
||||
surfaceId={surfaceId}
|
||||
tree={tree}
|
||||
onAction={onAction!}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
27
src/renderer/a2ui/surfaceAssociation.ts
Normal file
27
src/renderer/a2ui/surfaceAssociation.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Surface-to-message association utilities.
|
||||
*
|
||||
* Computes the turn index for a message based on its position
|
||||
* in the message array, enabling inline surface rendering.
|
||||
*/
|
||||
|
||||
import type { ChatMessage } from '../../main/shared/electronApi';
|
||||
|
||||
/**
|
||||
* Compute the turn index for a message at a given position.
|
||||
*
|
||||
* Turn index is defined as the 0-based count of user messages
|
||||
* seen up to and including the given position, minus 1.
|
||||
* System and tool messages do not affect the count.
|
||||
*
|
||||
* Returns -1 if no user message has been seen at or before the index.
|
||||
*/
|
||||
export function computeTurnIndex(messages: ChatMessage[], currentIndex: number): number {
|
||||
let userCount = 0;
|
||||
for (let i = 0; i <= currentIndex; i++) {
|
||||
if (messages[i].role === 'user') {
|
||||
userCount++;
|
||||
}
|
||||
}
|
||||
return userCount - 1;
|
||||
}
|
||||
@@ -13,9 +13,22 @@ interface UseA2UISurfaceInput {
|
||||
conversationId: string | null;
|
||||
}
|
||||
|
||||
export interface SurfaceEntry {
|
||||
surfaceId: string;
|
||||
tree: A2UIResolvedComponent[];
|
||||
}
|
||||
|
||||
interface UseA2UISurfaceResult {
|
||||
/** All active surface trees for this conversation */
|
||||
surfaces: Array<{ surfaceId: string; tree: A2UIResolvedComponent[] }>;
|
||||
surfaces: SurfaceEntry[];
|
||||
/** 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 that the user has dismissed */
|
||||
dismissedSurfaceIds: Set<string>;
|
||||
/** Dismiss a surface by ID */
|
||||
dismissSurface: (surfaceId: string) => void;
|
||||
/** Dispatch an action back to the main process */
|
||||
dispatchAction: (action: A2UIClientAction) => void;
|
||||
/** Update a local data binding (for form inputs) */
|
||||
@@ -30,6 +43,7 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
||||
const { conversationId } = input;
|
||||
const managerRef = useRef<A2UISurfaceManager>(new A2UISurfaceManager());
|
||||
const [renderTick, setRenderTick] = useState(0);
|
||||
const [dismissedSurfaceIds, setDismissedSurfaceIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Subscribe to surface changes
|
||||
useEffect(() => {
|
||||
@@ -58,8 +72,9 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
||||
};
|
||||
}, [conversationId]);
|
||||
|
||||
// Clear surfaces when conversation changes
|
||||
// Clear surfaces and dismissed set when conversation changes
|
||||
useEffect(() => {
|
||||
setDismissedSurfaceIds(new Set());
|
||||
return () => {
|
||||
if (conversationId) {
|
||||
managerRef.current.clearConversation(conversationId);
|
||||
@@ -83,6 +98,52 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
||||
}));
|
||||
}, [conversationId, renderTick]);
|
||||
|
||||
const surfacesByTurn = useMemo(() => {
|
||||
void renderTick;
|
||||
|
||||
const map = new Map<number, SurfaceEntry[]>();
|
||||
if (!conversationId) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const manager = managerRef.current;
|
||||
const surfaceIds = manager.getSurfaceIds(conversationId);
|
||||
|
||||
for (const surfaceId of surfaceIds) {
|
||||
const surface = manager.getSurface(surfaceId);
|
||||
const turnIndex = (surface?.metadata?.turnIndex as number) ?? -1;
|
||||
const entry: SurfaceEntry = { surfaceId, tree: manager.resolveTree(surfaceId) };
|
||||
|
||||
const existing = map.get(turnIndex);
|
||||
if (existing) {
|
||||
existing.push(entry);
|
||||
} else {
|
||||
map.set(turnIndex, [entry]);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}, [conversationId, renderTick]);
|
||||
|
||||
const latestSurfaceId = useMemo(() => {
|
||||
void renderTick;
|
||||
|
||||
if (!conversationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ids = managerRef.current.getSurfaceIds(conversationId);
|
||||
return ids.length > 0 ? ids[ids.length - 1] : null;
|
||||
}, [conversationId, renderTick]);
|
||||
|
||||
const dismissSurface = useCallback((surfaceId: string) => {
|
||||
setDismissedSurfaceIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(surfaceId);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const dispatchAction = useCallback((action: A2UIClientAction) => {
|
||||
window.electronAPI?.chat.dispatchA2UIAction?.(action);
|
||||
}, []);
|
||||
@@ -103,6 +164,10 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
||||
|
||||
return {
|
||||
surfaces,
|
||||
surfacesByTurn,
|
||||
latestSurfaceId,
|
||||
dismissedSurfaceIds,
|
||||
dismissSurface,
|
||||
dispatchAction,
|
||||
updateLocalData,
|
||||
getDataModel,
|
||||
|
||||
@@ -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