feat: replay of took calls for a2ui elements
This commit is contained in:
@@ -3,9 +3,14 @@
|
||||
*
|
||||
* Computes the turn index for a message based on its position
|
||||
* in the message array, enabling inline surface rendering.
|
||||
*
|
||||
* Also provides replay logic to reconstruct A2UI surfaces from
|
||||
* persisted tool calls when reloading a saved conversation.
|
||||
*/
|
||||
|
||||
import type { ChatMessage } from '../../main/shared/electronApi';
|
||||
import type { A2UIServerMessage } from '../../main/a2ui/types';
|
||||
import { isRenderTool, generateFromToolCall } from '../../main/a2ui/generator';
|
||||
|
||||
/**
|
||||
* Compute the turn index for a message at a given position.
|
||||
@@ -25,3 +30,70 @@ export function computeTurnIndex(messages: ChatMessage[], currentIndex: number):
|
||||
}
|
||||
return userCount - 1;
|
||||
}
|
||||
|
||||
interface StoredToolCall {
|
||||
name: string;
|
||||
args?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay A2UI surfaces from persisted chat messages.
|
||||
*
|
||||
* Scans assistant messages for render tool calls stored in their
|
||||
* `toolCalls` JSON field and regenerates the A2UI server messages
|
||||
* needed to reconstruct the surfaces in the manager.
|
||||
*
|
||||
* @param conversationId - The conversation ID for surface creation
|
||||
* @param messages - The full ordered message history from the database
|
||||
* @returns Array of A2UI server messages to feed into A2UISurfaceManager
|
||||
*/
|
||||
export function replaySurfacesFromMessages(
|
||||
conversationId: string,
|
||||
messages: ChatMessage[],
|
||||
): A2UIServerMessage[] {
|
||||
const allA2UIMessages: A2UIServerMessage[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
if (message.role !== 'assistant' || !message.toolCalls) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let toolCalls: StoredToolCall[];
|
||||
try {
|
||||
toolCalls = JSON.parse(message.toolCalls) as StoredToolCall[];
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Array.isArray(toolCalls)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const turnIndex = computeTurnIndex(messages, i);
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
if (!isRenderTool(toolCall.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const a2uiMessages = generateFromToolCall(
|
||||
conversationId,
|
||||
toolCall.name,
|
||||
toolCall.args ?? {},
|
||||
);
|
||||
|
||||
if (a2uiMessages) {
|
||||
// Inject turnIndex into createSurface metadata, matching live behavior
|
||||
for (const msg of a2uiMessages) {
|
||||
if (msg.type === 'createSurface') {
|
||||
msg.metadata = { ...msg.metadata, turnIndex };
|
||||
}
|
||||
}
|
||||
allA2UIMessages.push(...a2uiMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allA2UIMessages;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { A2UISurfaceManager } from './A2UISurfaceManager';
|
||||
import { replaySurfacesFromMessages } from './surfaceAssociation';
|
||||
import type { A2UIResolvedComponent, A2UIServerMessage, A2UIClientAction } from '../../main/a2ui/types';
|
||||
import type { ChatMessage } from '../../main/shared/electronApi';
|
||||
|
||||
interface UseA2UISurfaceInput {
|
||||
conversationId: string | null;
|
||||
@@ -37,6 +39,8 @@ interface UseA2UISurfaceResult {
|
||||
getDataModel: (surfaceId: string) => Record<string, unknown>;
|
||||
/** Clear all surfaces for the conversation */
|
||||
clearSurfaces: () => void;
|
||||
/** Replay surfaces from persisted chat messages */
|
||||
replayFromMessages: (messages: ChatMessage[]) => void;
|
||||
}
|
||||
|
||||
export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult {
|
||||
@@ -162,6 +166,17 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
const replayFromMessages = useCallback((msgs: ChatMessage[]) => {
|
||||
if (!conversationId) return;
|
||||
const manager = managerRef.current;
|
||||
// Only replay if no surfaces exist yet (avoid duplicates on re-render)
|
||||
if (manager.getSurfaceIds(conversationId).length > 0) return;
|
||||
const a2uiMessages = replaySurfacesFromMessages(conversationId, msgs);
|
||||
for (const msg of a2uiMessages) {
|
||||
manager.processMessage(msg);
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
return {
|
||||
surfaces,
|
||||
surfacesByTurn,
|
||||
@@ -172,5 +187,6 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
||||
updateLocalData,
|
||||
getDataModel,
|
||||
clearSurfaces,
|
||||
replayFromMessages,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
dismissSurface,
|
||||
dispatchAction,
|
||||
updateLocalData,
|
||||
replayFromMessages,
|
||||
} = useA2UISurface({ conversationId });
|
||||
|
||||
// Current turn index for associating streaming surfaces
|
||||
@@ -102,12 +103,15 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
]);
|
||||
|
||||
if (conv) setConversation(conv);
|
||||
if (msgs) setMessages(msgs);
|
||||
if (msgs) {
|
||||
setMessages(msgs);
|
||||
replayFromMessages(msgs);
|
||||
}
|
||||
if (modelsResult?.models) setAvailableModels(modelsResult.models);
|
||||
} catch (error) {
|
||||
console.error('Failed to load chat data:', error);
|
||||
}
|
||||
}, [conversationId]);
|
||||
}, [conversationId, replayFromMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
checkReady();
|
||||
|
||||
Reference in New Issue
Block a user