import { useState } from "react"; import { getApiErrorMessage } from "../../../api/client"; import { Icon } from "../../../components/Icon"; import type { RejectionReason, ReviewBundle } from "../types"; interface DocumentReviewPanelProps { bundle: ReviewBundle | undefined; error: unknown; isLoading: boolean; isDecisionPending: boolean; decisionError: unknown; onApprove: () => void; onReject: (reason: RejectionReason) => void; onRetry: () => void; } const REJECTION_REASONS: { value: RejectionReason; label: string }[] = [ { value: "RIGHTS_NOT_VERIFIED", label: "资料权利或授权未核实" }, { value: "CONTENT_QUALITY_REJECTED", label: "内容质量不满足入库要求" }, { value: "CLOUD_PROCESSING_REJECTED", label: "不允许提交云端处理" }, ]; function reviewStateLabel(state: string): string { const labels: Record = { LOCAL_PARSED_PENDING_CLOUD_REVIEW: "等待人工复核", CLOUD_APPROVED: "已批准出域", REJECTED: "已拒绝出域", OCR_REQUIRED: "需要可靠 OCR", }; return labels[state] ?? state; } export function DocumentReviewPanel({ bundle, error, isLoading, isDecisionPending, decisionError, onApprove, onReject, onRetry, }: DocumentReviewPanelProps) { const [confirmed, setConfirmed] = useState(false); const [rejectionReason, setRejectionReason] = useState("RIGHTS_NOT_VERIFIED"); if (isLoading) { return (
); } if (error !== null) { return (

复核资料暂不可用

{getApiErrorMessage(error)}

); } if (bundle === undefined) { return (

选择文档开始复核

上传与解析成功不代表允许向量化,必须先检查出域清单。

); } const version = bundle.version; if (version === null) { return (

{bundle.document.filename}

解析版本尚未生成,请等待本地 Worker 或刷新文档。

); } const expectedChunksMatch = version.expected_chunk_count === bundle.chunks.length; const reviewable = version.review_state === "LOCAL_PARSED_PENDING_CLOUD_REVIEW" && version.status === "PROCESSING" && version.outbound_manifest_sha256 !== null && bundle.chunks.length > 0 && expectedChunksMatch; return (
MANIFEST-BOUND REVIEW

{bundle.document.filename}

{reviewStateLabel(version.review_state)}
Revision
{version.review_revision}
版本状态
{version.status}
页 / 块 / Chunk
{bundle.pages.length} / {bundle.blocks.length} / {bundle.chunks.length}
出域清单 SHA-256 {version.outbound_manifest_sha256 ?? "尚未生成"}
Parser / Chunk Profile {version.parser_profile_hash} {version.chunk_profile_hash}
Cloud Policy {version.cloud_policy_id}
{!expectedChunksMatch && (
期望 {version.expected_chunk_count} 个 Chunk,但完整复核包返回 {bundle.chunks.length} 个,已禁止批准。
)}

本地解析页

{bundle.pages.length}
{bundle.pages.length === 0 ? (

暂无可复核页面;PDF 可能需要 OCR。

) : (
{bundle.pages.map((page) => (
逻辑页 {page.page_number ?? page.ordinal + 1} · 行 {page.line_start}- {page.line_end}

{page.text}

{page.text_sha256}
))}
)}

结构化块

{bundle.blocks.length}
{bundle.blocks.length === 0 ? (

暂无结构化块。

) : (
{bundle.blocks.map((block) => (
{block.kind} · {block.section_path.join(" / ") || "未标注章节"} · 行 {block.line_start}-{block.line_end}

{block.text}

{block.anchor_id}
))}
)}

拟出域 Chunk

{bundle.chunks.length}
{bundle.chunks.length === 0 ? (

没有可向量化 Chunk。

) : (
{bundle.chunks.map((chunk) => (
Chunk {chunk.ordinal + 1} {chunk.token_count} tokens

{chunk.cloud_text}

{chunk.section_path.length === 0 ? "未标注章节" : chunk.section_path.join(" / ")} {chunk.cloud_text_sha256.slice(0, 18)}…
))}
)}
必须由人工确认此精确清单 批准会绑定当前 revision 与 manifest;内容发生变化后旧确认自动失效。请勿上传未获授权资料。
{decisionError !== null && (

{getApiErrorMessage(decisionError)}

)}
); }