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 ? (
) : null}
))}
{docs.length ? (
) : null}
);
}
const NEW_SKILL_TEMPLATE = `---
name: my-skill
description: What this skill does and when the agent should use it.
---
Steps the agent should follow.
`;
function AgentSkills({
onSkillsChange,
}: {
onSkillsChange: (skills: AgentSkillCatalogEntry[]) => void;
}) {
const { t } = useLingui();
const [loading, setLoading] = useState(true);
const [skills, setSkills] = useState([]);
const [open, setOpen] = useState(null);
const [creating, setCreating] = useState(false);
const [draft, setDraft] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const [confirmingDelete, setConfirmingDelete] = useState(false);
const generation = useRef(0);
const selection = useRef(0);
async function refresh() {
const current = ++generation.current;
const list = await rpc.agentSkills.list();
if (current !== generation.current) return;
setSkills(list);
onSkillsChange(list);
}
useEffect(() => {
const current = ++generation.current;
void rpc.agentSkills
.list()
.then((list) => {
if (current !== generation.current) return;
setSkills(list);
setLoading(false);
})
.catch(() => {
if (current !== generation.current) return;
setSkills([]);
setLoading(false);
setError(t`Could not load`);
});
return () => {
generation.current += 1;
selection.current += 1;
};
}, [t]);
async function openSkill(entry: AgentSkillCatalogEntry) {
if (busy) return;
const current = ++selection.current;
setCreating(false);
setConfirmingDelete(false);
setError(null);
try {
const skill = await rpc.agentSkills.get({ skillId: entry.id });
// A slower response must not clobber a newer selection or a fresh draft.
if (current !== selection.current) return;
setOpen(skill);
setDraft(skill.content);
} catch {
if (current !== selection.current) return;
setError(t`Could not load skill`);
}
}
async function save() {
if (busy || !draft.trim()) return;
setBusy(true);
setError(null);
try {
if (creating) {
await rpc.agentSkills.create({ content: draft });
} else if (open) {
await rpc.agentSkills.update({ skillId: open.id, content: draft });
}
setOpen(null);
setCreating(false);
setConfirmingDelete(false);
try {
await refresh();
} catch {
setError(t`Could not load`);
}
} catch {
setError(t`Could not save skill`);
} finally {
setBusy(false);
}
}
async function remove(skill: AgentSkill) {
if (busy) return;
if (!confirmingDelete) {
setConfirmingDelete(true);
return;
}
const skillId = skill.id;
setBusy(true);
setError(null);
try {
await rpc.agentSkills.remove({ skillId });
if (open?.id === skillId) {
setOpen(null);
setConfirmingDelete(false);
}
try {
await refresh();
} catch {
setError(t`Could not load`);
}
} catch {
setError(t`Could not delete skill`);
} finally {
setBusy(false);
}
}
const editorOpen = creating || open;
return (
{error ?
{error}
: null}
{loading ?
: null}
{!loading && skills.length === 0 && !editorOpen && !error ? (
No skills yet
) : null}
{!editorOpen
? skills.map((entry) => (
))
: null}
{editorOpen ? (
{open ? (
{open.name}
) : null}
) : (
)}
);
}