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 { 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 { 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 { 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 { 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 { createAssistant, deleteAssistant, fetchASRModels, fetchAssistantOpenerAudioPcmBuffer, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchTools, fetchVoices, generateAssistantOpenerAudio, previewVoice, updateAssistant as updateAssistantApi } from '../services/backendApi';
|
||||||
import { useDebugPrefsStore } from '../stores/debugPrefsStore';
|
import { useDebugPrefsStore } from '../stores/debugPrefsStore';
|
||||||
@@ -877,9 +893,13 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
|
|
||||||
{selectedAssistant.configMode === 'fastgpt' && (
|
{selectedAssistant.configMode === 'fastgpt' && (
|
||||||
<div className="space-y-2">
|
<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)
|
<Key className="w-4 h-4 mr-2 text-primary" /> 搴旂敤 ID (APP ID)
|
||||||
|
<span className="text-sm text-white">?? ID (APP ID)</span>
|
||||||
</label>
|
</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
|
<Input
|
||||||
value={selectedAssistant.appId || ''}
|
value={selectedAssistant.appId || ''}
|
||||||
onChange={(e) => updateAssistant('appId', e.target.value)}
|
onChange={(e) => updateAssistant('appId', e.target.value)}
|
||||||
@@ -2221,13 +2241,6 @@ const extractDynamicTemplateKeys = (text: string): string[] => {
|
|||||||
return Array.from(keys);
|
return Array.from(keys);
|
||||||
};
|
};
|
||||||
|
|
||||||
type DebugTranscriptMessage = {
|
|
||||||
role: 'user' | 'model' | 'tool';
|
|
||||||
text: string;
|
|
||||||
responseId?: string;
|
|
||||||
ttfbMs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DebugPromptPendingResult = {
|
type DebugPromptPendingResult = {
|
||||||
toolCallId: string;
|
toolCallId: string;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
@@ -2399,33 +2412,6 @@ const normalizeFastGPTInteractiveFields = (rawForm: unknown[]): DebugFastGPTInte
|
|||||||
return resolved;
|
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 ---
|
// --- Debug Drawer Component ---
|
||||||
export const DebugDrawer: React.FC<{
|
export const DebugDrawer: React.FC<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -2483,7 +2469,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [mode, setMode] = useState<'text' | 'voice' | 'video'>('text');
|
const [mode, setMode] = useState<'text' | 'voice' | 'video'>('text');
|
||||||
const [messages, setMessages] = useState<DebugTranscriptMessage[]>([]);
|
const [messages, setMessages] = useState<DebugTranscriptRow[]>([]);
|
||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [callStatus, setCallStatus] = useState<'idle' | 'calling' | 'active'>('idle');
|
const [callStatus, setCallStatus] = useState<'idle' | 'calling' | 'active'>('idle');
|
||||||
@@ -2581,8 +2567,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
const pendingResolveRef = useRef<(() => void) | null>(null);
|
const pendingResolveRef = useRef<(() => void) | null>(null);
|
||||||
const pendingRejectRef = useRef<((e: Error) => void) | null>(null);
|
const pendingRejectRef = useRef<((e: Error) => void) | null>(null);
|
||||||
const submittedMetadataRef = useRef<Record<string, any> | null>(null);
|
const submittedMetadataRef = useRef<Record<string, any> | null>(null);
|
||||||
const assistantDraftIndexRef = useRef<number | null>(null);
|
const assistantDraftRowIdRef = useRef<string | null>(null);
|
||||||
const assistantResponseIndexByIdRef = useRef<Map<string, number>>(new Map());
|
|
||||||
const pendingTtfbByResponseIdRef = useRef<Map<string, number>>(new Map());
|
const pendingTtfbByResponseIdRef = useRef<Map<string, number>>(new Map());
|
||||||
const interruptedResponseIdsRef = useRef<Set<string>>(new Set());
|
const interruptedResponseIdsRef = useRef<Set<string>>(new Set());
|
||||||
const interruptedDropNoticeKeysRef = 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 micProcessorRef = useRef<ScriptProcessorNode | null>(null);
|
||||||
const micGainRef = useRef<GainNode | null>(null);
|
const micGainRef = useRef<GainNode | null>(null);
|
||||||
const micFrameBufferRef = useRef<Uint8Array>(new Uint8Array(0));
|
const micFrameBufferRef = useRef<Uint8Array>(new Uint8Array(0));
|
||||||
const userDraftIndexRef = useRef<number | null>(null);
|
const userDraftRowIdRef = useRef<string | null>(null);
|
||||||
const lastUserFinalRef = useRef<string>('');
|
const lastUserFinalRef = useRef<string>('');
|
||||||
const debugVolumePercentRef = useRef<number>(50);
|
const debugVolumePercentRef = useRef<number>(50);
|
||||||
const clientToolEnabledMapRef = useRef<Record<string, boolean>>(clientToolEnabledMap);
|
const clientToolEnabledMapRef = useRef<Record<string, boolean>>(clientToolEnabledMap);
|
||||||
@@ -2647,8 +2632,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
}, [assistant.tools, tools, clientToolEnabledMap]);
|
}, [assistant.tools, tools, clientToolEnabledMap]);
|
||||||
|
|
||||||
const clearResponseTracking = () => {
|
const clearResponseTracking = () => {
|
||||||
assistantDraftIndexRef.current = null;
|
assistantDraftRowIdRef.current = null;
|
||||||
assistantResponseIndexByIdRef.current.clear();
|
|
||||||
pendingTtfbByResponseIdRef.current.clear();
|
pendingTtfbByResponseIdRef.current.clear();
|
||||||
interruptedResponseIdsRef.current.clear();
|
interruptedResponseIdsRef.current.clear();
|
||||||
interruptedDropNoticeKeysRef.current.clear();
|
interruptedDropNoticeKeysRef.current.clear();
|
||||||
@@ -2660,6 +2644,18 @@ export const DebugDrawer: React.FC<{
|
|||||||
return responseId || undefined;
|
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 noteInterruptedDrop = (responseId: string, kind: 'ttfb' | 'delta' | 'final') => {
|
||||||
const key = `${responseId}:${kind}`;
|
const key = `${responseId}:${kind}`;
|
||||||
if (interruptedDropNoticeKeysRef.current.has(key)) return;
|
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;
|
const oldest = interruptedDropNoticeKeysRef.current.values().next().value as string | undefined;
|
||||||
if (oldest) interruptedDropNoticeKeysRef.current.delete(oldest);
|
if (oldest) interruptedDropNoticeKeysRef.current.delete(oldest);
|
||||||
}
|
}
|
||||||
setMessages((prev) => [
|
setMessages((prev) =>
|
||||||
...prev,
|
appendNoticeRow(prev, `drop stale ${kind} from interrupted response ${responseId}`)
|
||||||
{
|
);
|
||||||
role: 'tool',
|
|
||||||
text: `drop stale ${kind} from interrupted response ${responseId}`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
@@ -2682,11 +2674,11 @@ export const DebugDrawer: React.FC<{
|
|||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
if (mode === 'text') {
|
if (mode === 'text') {
|
||||||
clearResponseTracking();
|
clearResponseTracking();
|
||||||
setMessages([]);
|
setMessages(resetTranscriptRows());
|
||||||
setTextSessionStarted(false);
|
setTextSessionStarted(false);
|
||||||
} else {
|
} else {
|
||||||
clearResponseTracking();
|
clearResponseTracking();
|
||||||
setMessages([]);
|
setMessages(resetTranscriptRows());
|
||||||
setCallStatus('idle');
|
setCallStatus('idle');
|
||||||
setAgentState('listening');
|
setAgentState('listening');
|
||||||
}
|
}
|
||||||
@@ -2967,17 +2959,18 @@ export const DebugDrawer: React.FC<{
|
|||||||
const statusCode = Number(resultPayload?.status?.code || 500);
|
const statusCode = Number(resultPayload?.status?.code || 500);
|
||||||
const statusMessage = String(resultPayload?.status?.message || 'error');
|
const statusMessage = String(resultPayload?.status?.message || 'error');
|
||||||
const displayName = toolDisplayName || String(resultPayload?.name || 'unknown_tool');
|
const displayName = toolDisplayName || String(resultPayload?.name || 'unknown_tool');
|
||||||
const resultText =
|
setMessages((prev) =>
|
||||||
statusCode === 200 && typeof resultPayload?.output?.result === 'number'
|
resolveToolResultRow(prev, {
|
||||||
? `result ${displayName} = ${resultPayload.output.result}`
|
toolCallId: String(resultPayload?.tool_call_id || '').trim(),
|
||||||
: `result ${displayName} status=${statusCode} ${statusMessage}`;
|
toolName: normalizeToolId(resultPayload?.name || 'unknown_tool'),
|
||||||
setMessages((prev) => [
|
toolDisplayName: displayName,
|
||||||
...prev,
|
source: 'client',
|
||||||
{
|
status: normalizeToolStatus(statusCode, statusMessage),
|
||||||
role: 'tool',
|
result: resultPayload?.output,
|
||||||
text: resultText,
|
error: statusCode >= 200 && statusCode < 300 ? undefined : resultPayload?.output ?? statusMessage,
|
||||||
},
|
rawResult: resultPayload,
|
||||||
]);
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopPromptVoicePlayback = () => {
|
const stopPromptVoicePlayback = () => {
|
||||||
@@ -3404,7 +3397,12 @@ export const DebugDrawer: React.FC<{
|
|||||||
setCallStatus('calling');
|
setCallStatus('calling');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCallStatus('active');
|
setCallStatus('active');
|
||||||
setMessages([{ role: 'model', text: assistant.opener || "Hello!" }]);
|
setMessages(
|
||||||
|
appendTextRow(resetTranscriptRows(), {
|
||||||
|
role: 'assistant',
|
||||||
|
text: assistant.opener || 'Hello!',
|
||||||
|
})
|
||||||
|
);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3412,7 +3410,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
try {
|
try {
|
||||||
setCallStatus('calling');
|
setCallStatus('calling');
|
||||||
clearResponseTracking();
|
clearResponseTracking();
|
||||||
setMessages([]);
|
setMessages(resetTranscriptRows());
|
||||||
lastUserFinalRef.current = '';
|
lastUserFinalRef.current = '';
|
||||||
setWsError('');
|
setWsError('');
|
||||||
setDynamicVariablesError('');
|
setDynamicVariablesError('');
|
||||||
@@ -3454,7 +3452,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
setCallStatus('idle');
|
setCallStatus('idle');
|
||||||
setAgentState('listening');
|
setAgentState('listening');
|
||||||
clearResponseTracking();
|
clearResponseTracking();
|
||||||
setMessages([]);
|
setMessages(resetTranscriptRows());
|
||||||
setTextPromptDialog({ open: false, message: '', promptType: 'text' });
|
setTextPromptDialog({ open: false, message: '', promptType: 'text' });
|
||||||
setChoicePromptDialog({ open: false, question: '', options: [] });
|
setChoicePromptDialog({ open: false, question: '', options: [] });
|
||||||
lastUserFinalRef.current = '';
|
lastUserFinalRef.current = '';
|
||||||
@@ -3464,8 +3462,14 @@ export const DebugDrawer: React.FC<{
|
|||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!inputText.trim()) return;
|
if (!inputText.trim()) return;
|
||||||
const userMsg = inputText;
|
const userMsg = inputText;
|
||||||
assistantDraftIndexRef.current = null;
|
assistantDraftRowIdRef.current = null;
|
||||||
setMessages(prev => [...prev, { role: 'user', text: userMsg }]);
|
setMessages((prev) =>
|
||||||
|
appendTextRow(prev, {
|
||||||
|
role: 'user',
|
||||||
|
text: userMsg,
|
||||||
|
isStreaming: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
setInputText('');
|
setInputText('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
@@ -3485,7 +3489,13 @@ export const DebugDrawer: React.FC<{
|
|||||||
wsRef.current?.send(JSON.stringify({ type: 'input.text', text: userMsg }));
|
wsRef.current?.send(JSON.stringify({ type: 'input.text', text: userMsg }));
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
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);
|
setIsLoading(false);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@@ -3497,7 +3507,13 @@ export const DebugDrawer: React.FC<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const errMessage = err?.message || 'Failed to connect to AI service.';
|
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);
|
setWsError(errMessage);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -3511,7 +3527,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
setDynamicVariablesError('');
|
setDynamicVariablesError('');
|
||||||
// Start every text debug run as a fresh session transcript.
|
// Start every text debug run as a fresh session transcript.
|
||||||
clearResponseTracking();
|
clearResponseTracking();
|
||||||
setMessages([]);
|
setMessages(resetTranscriptRows());
|
||||||
lastUserFinalRef.current = '';
|
lastUserFinalRef.current = '';
|
||||||
// Force a fresh WS session so updated assistant runtime config
|
// Force a fresh WS session so updated assistant runtime config
|
||||||
// (voice/model/provider/speed) is applied on session.start.
|
// (voice/model/provider/speed) is applied on session.start.
|
||||||
@@ -3809,7 +3825,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
pendingResolveRef.current = null;
|
pendingResolveRef.current = null;
|
||||||
pendingRejectRef.current = null;
|
pendingRejectRef.current = null;
|
||||||
clearResponseTracking();
|
clearResponseTracking();
|
||||||
userDraftIndexRef.current = null;
|
userDraftRowIdRef.current = null;
|
||||||
lastUserFinalRef.current = '';
|
lastUserFinalRef.current = '';
|
||||||
micFrameBufferRef.current = new Uint8Array(0);
|
micFrameBufferRef.current = new Uint8Array(0);
|
||||||
stopPromptVoicePlayback();
|
stopPromptVoicePlayback();
|
||||||
@@ -3916,8 +3932,10 @@ export const DebugDrawer: React.FC<{
|
|||||||
const oldest = interruptedResponseIdsRef.current.values().next().value as string | undefined;
|
const oldest = interruptedResponseIdsRef.current.values().next().value as string | undefined;
|
||||||
if (oldest) interruptedResponseIdsRef.current.delete(oldest);
|
if (oldest) interruptedResponseIdsRef.current.delete(oldest);
|
||||||
}
|
}
|
||||||
|
pendingTtfbByResponseIdRef.current.delete(interruptedResponseId);
|
||||||
|
setMessages((prev) => trimInterruptedResponseRows(prev, interruptedResponseId));
|
||||||
}
|
}
|
||||||
assistantDraftIndexRef.current = null;
|
assistantDraftRowIdRef.current = null;
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
stopPlaybackImmediately();
|
stopPlaybackImmediately();
|
||||||
setAgentState('waiting');
|
setAgentState('waiting');
|
||||||
@@ -3934,29 +3952,16 @@ export const DebugDrawer: React.FC<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (responseId) {
|
if (responseId) {
|
||||||
const indexed = assistantResponseIndexByIdRef.current.get(responseId);
|
setMessages((prev) => {
|
||||||
if (typeof indexed === 'number') {
|
const nextRows = attachAssistantTtfb(prev, { responseId, ttfbMs });
|
||||||
setMessages((prev) => {
|
if (nextRows === prev) {
|
||||||
if (!prev[indexed] || prev[indexed].role !== 'model') return prev;
|
pendingTtfbByResponseIdRef.current.set(responseId, ttfbMs);
|
||||||
const next = [...prev];
|
}
|
||||||
next[indexed] = { ...next[indexed], ttfbMs };
|
return nextRows;
|
||||||
return next;
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
pendingTtfbByResponseIdRef.current.set(responseId, ttfbMs);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages((prev) => {
|
setMessages((prev) => attachAssistantTtfb(prev, { ttfbMs }));
|
||||||
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;
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3967,24 +3972,35 @@ export const DebugDrawer: React.FC<{
|
|||||||
const toolDisplayName = String(payload?.tool_display_name || toolCall?.displayName || toolName);
|
const toolDisplayName = String(payload?.tool_display_name || toolCall?.displayName || toolName);
|
||||||
const executor = String(toolCall?.executor || 'server').toLowerCase();
|
const executor = String(toolCall?.executor || 'server').toLowerCase();
|
||||||
const rawArgs = String(toolCall?.function?.arguments || '');
|
const rawArgs = String(toolCall?.function?.arguments || '');
|
||||||
const argText = rawArgs.length > 160 ? `${rawArgs.slice(0, 160)}...` : rawArgs;
|
const turnId = extractTurnId(payload);
|
||||||
setMessages((prev) => [
|
const utteranceId = extractUtteranceId(payload);
|
||||||
...prev,
|
const responseId = extractResponseId(payload);
|
||||||
{
|
let parsedArgsValue: unknown = rawArgs || undefined;
|
||||||
role: 'tool',
|
if (rawArgs) {
|
||||||
text: `call ${toolDisplayName} executor=${executor}${argText ? ` args=${argText}` : ''}`,
|
try {
|
||||||
},
|
parsedArgsValue = JSON.parse(rawArgs);
|
||||||
]);
|
} catch {
|
||||||
if (executor === 'client' && toolCallId && ws.readyState === WebSocket.OPEN) {
|
parsedArgsValue = rawArgs;
|
||||||
let parsedArgs: Record<string, any> = {};
|
|
||||||
if (rawArgs) {
|
|
||||||
try {
|
|
||||||
const candidate = JSON.parse(rawArgs);
|
|
||||||
parsedArgs = candidate && typeof candidate === 'object' ? candidate : {};
|
|
||||||
} catch {
|
|
||||||
parsedArgs = {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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(
|
const waitForResponseRaw = Boolean(
|
||||||
payload?.wait_for_response ?? toolCall?.wait_for_response ?? toolCall?.waitForResponse ?? false
|
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') {
|
if (type === 'assistant.tool_result') {
|
||||||
const result = payload?.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 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 statusCode = Number(result?.status?.code || 500);
|
||||||
const statusMessage = String(result?.status?.message || 'error');
|
const statusMessage = String(result?.status?.message || 'error');
|
||||||
const source = String(payload?.source || 'server');
|
const source = String(payload?.source || 'server');
|
||||||
const output = result?.output;
|
const turnId = extractTurnId(payload);
|
||||||
const resultText =
|
const utteranceId = extractUtteranceId(payload);
|
||||||
statusCode === 200
|
setMessages((prev) =>
|
||||||
? `result ${toolDisplayName} source=${source} ${JSON.stringify(output)}`
|
resolveToolResultRow(prev, {
|
||||||
: `result ${toolDisplayName} source=${source} status=${statusCode} ${statusMessage}`;
|
toolCallId,
|
||||||
setMessages((prev) => [...prev, { role: 'tool', text: resultText }]);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4304,17 +4332,17 @@ export const DebugDrawer: React.FC<{
|
|||||||
if (type === 'transcript.delta') {
|
if (type === 'transcript.delta') {
|
||||||
const delta = String(payload.text || '');
|
const delta = String(payload.text || '');
|
||||||
if (!delta) return;
|
if (!delta) return;
|
||||||
|
const turnId = extractTurnId(payload);
|
||||||
|
const utteranceId = extractUtteranceId(payload);
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const idx = userDraftIndexRef.current;
|
const nextState = updateUserDraftRow(prev, {
|
||||||
if (idx === null || !prev[idx] || prev[idx].role !== 'user') {
|
draftRowId: userDraftRowIdRef.current,
|
||||||
const next = [...prev, { role: 'user' as const, text: delta }];
|
text: delta,
|
||||||
userDraftIndexRef.current = next.length - 1;
|
turnId,
|
||||||
return next;
|
utteranceId,
|
||||||
}
|
});
|
||||||
const next = [...prev];
|
userDraftRowIdRef.current = nextState.draftRowId;
|
||||||
// ASR interim is typically the latest partial text, not a true text delta.
|
return nextState.rows;
|
||||||
next[idx] = { ...next[idx], text: delta };
|
|
||||||
return next;
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4322,31 +4350,25 @@ export const DebugDrawer: React.FC<{
|
|||||||
if (type === 'transcript.final') {
|
if (type === 'transcript.final') {
|
||||||
const finalText = String(payload.text || '');
|
const finalText = String(payload.text || '');
|
||||||
if (!finalText) {
|
if (!finalText) {
|
||||||
userDraftIndexRef.current = null;
|
userDraftRowIdRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (lastUserFinalRef.current === finalText) {
|
if (lastUserFinalRef.current === finalText) {
|
||||||
userDraftIndexRef.current = null;
|
userDraftRowIdRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const turnId = extractTurnId(payload);
|
||||||
|
const utteranceId = extractUtteranceId(payload);
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const idx = userDraftIndexRef.current;
|
const nextState = finalizeUserDraftRow(prev, {
|
||||||
userDraftIndexRef.current = null;
|
draftRowId: userDraftRowIdRef.current,
|
||||||
if (idx !== null && prev[idx] && prev[idx].role === 'user') {
|
text: finalText,
|
||||||
const next = [...prev];
|
turnId,
|
||||||
next[idx] = { ...next[idx], text: finalText || next[idx].text };
|
utteranceId,
|
||||||
lastUserFinalRef.current = finalText;
|
});
|
||||||
return next;
|
userDraftRowIdRef.current = nextState.draftRowId;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
lastUserFinalRef.current = finalText;
|
lastUserFinalRef.current = finalText;
|
||||||
return [...prev, { role: 'user', text: finalText }];
|
return nextState.rows;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4354,143 +4376,56 @@ export const DebugDrawer: React.FC<{
|
|||||||
if (type === 'assistant.response.delta') {
|
if (type === 'assistant.response.delta') {
|
||||||
const delta = String(payload.text || '');
|
const delta = String(payload.text || '');
|
||||||
if (!delta) return;
|
if (!delta) return;
|
||||||
|
const turnId = extractTurnId(payload);
|
||||||
|
const utteranceId = extractUtteranceId(payload);
|
||||||
const responseId = extractResponseId(payload);
|
const responseId = extractResponseId(payload);
|
||||||
if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
|
if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
|
||||||
noteInterruptedDrop(responseId, 'delta');
|
noteInterruptedDrop(responseId, 'delta');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
let idx = assistantDraftIndexRef.current;
|
const pendingTtfb = responseId ? pendingTtfbByResponseIdRef.current.get(responseId) : undefined;
|
||||||
if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
|
const nextState = updateAssistantDeltaRow(prev, {
|
||||||
// Tool records can be appended between assistant chunks; recover the
|
draftRowId: assistantDraftRowIdRef.current,
|
||||||
// latest model row instead of creating a duplicate assistant row.
|
delta,
|
||||||
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
turnId,
|
||||||
if (prev[i]?.role === 'model') {
|
utteranceId,
|
||||||
if (
|
responseId,
|
||||||
responseId
|
ttfbMs: pendingTtfb,
|
||||||
&& prev[i].responseId
|
});
|
||||||
&& prev[i].responseId !== responseId
|
assistantDraftRowIdRef.current = nextState.draftRowId;
|
||||||
) {
|
if (responseId && typeof pendingTtfb === 'number') {
|
||||||
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);
|
|
||||||
pendingTtfbByResponseIdRef.current.delete(responseId);
|
pendingTtfbByResponseIdRef.current.delete(responseId);
|
||||||
}
|
}
|
||||||
next[idx] = nextMessage;
|
return nextState.rows;
|
||||||
if (responseId) {
|
|
||||||
assistantResponseIndexByIdRef.current.set(responseId, idx);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'assistant.response.final') {
|
if (type === 'assistant.response.final') {
|
||||||
const finalText = String(payload.text || '');
|
const finalText = String(payload.text || '');
|
||||||
|
const turnId = extractTurnId(payload);
|
||||||
|
const utteranceId = extractUtteranceId(payload);
|
||||||
const responseId = extractResponseId(payload);
|
const responseId = extractResponseId(payload);
|
||||||
if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
|
if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
|
||||||
noteInterruptedDrop(responseId, 'final');
|
noteInterruptedDrop(responseId, 'final');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
let idx = assistantDraftIndexRef.current;
|
const pendingTtfb = responseId ? pendingTtfbByResponseIdRef.current.get(responseId) : undefined;
|
||||||
assistantDraftIndexRef.current = null;
|
const nextState = finalizeAssistantTextRow(prev, {
|
||||||
if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
|
draftRowId: assistantDraftRowIdRef.current,
|
||||||
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
text: finalText,
|
||||||
if (prev[i]?.role === 'model') {
|
turnId,
|
||||||
if (
|
utteranceId,
|
||||||
responseId
|
responseId,
|
||||||
&& prev[i].responseId
|
ttfbMs: pendingTtfb,
|
||||||
&& prev[i].responseId !== responseId
|
});
|
||||||
) {
|
assistantDraftRowIdRef.current = nextState.draftRowId;
|
||||||
break;
|
if (responseId && typeof pendingTtfb === 'number') {
|
||||||
}
|
pendingTtfbByResponseIdRef.current.delete(responseId);
|
||||||
idx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (prev[i]?.role === 'user') break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (idx !== null && prev[idx] && prev[idx].role === 'model') {
|
return nextState.rows;
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -4521,7 +4456,8 @@ export const DebugDrawer: React.FC<{
|
|||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
wsReadyRef.current = false;
|
wsReadyRef.current = false;
|
||||||
setTextSessionStarted(false);
|
setTextSessionStarted(false);
|
||||||
userDraftIndexRef.current = null;
|
userDraftRowIdRef.current = null;
|
||||||
|
assistantDraftRowIdRef.current = null;
|
||||||
stopPlaybackImmediately();
|
stopPlaybackImmediately();
|
||||||
if (wsStatusRef.current !== 'error') setWsStatus('disconnected');
|
if (wsStatusRef.current !== 'error') setWsStatus('disconnected');
|
||||||
};
|
};
|
||||||
@@ -5016,7 +4952,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
<p className="text-xs">暂无对话记录</p>
|
<p className="text-xs">暂无对话记录</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="pb-4" />
|
<TranscriptList scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="pb-4" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user