159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card, CardHeader } from "@/components/ui/Card";
|
|
import { extractionApi } from "@/lib/api";
|
|
import { describeError } from "@/lib/utils/error";
|
|
import { formatDateTime } from "@/lib/utils/format";
|
|
import type { MemoryCandidate, WorldEntity } from "@/types/api";
|
|
|
|
/**
|
|
* 候选审核面板(跨来源统一入口)。
|
|
* 记忆候选(cycle 大周天 / import 资料导入)与世界实体候选逐条 approve/reject。
|
|
* 审核后 router.refresh()。与 ImportWizard 的批量审核互补——此处为跨来源统一待审队列。
|
|
*/
|
|
export function CandidateReviewPanel({
|
|
memoryCandidates,
|
|
worldProposals,
|
|
}: {
|
|
memoryCandidates: MemoryCandidate[];
|
|
worldProposals: WorldEntity[];
|
|
}) {
|
|
const router = useRouter();
|
|
const [busyId, setBusyId] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function reviewMemory(id: string, approve: boolean) {
|
|
setBusyId(id);
|
|
setError(null);
|
|
try {
|
|
if (approve) await extractionApi.approveMemoryCandidate(id);
|
|
else await extractionApi.rejectMemoryCandidate(id);
|
|
router.refresh();
|
|
} catch (e) {
|
|
setError(describeError(e));
|
|
} finally {
|
|
setBusyId(null);
|
|
}
|
|
}
|
|
async function reviewWorld(id: string, approve: boolean) {
|
|
setBusyId(id);
|
|
setError(null);
|
|
try {
|
|
if (approve) await extractionApi.approveWorldEntity(id);
|
|
else await extractionApi.rejectWorldEntity(id);
|
|
router.refresh();
|
|
} catch (e) {
|
|
setError(describeError(e));
|
|
} finally {
|
|
setBusyId(null);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card className="p-5">
|
|
<CardHeader
|
|
title="记忆候选"
|
|
subtitle="来自大周天反思与资料导入的待审记忆,逐条裁定。"
|
|
/>
|
|
{memoryCandidates.length === 0 ? (
|
|
<p className="text-sm text-muted">暂无待审记忆候选。</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{memoryCandidates.map((c) => (
|
|
<li key={c.id} className="rounded-lg bg-glass/40 px-3 py-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="rounded bg-gold/15 px-1.5 py-0.5 text-[10px] text-gold">
|
|
{c.type}
|
|
</span>
|
|
<span className="font-serif text-sm font-medium text-fg">
|
|
{c.title}
|
|
</span>
|
|
<span className="text-[10px] text-muted">
|
|
{c.import_job_id ? "资料导入" : c.cycle_id ? "大周天" : ""}
|
|
{` · ${formatDateTime(c.created_at)}`}
|
|
</span>
|
|
<span className="ml-auto font-mono text-[10px] text-muted">
|
|
{(c.confidence ?? 0).toFixed(2)}
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 text-xs text-fg/80">{c.content}</p>
|
|
<div className="mt-1.5 flex gap-2">
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
loading={busyId === c.id}
|
|
onClick={() => reviewMemory(c.id, true)}
|
|
>
|
|
通过
|
|
</Button>
|
|
<Button
|
|
variant="danger"
|
|
size="sm"
|
|
loading={busyId === c.id}
|
|
onClick={() => reviewMemory(c.id, false)}
|
|
>
|
|
驳回
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Card>
|
|
|
|
<Card className="p-5">
|
|
<CardHeader
|
|
title="世界实体候选"
|
|
subtitle="待确认的世界实体,通过后进入世界模型(confirmed)。"
|
|
/>
|
|
{worldProposals.length === 0 ? (
|
|
<p className="text-sm text-muted">暂无待审世界实体候选。</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{worldProposals.map((e) => (
|
|
<li key={e.id} className="rounded-lg bg-glass/40 px-3 py-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="rounded bg-gold/15 px-1.5 py-0.5 text-[10px] text-gold">
|
|
{e.type}
|
|
</span>
|
|
<span className="font-serif text-sm font-medium text-fg">
|
|
{e.name}
|
|
</span>
|
|
<span className="ml-auto font-mono text-[10px] text-muted">
|
|
{e.confidence.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
<div className="mt-1.5 flex gap-2">
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
loading={busyId === e.id}
|
|
onClick={() => reviewWorld(e.id, true)}
|
|
>
|
|
通过
|
|
</Button>
|
|
<Button
|
|
variant="danger"
|
|
size="sm"
|
|
loading={busyId === e.id}
|
|
onClick={() => reviewWorld(e.id, false)}
|
|
>
|
|
驳回
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Card>
|
|
|
|
{error && <p className="text-xs text-cinnabar">{error}</p>}
|
|
</div>
|
|
);
|
|
}
|