webapp/components/session-configuration-panel.tsx (265 lines of code) (raw):

import React, { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Plus, Edit, Trash, Check, AlertCircle } from "lucide-react"; import { toolTemplates } from "@/lib/tool-templates"; import { ToolConfigurationDialog } from "./tool-configuration-dialog"; import { BackendTag } from "./backend-tag"; import { useBackendTools } from "@/lib/use-backend-tools"; interface SessionConfigurationPanelProps { callStatus: string; onSave: (config: any) => void; } const SessionConfigurationPanel: React.FC<SessionConfigurationPanelProps> = ({ callStatus, onSave, }) => { const [instructions, setInstructions] = useState( "You are a helpful assistant in a phone call." ); const [voice, setVoice] = useState("ash"); const [tools, setTools] = useState<string[]>([]); const [editingIndex, setEditingIndex] = useState<number | null>(null); const [editingSchemaStr, setEditingSchemaStr] = useState(""); const [isJsonValid, setIsJsonValid] = useState(true); const [openDialog, setOpenDialog] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(""); const [saveStatus, setSaveStatus] = useState< "idle" | "saving" | "saved" | "error" >("idle"); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // Custom hook to fetch backend tools every 3 seconds const backendTools = useBackendTools("http://localhost:8081/tools", 3000); // Track changes to determine if there are unsaved modifications useEffect(() => { setHasUnsavedChanges(true); }, [instructions, voice, tools]); // Reset save status after a delay when saved useEffect(() => { if (saveStatus === "saved") { const timer = setTimeout(() => { setSaveStatus("idle"); }, 3000); return () => clearTimeout(timer); } }, [saveStatus]); const handleSave = async () => { setSaveStatus("saving"); try { await onSave({ instructions, voice, tools: tools.map((tool) => JSON.parse(tool)), }); setSaveStatus("saved"); setHasUnsavedChanges(false); } catch (error) { setSaveStatus("error"); } }; const handleAddTool = () => { setEditingIndex(null); setEditingSchemaStr(""); setSelectedTemplate(""); setIsJsonValid(true); setOpenDialog(true); }; const handleEditTool = (index: number) => { setEditingIndex(index); setEditingSchemaStr(tools[index] || ""); setSelectedTemplate(""); setIsJsonValid(true); setOpenDialog(true); }; const handleDeleteTool = (index: number) => { const newTools = [...tools]; newTools.splice(index, 1); setTools(newTools); }; const handleDialogSave = () => { try { JSON.parse(editingSchemaStr); } catch { return; } const newTools = [...tools]; if (editingIndex === null) { newTools.push(editingSchemaStr); } else { newTools[editingIndex] = editingSchemaStr; } setTools(newTools); setOpenDialog(false); }; const handleTemplateChange = (val: string) => { setSelectedTemplate(val); // Determine if the selected template is from local or backend let templateObj = toolTemplates.find((t) => t.name === val) || backendTools.find((t: any) => t.name === val); if (templateObj) { setEditingSchemaStr(JSON.stringify(templateObj, null, 2)); setIsJsonValid(true); } }; const onSchemaChange = (value: string) => { setEditingSchemaStr(value); try { JSON.parse(value); setIsJsonValid(true); } catch { setIsJsonValid(false); } }; const getToolNameFromSchema = (schema: string): string => { try { const parsed = JSON.parse(schema); return parsed?.name || "Untitled Tool"; } catch { return "Invalid JSON"; } }; const isBackendTool = (name: string): boolean => { return backendTools.some((t: any) => t.name === name); }; return ( <Card className="flex flex-col h-full w-full mx-auto"> <CardHeader className="pb-0 px-4 sm:px-6"> <div className="flex items-center justify-between"> <CardTitle className="text-base font-semibold"> Session Configuration </CardTitle> <div className="flex items-center gap-2"> {saveStatus === "error" ? ( <span className="text-xs text-red-500 flex items-center gap-1"> <AlertCircle className="h-3 w-3" /> Save failed </span> ) : hasUnsavedChanges ? ( <span className="text-xs text-muted-foreground">Not saved</span> ) : ( <span className="text-xs text-muted-foreground flex items-center gap-1"> <Check className="h-3 w-3" /> Saved </span> )} </div> </div> </CardHeader> <CardContent className="flex-1 p-3 sm:p-5"> <ScrollArea className="h-full"> <div className="space-y-4 sm:space-y-6 m-1"> <div className="space-y-2"> <label className="text-sm font-medium leading-none"> Instructions </label> <Textarea placeholder="Enter instructions" className="min-h-[100px] resize-none" value={instructions} onChange={(e) => setInstructions(e.target.value)} /> </div> <div className="space-y-2"> <label className="text-sm font-medium leading-none">Voice</label> <Select value={voice} onValueChange={setVoice}> <SelectTrigger className="w-full"> <SelectValue placeholder="Select voice" /> </SelectTrigger> <SelectContent> {["ash", "ballad", "coral", "sage", "verse"].map((v) => ( <SelectItem key={v} value={v}> {v} </SelectItem> ))} </SelectContent> </Select> </div> <div className="space-y-2"> <label className="text-sm font-medium leading-none">Tools</label> <div className="space-y-2"> {tools.map((tool, index) => { const name = getToolNameFromSchema(tool); const backend = isBackendTool(name); return ( <div key={index} className="flex items-center justify-between rounded-md border p-2 sm:p-3 gap-2" > <span className="text-sm truncate flex-1 min-w-0 flex items-center"> {name} {backend && <BackendTag />} </span> <div className="flex gap-1 flex-shrink-0"> <Button variant="ghost" size="icon" onClick={() => handleEditTool(index)} className="h-8 w-8" > <Edit className="h-4 w-4" /> </Button> <Button variant="ghost" size="icon" onClick={() => handleDeleteTool(index)} className="h-8 w-8" > <Trash className="h-4 w-4" /> </Button> </div> </div> ); })} <Button variant="outline" className="w-full" onClick={handleAddTool} > <Plus className="h-4 w-4 mr-2" /> Add Tool </Button> </div> </div> <Button className="w-full mt-4" onClick={handleSave} disabled={saveStatus === "saving" || !hasUnsavedChanges} > {saveStatus === "saving" ? ( "Saving..." ) : saveStatus === "saved" ? ( <span className="flex items-center"> Saved Successfully <Check className="ml-2 h-4 w-4" /> </span> ) : saveStatus === "error" ? ( "Error Saving" ) : ( "Save Configuration" )} </Button> </div> </ScrollArea> </CardContent> <ToolConfigurationDialog open={openDialog} onOpenChange={setOpenDialog} editingIndex={editingIndex} selectedTemplate={selectedTemplate} editingSchemaStr={editingSchemaStr} isJsonValid={isJsonValid} onTemplateChange={handleTemplateChange} onSchemaChange={onSchemaChange} onSave={handleDialogSave} backendTools={backendTools} /> </Card> ); }; export default SessionConfigurationPanel;