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.