Update debug drawer records style

This commit is contained in:
Xin Wang
2026-03-13 07:09:42 +08:00
parent 5eec8f2b30
commit def6a11338

View File

@@ -3,6 +3,22 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Plus, Search, Play, Square, Copy, Trash2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Database, Server, Zap, ExternalLink, Key, BrainCircuit, Ear, Book, Filter } from 'lucide-react';
import { Button, Input, Badge, Drawer, Dialog, Switch } from '../components/UI';
import TranscriptList from '../components/debug-transcript/TranscriptList';
import type { DebugTranscriptRow } from '../components/debug-transcript/types';
import {
appendNoticeRow,
appendTextRow,
attachAssistantTtfb,
finalizeAssistantTextRow,
finalizeUserDraftRow,
normalizeToolStatus,
resetTranscriptRows,
resolveToolResultRow,
trimInterruptedResponseRows,
updateAssistantDeltaRow,
updateUserDraftRow,
upsertToolCallRow,
} from '../components/debug-transcript/message-utils';
import { ASRModel, Assistant, AssistantOpenerToolCall, KnowledgeBase, LLMModel, TabValue, Tool, Voice } from '../types';
import { createAssistant, deleteAssistant, fetchASRModels, fetchAssistantOpenerAudioPcmBuffer, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchTools, fetchVoices, generateAssistantOpenerAudio, previewVoice, updateAssistant as updateAssistantApi } from '../services/backendApi';
import { useDebugPrefsStore } from '../stores/debugPrefsStore';
@@ -877,9 +893,13 @@ export const AssistantsPage: React.FC = () => {
{selectedAssistant.configMode === 'fastgpt' && (
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center">
<label className="hidden">
<Key className="w-4 h-4 mr-2 text-primary" /> ID (APP ID)
<span className="text-sm text-white">?? ID (APP ID)</span>
</label>
<div className="text-sm font-medium text-white flex items-center">
<Key className="w-4 h-4 mr-2 text-primary" /> ID (APP ID)
</div>
<Input
value={selectedAssistant.appId || ''}
onChange={(e) => updateAssistant('appId', e.target.value)}
@@ -2221,13 +2241,6 @@ const extractDynamicTemplateKeys = (text: string): string[] => {
return Array.from(keys);
};
type DebugTranscriptMessage = {
role: 'user' | 'model' | 'tool';
text: string;
responseId?: string;
ttfbMs?: number;
};
type DebugPromptPendingResult = {
toolCallId: string;
toolName: string;
@@ -2399,33 +2412,6 @@ const normalizeFastGPTInteractiveFields = (rawForm: unknown[]): DebugFastGPTInte
return resolved;
};
// Stable transcription log so the scroll container is not recreated on every render (avoids scroll jumping)
const TranscriptionLog: React.FC<{
scrollRef: React.RefObject<HTMLDivElement | null>;
messages: DebugTranscriptMessage[];
isLoading: boolean;
className?: string;
}> = ({ scrollRef, messages, isLoading, className = '' }) => (
<div ref={scrollRef} className={`overflow-y-auto overflow-x-hidden space-y-4 p-2 border border-white/5 rounded-md bg-black/20 min-h-0 custom-scrollbar ${className}`}>
{messages.length === 0 && <div className="text-center text-muted-foreground text-xs py-4"></div>}
{messages.map((m, i) => (
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] rounded-lg px-3 py-2 text-sm ${m.role === 'user' ? 'bg-primary text-primary-foreground' : m.role === 'tool' ? 'bg-amber-500/10 border border-amber-400/30 text-amber-100' : 'bg-card border border-white/10 shadow-sm text-foreground'}`}>
<div className="mb-0.5 flex items-center gap-1.5">
<span className="text-[10px] opacity-70 uppercase tracking-wider">{m.role === 'user' ? 'Me' : m.role === 'tool' ? 'Tool' : 'AI'}</span>
{m.role === 'model' && typeof m.ttfbMs === 'number' && Number.isFinite(m.ttfbMs) && (
<span className="rounded border border-cyan-300/40 bg-cyan-500/10 px-1.5 py-0.5 text-[10px] text-cyan-200">
TTFB {Math.round(m.ttfbMs)}ms
</span>
)}
</div>
{m.text}
</div>
</div>
))}
</div>
);
// --- Debug Drawer Component ---
export const DebugDrawer: React.FC<{
isOpen: boolean;
@@ -2483,7 +2469,7 @@ export const DebugDrawer: React.FC<{
};
const [mode, setMode] = useState<'text' | 'voice' | 'video'>('text');
const [messages, setMessages] = useState<DebugTranscriptMessage[]>([]);
const [messages, setMessages] = useState<DebugTranscriptRow[]>([]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [callStatus, setCallStatus] = useState<'idle' | 'calling' | 'active'>('idle');
@@ -2581,8 +2567,7 @@ export const DebugDrawer: React.FC<{
const pendingResolveRef = useRef<(() => void) | null>(null);
const pendingRejectRef = useRef<((e: Error) => void) | null>(null);
const submittedMetadataRef = useRef<Record<string, any> | null>(null);
const assistantDraftIndexRef = useRef<number | null>(null);
const assistantResponseIndexByIdRef = useRef<Map<string, number>>(new Map());
const assistantDraftRowIdRef = useRef<string | null>(null);
const pendingTtfbByResponseIdRef = useRef<Map<string, number>>(new Map());
const interruptedResponseIdsRef = useRef<Set<string>>(new Set());
const interruptedDropNoticeKeysRef = useRef<Set<string>>(new Set());
@@ -2606,7 +2591,7 @@ export const DebugDrawer: React.FC<{
const micProcessorRef = useRef<ScriptProcessorNode | null>(null);
const micGainRef = useRef<GainNode | null>(null);
const micFrameBufferRef = useRef<Uint8Array>(new Uint8Array(0));
const userDraftIndexRef = useRef<number | null>(null);
const userDraftRowIdRef = useRef<string | null>(null);
const lastUserFinalRef = useRef<string>('');
const debugVolumePercentRef = useRef<number>(50);
const clientToolEnabledMapRef = useRef<Record<string, boolean>>(clientToolEnabledMap);
@@ -2647,8 +2632,7 @@ export const DebugDrawer: React.FC<{
}, [assistant.tools, tools, clientToolEnabledMap]);
const clearResponseTracking = () => {
assistantDraftIndexRef.current = null;
assistantResponseIndexByIdRef.current.clear();
assistantDraftRowIdRef.current = null;
pendingTtfbByResponseIdRef.current.clear();
interruptedResponseIdsRef.current.clear();
interruptedDropNoticeKeysRef.current.clear();
@@ -2660,6 +2644,18 @@ export const DebugDrawer: React.FC<{
return responseId || undefined;
};
const extractTurnId = (payload: any): string | undefined => {
const turnIdRaw = payload?.data?.turn_id ?? payload?.turn_id ?? payload?.turnId;
const turnId = String(turnIdRaw || '').trim();
return turnId || undefined;
};
const extractUtteranceId = (payload: any): string | undefined => {
const utteranceIdRaw = payload?.data?.utterance_id ?? payload?.utterance_id ?? payload?.utteranceId;
const utteranceId = String(utteranceIdRaw || '').trim();
return utteranceId || undefined;
};
const noteInterruptedDrop = (responseId: string, kind: 'ttfb' | 'delta' | 'final') => {
const key = `${responseId}:${kind}`;
if (interruptedDropNoticeKeysRef.current.has(key)) return;
@@ -2668,13 +2664,9 @@ export const DebugDrawer: React.FC<{
const oldest = interruptedDropNoticeKeysRef.current.values().next().value as string | undefined;
if (oldest) interruptedDropNoticeKeysRef.current.delete(oldest);
}
setMessages((prev) => [
...prev,
{
role: 'tool',
text: `drop stale ${kind} from interrupted response ${responseId}`,
},
]);
setMessages((prev) =>
appendNoticeRow(prev, `drop stale ${kind} from interrupted response ${responseId}`)
);
};
// Initialize
@@ -2682,11 +2674,11 @@ export const DebugDrawer: React.FC<{
if (isOpen) {
if (mode === 'text') {
clearResponseTracking();
setMessages([]);
setMessages(resetTranscriptRows());
setTextSessionStarted(false);
} else {
clearResponseTracking();
setMessages([]);
setMessages(resetTranscriptRows());
setCallStatus('idle');
setAgentState('listening');
}
@@ -2967,17 +2959,18 @@ export const DebugDrawer: React.FC<{
const statusCode = Number(resultPayload?.status?.code || 500);
const statusMessage = String(resultPayload?.status?.message || 'error');
const displayName = toolDisplayName || String(resultPayload?.name || 'unknown_tool');
const resultText =
statusCode === 200 && typeof resultPayload?.output?.result === 'number'
? `result ${displayName} = ${resultPayload.output.result}`
: `result ${displayName} status=${statusCode} ${statusMessage}`;
setMessages((prev) => [
...prev,
{
role: 'tool',
text: resultText,
},
]);
setMessages((prev) =>
resolveToolResultRow(prev, {
toolCallId: String(resultPayload?.tool_call_id || '').trim(),
toolName: normalizeToolId(resultPayload?.name || 'unknown_tool'),
toolDisplayName: displayName,
source: 'client',
status: normalizeToolStatus(statusCode, statusMessage),
result: resultPayload?.output,
error: statusCode >= 200 && statusCode < 300 ? undefined : resultPayload?.output ?? statusMessage,
rawResult: resultPayload,
})
);
};
const stopPromptVoicePlayback = () => {
@@ -3404,7 +3397,12 @@ export const DebugDrawer: React.FC<{
setCallStatus('calling');
setTimeout(() => {
setCallStatus('active');
setMessages([{ role: 'model', text: assistant.opener || "Hello!" }]);
setMessages(
appendTextRow(resetTranscriptRows(), {
role: 'assistant',
text: assistant.opener || 'Hello!',
})
);
}, 1500);
return;
}
@@ -3412,7 +3410,7 @@ export const DebugDrawer: React.FC<{
try {
setCallStatus('calling');
clearResponseTracking();
setMessages([]);
setMessages(resetTranscriptRows());
lastUserFinalRef.current = '';
setWsError('');
setDynamicVariablesError('');
@@ -3454,7 +3452,7 @@ export const DebugDrawer: React.FC<{
setCallStatus('idle');
setAgentState('listening');
clearResponseTracking();
setMessages([]);
setMessages(resetTranscriptRows());
setTextPromptDialog({ open: false, message: '', promptType: 'text' });
setChoicePromptDialog({ open: false, question: '', options: [] });
lastUserFinalRef.current = '';
@@ -3464,8 +3462,14 @@ export const DebugDrawer: React.FC<{
const handleSend = async () => {
if (!inputText.trim()) return;
const userMsg = inputText;
assistantDraftIndexRef.current = null;
setMessages(prev => [...prev, { role: 'user', text: userMsg }]);
assistantDraftRowIdRef.current = null;
setMessages((prev) =>
appendTextRow(prev, {
role: 'user',
text: userMsg,
isStreaming: false,
})
);
setInputText('');
setIsLoading(true);
@@ -3485,7 +3489,13 @@ export const DebugDrawer: React.FC<{
wsRef.current?.send(JSON.stringify({ type: 'input.text', text: userMsg }));
} else {
setTimeout(() => {
setMessages(prev => [...prev, { role: 'model', text: `[Mock Response]: Received "${userMsg}"` }]);
setMessages((prev) =>
appendTextRow(prev, {
role: 'assistant',
text: `[Mock Response]: Received "${userMsg}"`,
isStreaming: false,
})
);
setIsLoading(false);
}, 1000);
}
@@ -3497,7 +3507,13 @@ export const DebugDrawer: React.FC<{
return;
}
const errMessage = err?.message || 'Failed to connect to AI service.';
setMessages(prev => [...prev, { role: 'model', text: `Error: ${errMessage}` }]);
setMessages((prev) =>
appendTextRow(prev, {
role: 'assistant',
text: `Error: ${errMessage}`,
isStreaming: false,
})
);
setWsError(errMessage);
setIsLoading(false);
} finally {
@@ -3511,7 +3527,7 @@ export const DebugDrawer: React.FC<{
setDynamicVariablesError('');
// Start every text debug run as a fresh session transcript.
clearResponseTracking();
setMessages([]);
setMessages(resetTranscriptRows());
lastUserFinalRef.current = '';
// Force a fresh WS session so updated assistant runtime config
// (voice/model/provider/speed) is applied on session.start.
@@ -3809,7 +3825,7 @@ export const DebugDrawer: React.FC<{
pendingResolveRef.current = null;
pendingRejectRef.current = null;
clearResponseTracking();
userDraftIndexRef.current = null;
userDraftRowIdRef.current = null;
lastUserFinalRef.current = '';
micFrameBufferRef.current = new Uint8Array(0);
stopPromptVoicePlayback();
@@ -3916,8 +3932,10 @@ export const DebugDrawer: React.FC<{
const oldest = interruptedResponseIdsRef.current.values().next().value as string | undefined;
if (oldest) interruptedResponseIdsRef.current.delete(oldest);
}
pendingTtfbByResponseIdRef.current.delete(interruptedResponseId);
setMessages((prev) => trimInterruptedResponseRows(prev, interruptedResponseId));
}
assistantDraftIndexRef.current = null;
assistantDraftRowIdRef.current = null;
setIsLoading(false);
stopPlaybackImmediately();
setAgentState('waiting');
@@ -3934,29 +3952,16 @@ export const DebugDrawer: React.FC<{
return;
}
if (responseId) {
const indexed = assistantResponseIndexByIdRef.current.get(responseId);
if (typeof indexed === 'number') {
setMessages((prev) => {
if (!prev[indexed] || prev[indexed].role !== 'model') return prev;
const next = [...prev];
next[indexed] = { ...next[indexed], ttfbMs };
return next;
});
} else {
pendingTtfbByResponseIdRef.current.set(responseId, ttfbMs);
}
setMessages((prev) => {
const nextRows = attachAssistantTtfb(prev, { responseId, ttfbMs });
if (nextRows === prev) {
pendingTtfbByResponseIdRef.current.set(responseId, ttfbMs);
}
return nextRows;
});
return;
}
setMessages((prev) => {
for (let i = prev.length - 1; i >= 0; i -= 1) {
if (prev[i]?.role === 'model') {
const next = [...prev];
next[i] = { ...next[i], ttfbMs };
return next;
}
}
return prev;
});
setMessages((prev) => attachAssistantTtfb(prev, { ttfbMs }));
return;
}
@@ -3967,24 +3972,35 @@ export const DebugDrawer: React.FC<{
const toolDisplayName = String(payload?.tool_display_name || toolCall?.displayName || toolName);
const executor = String(toolCall?.executor || 'server').toLowerCase();
const rawArgs = String(toolCall?.function?.arguments || '');
const argText = rawArgs.length > 160 ? `${rawArgs.slice(0, 160)}...` : rawArgs;
setMessages((prev) => [
...prev,
{
role: 'tool',
text: `call ${toolDisplayName} executor=${executor}${argText ? ` args=${argText}` : ''}`,
},
]);
if (executor === 'client' && toolCallId && ws.readyState === WebSocket.OPEN) {
let parsedArgs: Record<string, any> = {};
if (rawArgs) {
try {
const candidate = JSON.parse(rawArgs);
parsedArgs = candidate && typeof candidate === 'object' ? candidate : {};
} catch {
parsedArgs = {};
}
const turnId = extractTurnId(payload);
const utteranceId = extractUtteranceId(payload);
const responseId = extractResponseId(payload);
let parsedArgsValue: unknown = rawArgs || undefined;
if (rawArgs) {
try {
parsedArgsValue = JSON.parse(rawArgs);
} catch {
parsedArgsValue = rawArgs;
}
}
setMessages((prev) =>
upsertToolCallRow(prev, {
toolCallId,
toolName,
toolDisplayName,
executor,
turnId,
utteranceId,
responseId,
args: parsedArgsValue,
rawCall: payload,
})
);
if (executor === 'client' && toolCallId && ws.readyState === WebSocket.OPEN) {
const parsedArgs =
parsedArgsValue && typeof parsedArgsValue === 'object' && !Array.isArray(parsedArgsValue)
? (parsedArgsValue as Record<string, any>)
: {};
const waitForResponseRaw = Boolean(
payload?.wait_for_response ?? toolCall?.wait_for_response ?? toolCall?.waitForResponse ?? false
);
@@ -4252,17 +4268,29 @@ export const DebugDrawer: React.FC<{
if (type === 'assistant.tool_result') {
const result = payload?.result || {};
const toolCallId = String(payload?.tool_call_id || result?.tool_call_id || '').trim();
const toolName = normalizeToolId(result?.name || 'unknown_tool');
const toolDisplayName = String(payload?.tool_display_name || toolName);
const toolDisplayName = String(payload?.tool_display_name || result?.tool_display_name || toolName);
const statusCode = Number(result?.status?.code || 500);
const statusMessage = String(result?.status?.message || 'error');
const source = String(payload?.source || 'server');
const output = result?.output;
const resultText =
statusCode === 200
? `result ${toolDisplayName} source=${source} ${JSON.stringify(output)}`
: `result ${toolDisplayName} source=${source} status=${statusCode} ${statusMessage}`;
setMessages((prev) => [...prev, { role: 'tool', text: resultText }]);
const turnId = extractTurnId(payload);
const utteranceId = extractUtteranceId(payload);
setMessages((prev) =>
resolveToolResultRow(prev, {
toolCallId,
toolName,
toolDisplayName,
turnId,
utteranceId,
responseId: extractResponseId(payload),
source,
status: normalizeToolStatus(statusCode, statusMessage),
result: result?.output,
error: statusCode >= 200 && statusCode < 300 ? undefined : result?.output ?? statusMessage,
rawResult: payload,
})
);
return;
}
@@ -4304,17 +4332,17 @@ export const DebugDrawer: React.FC<{
if (type === 'transcript.delta') {
const delta = String(payload.text || '');
if (!delta) return;
const turnId = extractTurnId(payload);
const utteranceId = extractUtteranceId(payload);
setMessages((prev) => {
const idx = userDraftIndexRef.current;
if (idx === null || !prev[idx] || prev[idx].role !== 'user') {
const next = [...prev, { role: 'user' as const, text: delta }];
userDraftIndexRef.current = next.length - 1;
return next;
}
const next = [...prev];
// ASR interim is typically the latest partial text, not a true text delta.
next[idx] = { ...next[idx], text: delta };
return next;
const nextState = updateUserDraftRow(prev, {
draftRowId: userDraftRowIdRef.current,
text: delta,
turnId,
utteranceId,
});
userDraftRowIdRef.current = nextState.draftRowId;
return nextState.rows;
});
return;
}
@@ -4322,31 +4350,25 @@ export const DebugDrawer: React.FC<{
if (type === 'transcript.final') {
const finalText = String(payload.text || '');
if (!finalText) {
userDraftIndexRef.current = null;
userDraftRowIdRef.current = null;
return;
}
if (lastUserFinalRef.current === finalText) {
userDraftIndexRef.current = null;
userDraftRowIdRef.current = null;
return;
}
const turnId = extractTurnId(payload);
const utteranceId = extractUtteranceId(payload);
setMessages((prev) => {
const idx = userDraftIndexRef.current;
userDraftIndexRef.current = null;
if (idx !== null && prev[idx] && prev[idx].role === 'user') {
const next = [...prev];
next[idx] = { ...next[idx], text: finalText || next[idx].text };
lastUserFinalRef.current = finalText;
return next;
}
const last = prev[prev.length - 1];
if (last?.role === 'user') {
const next = [...prev];
next[next.length - 1] = { ...last, text: finalText };
lastUserFinalRef.current = finalText;
return next;
}
const nextState = finalizeUserDraftRow(prev, {
draftRowId: userDraftRowIdRef.current,
text: finalText,
turnId,
utteranceId,
});
userDraftRowIdRef.current = nextState.draftRowId;
lastUserFinalRef.current = finalText;
return [...prev, { role: 'user', text: finalText }];
return nextState.rows;
});
return;
}
@@ -4354,143 +4376,56 @@ export const DebugDrawer: React.FC<{
if (type === 'assistant.response.delta') {
const delta = String(payload.text || '');
if (!delta) return;
const turnId = extractTurnId(payload);
const utteranceId = extractUtteranceId(payload);
const responseId = extractResponseId(payload);
if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
noteInterruptedDrop(responseId, 'delta');
return;
}
setMessages((prev) => {
let idx = assistantDraftIndexRef.current;
if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
// Tool records can be appended between assistant chunks; recover the
// latest model row instead of creating a duplicate assistant row.
for (let i = prev.length - 1; i >= 0; i -= 1) {
if (prev[i]?.role === 'model') {
if (
responseId
&& prev[i].responseId
&& prev[i].responseId !== responseId
) {
break;
}
idx = i;
assistantDraftIndexRef.current = i;
break;
}
if (prev[i]?.role === 'user') break;
}
}
if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
const last = prev[prev.length - 1];
if (last?.role === 'model' && last.text === delta) {
return prev;
}
const nextMessage: DebugTranscriptMessage = { role: 'model' as const, text: delta };
if (responseId) {
nextMessage.responseId = responseId;
if (pendingTtfbByResponseIdRef.current.has(responseId)) {
nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
pendingTtfbByResponseIdRef.current.delete(responseId);
}
}
const next = [...prev, nextMessage];
assistantDraftIndexRef.current = next.length - 1;
if (responseId) {
assistantResponseIndexByIdRef.current.set(responseId, next.length - 1);
}
return next;
}
const next = [...prev];
const nextMessage = { ...next[idx], text: next[idx].text + delta };
if (responseId && !nextMessage.responseId) {
nextMessage.responseId = responseId;
}
if (
responseId
&& typeof nextMessage.ttfbMs !== 'number'
&& pendingTtfbByResponseIdRef.current.has(responseId)
) {
nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
const pendingTtfb = responseId ? pendingTtfbByResponseIdRef.current.get(responseId) : undefined;
const nextState = updateAssistantDeltaRow(prev, {
draftRowId: assistantDraftRowIdRef.current,
delta,
turnId,
utteranceId,
responseId,
ttfbMs: pendingTtfb,
});
assistantDraftRowIdRef.current = nextState.draftRowId;
if (responseId && typeof pendingTtfb === 'number') {
pendingTtfbByResponseIdRef.current.delete(responseId);
}
next[idx] = nextMessage;
if (responseId) {
assistantResponseIndexByIdRef.current.set(responseId, idx);
}
return next;
return nextState.rows;
});
return;
}
if (type === 'assistant.response.final') {
const finalText = String(payload.text || '');
const turnId = extractTurnId(payload);
const utteranceId = extractUtteranceId(payload);
const responseId = extractResponseId(payload);
if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
noteInterruptedDrop(responseId, 'final');
return;
}
setMessages((prev) => {
let idx = assistantDraftIndexRef.current;
assistantDraftIndexRef.current = null;
if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
for (let i = prev.length - 1; i >= 0; i -= 1) {
if (prev[i]?.role === 'model') {
if (
responseId
&& prev[i].responseId
&& prev[i].responseId !== responseId
) {
break;
}
idx = i;
break;
}
if (prev[i]?.role === 'user') break;
}
const pendingTtfb = responseId ? pendingTtfbByResponseIdRef.current.get(responseId) : undefined;
const nextState = finalizeAssistantTextRow(prev, {
draftRowId: assistantDraftRowIdRef.current,
text: finalText,
turnId,
utteranceId,
responseId,
ttfbMs: pendingTtfb,
});
assistantDraftRowIdRef.current = nextState.draftRowId;
if (responseId && typeof pendingTtfb === 'number') {
pendingTtfbByResponseIdRef.current.delete(responseId);
}
if (idx !== null && prev[idx] && prev[idx].role === 'model') {
const next = [...prev];
const nextMessage = { ...next[idx], text: finalText || next[idx].text };
if (responseId && !nextMessage.responseId) {
nextMessage.responseId = responseId;
}
if (
responseId
&& typeof nextMessage.ttfbMs !== 'number'
&& pendingTtfbByResponseIdRef.current.has(responseId)
) {
nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
pendingTtfbByResponseIdRef.current.delete(responseId);
}
next[idx] = nextMessage;
if (responseId) {
assistantResponseIndexByIdRef.current.set(responseId, idx);
}
return next;
}
if (!finalText) return prev;
const last = prev[prev.length - 1];
if (last?.role === 'model') {
if (last.text === finalText) return prev;
if (finalText.startsWith(last.text) || last.text.startsWith(finalText)) {
const next = [...prev];
next[next.length - 1] = { ...last, text: finalText };
return next;
}
}
const nextMessage: DebugTranscriptMessage = { role: 'model', text: finalText };
if (responseId) {
nextMessage.responseId = responseId;
if (pendingTtfbByResponseIdRef.current.has(responseId)) {
nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
pendingTtfbByResponseIdRef.current.delete(responseId);
}
}
const next = [...prev, nextMessage];
if (responseId) {
assistantResponseIndexByIdRef.current.set(responseId, next.length - 1);
}
return next;
return nextState.rows;
});
setIsLoading(false);
return;
@@ -4521,7 +4456,8 @@ export const DebugDrawer: React.FC<{
ws.onclose = () => {
wsReadyRef.current = false;
setTextSessionStarted(false);
userDraftIndexRef.current = null;
userDraftRowIdRef.current = null;
assistantDraftRowIdRef.current = null;
stopPlaybackImmediately();
if (wsStatusRef.current !== 'error') setWsStatus('disconnected');
};
@@ -5016,7 +4952,7 @@ export const DebugDrawer: React.FC<{
<p className="text-xs"></p>
</div>
) : (
<TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="pb-4" />
<TranscriptList scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="pb-4" />
)}
</div>
</div>