import { Trans, useLingui } from "@lingui/react/macro"; import type { AgentSkill, AgentSkillCatalogEntry, MemoryDocument } from "@rakazo/contracts"; import { Button, Skeleton, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, } from "@rakazo/ui-web"; import { useEffect, useRef, useState } from "react"; import { downloadArtifactBytes } from "../lib/artifact-open"; import { rpc } from "../lib/rpc"; const fieldClass = "mt-2 w-full font-mono text-ui-body-sm leading-relaxed"; function rowClass(open: boolean): string { return `h-auto w-full justify-start whitespace-normal px-2.5 py-2.5 text-start ${open ? "bg-muted" : ""}`; } /** * Bot memory and the current user's skills in this space. * User-scoped memory shared across bots lives in the Memory settings overlay. */ export function KnowledgeSection({ botId, onSkillsChange, }: { botId: string; onSkillsChange: (skills: AgentSkillCatalogEntry[]) => void; }) { const { t } = useLingui(); return (
Memory Skills rpc.memory.list({ botId, scope: "bot" })} exportFilename="memory.md" testId="bot-knowledge-memory" />
); } /** The space's shared memory documents, mounted in the Memory settings overlay. */ export function SpaceMemorySection() { return (
Shared documents
rpc.memory.list({ scope: "user" })} exportFilename="space-memory.md" testId="space-memory-list" />
); } function MemoryDocumentList({ load, exportFilename, testId, }: { load: () => Promise; exportFilename: string; testId: string; }) { const { t } = useLingui(); const [loading, setLoading] = useState(true); const [docs, setDocs] = useState([]); const [openId, setOpenId] = useState(null); const [draft, setDraft] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const generation = useRef(0); const loadRef = useRef(load); loadRef.current = load; useEffect(() => { const current = ++generation.current; void loadRef .current() .then((list) => { if (current !== generation.current) return; setDocs(list); setLoading(false); }) .catch(() => { if (current !== generation.current) return; setDocs([]); setLoading(false); setError(t`Could not load`); }); return () => { generation.current += 1; }; }, [t]); function openDoc(doc: MemoryDocument) { setOpenId(doc.id); setDraft(doc.content); setError(null); } async function save(doc: MemoryDocument) { if (busy) return; setBusy(true); setError(null); try { const updated = await rpc.memory.update({ documentId: doc.id, content: draft }); setDocs((current) => current.map((entry) => (entry.id === updated.id ? updated : entry))); setOpenId(null); } catch { setError(t`Could not save`); } finally { setBusy(false); } } async function exportMarkdown() { if (busy) return; setBusy(true); setError(null); try { // Re-fetch through the section's own loader so the file is current, // then sync the list so the export matches what is displayed. const fresh = await loadRef.current(); generation.current += 1; setDocs(fresh); const markdown = fresh.map((doc) => `# ${doc.path}\n\n${doc.content}`).join("\n\n"); downloadArtifactBytes(exportFilename, "text/markdown", new TextEncoder().encode(markdown)); } catch { setError(t`Could not load`); } finally { setBusy(false); } } return (
{error ?
{error}
: null} {loading ? : null} {!loading && docs.length === 0 && !error ? (
Nothing remembered yet
) : null} {docs.map((doc) => (
{openId === doc.id ? (