Add debug transcript components
This commit is contained in:
67
web/components/debug-transcript/MessageText.tsx
Normal file
67
web/components/debug-transcript/MessageText.tsx
Normal file
@@ -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<DebugTranscriptTextRow['role'], string> = {
|
||||||
|
user: 'Me',
|
||||||
|
assistant: 'AI',
|
||||||
|
notice: 'Debug',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageText: React.FC<{
|
||||||
|
row: DebugTranscriptTextRow;
|
||||||
|
}> = ({ row }) => {
|
||||||
|
if (row.role === 'notice') {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="max-w-full rounded-md border border-white/10 bg-black/25 px-3 py-2 text-[11px] text-muted-foreground">
|
||||||
|
<div className="mb-1 flex items-center gap-2">
|
||||||
|
<span className="uppercase tracking-[0.14em] opacity-70">{roleLabelMap[row.role]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-pre-wrap break-words">{row.text}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUser = row.role === 'user';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex', isUser ? 'justify-end' : 'justify-start')}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'max-w-[85%] rounded-lg px-3 py-2 text-sm',
|
||||||
|
isUser
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-card border border-white/10 shadow-sm text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mb-1 flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="text-[10px] uppercase tracking-wider opacity-70">
|
||||||
|
{roleLabelMap[row.role]}
|
||||||
|
</span>
|
||||||
|
{row.role === 'assistant' &&
|
||||||
|
typeof row.ttfbMs === 'number' &&
|
||||||
|
Number.isFinite(row.ttfbMs) && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-cyan-300/40 bg-cyan-500/10 px-1.5 py-0.5 text-[10px] text-cyan-200"
|
||||||
|
>
|
||||||
|
TTFB {Math.round(row.ttfbMs)}ms
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{row.role === 'assistant' && row.isStreaming && (
|
||||||
|
<span className="inline-flex h-2 w-2 animate-pulse rounded-full bg-primary/80" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-pre-wrap break-words">{row.text}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(MessageText);
|
||||||
|
|
||||||
162
web/components/debug-transcript/MessageTool.tsx
Normal file
162
web/components/debug-transcript/MessageTool.tsx
Normal file
@@ -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 (
|
||||||
|
<details
|
||||||
|
open={defaultOpen}
|
||||||
|
className="rounded-md border border-white/10 bg-black/20"
|
||||||
|
>
|
||||||
|
<summary className="cursor-pointer list-none px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</summary>
|
||||||
|
<pre className="overflow-x-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-3 text-[11px] leading-5 text-foreground/90">
|
||||||
|
{formattedValue}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={cn(nested ? 'w-full' : 'flex justify-start')}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full max-w-full border border-amber-400/30 bg-amber-500/10 p-3 text-amber-50',
|
||||||
|
nested ? 'rounded-md' : 'rounded-lg'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-amber-300/30 bg-black/20 text-amber-200">
|
||||||
|
<Wrench className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-medium text-foreground">
|
||||||
|
{row.toolDisplayName || row.toolName}
|
||||||
|
</div>
|
||||||
|
<div className="truncate text-[11px] text-amber-100/70">
|
||||||
|
{row.toolName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant={statusBadge.variant} className={statusBadge.className}>
|
||||||
|
{statusBadge.label}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-white/15 bg-black/10 text-[10px] uppercase">
|
||||||
|
{row.executor}
|
||||||
|
</Badge>
|
||||||
|
{row.source && (
|
||||||
|
<Badge variant="outline" className="border-white/15 bg-black/10 text-[10px] uppercase">
|
||||||
|
{row.source}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-[11px] text-amber-100/70">
|
||||||
|
tool_call_id: <span className="font-mono">{row.toolCallId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2 text-amber-100 hover:bg-white/10 hover:text-foreground"
|
||||||
|
onClick={() => setIsExpanded((value) => !value)}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid transition-all',
|
||||||
|
isExpanded ? 'mt-3 grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="min-h-0 overflow-hidden">
|
||||||
|
<div className="space-y-2 pt-1">
|
||||||
|
<Section label="Arguments" value={row.args} defaultOpen={row.status === 'pending'} />
|
||||||
|
<Section label="Result" value={row.result} />
|
||||||
|
<Section label="Error" value={row.error} defaultOpen />
|
||||||
|
<Section label="Raw call" value={row.rawCall} defaultOpen={false} />
|
||||||
|
<Section label="Raw result" value={row.rawResult} defaultOpen={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(MessageTool);
|
||||||
53
web/components/debug-transcript/TranscriptAssistantBlock.tsx
Normal file
53
web/components/debug-transcript/TranscriptAssistantBlock.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="w-full max-w-[85%] rounded-lg border border-white/10 bg-card px-3 py-2 text-sm text-foreground shadow-sm">
|
||||||
|
<div className="mb-1 flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="text-[10px] uppercase tracking-wider opacity-70">AI</span>
|
||||||
|
{typeof message?.ttfbMs === 'number' && Number.isFinite(message.ttfbMs) && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-cyan-300/40 bg-cyan-500/10 px-1.5 py-0.5 text-[10px] text-cyan-200"
|
||||||
|
>
|
||||||
|
TTFB {Math.round(message.ttfbMs)}ms
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{tools.length > 0 && (
|
||||||
|
<Badge variant="outline" className="border-white/15 bg-black/10 px-1.5 py-0.5 text-[10px]">
|
||||||
|
{tools.length} tool{tools.length > 1 ? 's' : ''}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isStreaming && <span className="inline-flex h-2 w-2 animate-pulse rounded-full bg-primary/80" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message?.text ? (
|
||||||
|
<div className="whitespace-pre-wrap break-words">{message.text}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{tools.length > 0 && (
|
||||||
|
<div className={message?.text ? 'mt-3 border-t border-white/10 pt-3' : 'mt-1'}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<MessageTool key={tool.id} row={tool} nested />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TranscriptAssistantBlock);
|
||||||
18
web/components/debug-transcript/TranscriptItem.tsx
Normal file
18
web/components/debug-transcript/TranscriptItem.tsx
Normal file
@@ -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 <MessageTool row={row} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MessageText row={row} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TranscriptItem);
|
||||||
|
|
||||||
123
web/components/debug-transcript/TranscriptList.tsx
Normal file
123
web/components/debug-transcript/TranscriptList.tsx
Normal file
@@ -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<DebugTranscriptRow, 'turnId' | 'utteranceId' | 'responseId'>) => {
|
||||||
|
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<string, AssistantRenderBlock>();
|
||||||
|
|
||||||
|
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<HTMLDivElement | null>;
|
||||||
|
messages: DebugTranscriptRow[];
|
||||||
|
isLoading: boolean;
|
||||||
|
className?: string;
|
||||||
|
}> = ({ scrollRef, messages, isLoading, className = '' }) => {
|
||||||
|
const renderItems = useMemo(() => buildRenderItems(messages), [messages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 overflow-y-auto overflow-x-hidden rounded-md border border-white/5 bg-black/20 p-2 min-h-0 custom-scrollbar',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{messages.length === 0 && !isLoading ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center space-y-3 text-muted-foreground/60">
|
||||||
|
<MessageSquare className="h-8 w-8 opacity-20" />
|
||||||
|
<p className="text-xs">鏆傛棤瀵硅瘽璁板綍</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4 pb-4">
|
||||||
|
{renderItems.map((item) =>
|
||||||
|
item.kind === 'assistant-block' ? (
|
||||||
|
<TranscriptAssistantBlock key={item.id} message={item.message} tools={item.tools} />
|
||||||
|
) : (
|
||||||
|
<TranscriptItem key={item.id} row={item.row} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TranscriptList);
|
||||||
637
web/components/debug-transcript/message-utils.ts
Normal file
637
web/components/debug-transcript/message-utils.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
36
web/components/debug-transcript/types.ts
Normal file
36
web/components/debug-transcript/types.ts
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user