Update debug drawer records style
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user