diff --git a/frontend-web/src/components/workspace/admin/admin-memory-page.tsx b/frontend-web/src/components/workspace/admin/admin-memory-page.tsx new file mode 100644 index 00000000..eae8f24b --- /dev/null +++ b/frontend-web/src/components/workspace/admin/admin-memory-page.tsx @@ -0,0 +1,371 @@ +"use client"; + +import { useState } from "react"; +import { Check, Loader2, Pencil, Plus, Trash2, X } from "lucide-react"; +import { toast } from "sonner"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { + useAdminAddMemoryEntry, + useAdminDeleteMemoryEntry, + useAdminReplaceMemoryEntry, + useUserMemory, +} from "@/core/admin/hooks"; +import { useAllUsers } from "@/core/auth/hooks"; + +function getEntries(data: Record): string[] { + const raw = (data as { entries?: unknown }).entries; + if (!Array.isArray(raw)) return []; + return raw.filter((e): e is string => typeof e === "string" && e.trim().length > 0); +} + +// ── Memory entry list with inline editing ────────────────────────────────── + +function MemoryEntryList({ + entries, + userId, + agent, +}: { + entries: string[]; + userId: string; + agent: string | null; +}) { + const [editIndex, setEditIndex] = useState(null); + const [editDraft, setEditDraft] = useState(""); + const [addDraft, setAddDraft] = useState(""); + const [isAdding, setIsAdding] = useState(false); + + const addMutation = useAdminAddMemoryEntry(userId); + const replaceMutation = useAdminReplaceMemoryEntry(userId); + const deleteMutation = useAdminDeleteMemoryEntry(userId); + + const isPending = + addMutation.isPending || replaceMutation.isPending || deleteMutation.isPending; + + function startEdit(index: number) { + setEditIndex(index); + setEditDraft(entries[index] ?? ""); + } + + function cancelEdit() { + setEditIndex(null); + setEditDraft(""); + } + + async function confirmEdit(index: number) { + const oldText = entries[index]; + if (!oldText || editDraft.trim() === oldText.trim()) { + cancelEdit(); + return; + } + try { + await replaceMutation.mutateAsync({ oldText, content: editDraft.trim(), agent }); + cancelEdit(); + } catch (err) { + toast.error(err instanceof Error ? err.message : "修改失败"); + } + } + + async function handleDelete(entry: string) { + try { + await deleteMutation.mutateAsync({ oldText: entry, agent }); + } catch (err) { + toast.error(err instanceof Error ? err.message : "删除失败"); + } + } + + async function handleAdd() { + const content = addDraft.trim(); + if (!content) return; + try { + await addMutation.mutateAsync({ content, agent }); + setAddDraft(""); + setIsAdding(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : "新增失败"); + } + } + + return ( +
+ {entries.length === 0 && !isAdding && ( +

暂无记忆条目

+ )} + + {entries.map((entry, i) => ( +
+ {editIndex === i ? ( +
+