170 lines
5.1 KiB
TypeScript
170 lines
5.1 KiB
TypeScript
"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>
|
||
);
|
||
}
|