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.
This commit is contained in:
@@ -1643,6 +1643,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
const [captureConfigView, setCaptureConfigView] = useState<string>('');
|
const [captureConfigView, setCaptureConfigView] = useState<string>('');
|
||||||
const [settingsDrawerOpen, setSettingsDrawerOpen] = useState(false);
|
const [settingsDrawerOpen, setSettingsDrawerOpen] = useState(false);
|
||||||
const [dynamicVariables, setDynamicVariables] = useState<DynamicVariableEntry[]>([]);
|
const [dynamicVariables, setDynamicVariables] = useState<DynamicVariableEntry[]>([]);
|
||||||
|
const [dynamicVariablesError, setDynamicVariablesError] = useState('');
|
||||||
const dynamicVariableSeqRef = useRef(0);
|
const dynamicVariableSeqRef = useRef(0);
|
||||||
const [wsUrl, setWsUrl] = useState<string>(() => {
|
const [wsUrl, setWsUrl] = useState<string>(() => {
|
||||||
const fromStorage = localStorage.getItem('debug_ws_url');
|
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));
|
extractDynamicTemplateKeys(String(assistant.opener || '')).forEach((key) => keys.add(key));
|
||||||
return Array.from(keys).sort();
|
return Array.from(keys).sort();
|
||||||
}, [assistant.opener, assistant.prompt]);
|
}, [assistant.opener, assistant.prompt]);
|
||||||
|
const missingRequiredDynamicVariableKeys = useMemo(() => {
|
||||||
|
const valuesByKey = new Map<string, string>();
|
||||||
|
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
|
// Media State
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
@@ -1758,6 +1771,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dynamicVariableSeqRef.current = 0;
|
dynamicVariableSeqRef.current = 0;
|
||||||
setDynamicVariables([]);
|
setDynamicVariables([]);
|
||||||
|
setDynamicVariablesError('');
|
||||||
}, [assistant.id, isOpen]);
|
}, [assistant.id, isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2059,6 +2073,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
setMessages([]);
|
setMessages([]);
|
||||||
lastUserFinalRef.current = '';
|
lastUserFinalRef.current = '';
|
||||||
setWsError('');
|
setWsError('');
|
||||||
|
setDynamicVariablesError('');
|
||||||
closeWs();
|
closeWs();
|
||||||
if (textTtsEnabled) await ensureAudioContext();
|
if (textTtsEnabled) await ensureAudioContext();
|
||||||
await ensureWsSession();
|
await ensureWsSession();
|
||||||
@@ -2068,8 +2083,13 @@ export const DebugDrawer: React.FC<{
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
stopVoiceCapture();
|
stopVoiceCapture();
|
||||||
setCallStatus('idle');
|
setCallStatus('idle');
|
||||||
|
const err = e as Error & { __dynamicVariables?: boolean };
|
||||||
|
if (err.__dynamicVariables) {
|
||||||
|
setWsStatus('disconnected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setWsStatus('error');
|
setWsStatus('error');
|
||||||
setWsError((e as Error)?.message || 'Failed to start voice call');
|
setWsError(err?.message || 'Failed to start voice call');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
void launchVoice();
|
void launchVoice();
|
||||||
@@ -2094,6 +2114,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setDynamicVariablesError('');
|
||||||
if (mode === 'text') {
|
if (mode === 'text') {
|
||||||
if (textTtsEnabled) await ensureAudioContext();
|
if (textTtsEnabled) await ensureAudioContext();
|
||||||
await ensureWsSession();
|
await ensureWsSession();
|
||||||
@@ -2114,7 +2135,12 @@ export const DebugDrawer: React.FC<{
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(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}` }]);
|
setMessages(prev => [...prev, { role: 'model', text: `Error: ${errMessage}` }]);
|
||||||
setWsError(errMessage);
|
setWsError(errMessage);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -2126,6 +2152,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
const handleTextLaunch = async () => {
|
const handleTextLaunch = async () => {
|
||||||
try {
|
try {
|
||||||
setWsError('');
|
setWsError('');
|
||||||
|
setDynamicVariablesError('');
|
||||||
// Start every text debug run as a fresh session transcript.
|
// Start every text debug run as a fresh session transcript.
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
lastUserFinalRef.current = '';
|
lastUserFinalRef.current = '';
|
||||||
@@ -2138,14 +2165,21 @@ export const DebugDrawer: React.FC<{
|
|||||||
setTextSessionStarted(true);
|
setTextSessionStarted(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
const err = e as Error & { __dynamicVariables?: boolean };
|
||||||
|
if (err.__dynamicVariables) {
|
||||||
|
setWsStatus('disconnected');
|
||||||
|
setTextSessionStarted(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setWsStatus('error');
|
setWsStatus('error');
|
||||||
setWsError((e as Error)?.message || 'Failed to connect');
|
setWsError(err?.message || 'Failed to connect');
|
||||||
setTextSessionStarted(false);
|
setTextSessionStarted(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addDynamicVariableRow = () => {
|
const addDynamicVariableRow = () => {
|
||||||
if (isDynamicVariablesLocked) return;
|
if (isDynamicVariablesLocked) return;
|
||||||
|
setDynamicVariablesError('');
|
||||||
setDynamicVariables((prev) => {
|
setDynamicVariables((prev) => {
|
||||||
if (prev.length >= DYNAMIC_VARIABLE_MAX_ITEMS) return prev;
|
if (prev.length >= DYNAMIC_VARIABLE_MAX_ITEMS) return prev;
|
||||||
return [...prev, { id: nextDynamicVariableId(), key: '', value: '' }];
|
return [...prev, { id: nextDynamicVariableId(), key: '', value: '' }];
|
||||||
@@ -2154,14 +2188,49 @@ export const DebugDrawer: React.FC<{
|
|||||||
|
|
||||||
const updateDynamicVariableRow = (rowId: string, field: 'key' | 'value', value: string) => {
|
const updateDynamicVariableRow = (rowId: string, field: 'key' | 'value', value: string) => {
|
||||||
if (isDynamicVariablesLocked) return;
|
if (isDynamicVariablesLocked) return;
|
||||||
|
setDynamicVariablesError('');
|
||||||
setDynamicVariables((prev) => prev.map((item) => (item.id === rowId ? { ...item, [field]: value } : item)));
|
setDynamicVariables((prev) => prev.map((item) => (item.id === rowId ? { ...item, [field]: value } : item)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeDynamicVariableRow = (rowId: string) => {
|
const removeDynamicVariableRow = (rowId: string) => {
|
||||||
if (isDynamicVariablesLocked) return;
|
if (isDynamicVariablesLocked) return;
|
||||||
|
setDynamicVariablesError('');
|
||||||
setDynamicVariables((prev) => prev.filter((item) => item.id !== rowId));
|
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<string, string>; error?: string } => {
|
const buildDynamicVariablesPayload = (): { variables: Record<string, string>; error?: string } => {
|
||||||
const variables: Record<string, string> = {};
|
const variables: Record<string, string> = {};
|
||||||
const nonEmptyRows = dynamicVariables
|
const nonEmptyRows = dynamicVariables
|
||||||
@@ -2329,8 +2398,10 @@ export const DebugDrawer: React.FC<{
|
|||||||
const fetchRuntimeMetadata = async (): Promise<Record<string, any>> => {
|
const fetchRuntimeMetadata = async (): Promise<Record<string, any>> => {
|
||||||
const dynamicVariablesResult = buildDynamicVariablesPayload();
|
const dynamicVariablesResult = buildDynamicVariablesPayload();
|
||||||
if (dynamicVariablesResult.error) {
|
if (dynamicVariablesResult.error) {
|
||||||
throw new Error(dynamicVariablesResult.error);
|
setDynamicVariablesError(dynamicVariablesResult.error);
|
||||||
|
throw createDynamicVariablesError(dynamicVariablesResult.error);
|
||||||
}
|
}
|
||||||
|
setDynamicVariablesError('');
|
||||||
const localResolved = buildLocalResolvedRuntime();
|
const localResolved = buildLocalResolvedRuntime();
|
||||||
const mergedMetadata: Record<string, any> = {
|
const mergedMetadata: Record<string, any> = {
|
||||||
...localResolved.sessionStartMetadata,
|
...localResolved.sessionStartMetadata,
|
||||||
@@ -2788,16 +2859,28 @@ export const DebugDrawer: React.FC<{
|
|||||||
<div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
|
<div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground">Dynamic Variables</p>
|
<p className="text-[10px] uppercase tracking-widest text-muted-foreground">Dynamic Variables</p>
|
||||||
<Button
|
<div className="flex items-center gap-1.5">
|
||||||
size="icon"
|
<Button
|
||||||
variant="ghost"
|
size="sm"
|
||||||
className="h-6 w-6"
|
variant="ghost"
|
||||||
onClick={addDynamicVariableRow}
|
className="h-6 px-2 text-[10px]"
|
||||||
disabled={isDynamicVariablesLocked || dynamicVariables.length >= DYNAMIC_VARIABLE_MAX_ITEMS}
|
onClick={importDynamicVariablesFromPlaceholders}
|
||||||
title={isDynamicVariablesLocked ? 'Disable editing while session is active' : 'Add variable'}
|
disabled={isDynamicVariablesLocked || requiredTemplateVariableKeys.length === 0 || dynamicVariables.length >= DYNAMIC_VARIABLE_MAX_ITEMS}
|
||||||
>
|
title="Import keys from {{placeholder}} in prompt/opener"
|
||||||
<Plus className="h-3.5 w-3.5" />
|
>
|
||||||
</Button>
|
Import
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={addDynamicVariableRow}
|
||||||
|
disabled={isDynamicVariablesLocked || dynamicVariables.length >= DYNAMIC_VARIABLE_MAX_ITEMS}
|
||||||
|
title={isDynamicVariablesLocked ? 'Disable editing while session is active' : 'Add variable'}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
Use placeholders like {'{{customer_name}}'} in prompt/opener.
|
Use placeholders like {'{{customer_name}}'} in prompt/opener.
|
||||||
@@ -2807,6 +2890,16 @@ export const DebugDrawer: React.FC<{
|
|||||||
Required: {requiredTemplateVariableKeys.join(', ')}
|
Required: {requiredTemplateVariableKeys.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{missingRequiredDynamicVariableKeys.length > 0 && (
|
||||||
|
<p className="text-[11px] text-red-300/90">
|
||||||
|
Missing required dynamic variable: {missingRequiredDynamicVariableKeys.join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{dynamicVariablesError && (
|
||||||
|
<p className="text-[11px] text-red-300/90">
|
||||||
|
{dynamicVariablesError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{dynamicVariables.length === 0 ? (
|
{dynamicVariables.length === 0 ? (
|
||||||
<div className="text-[11px] text-muted-foreground/80 border border-dashed border-white/10 rounded-md px-2 py-2">
|
<div className="text-[11px] text-muted-foreground/80 border border-dashed border-white/10 rounded-md px-2 py-2">
|
||||||
No variables added.
|
No variables added.
|
||||||
|
|||||||
Reference in New Issue
Block a user