import React, { useEffect, useMemo, useRef, useState } from 'react'; import { commitment, composeSystem, modelLabel, parsePersona, personaLines, setModelField, TASK_CLASSES } from '../core/context'; import { CONTEXT_INFO, CONTEXT_KINDS, type ContextKind } from '../core/windows'; import { ContextWindow } from './ContextWindow'; import type { ModuleWindowProps, UIFModule } from '../core/modules'; import type { ContextApi } from './contextStore'; import { useUIF, type UIFContextValue } from './UIFContext'; const ACCEPT: Record = { history: '.history,.jsonl,application/json', memory: '.memory,.jsonl,application/json', prompt: '.prompt,.txt,text/plain', persona: '.persona,.json,application/json', model: '.model,.yaml,.yml,text/plain', }; function download(name: string, text: string) { const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' })); const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } /** Renders every open window, whichever module it belongs to, in the field's frame. */ export function ModuleWindows() { const uif = useUIF(); const { windows } = uif; return ( <> {windows.defs .filter(d => windows.open[d.id]) .map(def => { const close = () => windows.hide(def.id); return ( windows.setRect(def.id, r)} onClose={close} > {Tools => } ); })} ); } /** The body of one context window: its file's import and export in the title bar, its contents, and a status note. */ function ContextFileBody({ kind, Tools }: { kind: ContextKind; Tools: ModuleWindowProps['Tools'] }) { const { context, storageKey } = useUIF(); const [note, setNote] = useState<{ text: string; error?: boolean } | null>(null); const fileRef = useRef(null); const fileName = `${storageKey}${CONTEXT_INFO[kind].file}`; useEffect(() => { if (!note) return; const t = setTimeout(() => setNote(null), 4000); return () => clearTimeout(t); }, [note]); const onFile = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; try { setNote({ text: context.importText(kind, await file.text()) }); } catch (err) { setNote({ text: (err as Error).message, error: true }); } }; return ( <> {kind === 'history' && } {kind === 'memory' && } {kind === 'prompt' && } {kind === 'persona' && } {kind === 'model' && } {note && (

{note.text}

)} ); } /** The built-in module: the five context files, in Savante's formats. */ export const contextModule: UIFModule = { id: 'context', label: 'Context', windows: CONTEXT_KINDS.map(kind => ({ id: kind, ...CONTEXT_INFO[kind], Body: ({ Tools }: ModuleWindowProps) => , detail: (uif: UIFContextValue) => { const c = uif.context; switch (kind) { case 'history': return `${c.history.length} exchange${c.history.length === 1 ? '' : 's'}`; case 'memory': return `${c.memory.length} note${c.memory.length === 1 ? '' : 's'}`; case 'prompt': return c.prompt.trim() ? `${c.prompt.trim().length} chars` : 'empty'; case 'persona': return c.persona ? String(c.persona.name ?? c.persona.persona ?? 'set') : 'none'; case 'model': return String(c.model.logical_model ?? 'auto'); } }, })), }; /** Records, Merkle root (rfc6962-sha256, as bankml) — shareable, reveals no content. */ function Commitment({ file, lines }: { file: string; lines: string[] }) { const [c, setC] = useState<{ records: number; merkle_root: string | null } | null>(null); useEffect(() => { let live = true; commitment(file, lines).then(r => live && setC(r)).catch(() => live && setC(null)); return () => { live = false; }; }, [file, lines]); if (!c) return null; return (
{c.records} record{c.records === 1 ? '' : 's'} {c.merkle_root && ( <> {' · root '} {c.merkle_root.slice(0, 16)}… )}
); } /** A button that asks once more before doing something it cannot undo (no browser dialogs). */ function ConfirmButton({ label, onConfirm }: { label: string; onConfirm(): void }) { const [armed, setArmed] = useState(false); useEffect(() => { if (!armed) return; const t = setTimeout(() => setArmed(false), 3000); return () => clearTimeout(t); }, [armed]); return ( ); } function when(ts: number) { return new Date(ts * 1000).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function HistoryBody({ context }: { context: ContextApi }) { const [q, setQ] = useState(''); const listRef = useRef(null); const shown = useMemo(() => { const needle = q.trim().toLowerCase(); const all = context.history.map((h, i) => ({ h, i })); return needle ? all.filter(({ h }) => `${h.user}\n${h.assistant}\n${h.output ?? ''}`.toLowerCase().includes(needle)) : all; }, [context.history, q]); useEffect(() => { const el = listRef.current; if (el && !q) el.scrollTop = el.scrollHeight; }, [shown.length, q]); return (
setQ(e.target.value)} placeholder="Search .history…" aria-label="Search history" />
{shown.length === 0 ? (

{q ? 'Nothing matches.' : 'No exchanges yet. Messages you send in chat are recorded here.'}

) : (
    {shown.map(({ h, i }) => (
  1. {when(h.ts)} {h.output && · {h.output}} {h.model && · {h.model}} {h.receipt !== undefined && ( {' '}· receipt )}
    {h.user}
    {h.assistant}
  2. ))}
)}
); } function MemoryBody({ context }: { context: ContextApi }) { const [text, setText] = useState(''); const notes = context.memory.map((n, i) => ({ n, i })).reverse(); const add = async (e: React.FormEvent) => { e.preventDefault(); if (!text.trim()) return; await context.remember(text); setText(''); }; return (
setText(e.target.value)} placeholder="Add a note to remember…" aria-label="New memory note" />
{notes.length === 0 ? (

No notes. Add one here, or press ◈ on a reply to keep it.

) : (
    {notes.map(({ n, i }) => (
  • {n.source.kind === 'delivery' ? '⏱' : n.source.kind === 'response' ? '◈' : '✎'} {n.text}
  • ))}
)}
Notes (✎ ◈) are sent with every message, newest first, up to 2,400 characters; deliveries (⏱) are kept, never sent.
); } function PromptBody({ context }: { context: ContextApi }) { const [preview, setPreview] = useState(false); const hasPersonaPrompt = !!context.persona?.system_prompt?.trim(); const composed = composeSystem({ prompt: context.prompt, persona: context.persona, source: context.promptSource, memory: context.memory }); return (