import { useState, useRef, useEffect, useCallback } from "react"; import { Activity, Camera, Clock3, FlaskConical, Mic, MicOff, Radio, RotateCcw, Save, SlidersHorizontal, Sparkles, Triangle, X, Zap, Send, } from "lucide-react"; import { STAGE_CONTRACTS } from "@/data/aceTopology"; import { BlendshapeMeter } from "@/components/eve/BlendshapeMeter"; import { listenOnce, speakText, canListen } from "@/lib/ace/eveVoice"; import { usePresenceStore, type StillChoice } from "@/store/presenceStore"; interface Props { open: boolean; onClose: () => void; } const STILL_OPTIONS: Array<{ id: StillChoice; label: string; hint: string }> = [ { id: "auto", label: "Auto by stage", hint: "Portrait idle · close-up on L1 talk / L2" }, { id: "natural", label: "Portrait · natural", hint: "eve-natural.png only" }, { id: "closeup", label: "Close-up · detail", hint: "eve-closeup.jpg only" }, ]; const CONTROLLER = import.meta.env.VITE_ACE_HTTP_URL || "http://127.0.0.1:8100"; interface LabSettings { model: string; temperature: number; topP: number; maxTokens: number; memoryTurns: number; } /** * Persona Lab — who she IS, as an editable dial. * * This is the bench where a real lab builds a person: instruction file, * sampling behaviour, memory depth. It lives in the backend drawer by * design — the front must stay free of anything that colours the judgment * of how close she came to being real. */ function PersonaLab() { const [text, setText] = useState(""); const [loaded, setLoaded] = useState(false); const [dirty, setDirty] = useState(false); const [status, setStatus] = useState(null); const [meta, setMeta] = useState<{ source: string; chars: number; memoryTurns: number } | null>( null, ); const [lab, setLab] = useState(null); const refresh = useCallback(async () => { try { const [p, s] = await Promise.all([ fetch(`${CONTROLLER}/v1/persona`).then((r) => r.json()), fetch(`${CONTROLLER}/v1/settings`).then((r) => r.json()), ]); setText(p.text || ""); setMeta({ source: p.source, chars: p.chars, memoryTurns: p.memoryTurns }); setLab({ model: s.model, temperature: s.temperature, topP: s.topP, maxTokens: s.maxTokens, memoryTurns: s.memoryTurns, }); setLoaded(true); setDirty(false); } catch { setStatus("controller offline"); } }, []); useEffect(() => { void refresh(); }, [refresh]); const save = async () => { setStatus("saving…"); try { const res = await fetch(`${CONTROLLER}/v1/persona`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }), }); const data = await res.json(); if (data.ok) { setStatus(`saved · ${data.chars} chars · hot-reloaded`); setDirty(false); void refresh(); } else { setStatus(data.error || "save failed"); } } catch { setStatus("save failed — controller unreachable"); } }; const patchLab = async (patch: Partial) => { if (!lab) return; const next = { ...lab, ...patch }; setLab(next); try { await fetch(`${CONTROLLER}/v1/settings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }); } catch { setStatus("settings not applied"); } }; const resetMemory = async () => { try { await fetch(`${CONTROLLER}/v1/memory/reset`, { method: "POST" }); setStatus("conversational memory cleared"); void refresh(); } catch { setStatus("reset failed"); } }; return ( <>
Persona Lab