Add personal memory and admin dashboard

This commit is contained in:
wuyang6
2026-05-13 14:04:42 +08:00
parent e5095268d8
commit a325098d12
8 changed files with 2081 additions and 0 deletions
@@ -3,10 +3,12 @@ import { ThreadListPrimitive } from "@assistant-ui/react";
import {
BarChart3Icon,
BotIcon,
BrainIcon,
ChevronDownIcon,
Clock3Icon,
LogOutIcon,
PanelLeftOpenIcon,
SaveIcon,
SearchIcon,
SquarePenIcon,
UserIcon,
@@ -86,6 +88,19 @@ type ClawStoredSession = {
model?: string;
};
type UserMemoryPayload = {
content: string;
line_count?: number;
updated_at?: string | null;
};
type SkillMemoryPayload = {
skill: string;
content: string;
line_count?: number;
updated_at?: string | null;
};
type ReplaySessionPayload = Parameters<typeof toReplayRepository>[0];
type SidebarSession = ClawSessionSummary & {
@@ -424,6 +439,12 @@ function AccountMenu({
null,
);
const [modelUsageOpen, setModelUsageOpen] = useState(false);
const [memoryOpen, setMemoryOpen] = useState(false);
const [userMemory, setUserMemory] = useState<UserMemoryPayload | null>(null);
const [skillMemories, setSkillMemories] = useState<SkillMemoryPayload[]>([]);
const [memoryDraft, setMemoryDraft] = useState("");
const [selectedMemorySkill, setSelectedMemorySkill] = useState("");
const [memorySaving, setMemorySaving] = useState(false);
useEffect(() => {
if (!open) return;
@@ -462,12 +483,67 @@ function AccountMenu({
});
}, [open]);
useEffect(() => {
if (!memoryOpen) return;
Promise.all([
fetch("/api/claw/memory/user", { cache: "no-store" }).then((res) =>
res.ok ? res.json() : null,
),
fetch("/api/claw/memory/skills", { cache: "no-store" }).then((res) =>
res.ok ? res.json() : [],
),
]).then(([userPayload, skillPayload]) => {
const nextUser = userPayload as UserMemoryPayload | null;
setUserMemory(nextUser);
setMemoryDraft(nextUser?.content ?? "");
setSelectedMemorySkill("");
setSkillMemories(
Array.isArray(skillPayload)
? (skillPayload as SkillMemoryPayload[])
: [],
);
});
}, [memoryOpen]);
async function logout() {
await fetch("/api/claw/auth/logout", { method: "POST" });
clearActiveSessionId();
onLogout();
}
async function saveUserMemory() {
setMemorySaving(true);
try {
const response = await fetch(
selectedMemorySkill
? `/api/claw/memory/skills/${encodeURIComponent(selectedMemorySkill)}`
: "/api/claw/memory/user",
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ content: memoryDraft }),
},
);
if (response.ok) {
if (selectedMemorySkill) {
const payload = (await response.json()) as SkillMemoryPayload;
setSkillMemories((current) =>
current.map((item) =>
item.skill === payload.skill ? payload : item,
),
);
setMemoryDraft(payload.content);
} else {
const payload = (await response.json()) as UserMemoryPayload;
setUserMemory(payload);
setMemoryDraft(payload.content);
}
}
} finally {
setMemorySaving(false);
}
}
const totalToolCalls = sessions.reduce(
(sum, session) => sum + (session.tool_calls ?? 0),
0,
@@ -608,6 +684,80 @@ function AccountMenu({
<div className="text-muted-foreground"></div>
) : null}
</div>
<div className="mt-3 rounded-md border bg-muted/30 p-2 text-xs">
<button
type="button"
className="flex w-full items-center justify-between gap-2 text-left"
onClick={() => setMemoryOpen((value) => !value)}
>
<span className="flex items-center gap-1.5 font-medium">
<BrainIcon className="size-3.5" />
</span>
<span className="flex items-center gap-1 text-muted-foreground">
{userMemory?.line_count ?? 0} · Skill{" "}
{skillMemories.length}
<ChevronDownIcon
className={`size-3.5 transition-transform ${
memoryOpen ? "rotate-180" : ""
}`}
/>
</span>
</button>
{memoryOpen ? (
<div className="mt-2 grid gap-2">
<div className="flex gap-1 overflow-x-auto">
<Button
size="xs"
variant={selectedMemorySkill ? "outline" : "secondary"}
onClick={() => {
setSelectedMemorySkill("");
setMemoryDraft(userMemory?.content ?? "");
}}
>
</Button>
{skillMemories.map((item) => (
<Button
key={item.skill}
size="xs"
variant={
selectedMemorySkill === item.skill
? "secondary"
: "outline"
}
onClick={() => {
setSelectedMemorySkill(item.skill);
setMemoryDraft(item.content);
}}
>
{item.skill}
</Button>
))}
</div>
<textarea
className="min-h-32 w-full resize-y rounded-md border bg-background p-2 font-mono text-xs outline-none"
value={memoryDraft}
onChange={(event) => setMemoryDraft(event.target.value)}
/>
<div className="flex items-center justify-between gap-2">
<span className="truncate text-muted-foreground">
Skill
{skillMemories.map((item) => item.skill).join("、") ||
"暂无"}
</span>
<Button
size="xs"
onClick={saveUserMemory}
disabled={memorySaving}
>
<SaveIcon className="size-3" />
</Button>
</div>
</div>
) : null}
</div>
<div className="mt-3 flex items-center justify-between gap-2 border-t pt-3">
<span className="text-muted-foreground text-xs">