// Kayser v2 — MVP additions // 1. Memory sheet: visible USER + MEMORY (Hermes-style), editable // 2. Expanded approval card sheet: reasoning, editable draft, alternatives // 3. "Don't do that again" correction sheet for auto-handled cards // 4. Floating undo bar after a real send // 5. New palette items wired to memory + skills // ============ MEMORY DATA (mirrors Hermes USER.md / MEMORY.md) ============ const KXV2_USER_PROFILE = [ { id: 'u1', text: "Wes Kayser, owner of Kayser Foundation Repair, Fort Worth TX.", pinned: true }, { id: 'u2', text: "Prefers short replies. Hates fluff. 'Just tell me what you did.'" }, { id: 'u3', text: "Texts > calls > email. Will answer texts on the road, ignores email until evening." }, { id: 'u4', text: "Tone with customers: warm but direct. Never apologetic. Signs off 'Wes'." }, { id: 'u5', text: "Schedule: on site 7am–4pm, office 4–6pm. Don't ping after 7pm unless urgent." }, ]; const KXV2_AGENT_MEMORY = [ { id: 'm1', text: "Garcia (Lina) usually pays day 5. Past 7 days = nudge, not panic. Husband Mateo decides on price.", learned: '3 weeks ago' }, { id: 'm2', text: "Crew B leaves the yard 15 min before scheduled start. Don't ping them too early.", learned: '2 weeks ago' }, { id: 'm3', text: "Weather reschedules: auto if forecast >60% rain on exterior work, ask for everything else.", learned: '6 days ago' }, { id: 'm4', text: "Wes likes to hand-write quotes over $15K. Don't auto-send those.", learned: '5 days ago' }, { id: 'm5', text: "Sherwin Williams receipts always go to job 1822 unless noted otherwise.", learned: '4 days ago' }, { id: 'm6', text: "Don't draft collections calls — only texts. Wes does the calls himself.", learned: '2 days ago', recent: true }, ]; // ============ MEMORY SHEET ============ function KxV2MemorySheet({ open, onClose }) { const [tab, setTab] = React.useState('user'); const [editing, setEditing] = React.useState(null); const [draftText, setDraftText] = React.useState(''); const [adding, setAdding] = React.useState(false); const [newFact, setNewFact] = React.useState(''); // Live mode populates these from /api/v1/memory; prototype mode keeps // the mock arrays so the standalone demo still shows realistic copy. const live = !!window.KX_TOKEN; const [userList, setUserList] = React.useState(live ? [] : KXV2_USER_PROFILE); const [agentList, setAgentList] = React.useState(live ? [] : KXV2_AGENT_MEMORY); // Hermes-enforced char ceilings — defaults match the constants in // atlas-api's memory_files.py but get overwritten from the server // payload on each fetch so we stay in sync if Hermes ever changes them. const [userMax, setUserMax] = React.useState(1375); const [agentMax, setAgentMax] = React.useState(2200); React.useEffect(() => { if (!open) return; setEditing(null); setAdding(false); setNewFact(''); // Re-fetch on every open so the sheet always reflects what the agent // has written since the last view. Best-effort — on failure we leave // whatever's already in state. if (!live || !window.kxApi) return; (async () => { try { const data = await window.kxApi('/memory'); // Server returns `memory: list[str]` (agent notes) and // `user: list[str]` (operator profile). The renderer wants // `[{id, text}]` so we wrap with a stable index-based id. const wrap = (arr, prefix) => (arr || []).map((text, i) => ({ id: prefix + i, text })); setUserList(wrap(data?.user, 'u')); setAgentList(wrap(data?.memory, 'm')); if (typeof data?.user_max === 'number') setUserMax(data.user_max); if (typeof data?.memory_max === 'number') setAgentMax(data.memory_max); } catch (_) { /* best-effort; kxApi handles 401 redirect */ } })(); }, [open, live]); const list = tab === 'user' ? userList : agentList; const setList = tab === 'user' ? setUserList : setAgentList; const userBytes = userList.reduce((n, e) => n + e.text.length, 0); const agentBytes = agentList.reduce((n, e) => n + e.text.length, 0); // In live mode the memory tool is owned by Hermes — POST /memory is a // no-op stub (see atlas-api/v1.py). Local edits would just snap back // on next fetch and we'd be lying to the user. Hide the edit/forget/ // add affordances and show a clear "ask in chat" hint instead. const editable = !live; const startEdit = (e) => { setEditing(e.id); setDraftText(e.text); }; const saveEdit = () => { setList(list.map(e => e.id === editing ? { ...e, text: draftText } : e)); setEditing(null); }; const forget = (id) => setList(list.filter(e => e.id !== id)); const addFact = () => { if (!newFact.trim()) return; setList([...list, { id: 'n' + Date.now(), text: newFact.trim(), recent: true }]); setNewFact(''); setAdding(false); }; return (
{[ { k: 'user', label: 'You', pct: Math.round(userBytes / userMax * 100), max: userMax, used: userBytes, n: userList.length }, { k: 'agent', label: 'My notes', pct: Math.round(agentBytes / agentMax * 100), max: agentMax, used: agentBytes, n: agentList.length }, ].map(s => ( ))}
{tab === 'user' ? (live ? "How you like to work. I read this every time we talk. To change something, just tell me in chat." : "How you like to work. I read this every time we talk.") : (live ? "What I've learned about your business. I edit this myself as I learn — to add or correct something, tell me in chat." : "What I've learned about your business. I edit this myself as I learn — you can correct anything.")}
{live && list.length === 0 && (
{tab === 'user' ? "Nothing here yet. I'll start learning how you like to work as we chat." : "Nothing here yet. I'll save useful facts about your business as I pick them up."}
)}
{list.map(e => (
{e.pinned && ◆ PINNED} {e.recent && NEW} {e.learned && learned {e.learned}} {editable && editing === e.id && ( <> )} {editable && editing !== e.id && ( <> )}
{editing === e.id ? (