"use client"; import {useEffect, useRef, useState} from "react"; import {ArrowRight, CheckCircle2, FileText, RefreshCw, ShieldCheck} from "lucide-react"; import AgentNav from "@/components/xingbang/AgentNav"; import AssistantHistory from "@/components/xingbang/AssistantHistory"; import ReviewerWorkspace from "@/components/xingbang/ReviewerWorkspace"; import {newRequestId} from "@/lib/request-id"; import "../data-access/workspace.css"; const tabs = [["overview", "项目概览"], ["findings", "材料与问题"], ["review", "全链路评审"], ["report", "报告"], ["experience", "评审经验"], ["time", "时间轨迹"]]; const statuses: Record = {queued: "已排队", running: "检查中", completed: "已完成", partial: "部分完成", failed: "未完成", cancelled: "已取消", skipped: "未执行", draft: "草稿", submitted: "已提交评审", published: "已发布", rejected: "已拒绝", rolled_back: "已停用", stale: "待更新", finalized: "已定稿"}; const fields = [["operating_cashflow", "经营现金流(万元)"], ["due_diligence_liabilities", "尽调负债(万元)"], ["financial_liabilities", "财报负债(万元)"], ["duplicate_asset_count", "重复设备数量"], ["collection_evidence", "回款依据"], ["collection_dependency", "是否依赖特定回款"]]; const categories = [["rule_omission", "检查遗漏"], ["false_positive", "误报"], ["judgment", "专业判断"], ["process", "流程建议"], ["expression", "表达修改"], ["factual", "事实纠正"], ["data", "数据纠正"]]; export default function XingbangWorkspace() { const [items, setItems] = useState([]), [project, setProject] = useState(null), [identity, setIdentity] = useState(null); const [tab, setTab] = useState("overview"), [error, setError] = useState(""), [notice, setNotice] = useState(""), [busy, setBusy] = useState(false), [search, setSearch] = useState(""); const [requirement, setRequirement] = useState(""), [plan, setPlan] = useState("corporate.pre-review"), [runId, setRunId] = useState(""), [document, setDocument] = useState(null); const [field, setField] = useState(fields[0][0]), [value, setValue] = useState(""), [reason, setReason] = useState(""); const [template, setTemplate] = useState("template.ai-pre-review"), [reportId, setReportId] = useState(""), [sectionEdits, setSectionEdits] = useState>({}); const [category, setCategory] = useState("rule_omission"), [feedbackNote, setFeedbackNote] = useState(""), [feedbackId, setFeedbackId] = useState(""); const [reviewOpinion,setReviewOpinion]=useState(""); const [experience, setExperience] = useState({title: "", trigger: "", action: "", exceptions: "", scope: "project", team: "", industry: "", required_field: "collection.evidence", exception_field: "collection.evidence", expires_at: "2027-09-11", condition_field: "collection.dependency", condition_value: "true"}); const selected = useRef(""); const headers = (json = true) => {const token = sessionStorage.getItem("xingbang-access-token"), role=sessionStorage.getItem("xingbang-demo-role"); return {...(json ? {"Content-Type": "application/json"} : {}), ...(token ? {Authorization: `Bearer ${token}`} : {}), ...(role ? {"X-Xingbang-Demo-Role":role} : {})};}; async function request(path: string, body?: any) { const response = await fetch(`/api/risk-agent/project-assistant${path}`, {method: body === undefined ? "GET" : "POST", headers: {...headers(), ...(/\/(runs|reports)$/.test(path) && body ? {"Idempotency-Key": newRequestId()} : {})}, body: body === undefined ? undefined : JSON.stringify(body)}); const data = await response.json(); if (!response.ok) throw Error(typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || "操作失败")); return data; } async function open(id: string) { selected.current = id; const data = await request(`/projects/${encodeURIComponent(id)}`); if (selected.current !== id) return; const activeTab=new URLSearchParams(location.search).get("tab"); setProject(data); history.replaceState({}, "", `?project=${encodeURIComponent(id)}${activeTab?`&tab=${encodeURIComponent(activeTab)}`:""}`); } async function load() {try {const data = await request("/projects"); setItems(data.projects); setIdentity(data.identity); const query = new URLSearchParams(location.search), id = query.get("project"), requestedTab=query.get("tab"); if(requestedTab) setTab(requestedTab==="findings"&&data.identity.role==="reviewer"?"review":requestedTab); if(data.identity.role === "reviewer") {setPlan("corporate.independent-review"); setTemplate("template.independent-review");} else {setPlan("corporate.pre-review"); setTemplate("template.ai-pre-review");} if (id) await open(id);} catch (e: any) {setError(e.message);}} async function act(path: string, body: any, message = "操作已记录") { setBusy(true); setError(""); setNotice(""); try {const result = await request(path, body); if (selected.current) await open(selected.current); setNotice(message); return result;} catch (e: any) {setError(e.message); return null;} finally {setBusy(false);} } async function download(path: string, name: string) { try {const r = await fetch(`/api/risk-agent/project-assistant${path}`, {headers: headers(false)}); if (!r.ok) throw Error((await r.json()).detail || "下载失败"); const url = URL.createObjectURL(await r.blob()); const a = window.document.createElement("a"); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 10000);} catch (e: any) {setError(e.message);} } useEffect(() => {load();}, []); const running = project?.runs?.some((r: any) => ["queued", "running"].includes(r.status)); useEffect(() => {if (!running || !project?.id) return; const id = project.id; const timer = setInterval(() => {if (selected.current === id) open(id).catch((e: any) => setError(e.message));}, 1500); return () => clearInterval(timer);}, [project?.id, running]); const canReview = identity?.preview || identity?.role === "reviewer"; const isManager = identity?.role === "manager"; const isReviewer = identity?.role === "reviewer"; const roleTitle = isReviewer ? "评审员独立评审" : identity?.role === "admin" ? "配置管理员项目视图" : "客户经理业务办理"; const availableTabs = tabs.filter(([key]) => identity?.role === "admin" ? ["overview", "time"].includes(key) : isManager ? !["experience", "review"].includes(key) : key !== "findings"); function goToFindings(){const target=isReviewer?"review":"findings";setTab(target);history.replaceState({},"",`?project=${encodeURIComponent(project.id)}&tab=${target}`);setTimeout(()=>window.document.getElementById(isReviewer?"full-review":"project-findings")?.scrollIntoView({behavior:"smooth",block:"start"}),80);} function back() {selected.current = ""; setProject(null); setDocument(null); setRunId(""); setReportId(""); setFeedbackId(""); setSectionEdits({}); history.replaceState({}, "", "/xingbang-workbench"); load();} async function claimAndOpen(item:any){ setBusy(true);setError("");setNotice(""); try {if(!item.reviewer_id) await request(`/projects/${encodeURIComponent(item.id)}/review-claim`,{});await open(item.id);setNotice(item.reviewer_id?"已打开你领取的评审项目。":"项目已领取,评审等待停止计时。");} catch(e:any){setError(e.message);await load();}finally{setBusy(false);} } const alerts = <>{error &&
{error}
}{notice &&
{notice}
}; if (!project) return
当前身份 · {roleTitle}

{isReviewer ? "待评审项目池" : "兴邦金租 · 对公项目工作台"}

{isReviewer ? "客户经理提交后进入项目池;按等待时间排序,领取后进入你的评审办理。" : "选择一个项目,按“材料—预审/评审—报告—提交”主线办理。"}

{alerts}
setSearch(e.target.value)}/>{identity?.role === "admin" && 进入配置治理}
{items.filter(x => (x.id + x.customer).toLowerCase().includes(search.toLowerCase())).map(x => {const returned=!isReviewer&&x.workflow_state==="returned";return })}
{isReviewer&&!items.length&&

项目池暂无待评审项目

客户经理正式提交后,项目会按等待时间自动进入这里。

}
; const base = `/projects/${encodeURIComponent(project.id)}`; const chosenRun = project.runs.find((r: any) => r.id === runId) || project.runs[0]; const chosenReport = project.reports.find((r: any) => r.id === reportId) || project.reports[0]; const finished = project.runs.filter((r: any) => ["completed", "partial"].includes(r.status) && (identity?.role === "admin" || r.run_kind === (isReviewer ? "independent_review" : "pre_review"))); const currentRun = finished[0]; const reportSourceRun = finished.find((r: any) => r.id === runId) || currentRun; const openFindings = project.findings.filter((f: any) => !["已解决", "不适用"].includes(f.status)); const evidence = project.runs.flatMap((r: any) => r.evidence || []); const hasReviewClaim = !isReviewer || project.reviewer_id === identity?.id; const reviewTopics = project.review_topics || []; const reviewedTopics = reviewTopics.filter((item:any)=>item.decision).length; const blockingTopics = reviewTopics.filter((item:any)=>["要求补件","转现场核查","认可AI判断"].includes(item.decision?.decision)); async function upload(file: File) { setBusy(true); setError(""); try {const form = new FormData(); form.append("file", file); const response = await fetch(`/api/risk-agent/project-assistant${base}/documents`, {method: "POST", headers: headers(false), body: form}); const data = await response.json(); if (!response.ok) throw Error(data.detail || "上传失败"); setDocument(data); await open(project.id); setNotice("材料已解析;请核对候选字段后确认,尚未自动修改项目事实。");} catch (e: any) {setError(e.message);} finally {setBusy(false);} } async function reportAction(action: string, payload: any = {}) {if (["regenerate", "finalize", "submit"].includes(action) && Object.keys(sectionEdits).length) {setError("请先保存尚未提交的章节修改"); return;} if(action === "submit" && !window.confirm("确认提交正式评审?提交后当前AI预审报告将作为不可覆盖的送审版本保留。")) return; const saved = await act(`${base}/reports/${chosenReport.id}/actions`, {action, revision: chosenReport.revision, payload}, action === "submit" ? "AI预审报告已提交正式评审。" : "报告操作已记录"); if(saved&&action==="submit"){location.href=`/review-pool?submitted=${encodeURIComponent(project.id)}`;return;} if (saved && payload.section_id) setSectionEdits(previous => {const next = {...previous}; delete next[payload.section_id]; return next;});} async function reviewDecision(findingId:string|null,decision:string,nodeId?:string,opinion?:string){return act(`${base}/review-decisions`,{finding_id:findingId,node_id:nodeId||null,decision,opinion:opinion??reviewOpinion},findingId||nodeId?"本项评审意见已保存。":decision==="批准"?"评审已批准,正在形成评审报告。":"项目已退回客户经理补件。");} async function completeReview(decision:string,opinion?:string){const saved=await reviewDecision(null,decision,undefined,opinion);if(!saved)return;if(decision==="批准"&&reportSourceRun){const report=await act(`${base}/reports`,{run_id:reportSourceRun.id,template_id:"template.independent-review"},"独立评审报告已生成,请确认后定稿。");if(report)setReportId(report.id);setTab("report");}} return
当前身份 · {roleTitle}

{project.customer}

{project.id} · {project.industry} · {project.stage} · {project.data_domain === "demo" ? "演示数据" : "正式数据"}

{alerts} {isReviewer&&!hasReviewClaim&&
该项目仍在待评审项目池领取后将停止等待计时,并记录由你开始办理。
} {identity?.role !== "admin" && hasReviewClaim &&