Initial MVP for QA asset backend
This commit is contained in:
138
hy_qa_asset_backend/frontend/components/QAReviewCard.tsx
Normal file
138
hy_qa_asset_backend/frontend/components/QAReviewCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user