Initial MVP for QA asset backend

This commit is contained in:
2026-07-05 17:44:15 +08:00
commit 95b5e09fd4
71 changed files with 6546 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
"use client";
import { Check, EyeOff, FileX, Pencil, ShieldAlert, Sparkles, X } from "lucide-react";
import type { ReactNode } from "react";
import { api } from "@/lib/api";
import type { RawQAItem } from "@/lib/types";
import RiskBadge from "./RiskBadge";
import StatusBadge from "./StatusBadge";
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<div>
<div className="mb-1 text-xs font-medium text-slate-500">{label}</div>
<div className="whitespace-pre-wrap text-sm leading-6 text-ink">{children || "-"}</div>
</div>
);
}
export default function QAReviewCard({ item, onChanged }: { item: RawQAItem; onChanged: () => void }) {
async function action(path: string) {
await api.post(`/raw-qa/${item.id}/${path}`, {});
onChanged();
}
async function editAndApprove() {
const standardQuestion = window.prompt("标准问题", item.suggested_standard_question ?? item.normalized_question ?? "");
if (standardQuestion === null) return;
const standardAnswer = window.prompt("标准回答", item.suggested_standard_answer ?? item.normalized_answer ?? "");
if (standardAnswer === null) return;
await api.patch(`/raw-qa/${item.id}`, {
suggested_standard_question: standardQuestion,
suggested_standard_answer: standardAnswer
});
await action("approve");
}
return (
<article className="rounded-md border border-line bg-white p-5">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-ink">{item.qa_code}</span>
<RiskBadge level={item.risk_level} />
<StatusBadge status={item.review_status} />
{item.need_desensitization ? (
<span className="rounded-md border border-amber-200 bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700">
</span>
) : null}
</div>
<div className="text-xs text-slate-500"> #{item.session_id}</div>
</div>
<div className="grid gap-5 lg:grid-cols-2">
<Field label="原始问题">{item.raw_question}</Field>
<Field label="问题整理版">{item.normalized_question}</Field>
<Field label="原始回答">{item.raw_answer}</Field>
<Field label="回答整理版">{item.normalized_answer}</Field>
<Field label="AI 建议标准问题">{item.suggested_standard_question}</Field>
<Field label="AI 建议标准回答">{item.suggested_standard_answer}</Field>
</div>
<div className="mt-5 grid gap-3 border-t border-line pt-4 text-sm md:grid-cols-2 xl:grid-cols-4">
<div>
<span className="text-slate-500"></span>
{item.primary_topic}
</div>
<div>
<span className="text-slate-500"></span>
{item.course_stage}
</div>
<div>
<span className="text-slate-500"></span>
{item.problem_tags.join("、") || "-"}
</div>
<div>
<span className="text-slate-500"></span>
{item.source_timestamp || "-"}
</div>
</div>
<div className="mt-4 rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900">
{item.risk_notes || "无风险说明"}
</div>
<div className="mt-5 flex flex-wrap gap-2">
<button
onClick={() => action("approve")}
className="inline-flex h-9 items-center gap-2 rounded-md bg-jade px-3 text-sm font-medium text-white hover:bg-teal-800"
>
<Check className="h-4 w-4" aria-hidden />
</button>
<button
onClick={editAndApprove}
className="inline-flex h-9 items-center gap-2 rounded-md border border-line bg-white px-3 text-sm font-medium text-ink hover:bg-slate-50"
>
<Pencil className="h-4 w-4" aria-hidden />
</button>
<button
onClick={() => action("revise")}
className="inline-flex h-9 items-center gap-2 rounded-md border border-line bg-white px-3 text-sm font-medium text-ink hover:bg-slate-50"
>
<Sparkles className="h-4 w-4" aria-hidden />
</button>
<button
onClick={() => action("reject")}
className="inline-flex h-9 items-center gap-2 rounded-md border border-line bg-white px-3 text-sm font-medium text-ink hover:bg-slate-50"
>
<FileX className="h-4 w-4" aria-hidden />
</button>
<button
onClick={() => action("forbid")}
className="inline-flex h-9 items-center gap-2 rounded-md border border-rose-200 bg-white px-3 text-sm font-medium text-rose-700 hover:bg-rose-50"
>
<X className="h-4 w-4" aria-hidden />
使
</button>
<button
onClick={() => action("mark-high-risk")}
className="inline-flex h-9 items-center gap-2 rounded-md border border-orange-200 bg-white px-3 text-sm font-medium text-orange-700 hover:bg-orange-50"
>
<ShieldAlert className="h-4 w-4" aria-hidden />
</button>
<button
onClick={() => action("mark-desensitization")}
className="inline-flex h-9 items-center gap-2 rounded-md border border-amber-200 bg-white px-3 text-sm font-medium text-amber-700 hover:bg-amber-50"
>
<EyeOff className="h-4 w-4" aria-hidden />
</button>
</div>
</article>
);
}