From d87d3616e92f1c041c7f54dabbb8fdf9005c0171 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 13 Mar 2026 07:11:48 +0800 Subject: [PATCH] Add debug transcript components --- .../debug-transcript/MessageText.tsx | 67 ++ .../debug-transcript/MessageTool.tsx | 162 +++++ .../TranscriptAssistantBlock.tsx | 53 ++ .../debug-transcript/TranscriptItem.tsx | 18 + .../debug-transcript/TranscriptList.tsx | 123 ++++ .../debug-transcript/message-utils.ts | 637 ++++++++++++++++++ web/components/debug-transcript/types.ts | 36 + 7 files changed, 1096 insertions(+) create mode 100644 web/components/debug-transcript/MessageText.tsx create mode 100644 web/components/debug-transcript/MessageTool.tsx create mode 100644 web/components/debug-transcript/TranscriptAssistantBlock.tsx create mode 100644 web/components/debug-transcript/TranscriptItem.tsx create mode 100644 web/components/debug-transcript/TranscriptList.tsx create mode 100644 web/components/debug-transcript/message-utils.ts create mode 100644 web/components/debug-transcript/types.ts diff --git a/web/components/debug-transcript/MessageText.tsx b/web/components/debug-transcript/MessageText.tsx new file mode 100644 index 0000000..6e46b05 --- /dev/null +++ b/web/components/debug-transcript/MessageText.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import { cn } from '@/lib/utils'; +import { Badge } from '@/components/UI'; + +import type { DebugTranscriptTextRow } from './types'; + +const roleLabelMap: Record = { + user: 'Me', + assistant: 'AI', + notice: 'Debug', +}; + +const MessageText: React.FC<{ + row: DebugTranscriptTextRow; +}> = ({ row }) => { + if (row.role === 'notice') { + return ( +
+
+
+ {roleLabelMap[row.role]} +
+
{row.text}
+
+
+ ); + } + + const isUser = row.role === 'user'; + + return ( +
+
+
+ + {roleLabelMap[row.role]} + + {row.role === 'assistant' && + typeof row.ttfbMs === 'number' && + Number.isFinite(row.ttfbMs) && ( + + TTFB {Math.round(row.ttfbMs)}ms + + )} + {row.role === 'assistant' && row.isStreaming && ( + + )} +
+
{row.text}
+
+
+ ); +}; + +export default React.memo(MessageText); + diff --git a/web/components/debug-transcript/MessageTool.tsx b/web/components/debug-transcript/MessageTool.tsx new file mode 100644 index 0000000..193d1e5 --- /dev/null +++ b/web/components/debug-transcript/MessageTool.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { ChevronDown, ChevronRight, Wrench } from 'lucide-react'; + +import { Badge, Button } from '@/components/UI'; +import { cn } from '@/lib/utils'; + +import type { DebugTranscriptToolRow } from './types'; + +const shouldAutoExpand = (status: DebugTranscriptToolRow['status']) => + status === 'pending' || status === 'error' || status === 'timeout'; + +const formatStructuredValue = (value: unknown) => { + if (value === undefined || value === null) return ''; + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) return ''; + try { + return JSON.stringify(JSON.parse(trimmed), null, 2); + } catch { + return value; + } + } + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +}; + +const getStatusBadge = (status: DebugTranscriptToolRow['status']) => { + if (status === 'success') { + return { label: 'Success', variant: 'success' as const, className: '' }; + } + if (status === 'pending') { + return { label: 'Pending', variant: 'warning' as const, className: '' }; + } + if (status === 'timeout') { + return { + label: 'Timeout', + variant: 'outline' as const, + className: 'border-orange-400/40 bg-orange-500/10 text-orange-200', + }; + } + return { + label: 'Error', + variant: 'outline' as const, + className: 'border-rose-400/40 bg-rose-500/10 text-rose-200', + }; +}; + +const Section: React.FC<{ + label: string; + value: unknown; + defaultOpen?: boolean; +}> = ({ label, value, defaultOpen = true }) => { + const formattedValue = useMemo(() => formatStructuredValue(value), [value]); + if (!formattedValue) return null; + + return ( +
+ + {label} + +
+        {formattedValue}
+      
+
+ ); +}; + +const MessageTool: React.FC<{ + row: DebugTranscriptToolRow; + nested?: boolean; +}> = ({ row, nested = false }) => { + const [isExpanded, setIsExpanded] = useState(() => shouldAutoExpand(row.status)); + + useEffect(() => { + setIsExpanded(shouldAutoExpand(row.status)); + }, [row.status]); + + const statusBadge = getStatusBadge(row.status); + + return ( +
+
+
+
+
+
+
+ +
+
+
+ {row.toolDisplayName || row.toolName} +
+
+ {row.toolName} +
+
+
+ + {statusBadge.label} + + + {row.executor} + + {row.source && ( + + {row.source} + + )} +
+
+ tool_call_id: {row.toolCallId} +
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default React.memo(MessageTool); diff --git a/web/components/debug-transcript/TranscriptAssistantBlock.tsx b/web/components/debug-transcript/TranscriptAssistantBlock.tsx new file mode 100644 index 0000000..da62571 --- /dev/null +++ b/web/components/debug-transcript/TranscriptAssistantBlock.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { Badge } from '@/components/UI'; + +import MessageTool from './MessageTool'; +import type { DebugTranscriptTextRow, DebugTranscriptToolRow } from './types'; + +const TranscriptAssistantBlock: React.FC<{ + message?: DebugTranscriptTextRow; + tools: DebugTranscriptToolRow[]; +}> = ({ message, tools }) => { + const isStreaming = Boolean(message?.isStreaming) || tools.some((tool) => tool.status === 'pending'); + + return ( +
+
+
+ AI + {typeof message?.ttfbMs === 'number' && Number.isFinite(message.ttfbMs) && ( + + TTFB {Math.round(message.ttfbMs)}ms + + )} + {tools.length > 0 && ( + + {tools.length} tool{tools.length > 1 ? 's' : ''} + + )} + {isStreaming && } +
+ + {message?.text ? ( +
{message.text}
+ ) : null} + + {tools.length > 0 && ( +
+
+ {tools.map((tool) => ( + + ))} +
+
+ )} +
+
+ ); +}; + +export default React.memo(TranscriptAssistantBlock); diff --git a/web/components/debug-transcript/TranscriptItem.tsx b/web/components/debug-transcript/TranscriptItem.tsx new file mode 100644 index 0000000..73a0ed8 --- /dev/null +++ b/web/components/debug-transcript/TranscriptItem.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import MessageText from './MessageText'; +import MessageTool from './MessageTool'; +import type { DebugTranscriptRow } from './types'; + +const TranscriptItem: React.FC<{ + row: DebugTranscriptRow; +}> = ({ row }) => { + if (row.kind === 'tool') { + return ; + } + + return ; +}; + +export default React.memo(TranscriptItem); + diff --git a/web/components/debug-transcript/TranscriptList.tsx b/web/components/debug-transcript/TranscriptList.tsx new file mode 100644 index 0000000..de45c26 --- /dev/null +++ b/web/components/debug-transcript/TranscriptList.tsx @@ -0,0 +1,123 @@ +import React, { useMemo } from 'react'; +import { MessageSquare } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +import TranscriptAssistantBlock from './TranscriptAssistantBlock'; +import TranscriptItem from './TranscriptItem'; +import type { DebugTranscriptRow, DebugTranscriptTextRow, DebugTranscriptToolRow } from './types'; + +type AssistantRenderBlock = { + kind: 'assistant-block'; + id: string; + message?: DebugTranscriptTextRow; + tools: DebugTranscriptToolRow[]; +}; + +type TranscriptRenderItem = + | { kind: 'row'; id: string; row: DebugTranscriptRow } + | AssistantRenderBlock; + +const getCorrelationKey = (row: Pick) => { + if (row.responseId) return `response:${row.responseId}`; + if (row.turnId && row.utteranceId) return `turn:${row.turnId}:utterance:${row.utteranceId}`; + if (row.turnId) return `turn:${row.turnId}`; + if (row.utteranceId) return `utterance:${row.utteranceId}`; + return ''; +}; + +const buildRenderItems = (messages: DebugTranscriptRow[]): TranscriptRenderItem[] => { + const items: TranscriptRenderItem[] = []; + const assistantBlocks = new Map(); + + messages.forEach((row) => { + if (row.kind === 'text' && row.role === 'assistant') { + const correlationKey = getCorrelationKey(row); + if (!correlationKey) { + items.push({ kind: 'row', id: row.id, row }); + return; + } + + const existingBlock = assistantBlocks.get(correlationKey); + if (existingBlock) { + existingBlock.message = row; + return; + } + + const block: AssistantRenderBlock = { + kind: 'assistant-block', + id: `assistant-block:${correlationKey}`, + message: row, + tools: [], + }; + assistantBlocks.set(correlationKey, block); + items.push(block); + return; + } + + if (row.kind === 'tool') { + const correlationKey = getCorrelationKey(row); + if (!correlationKey) { + items.push({ kind: 'row', id: row.id, row }); + return; + } + + const existingBlock = assistantBlocks.get(correlationKey); + if (existingBlock) { + existingBlock.tools.push(row); + return; + } + + const block: AssistantRenderBlock = { + kind: 'assistant-block', + id: `assistant-block:${correlationKey}`, + tools: [row], + }; + assistantBlocks.set(correlationKey, block); + items.push(block); + return; + } + + items.push({ kind: 'row', id: row.id, row }); + }); + + return items; +}; + +const TranscriptList: React.FC<{ + scrollRef: React.RefObject; + messages: DebugTranscriptRow[]; + isLoading: boolean; + className?: string; +}> = ({ scrollRef, messages, isLoading, className = '' }) => { + const renderItems = useMemo(() => buildRenderItems(messages), [messages]); + + return ( +
+ {messages.length === 0 && !isLoading ? ( +
+ +

鏆傛棤瀵硅瘽璁板綍

+
+ ) : ( +
+ {renderItems.map((item) => + item.kind === 'assistant-block' ? ( + + ) : ( + + ) + )} +
+ )} +
+ ); +}; + +export default React.memo(TranscriptList); diff --git a/web/components/debug-transcript/message-utils.ts b/web/components/debug-transcript/message-utils.ts new file mode 100644 index 0000000..a4dd309 --- /dev/null +++ b/web/components/debug-transcript/message-utils.ts @@ -0,0 +1,637 @@ +import type { + DebugTranscriptRow, + DebugTranscriptTextRole, + DebugTranscriptTextRow, + DebugTranscriptToolRow, + DebugTranscriptToolStatus, +} from './types'; + +let rowCounter = 0; + +const createRowId = (prefix: string) => `${prefix}_${Date.now()}_${++rowCounter}`; + +const isTextRow = ( + row: DebugTranscriptRow | undefined | null +): row is DebugTranscriptTextRow => row?.kind === 'text'; + +const isToolRow = ( + row: DebugTranscriptRow | undefined | null +): row is DebugTranscriptToolRow => row?.kind === 'tool'; + +const findRowIndexById = (rows: DebugTranscriptRow[], rowId?: string | null) => + rowId ? rows.findIndex((row) => row.id === rowId) : -1; + +const findAssistantRowIndexByResponseId = (rows: DebugTranscriptRow[], responseId?: string) => { + if (!responseId) return -1; + return rows.findIndex( + (row) => isTextRow(row) && row.role === 'assistant' && row.responseId === responseId + ); +}; + +const findLastAssistantCandidateIndex = ( + rows: DebugTranscriptRow[], + responseId?: string +) => { + for (let i = rows.length - 1; i >= 0; i -= 1) { + const row = rows[i]; + if (isTextRow(row) && row.role === 'assistant') { + if (responseId && row.responseId && row.responseId !== responseId) { + break; + } + return i; + } + if (isTextRow(row) && row.role === 'user') { + break; + } + } + + return -1; +}; + +const createTextRow = ({ + role, + text, + turnId, + utteranceId, + responseId, + ttfbMs, + isStreaming, +}: { + role: DebugTranscriptTextRole; + text: string; + turnId?: string; + utteranceId?: string; + responseId?: string; + ttfbMs?: number; + isStreaming?: boolean; +}): DebugTranscriptTextRow => ({ + kind: 'text', + id: createRowId(role), + role, + text, + ...(turnId ? { turnId } : {}), + ...(utteranceId ? { utteranceId } : {}), + ...(responseId ? { responseId } : {}), + ...(typeof ttfbMs === 'number' ? { ttfbMs } : {}), + ...(typeof isStreaming === 'boolean' ? { isStreaming } : {}), +}); + +const createToolRow = ({ + toolCallId, + toolName, + toolDisplayName, + executor, + turnId, + utteranceId, + responseId, + source, + status, + args, + result, + error, + rawCall, + rawResult, +}: { + toolCallId: string; + toolName: string; + toolDisplayName: string; + executor: string; + turnId?: string; + utteranceId?: string; + responseId?: string; + source?: string; + status: DebugTranscriptToolStatus; + args?: unknown; + result?: unknown; + error?: unknown; + rawCall?: unknown; + rawResult?: unknown; +}): DebugTranscriptToolRow => ({ + kind: 'tool', + id: toolCallId || createRowId('tool'), + toolCallId, + toolName, + toolDisplayName, + executor, + ...(turnId ? { turnId } : {}), + ...(utteranceId ? { utteranceId } : {}), + ...(responseId ? { responseId } : {}), + ...(source ? { source } : {}), + status, + ...(args !== undefined ? { args } : {}), + ...(result !== undefined ? { result } : {}), + ...(error !== undefined ? { error } : {}), + ...(rawCall !== undefined ? { rawCall } : {}), + ...(rawResult !== undefined ? { rawResult } : {}), +}); + +export const resetTranscriptRows = (): DebugTranscriptRow[] => []; + +export const appendTextRow = ( + rows: DebugTranscriptRow[], + { + role, + text, + turnId, + utteranceId, + responseId, + ttfbMs, + isStreaming, + }: { + role: DebugTranscriptTextRole; + text: string; + turnId?: string; + utteranceId?: string; + responseId?: string; + ttfbMs?: number; + isStreaming?: boolean; + } +): DebugTranscriptRow[] => [ + ...rows, + createTextRow({ role, text, turnId, utteranceId, responseId, ttfbMs, isStreaming }), +]; + +export const appendNoticeRow = (rows: DebugTranscriptRow[], text: string) => + appendTextRow(rows, { role: 'notice', text, isStreaming: false }); + +export const updateUserDraftRow = ( + rows: DebugTranscriptRow[], + { + draftRowId, + text, + turnId, + utteranceId, + }: { + draftRowId?: string | null; + text: string; + turnId?: string; + utteranceId?: string; + } +): { rows: DebugTranscriptRow[]; draftRowId: string } => { + const rowIndex = findRowIndexById(rows, draftRowId); + if (rowIndex !== -1) { + const row = rows[rowIndex]; + if (isTextRow(row) && row.role === 'user') { + const nextRows = [...rows]; + nextRows[rowIndex] = { + ...row, + text, + turnId: row.turnId || turnId, + utteranceId: row.utteranceId || utteranceId, + isStreaming: true, + }; + return { rows: nextRows, draftRowId: row.id }; + } + } + + const nextRow = createTextRow({ + role: 'user', + text, + turnId, + utteranceId, + isStreaming: true, + }); + return { + rows: [...rows, nextRow], + draftRowId: nextRow.id, + }; +}; + +export const finalizeUserDraftRow = ( + rows: DebugTranscriptRow[], + { + draftRowId, + text, + turnId, + utteranceId, + }: { + draftRowId?: string | null; + text: string; + turnId?: string; + utteranceId?: string; + } +): { rows: DebugTranscriptRow[]; draftRowId: null } => { + const rowIndex = findRowIndexById(rows, draftRowId); + if (rowIndex !== -1) { + const row = rows[rowIndex]; + if (isTextRow(row) && row.role === 'user') { + const nextRows = [...rows]; + nextRows[rowIndex] = { + ...row, + text: text || row.text, + turnId: row.turnId || turnId, + utteranceId: row.utteranceId || utteranceId, + isStreaming: false, + }; + return { rows: nextRows, draftRowId: null }; + } + } + + if (!text) { + return { rows, draftRowId: null }; + } + + return { + rows: appendTextRow(rows, { + role: 'user', + text, + turnId, + utteranceId, + isStreaming: false, + }), + draftRowId: null, + }; +}; + +export const updateAssistantDeltaRow = ( + rows: DebugTranscriptRow[], + { + draftRowId, + delta, + turnId, + utteranceId, + responseId, + ttfbMs, + }: { + draftRowId?: string | null; + delta: string; + turnId?: string; + utteranceId?: string; + responseId?: string; + ttfbMs?: number; + } +): { rows: DebugTranscriptRow[]; draftRowId: string } => { + let rowIndex = findRowIndexById(rows, draftRowId); + if ( + rowIndex !== -1 && + (!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant') + ) { + rowIndex = -1; + } + + if (rowIndex === -1) { + rowIndex = findAssistantRowIndexByResponseId(rows, responseId); + } + + if (rowIndex === -1) { + rowIndex = findLastAssistantCandidateIndex(rows, responseId); + } + + if (rowIndex === -1) { + const lastRow = rows[rows.length - 1]; + if ( + isTextRow(lastRow) && + lastRow.role === 'assistant' && + lastRow.text === delta && + lastRow.responseId === responseId + ) { + return { + rows, + draftRowId: lastRow.id, + }; + } + + const nextRow = createTextRow({ + role: 'assistant', + text: delta, + turnId, + utteranceId, + responseId, + ttfbMs, + isStreaming: true, + }); + return { + rows: [...rows, nextRow], + draftRowId: nextRow.id, + }; + } + + const row = rows[rowIndex]; + if (!isTextRow(row) || row.role !== 'assistant') { + return { + rows, + draftRowId: draftRowId || createRowId('assistant'), + }; + } + + const nextRows = [...rows]; + nextRows[rowIndex] = { + ...row, + text: row.text + delta, + turnId: row.turnId || turnId, + utteranceId: row.utteranceId || utteranceId, + responseId: row.responseId || responseId, + ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs, + isStreaming: true, + }; + + return { + rows: nextRows, + draftRowId: row.id, + }; +}; + +export const finalizeAssistantTextRow = ( + rows: DebugTranscriptRow[], + { + draftRowId, + text, + turnId, + utteranceId, + responseId, + ttfbMs, + }: { + draftRowId?: string | null; + text: string; + turnId?: string; + utteranceId?: string; + responseId?: string; + ttfbMs?: number; + } +): { rows: DebugTranscriptRow[]; draftRowId: null } => { + let rowIndex = findRowIndexById(rows, draftRowId); + if ( + rowIndex !== -1 && + (!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant') + ) { + rowIndex = -1; + } + + if (rowIndex === -1) { + rowIndex = findAssistantRowIndexByResponseId(rows, responseId); + } + + if (rowIndex === -1) { + rowIndex = findLastAssistantCandidateIndex(rows, responseId); + } + + if (rowIndex !== -1) { + const row = rows[rowIndex]; + if (isTextRow(row) && row.role === 'assistant') { + const nextRows = [...rows]; + nextRows[rowIndex] = { + ...row, + text: text || row.text, + turnId: row.turnId || turnId, + utteranceId: row.utteranceId || utteranceId, + responseId: row.responseId || responseId, + ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs, + isStreaming: false, + }; + return { + rows: nextRows, + draftRowId: null, + }; + } + } + + if (!text) { + return { rows, draftRowId: null }; + } + + const lastRow = rows[rows.length - 1]; + if ( + isTextRow(lastRow) && + lastRow.role === 'assistant' && + (!responseId || !lastRow.responseId || lastRow.responseId === responseId) + ) { + if (lastRow.text === text) { + return { rows, draftRowId: null }; + } + + if (text.startsWith(lastRow.text) || lastRow.text.startsWith(text)) { + const nextRows = [...rows]; + nextRows[nextRows.length - 1] = { + ...lastRow, + text, + turnId: lastRow.turnId || turnId, + utteranceId: lastRow.utteranceId || utteranceId, + responseId: lastRow.responseId || responseId, + ttfbMs: typeof lastRow.ttfbMs === 'number' ? lastRow.ttfbMs : ttfbMs, + isStreaming: false, + }; + return { + rows: nextRows, + draftRowId: null, + }; + } + } + + return { + rows: appendTextRow(rows, { + role: 'assistant', + text, + turnId, + utteranceId, + responseId, + ttfbMs, + isStreaming: false, + }), + draftRowId: null, + }; +}; + +export const attachAssistantTtfb = ( + rows: DebugTranscriptRow[], + { + responseId, + ttfbMs, + }: { + responseId?: string; + ttfbMs: number; + } +) => { + const rowIndex = + findAssistantRowIndexByResponseId(rows, responseId) !== -1 + ? findAssistantRowIndexByResponseId(rows, responseId) + : findLastAssistantCandidateIndex(rows, responseId); + + if (rowIndex === -1) { + return rows; + } + + const row = rows[rowIndex]; + if (!isTextRow(row) || row.role !== 'assistant') { + return rows; + } + + const nextRows = [...rows]; + nextRows[rowIndex] = { + ...row, + ttfbMs, + }; + return nextRows; +}; + +export const trimInterruptedResponseRows = ( + rows: DebugTranscriptRow[], + responseId?: string +) => { + if (!responseId) return rows; + return rows.filter((row) => row.responseId !== responseId); +}; + +export const upsertToolCallRow = ( + rows: DebugTranscriptRow[], + { + toolCallId, + toolName, + toolDisplayName, + executor, + turnId, + utteranceId, + responseId, + args, + rawCall, + }: { + toolCallId: string; + toolName: string; + toolDisplayName: string; + executor: string; + turnId?: string; + utteranceId?: string; + responseId?: string; + args?: unknown; + rawCall?: unknown; + } +) => { + const rowIndex = rows.findIndex( + (row) => isToolRow(row) && row.toolCallId === toolCallId + ); + if (rowIndex === -1) { + return [ + ...rows, + createToolRow({ + toolCallId, + toolName, + toolDisplayName, + executor, + turnId, + utteranceId, + responseId, + status: 'pending', + args, + rawCall, + }), + ]; + } + + const row = rows[rowIndex]; + if (!isToolRow(row)) { + return rows; + } + + const nextRows = [...rows]; + nextRows[rowIndex] = { + ...row, + toolName, + toolDisplayName, + executor, + turnId: row.turnId || turnId, + utteranceId: row.utteranceId || utteranceId, + responseId: row.responseId || responseId, + status: 'pending', + args: args !== undefined ? args : row.args, + rawCall: rawCall !== undefined ? rawCall : row.rawCall, + }; + return nextRows; +}; + +export const normalizeToolStatus = ( + statusCode?: number, + statusMessage?: string +): DebugTranscriptToolStatus => { + if (statusCode === 504 || String(statusMessage || '').toLowerCase().includes('timeout')) { + return 'timeout'; + } + + if (typeof statusCode === 'number' && statusCode >= 200 && statusCode < 300) { + return 'success'; + } + + return 'error'; +}; + +export const resolveToolResultRow = ( + rows: DebugTranscriptRow[], + { + toolCallId, + toolName, + toolDisplayName, + executor, + turnId, + utteranceId, + responseId, + source, + status, + args, + result, + error, + rawCall, + rawResult, + }: { + toolCallId: string; + toolName: string; + toolDisplayName: string; + executor?: string; + turnId?: string; + utteranceId?: string; + responseId?: string; + source?: string; + status: DebugTranscriptToolStatus; + args?: unknown; + result?: unknown; + error?: unknown; + rawCall?: unknown; + rawResult?: unknown; + } +) => { + const rowIndex = rows.findIndex( + (row) => isToolRow(row) && row.toolCallId === toolCallId + ); + if (rowIndex === -1) { + return [ + ...rows, + createToolRow({ + toolCallId, + toolName, + toolDisplayName, + executor: executor || 'server', + turnId, + utteranceId, + responseId, + source, + status, + args, + result, + error, + rawCall, + rawResult, + }), + ]; + } + + const row = rows[rowIndex]; + if (!isToolRow(row)) { + return rows; + } + + const nextRows = [...rows]; + nextRows[rowIndex] = { + ...row, + toolName: toolName || row.toolName, + toolDisplayName: toolDisplayName || row.toolDisplayName, + executor: executor || row.executor, + turnId: row.turnId || turnId, + utteranceId: row.utteranceId || utteranceId, + responseId: row.responseId || responseId, + source: source || row.source, + status, + args: args !== undefined ? args : row.args, + result: result !== undefined ? result : row.result, + error, + rawCall: rawCall !== undefined ? rawCall : row.rawCall, + rawResult: rawResult !== undefined ? rawResult : row.rawResult, + }; + return nextRows; +}; diff --git a/web/components/debug-transcript/types.ts b/web/components/debug-transcript/types.ts new file mode 100644 index 0000000..4e84c85 --- /dev/null +++ b/web/components/debug-transcript/types.ts @@ -0,0 +1,36 @@ +export type DebugTranscriptTextRole = 'user' | 'assistant' | 'notice'; + +export type DebugTranscriptToolStatus = 'pending' | 'success' | 'error' | 'timeout'; + +export type DebugTranscriptCorrelation = { + turnId?: string; + utteranceId?: string; + responseId?: string; +}; + +export type DebugTranscriptTextRow = { + kind: 'text'; + id: string; + role: DebugTranscriptTextRole; + text: string; + ttfbMs?: number; + isStreaming?: boolean; +} & DebugTranscriptCorrelation; + +export type DebugTranscriptToolRow = { + kind: 'tool'; + id: string; + toolCallId: string; + toolName: string; + toolDisplayName: string; + executor: string; + source?: string; + status: DebugTranscriptToolStatus; + args?: unknown; + result?: unknown; + error?: unknown; + rawCall?: unknown; + rawResult?: unknown; +} & DebugTranscriptCorrelation; + +export type DebugTranscriptRow = DebugTranscriptTextRow | DebugTranscriptToolRow;