Files
hunjing/frontend/src/components/memory/MemoryCard.tsx
T
2026-08-21 16:00:13 +08:00

170 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/Button";
import { SensitivityBadge } from "@/components/ui/StatusBadge";
import { describeError } from "@/lib/utils/error";
import { MEMORY_SOURCE_LABEL, MEMORY_TYPE_LABEL } from "@/lib/constants/labels";
import { memoryApi } from "@/lib/api";
import { formatDateTime, formatPercent } from "@/lib/utils/format";
import { cn } from "@/lib/utils/cn";
import type { CreateMemoryRequest, Memory } from "@/types/api";
import { MemoryEditor } from "./MemoryEditor";
/** 单条记忆卡片(设计稿 memory-preview.html §mem-card)
* 类型色标(.mem-type)、置信度/重要性进度条(.meta-bar .fill)、
* 敏感度徽章、证据来源(.mem-foot)*/
export function MemoryCard({
memory,
characterId,
onChanged,
onError,
}: {
memory: Memory;
characterId?: string;
onChanged: (kind: "update" | "delete", payload: Memory | string) => void;
onError: (message: string) => void;
}) {
const [editing, setEditing] = useState(false);
const [confirming, setConfirming] = useState(false);
const [busy, setBusy] = useState(false);
const handleUpdate = async (body: CreateMemoryRequest) => {
setBusy(true);
try {
const updated = await memoryApi.updateMemory(memory.id, body);
onChanged("update", updated);
setEditing(false);
} catch (err) {
onError(describeError(err));
} finally {
setBusy(false);
}
};
const handleDelete = async () => {
setBusy(true);
try {
await memoryApi.deleteMemory(memory.id);
onChanged("delete", memory.id);
} catch (err) {
onError(describeError(err));
setConfirming(false);
} finally {
setBusy(false);
}
};
if (editing) {
return (
<MemoryEditor
characterId={characterId}
initial={memory}
onSubmit={handleUpdate}
onCancel={() => setEditing(false)}
/>
);
}
const sensClass = memory.sensitivity === "high" ? "high-sens" : "";
return (
<div className={cn("mem-card", sensClass)}>
{/* 标题行(设计稿 .mem-head) */}
<div className="mem-head">
<span className="mem-title">{memory.title || "无标题记忆"}</span>
<span className={cn("mem-type", memory.type)}>
{MEMORY_TYPE_LABEL[memory.type] ?? memory.type}
</span>
<SensitivityBadge level={memory.sensitivity} />
</div>
{/* 记忆内容(设计稿 .mem-content) */}
<p className="mem-content">{memory.content}</p>
{/* 元数据行:置信度 + 重要性进度条(设计稿 .mem-meta) */}
<div className="mem-meta">
<span className="meta-item conf">
<span className="lab">置信</span>
<span className="meta-bar">
<span
className="fill"
style={{
width: `${Math.round((memory.confidence ?? 0) * 100)}%`,
}}
/>
</span>
<span className="vv">{formatPercent(memory.confidence)}</span>
</span>
<span className="meta-item imp">
<span className="lab">重要</span>
<span className="meta-bar">
<span
className="fill"
style={{
width: `${Math.round((memory.importance ?? 0) * 100)}%`,
background: "var(--gilt)",
}}
/>
</span>
<span className="vv">{formatPercent(memory.importance)}</span>
</span>
</div>
{/* 脚注(设计稿 .mem-foot) */}
<div className="mem-foot">
<span className="evi">
来源:{MEMORY_SOURCE_LABEL[memory.source] ?? memory.source}
</span>
<span className="evi">
{formatDateTime(memory.updated_at ?? memory.created_at)}
</span>
{memory.evidence_ids && memory.evidence_ids.length > 0 && (
<span className="evi">证据·{memory.evidence_ids.length} 条</span>
)}
<span className="act">
<button type="button" onClick={() => setEditing(true)}>
编辑
</button>
<button
type="button"
className="primary"
onClick={() => setConfirming(true)}
disabled={busy}
>
删除
</button>
</span>
</div>
{/* 删除确认 */}
{confirming && (
<div className="mt-3 flex flex-wrap items-center justify-between gap-2 rounded-lg border border-cinnabar/30 bg-cinnabar/10 px-3 py-2 text-xs text-cinnabar backdrop-blur-sm">
<span>删除后此记忆将不再注入对话上下文,确认删除?</span>
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => setConfirming(false)}
disabled={busy}
>
取消
</Button>
<Button
size="sm"
variant="danger"
onClick={handleDelete}
loading={busy}
>
确认删除
</Button>
</div>
</div>
)}
</div>
);
}