// ============================================================================ // public/screens/agent-editor.jsx — Full Agent Editor (Phase 4) // ---------------------------------------------------------------------------- // Opened when clicking an agent card (agents.jsx sends go("agent_edit:ID")). // Sections: // Left column — Identity, Prompt, Opening Script, Objection Map // Right column — Voice Picker, LLM Config, Temperature, Version History // Bottom — Test Call (Hidrogen Live preview) // // The editor auto-saves on blur (debounced PATCH). Publish creates a version. // ============================================================================ function AgentEditorScreen({ agentId, go }) { const [agent, setAgent] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [saving, setSaving] = React.useState(false); const [dirty, setDirty] = React.useState(false); const [versions, setVers] = React.useState([]); const [voices, setVoices] = React.useState([]); const [showVoice, setShowVoice] = React.useState(false); const [showVersions, setShowVersions] = React.useState(false); const [publishNote, setPublishNote] = React.useState(""); const [publishing, setPublishing] = React.useState(false); const [tab, setTab] = React.useState("prompt"); // prompt | voice | llm | objections | test // Load agent const loadAgent = React.useCallback(async () => { setLoading(true); setError(null); const r = await VoaisAPI.get("/api/agents/" + agentId); if (r.ok && r.data?.ok) { const a = r.data.agent; // Parse objection_json if string if (typeof a.objection_json === "string") { try { a.objection_json = JSON.parse(a.objection_json); } catch (_) { a.objection_json = null; } } setAgent(a); } else { setError(r.data?.msg || "Failed to load agent."); } setLoading(false); }, [agentId]); // Load voices catalog const loadVoices = React.useCallback(async () => { const r = await VoaisAPI.get("/api/voices"); if (r.ok && r.data?.ok) setVoices(r.data.voices); }, []); // Load versions const loadVersions = React.useCallback(async () => { const r = await VoaisAPI.get("/api/agents/" + agentId + "/versions"); if (r.ok && r.data?.ok) setVers(r.data.versions); }, [agentId]); React.useEffect(() => { loadAgent(); loadVoices(); loadVersions(); }, [loadAgent, loadVoices, loadVersions]); // Update agent field locally const updateField = (field, value) => { setAgent(prev => ({ ...prev, [field]: value })); setDirty(true); }; // Save (PATCH) — debounced auto-save or manual const saveRef = React.useRef(null); const save = React.useCallback(async (fields) => { if (!fields || !Object.keys(fields).length) return; setSaving(true); const r = await VoaisAPI.patch("/api/agents/" + agentId, fields); setSaving(false); if (r.ok) setDirty(false); return r; }, [agentId]); // Auto-save on blur with debounce const debouncedSave = React.useCallback((fields) => { if (saveRef.current) clearTimeout(saveRef.current); saveRef.current = setTimeout(() => save(fields), 800); }, [save]); // Publish const handlePublish = async () => { // Save any pending changes first if (dirty) { await save(buildPatchPayload()); } setPublishing(true); const r = await VoaisAPI.post("/api/agents/" + agentId + "/publish", { note: publishNote || null }); setPublishing(false); if (r.ok) { setPublishNote(""); loadAgent(); loadVersions(); } else { alert(r.data?.msg || "Publish failed."); } }; // Rollback const handleRollback = async (vid) => { if (!confirm("Restore this version? Current unsaved changes will be lost.")) return; const r = await VoaisAPI.post("/api/agents/" + agentId + "/versions/" + vid + "/rollback"); if (r.ok) { loadAgent(); loadVersions(); } else { alert(r.data?.msg || "Rollback failed."); } }; // Build patch payload from current agent state const buildPatchPayload = () => { if (!agent) return {}; return { name: agent.name, role: agent.role, description: agent.description, language: agent.language, system_prompt: agent.system_prompt, opening_script: agent.opening_script, voice_tone: agent.voice_tone, voice_id: agent.voice_id, llm_provider: agent.llm_provider, llm_model: agent.llm_model, temperature: agent.temperature, objection_json: agent.objection_json, }; }; if (loading) return ; if (error) return (
{error}
} onClick={() => go("agents")}>Back to agents
); if (!agent) return null; const selectedVoice = voices.find(v => v.id === agent.voice_id); const statusTone = agent.status === "active" ? "green" : agent.status === "draft" ? "gray" : "yellow"; return (
{/* ── Top bar ──────────────────────────────────────────────────── */}
} onClick={() => go("agents")}>Agents
{agent.name} {agent.code} {agent.status}
{saving && Saving...} {dirty && !saving && Unsaved changes} setShowVersions(true)} icon={}> v{agent.published_version || 0} } onClick={handlePublish} disabled={publishing}> {publishing ? "Publishing..." : "Publish"}
{/* ── Tab nav ──────────────────────────────────────────────────── */} {/* ── Prompt tab ───────────────────────────────────────────────── */} {tab === "prompt" && (
Identity
updateField("name", e.target.value)} onBlur={() => debouncedSave({ name: agent.name })}/> updateField("role", e.target.value)} onBlur={() => debouncedSave({ role: agent.role })}/>