From 6a9b5fcff4afd761da8debf8b7b3c14a21ba8d6f Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 27 Feb 2026 11:39:53 +0800 Subject: [PATCH] Add error handling for dynamic variables in DebugDrawer component. Introduce state for dynamic variable errors and implement validation logic to manage required keys. Update methods to reset error state and handle errors during dynamic variable operations, enhancing user feedback and session management. --- web/pages/Assistants.tsx | 121 ++++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 14 deletions(-) diff --git a/web/pages/Assistants.tsx b/web/pages/Assistants.tsx index dac4bf7..e8af420 100644 --- a/web/pages/Assistants.tsx +++ b/web/pages/Assistants.tsx @@ -1643,6 +1643,7 @@ export const DebugDrawer: React.FC<{ const [captureConfigView, setCaptureConfigView] = useState(''); const [settingsDrawerOpen, setSettingsDrawerOpen] = useState(false); const [dynamicVariables, setDynamicVariables] = useState([]); + const [dynamicVariablesError, setDynamicVariablesError] = useState(''); const dynamicVariableSeqRef = useRef(0); const [wsUrl, setWsUrl] = useState(() => { const fromStorage = localStorage.getItem('debug_ws_url'); @@ -1666,6 +1667,18 @@ export const DebugDrawer: React.FC<{ extractDynamicTemplateKeys(String(assistant.opener || '')).forEach((key) => keys.add(key)); return Array.from(keys).sort(); }, [assistant.opener, assistant.prompt]); + const missingRequiredDynamicVariableKeys = useMemo(() => { + const valuesByKey = new Map(); + for (const row of dynamicVariables) { + const key = row.key.trim(); + if (!key || valuesByKey.has(key)) continue; + valuesByKey.set(key, row.value); + } + return requiredTemplateVariableKeys.filter((key) => { + if (!valuesByKey.has(key)) return true; + return valuesByKey.get(key) === ''; + }); + }, [dynamicVariables, requiredTemplateVariableKeys]); // Media State const videoRef = useRef(null); @@ -1758,6 +1771,7 @@ export const DebugDrawer: React.FC<{ useEffect(() => { dynamicVariableSeqRef.current = 0; setDynamicVariables([]); + setDynamicVariablesError(''); }, [assistant.id, isOpen]); useEffect(() => { @@ -2059,6 +2073,7 @@ export const DebugDrawer: React.FC<{ setMessages([]); lastUserFinalRef.current = ''; setWsError(''); + setDynamicVariablesError(''); closeWs(); if (textTtsEnabled) await ensureAudioContext(); await ensureWsSession(); @@ -2068,8 +2083,13 @@ export const DebugDrawer: React.FC<{ console.error(e); stopVoiceCapture(); setCallStatus('idle'); + const err = e as Error & { __dynamicVariables?: boolean }; + if (err.__dynamicVariables) { + setWsStatus('disconnected'); + return; + } setWsStatus('error'); - setWsError((e as Error)?.message || 'Failed to start voice call'); + setWsError(err?.message || 'Failed to start voice call'); } }; void launchVoice(); @@ -2094,6 +2114,7 @@ export const DebugDrawer: React.FC<{ setIsLoading(true); try { + setDynamicVariablesError(''); if (mode === 'text') { if (textTtsEnabled) await ensureAudioContext(); await ensureWsSession(); @@ -2114,7 +2135,12 @@ export const DebugDrawer: React.FC<{ } } catch (e) { console.error(e); - const errMessage = (e as Error)?.message || 'Failed to connect to AI service.'; + const err = e as Error & { __dynamicVariables?: boolean }; + if (err.__dynamicVariables) { + setIsLoading(false); + return; + } + const errMessage = err?.message || 'Failed to connect to AI service.'; setMessages(prev => [...prev, { role: 'model', text: `Error: ${errMessage}` }]); setWsError(errMessage); setIsLoading(false); @@ -2126,6 +2152,7 @@ export const DebugDrawer: React.FC<{ const handleTextLaunch = async () => { try { setWsError(''); + setDynamicVariablesError(''); // Start every text debug run as a fresh session transcript. setMessages([]); lastUserFinalRef.current = ''; @@ -2138,14 +2165,21 @@ export const DebugDrawer: React.FC<{ setTextSessionStarted(true); } catch (e) { console.error(e); + const err = e as Error & { __dynamicVariables?: boolean }; + if (err.__dynamicVariables) { + setWsStatus('disconnected'); + setTextSessionStarted(false); + return; + } setWsStatus('error'); - setWsError((e as Error)?.message || 'Failed to connect'); + setWsError(err?.message || 'Failed to connect'); setTextSessionStarted(false); } }; const addDynamicVariableRow = () => { if (isDynamicVariablesLocked) return; + setDynamicVariablesError(''); setDynamicVariables((prev) => { if (prev.length >= DYNAMIC_VARIABLE_MAX_ITEMS) return prev; return [...prev, { id: nextDynamicVariableId(), key: '', value: '' }]; @@ -2154,14 +2188,49 @@ export const DebugDrawer: React.FC<{ const updateDynamicVariableRow = (rowId: string, field: 'key' | 'value', value: string) => { if (isDynamicVariablesLocked) return; + setDynamicVariablesError(''); setDynamicVariables((prev) => prev.map((item) => (item.id === rowId ? { ...item, [field]: value } : item))); }; const removeDynamicVariableRow = (rowId: string) => { if (isDynamicVariablesLocked) return; + setDynamicVariablesError(''); setDynamicVariables((prev) => prev.filter((item) => item.id !== rowId)); }; + const importDynamicVariablesFromPlaceholders = () => { + if (isDynamicVariablesLocked) return; + setDynamicVariablesError(''); + setDynamicVariables((prev) => { + if (!requiredTemplateVariableKeys.length || prev.length >= DYNAMIC_VARIABLE_MAX_ITEMS) { + return prev; + } + const existingKeys = new Set( + prev + .map((item) => item.key.trim()) + .filter((key) => key !== '') + ); + const next = [...prev]; + for (const key of requiredTemplateVariableKeys) { + if (next.length >= DYNAMIC_VARIABLE_MAX_ITEMS) break; + if (existingKeys.has(key)) continue; + next.push({ + id: nextDynamicVariableId(), + key, + value: '', + }); + existingKeys.add(key); + } + return next; + }); + }; + + const createDynamicVariablesError = (message: string): Error & { __dynamicVariables?: boolean } => { + const error = new Error(message) as Error & { __dynamicVariables?: boolean }; + error.__dynamicVariables = true; + return error; + }; + const buildDynamicVariablesPayload = (): { variables: Record; error?: string } => { const variables: Record = {}; const nonEmptyRows = dynamicVariables @@ -2329,8 +2398,10 @@ export const DebugDrawer: React.FC<{ const fetchRuntimeMetadata = async (): Promise> => { const dynamicVariablesResult = buildDynamicVariablesPayload(); if (dynamicVariablesResult.error) { - throw new Error(dynamicVariablesResult.error); + setDynamicVariablesError(dynamicVariablesResult.error); + throw createDynamicVariablesError(dynamicVariablesResult.error); } + setDynamicVariablesError(''); const localResolved = buildLocalResolvedRuntime(); const mergedMetadata: Record = { ...localResolved.sessionStartMetadata, @@ -2788,16 +2859,28 @@ export const DebugDrawer: React.FC<{

Dynamic Variables

- +
+ + +

Use placeholders like {'{{customer_name}}'} in prompt/opener. @@ -2807,6 +2890,16 @@ export const DebugDrawer: React.FC<{ Required: {requiredTemplateVariableKeys.join(', ')}

)} + {missingRequiredDynamicVariableKeys.length > 0 && ( +

+ Missing required dynamic variable: {missingRequiredDynamicVariableKeys.join(', ')} +

+ )} + {dynamicVariablesError && ( +

+ {dynamicVariablesError} +

+ )} {dynamicVariables.length === 0 ? (
No variables added.