2026-09-08 02:49:06 +08:00
"use client" ;
2026-09-11 15:56:28 +08:00
import { useEffect , useRef , useState } from "react" ;
2026-09-15 10:31:50 +08:00
import { ArrowRight , CheckCircle2 , FileText , RefreshCw , ShieldCheck } from "lucide-react" ;
2026-09-11 15:56:28 +08:00
import AgentNav from "@/components/xingbang/AgentNav" ;
import AssistantHistory from "@/components/xingbang/AssistantHistory" ;
2026-09-15 10:31:50 +08:00
import ReviewerWorkspace from "@/components/xingbang/ReviewerWorkspace" ;
2026-09-14 11:53:29 +08:00
import { newRequestId } from "@/lib/request-id" ;
2026-09-08 02:49:06 +08:00
import "../data-access/workspace.css" ;
2026-09-15 10:31:50 +08:00
const tabs = [[ "overview" , "项目概览" ], [ "findings" , "材料与问题" ], [ "review" , "全链路评审" ], [ "report" , "报告" ], [ "experience" , "评审经验" ], [ "time" , "时间轨迹" ]];
const statuses : Record < string , string > = { queued : "已排队" , running : "检查中" , completed : "已完成" , partial : "部分完成" , failed : "未完成" , cancelled : "已取消" , skipped : "未执行" , draft : "草稿" , submitted : "已提交评审" , published : "已发布" , rejected : "已拒绝" , rolled_back : "已停用" , stale : "待更新" , finalized : "已定稿" };
2026-09-11 15:56:28 +08:00
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 < any [] >([]), [ project , setProject ] = useState < any >( null ), [ identity , setIdentity ] = useState < any >( null );
const [ tab , setTab ] = useState ( "overview" ), [ error , setError ] = useState ( "" ), [ notice , setNotice ] = useState ( "" ), [ busy , setBusy ] = useState ( false ), [ search , setSearch ] = useState ( "" );
2026-09-15 10:31:50 +08:00
const [ requirement , setRequirement ] = useState ( "" ), [ plan , setPlan ] = useState ( "corporate.pre-review" ), [ runId , setRunId ] = useState ( "" ), [ document , setDocument ] = useState < any >( 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 < Record < string , string >>({});
2026-09-11 15:56:28 +08:00
const [ category , setCategory ] = useState ( "rule_omission" ), [ feedbackNote , setFeedbackNote ] = useState ( "" ), [ feedbackId , setFeedbackId ] = useState ( "" );
2026-09-15 11:09:06 +08:00
const [ reviewOpinion , setReviewOpinion ] = useState ( "" );
2026-09-11 15:56:28 +08:00
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 ( "" );
2026-09-14 13:18:15 +08:00
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 } : {})};};
2026-09-11 15:56:28 +08:00
async function request ( path : string , body? : any ) {
2026-09-14 11:53:29 +08:00
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 )});
2026-09-11 15:56:28 +08:00
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 ;
2026-09-15 10:31:50 +08:00
const activeTab = new URLSearchParams ( location . search ). get ( "tab" );
setProject ( data ); history . replaceState ({}, "" , `?project= ${ encodeURIComponent ( id ) }${ activeTab ? `&tab= ${ encodeURIComponent ( activeTab ) } ` : "" } ` );
2026-09-11 15:56:28 +08:00
}
2026-09-15 10:31:50 +08:00
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 );}}
2026-09-11 15:56:28 +08:00
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" ;
2026-09-15 10:31:50 +08:00
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 );}
2026-09-11 15:56:28 +08:00
function back() {selected . current = "" ; setProject ( null ); setDocument ( null ); setRunId ( "" ); setReportId ( "" ); setFeedbackId ( "" ); setSectionEdits ({}); history . replaceState ({}, "" , "/xingbang-workbench" ); load ();}
2026-09-15 10:31:50 +08:00
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 );}
}
2026-09-11 15:56:28 +08:00
const alerts = <>{ error && < div role = "alert" className = "ra-alert error" >{ error }</ div >}{ notice && < div role = "status" className = "ra-alert" >{ notice }</ div >}</>;
2026-09-15 10:31:50 +08:00
if ( ! project ) return < div className = "ra-app" >< AgentNav active = "overview" identity = { identity }/>< main className = "xb-overview" >< div className = "xb-overview-heading" >< div >< span className = { `xb-role-banner role- ${ identity ? . role } ` }> 当前身份 · { roleTitle }</ span >< h1 >{ isReviewer ? "待评审项目池" : "兴邦金租 · 对公项目工作台" }</ h1 >< p >{ isReviewer ? "客户经理提交后进入项目池;按等待时间排序,领取后进入你的评审办理。" : "选择一个项目,按“材料—预审/评审—报告—提交”主线办理。" }</ p ></ div >< button className = "ra-button" onClick = { load }>< RefreshCw size = { 15 }/> 刷新 </ button ></ div >{ alerts }< div className = "xb-actions" >< input aria-label = "搜索项目" placeholder = "项目编号 / 客户名称" value = { search } onChange = { e => setSearch ( e . target . value )}/>{ identity ? . role === "admin" && < a className = "ra-button" href = "/data-access" > 进入配置治理 </ a >}</ div >< div className = "xb-project-grid" >{ items . filter ( x => ( x . id + x . customer ). toLowerCase (). includes ( search . toLowerCase ())). map ( x => < button className = "xb-project-card" key = { x . id } disabled = { busy } onClick = {() => { setTab ( "overview" ); isReviewer ? claimAndOpen ( x ) : open ( x . id ). catch ( e => setError ( e . message ));}}>< span >{ isReviewer ? ( x . reviewer_id ? "我已领取" : "项目池待领取" ) : ( x . data_domain === "demo" ? "演示项目" : "正式项目" )}</ span >< h2 >{ x . customer }</ h2 >< p >{ x . id } · { x . industry } · { x . stage }</ p >{ isReviewer && x . submitted_at && < div className = "xb-review-clock" >< small > 客户经理提交 </ small >< b >{ new Date ( x . submitted_at ). toLocaleString ()}</ b >< strong > 已等待 { durationText ( x . review_wait_hours )}</ strong >< span >{ x . review_state }</ span ></ div >}< div className = "xb-next" >< small >{ isReviewer ? "办理操作" : "建议下一步" }</ small >< b >{ x . next_action }</ b ></ div >< footer >< b >{ x . open_findings } 项待处理 </ b >< small >{ statuses [ x . latest_run ? . status ] || "尚未检查" }</ small >< ArrowRight size = { 16 }/></ footer ></ button >)}</ div >{ isReviewer &&! items . length && < div className = "ra-empty" >< h2 > 项目池暂无待评审项目 </ h2 >< p > 客户经理正式提交后,项目会按等待时间自动进入这里。 </ p ></ div >}</ main ></ div >;
2026-09-11 15:56:28 +08:00
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 ];
2026-09-15 10:31:50 +08:00
const finished = project . runs . filter (( r : any ) => [ "completed" , "partial" ]. includes ( r . status ) && ( identity ? . role === "admin" || r . run_kind === ( isReviewer ? "independent_review" : "pre_review" )));
2026-09-11 15:56:28 +08:00
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 || []);
2026-09-15 10:31:50 +08:00
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 ));
2026-09-11 15:56:28 +08:00
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 );}
}
2026-09-15 10:31:50 +08:00
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 === "批准" ? "评审已批准,正在形成评审报告。" : "项目已退回客户经理补件。" );}
2026-09-15 11:43:15 +08:00
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" );}}
2026-09-15 10:31:50 +08:00
return < div className = "ra-app" >< AgentNav active = "overview" project = { project . id } identity = { identity }/>< main className = "xb-overview" >
< button className = "xb-back" onClick = { back }> ← 返回项目列表 </ button >< div className = "xb-overview-heading" >< div >< span className = { `xb-role-banner role- ${ identity ? . role } ` }> 当前身份 · { roleTitle }</ span >< h1 >{ project . customer }</ h1 >< p >{ project . id } · { project . industry } · { project . stage } · { project . data_domain === "demo" ? "演示数据" : "正式数据" }</ p ></ div >< button className = "ra-button" onClick = {() => open ( project . id )}>< RefreshCw size = { 15 }/> 刷新 </ button ></ div >{ alerts }
{ isReviewer &&! hasReviewClaim && < section className = "xb-task xb-claim-gate" >< div >< b > 该项目仍在待评审项目池 </ b >< small > 领取后将停止等待计时,并记录由你开始办理。 </ small ></ div >< button className = "ra-button primary" disabled = { busy } onClick = {() => claimAndOpen ( project )}> 领取并开始评审 </ button ></ section >}
{ identity ? . role !== "admin" && hasReviewClaim && < section className = "xb-task" >< label className = "ra-field" >< span >{ isReviewer ? "本次独立评审关注点" : "本次AI预审要求" } (可选,不改变正式规则) </ span >< textarea value = { requirement } onChange = { e => setRequirement ( e . target . value )} maxLength = { 2000 } placeholder = { isReviewer ? "例如:独立核对还款来源与租赁物权属,不沿用客户经理结论" : "例如:重点核对现金流与回款依据,说明哪些材料还需要补充" }/></ label >< div className = "xb-actions" >< select aria-label = "业务检查方案" value = { plan } onChange = { e => setPlan ( e . target . value )}>{ project . plans . map (( p : any ) => < option value = { p . id } key = { p . id }>{ p . name } · { p . version }</ option >)}</ select >< button className = "ra-button primary" disabled = { busy || running } onClick = {() => act ( ` ${ base } /runs` , { requirement , plan_id : plan , revision : project.revision }, ` ${ isReviewer ? "独立评审" : "AI预审" } 已进入队列;离开页面不会丢失任务。` )}>< ShieldCheck size = { 16 }/>{ running ? "执行中" : isReviewer ? "发起独立评审" : "开始AI预审" }</ button ></ div >< small > 底层由 mini - openclaw 调用当前方案冻结的 Skills 与规则,结果可逐项追溯。 </ small ></ section >}
< nav className = "xb-tabs" >{ availableTabs . map ( t => < button key = { t [ 0 ]} className = { tab === t [ 0 ] ? "active" : "" } onClick = {() => { setTab ( t [ 0 ]); history . replaceState ({}, "" , `?project= ${ encodeURIComponent ( project . id ) } &tab= ${ encodeURIComponent ( t [ 0 ]) } ` );}}>{ isReviewer && t [ 0 ] === "findings" ? "全链路评审" : t [ 1 ]}</ button >)}{ identity ? . role === "admin" && < a href = "/data-access" > 配置治理 </ a >}</ nav >
{ tab === "review" && isReviewer && < ReviewerWorkspace topics = { reviewTopics } reviewed = { reviewedTopics } blocking = { blockingTopics } busy = { busy ||! hasReviewClaim } opinion = { reviewOpinion } setOpinion = { setReviewOpinion } onDecision = {( nodeId :string , decision :string , opinion? :string ) => reviewDecision ( null , decision , nodeId , opinion )} onComplete = { completeReview } hasRun = { !! currentRun }/>}{ /* Reviewer decisions stay separate from manager issue handling. */ }
{ tab === "overview" && <>< section className = "xb-cards" >< article >< b > 当前数据版本 </ b >< strong > v { project . data_revision }</ strong >< small >{ project . documents . length } 份材料 </ small ></ article >< button className = "xb-metric-card" onClick = { goToFindings }>< b > 未处理问题 </ b >< strong >{ openFindings . length }</ strong >< small >{ openFindings [ 0 ] ? ` ${ openFindings [ 0 ]. title } · 点击查看怎么解决` : "当前没有待处理问题" }</ small >< ArrowRight size = { 17 }/></ button >< article >< b > 最近运行 </ b >< strong >{ statuses [ project . runs [ 0 ] ? . status ] || "未检查" }</ strong >< small >{ project . runs [ 0 ] ? . created_at ? new Date ( project . runs [ 0 ]. created_at ). toLocaleString () : "点击检查项目" }</ small ></ article ></ section >
< section className = "xb-panel xb-wide" >< h2 > AI检查结果 </ h2 >< p > 先看业务结论;数据来源和解决建议可直接展开,技术信息放在最后。 </ p >{ project . runs . length ? <>< select aria-label = "选择检查历史" value = { chosenRun ? . id || "" } onChange = { e => setRunId ( e . target . value )}>{ project . runs . map (( r : any ) => < option key = { r . id } value = { r . id }>{ new Date ( r . created_at ). toLocaleString ()} · { statuses [ r . status ]} · { r . id . slice ( - 8 )}</ option >)}</ select >< p > 本次要求: { chosenRun . requirement || "无额外要求" }</ p >{[ "queued" , "running" ]. includes ( chosenRun . status ) && < button className = "ra-button" onClick = {() => act ( ` ${ base } /runs/ ${ chosenRun . id } /cancel` , {}, "已取消;迟到结果不会发布。" )}> 取消本次运行 </ button >}{ chosenRun . error && < p role = "alert" >{ chosenRun . error }</ p >}{ chosenRun . steps . map (( step :any ) => < BusinessStep key = { step . step_id } step = { step } evidence = { evidence } statuses = { statuses }/>)}{ chosenRun . data_errors ? . map (( e :any , i :number ) => < p key = { i } className = "xb-warning" > 数据读取未完成: { e . message }</ p >)}< details >< summary > 技术追溯信息 </ summary >< pre >{ JSON . stringify ({ snapshot :chosenRun.snapshot_id , versions :chosenRun.component_versions , changes :chosenRun.change_summary }, null , 2 )}</ pre ></ details ></> : < p > 尚无运行记录。 </ p >}</ section >
< details className = "xb-panel xb-wide" >< summary > 业务历史与操作留痕 </ summary >< AssistantHistory key = { project . id } projectId = { project . id } records = { project . legacy_records } events = { project . events } request = { request }/>{ project . migration_note && < p >{ project . migration_note } ;已保留 { project . legacy_records . length } 条历史记录。 </ p >}</ details ></>}
2026-09-11 15:56:28 +08:00
{ tab === "findings" && <>< section className = "xb-panel xb-wide" >< h2 > 材料档案 </ h2 >< p > 上传后先查看解析结果,确认候选值才更新项目。旧版本与来源保留;扫描件无法识别时会提示补充。 </ p >< label className = "ra-field" >< span > 上传 TXT / CSV / DOCX / XLSX / PDF / 图片(最多 10 MB ) </ span >< input aria-label = "上传项目材料" type = "file" accept = ".txt,.csv,.docx,.xlsx,.pdf,.png,.jpg,.jpeg" disabled = { busy } onChange = { e => { if ( e . target . files ? .[ 0 ]) upload ( e . target . files [ 0 ]); e . target . value = "" ;}}/></ label >{ project . documents . map (( d : any ) => < div className = "xb-finding" key = { d . id }>< div >< b >{ d . name }</ b >< p > 版本 { d . version } · { d . status } · { d . size } 字节 </ p ></ div >< button className = "ra-button" onClick = { async () => { try { setDocument ( await request ( ` ${ base } /documents/ ${ d . id } ` ));} catch ( e : any ) { setError ( e . message );}}}> 查看解析 </ button >< button className = "ra-button" onClick = {() => download ( ` ${ base } /documents/ ${ d . id } /download` , d . name )}> 下载原件 </ button ></ div >)}
{ document && < div className = "xb-document" >< h3 >{ document . name }</ h3 >{ document . warnings . map (( w : string , i : number ) => < p className = "xb-warning" key = { i }>{ w }</ p >)}< label className = "ra-field" >< span > 来源确认或补件说明 </ span >< input value = { reason } onChange = { e => setReason ( e . target . value )} placeholder = "说明已核对的期间、口径和依据" /></ label >{ document . candidates . map (( c : any ) => < div className = "xb-finding" key = { c . id }>< div >< b >{ c . field } = { String ( c . value ?? "缺失" )}</ b >< p >{ c . locator } · { c . period || "期间未明" } · { c . unit || "单位未明" } · { c . scope || "口径未明" }</ p ></ div >< button className = "ra-button" disabled = { busy || ! reason . trim ()} onClick = {() => act ( ` ${ base } /supplements` , { candidate_ids : [ c . id ], revision : project.revision , reason }, "候选值已确认;请重新检查项目。" )}> 确认该来源 </ button ></ div >)}< details >< summary > 解析原文与定位 </ summary >{ document . chunks . map (( c : any , i : number ) => < p key = { i }>< b >{ c . locator }</ b > { c . text }{ c . formula && < small > 公式: { c . formula } ;缓存: { c . cached_value || "无" }</ small >}</ p >)}</ details ></ div >}</ section >
2026-09-15 10:31:50 +08:00
< section className = "xb-panel xb-wide" id = "project-findings" >< h2 >{ isReviewer ? "全链路评审" : "问题与解决" }</ h2 >< p >{ isReviewer ? "逐项查看数据来源、AI判断和客户经理说明,再选择评审结论。" : "每个问题都说明影响和下一步;处理完成后重新AI预审。" }</ p >{ isReviewer && < label className = "ra-field xb-review-opinion" >< span > 常用评审意见(选择后应用到下方操作) </ span >< select value = { reviewOpinion } onChange = { e => setReviewOpinion ( e . target . value )}>< option > 请补充最新材料并说明数据口径 </ option >< option > 证据充分,同意 AI判断 </ option >< option > AI判断不适用于本项目 ,以人工核实为准 </ option >< option > 请补充现场核实记录 </ option ></ select ></ label >}{ ! project . findings . length && < p > 尚无问题记录,请先执行检查。 </ p >}{[... project . findings ]. sort (( a :any , b :any ) => Number ([ "已解决" , "不适用" ]. includes ( a . status )) - Number ([ "已解决" , "不适用" ]. includes ( b . status ))). map (( finding :any ) => < FindingCard key = { finding . id } finding = { finding } evidence = { evidence } isReviewer = { isReviewer } busy = { busy ||! hasReviewClaim } reason = { reason } onManager = {( status :string ) => act ( ` ${ base } /findings/ ${ finding . id } /actions` ,{ action :status , revision :finding.revision , note :reason })} onReviewer = {( decision :string ) => reviewDecision ( finding . id , decision )} decision = { project . review_decisions ? . find (( item :any ) => item . finding_id === finding . id )}/>)}{ ! isReviewer && <>< div className = "xb-form-grid" >< label className = "ra-field" >< span > 人工补充字段 </ span >< select value = { field } onChange = { e => setField ( e . target . value )}>{ fields . map ( f => < option key = { f [ 0 ]} value = { f [ 0 ]}>{ f [ 1 ]}</ option >)}</ select ></ label >< label className = "ra-field" >< span > 确认值(留空表示资料仍缺失) </ span >< input value = { value } onChange = { e => setValue ( e . target . value )} placeholder = { field === "collection_dependency" ? "true 或 false" : "输入实际核对值" }/></ label >< label className = "ra-field" >< span > 处理说明 </ span >< input value = { reason } onChange = { e => setReason ( e . target . value )} placeholder = "说明材料、口径或核实情况" /></ label ></ div >< button className = "ra-button primary" disabled = { busy || ! reason . trim ()} onClick = {() => act ( ` ${ base } /supplements` , { changes : {[ field ] : value === "" ? null : field === "collection_dependency" ? value === "true" ? true : value === "false" ? false : value : value }, finding_ids : [], revision : project.revision , reason }, "补件已保存,原来源已保留;请重新检查确认问题变化。" )}> 保存补件 </ button ></>}{ isReviewer && < div className = "xb-review-final" >< div >< CheckCircle2 size = { 21 }/>< span >< b > 完成全链路核查 </ b >< small > AI只提供建议 ,批准或退回由评审员确认。 </ small ></ span ></ div >< button className = "ra-button" disabled = { busy ||! hasReviewClaim } onClick = {() => completeReview ( "退回" )}> 退回补充材料 </ button >< button className = "ra-button primary" disabled = { busy ||! hasReviewClaim ||! currentRun } onClick = {() => completeReview ( "批准" )}> 批准并生成评审报告 </ button ></ div >}</ section ></>}
{ tab === "report" && < section className = "xb-panel xb-wide" >< h2 >{ isReviewer ? "独立评审报告" : "AI预审报告" }</ h2 >< p > 模板、运行快照、 Skills 与规则版本随报告冻结;每个历史版本都可以重新打开。 </ p >< div className = "xb-actions" >< select aria-label = "报告模板" value = { template } onChange = { e => setTemplate ( e . target . value )}>{ isReviewer ? <>< option value = "template.independent-review" > 独立评审报告 </ option >< option value = "template.committee-brief" > 上会摘要 </ option ></> : < option value = "template.ai-pre-review" > AI预审意见 </ option >}</ select >< select aria-label = "报告来源运行" value = { reportSourceRun ? . id || "" } onChange = { e => setRunId ( e . target . value )}>{ finished . map (( r : any ) => < option key = { r . id } value = { r . id }>{ new Date ( r . created_at ). toLocaleString ()} · { statuses [ r . status ]}</ option >)}</ select >< button className = "ra-button primary" disabled = { busy || ! currentRun } onClick = { async () => { const r = await act ( ` ${ base } /reports` , { run_id : reportSourceRun.id , template_id : template }); if ( r ) setReportId ( r . id );}}>< FileText size = { 15 }/> 生成报告 </ button ></ div >{ chosenReport && < div className = "xb-report" >< select aria-label = "选择报告版本" value = { chosenReport . id } onChange = { e => { setReportId ( e . target . value ); setSectionEdits ({});}}>{ project . reports . map (( r : any ) => < option key = { r . id } value = { r . id }>{ r . template . name } · { statuses [ r . status ]} · { r . id . slice ( - 8 )}</ option >)}</ select >< p >{ chosenReport . disclaimer } · { statuses [ chosenReport . status ]}</ p >< small > 来源快照: { chosenReport . snapshot_id }</ small >< div className = "xb-actions" >< button className = "ra-button" onClick = {() => download ( ` ${ base } /reports/ ${ chosenReport . id } /export` , ` ${ project . id } - ${ chosenReport . template . name } .docx` )}> 导出 Word </ button >< button className = "ra-button" disabled = { busy || ! currentRun || [ "finalized" , "submitted" ]. includes ( chosenReport . status )} onClick = {() => reportAction ( "regenerate" , { run_id : currentRun.id })}> 按最新运行更新候选 </ button >{ isManager && < button className = "ra-button primary" disabled = { busy || chosenReport . status === "submitted" } onClick = {() => reportAction ( "submit" )}> 提交正式评审 </ button >}{ canReview && < button className = "ra-button primary" disabled = { busy || chosenReport . status === "finalized" } onClick = {() => reportAction ( "finalize" )}> 确认评审定稿 </ button >}</ div >{ chosenReport . sections . map (( s : any ) => < article key = { s . id }>< h3 >{ s . title } { s . stale ? "· 来源已过期" : "" } { s . locked ? "· 已锁定" : "" } { s . confirmed ? "· 已确认" : "" }</ h3 >< textarea aria-label = { `编辑 ${ s . title } ` } className = "xb-section-text" disabled = { s . locked || [ "overview" , "findings" ]. includes ( s . id ) || [ "finalized" , "submitted" ]. includes ( chosenReport . status )} value = { sectionEdits [ s . id ] ?? s . content } onChange = { e => setSectionEdits ({... sectionEdits , [ s . id ] : e . target . value })}/>{ ! [ "finalized" , "submitted" ]. includes ( chosenReport . status ) && < div className = "xb-actions" >< button className = "ra-button" disabled = { busy || s . locked || [ "overview" , "findings" ]. includes ( s . id )} onClick = {() => reportAction ( "section" , { section_id : s.id , content : sectionEdits [ s . id ] ?? s . content , confirmed : false })}> 保存修改 </ button >< button className = "ra-button" disabled = { busy || s . stale } onClick = {() => reportAction ( "section" , { section_id : s.id , content : sectionEdits [ s . id ] ?? s . content , confirmed : true , locked : true })}> 确认并锁定 </ button >{ s . locked && < button className = "ra-button" disabled = { busy } onClick = {() => reportAction ( "section" , { section_id : s.id , locked : false , confirmed : false })}> 解除锁定 </ button >}</ div >}{ s . candidate && < details open >< summary > 更新候选(未覆盖当前正文) </ summary >< pre >{ s . candidate . content }</ pre >< button className = "ra-button" disabled = { busy } onClick = {() => reportAction ( "section" , { section_id : s.id , accept_candidate : true })}> 采纳候选内容 </ button ></ details >}</ article >)}</ div >}</ section >}
2026-09-11 15:56:28 +08:00
{ tab === "experience" && < section className = "xb-panel xb-wide" >< h2 > 专业反馈与经验复用 </ h2 >< p > 先记录具体纠正,再形成经验草稿。经验需明确适用和例外条件,评测后审核发布;不会自动变成正式制度。 </ p >< div className = "xb-form-grid" >< label className = "ra-field" >< span > 反馈类别 </ span >< select value = { category } onChange = { e => setCategory ( e . target . value )}>{ categories . map ( c => < option key = { c [ 0 ]} value = { c [ 0 ]}>{ c [ 1 ]}</ option >)}</ select ></ label >< label className = "ra-field" >< span > 反馈内容(关联最近一次已结束运行) </ span >< textarea value = { feedbackNote } onChange = { e => setFeedbackNote ( e . target . value )}/></ label ></ div >< button className = "ra-button" disabled = { busy || ! currentRun || ! feedbackNote . trim ()} onClick = { async () => { const f = await act ( ` ${ base } /feedback` , { category , note : feedbackNote , target_kind : "run" , target_id : currentRun.id }); if ( f ) setFeedbackId ( f . id );}}> 记录反馈 </ button >
< div className = "xb-actions" >< button className = "ra-button" disabled = { busy || running || ! feedbackId } onClick = {() => act ( ` ${ base } /runs` , { feedback_id : feedbackId , requirement : "根据选定专业反馈提炼经验草稿,不改变正式制度" , plan_id : plan , revision : project.revision }, "经验提炼任务已排队;结果仅为本项目草稿,未配置模型时会明确失败。" )}> 从选定反馈提炼经验草稿 </ button ></ div >< h3 > 创建经验草稿 </ h3 >< label className = "ra-field" >< span > 来源反馈 </ span >< select value = { feedbackId } onChange = { e => setFeedbackId ( e . target . value )}>< option value = "" > 请选择反馈 </ option >{ project . feedback . map (( f : any ) => < option key = { f . id } value = { f . id }>{ f . note }</ option >)}</ select ></ label >< div className = "xb-form-grid" >{[[ "title" , "经验标题" ], [ "trigger" , "适用条件说明" ], [ "action" , "建议核查动作" ], [ "exceptions" , "不适用 / 例外说明" ]]. map (([ key , label ]) => < label className = "ra-field" key = { key }>< span >{ label }</ span >< input value = {( experience as any )[ key ]} onChange = { e => setExperience ({... experience , [ key ] : e . target . value })}/></ label >)}< label className = "ra-field" >< span > 触发条件 </ span >< select value = { experience . condition_field } onChange = { e => setExperience ({... experience , condition_field : e.target.value })}>< option value = "collection.dependency" > 依赖特定回款 </ option ></ select >< select aria-label = "触发条件值" value = { experience . condition_value } onChange = { e => setExperience ({... experience , condition_value : e.target.value })}>< option value = "true" > 是 </ option >< option value = "false" > 否 </ option ></ select ></ label >< label className = "ra-field" >< span > 执行动作与例外 </ span >< p > 检查回款依据是否缺失;已有到账依据时不再触发。 </ p ></ label >< label className = "ra-field" >< span > 范围 </ span >< select value = { experience . scope } onChange = { e => setExperience ({... experience , scope : e.target.value })}>< option value = "project" > 仅本项目 </ option >< option value = "team" > 指定团队 </ option ></ select ></ label >{ experience . scope === "team" && < label className = "ra-field" >< span > 团队 </ span >< input value = { experience . team } placeholder = "本机演示可填 demo" onChange = { e => setExperience ({... experience , team : e.target.value })}/></ label >}< label className = "ra-field" >< span > 有效期 </ span >< input type = "date" value = { experience . expires_at } onChange = { e => setExperience ({... experience , expires_at : e.target.value })}/></ label ></ div >< button className = "ra-button primary" disabled = { busy || ! feedbackId || ! experience . title || ! experience . trigger || ! experience . action || ! experience . exceptions } onClick = {() => { const { condition_field , condition_value , ... body } = experience ; act ( ` ${ base } /experiences` , {... body , feedback_id : feedbackId , conditions : {[ condition_field ] : condition_value === "true" }, expires_at : experience.expires_at + "T23:59:59+08:00" }, "经验草稿已保存,尚未生效。" );}}> 保存经验草稿 </ button >
{ project . experiences . map (( x : any ) => < article className = "xb-panel" key = { x . id }>< span className = "xb-status" >{ statuses [ x . status ]}</ span >< h3 >{ x . title }</ h3 >< p > 适用: { x . trigger } ;例外: { x . exceptions }</ p >< p >{ x . action } · { x . scope === "team" ? `团队 ${ x . team } ` : "仅本项目" } · 有效至 { x . expires_at }</ p >< div className = "xb-actions" >{ x . status === "draft" && < button className = "ra-button" disabled = { busy } onClick = {() => act ( ` ${ base } /experiences/ ${ x . id } /actions` , { action : "evaluate" , revision : x.revision , note : "隔离留出集评测" })}> 评测适用与反例 </ button >}{ canReview && x . status === "draft" && < button className = "ra-button" disabled = { busy || ! x . evaluation ? . passed } onClick = {() => act ( ` ${ base } /experiences/ ${ x . id } /actions` , { action : "publish" , revision : x.revision , note : "确认脱敏条件与检查动作后发布" })}> 审核发布 </ button >}{ canReview && x . status === "published" && < button className = "ra-button" disabled = { busy } onClick = {() => act ( ` ${ base } /experiences/ ${ x . id } /actions` , { action : "rollback" , revision : x.revision , note : "停用经验,后续运行不再应用" })}> 停用回滚 </ button >}</ div >{ x . evaluation && < details >< summary > 评测结果: { x . evaluation . passed ? "通过" : "未通过" }</ summary >< pre >{ JSON . stringify ( x . evaluation , null , 2 )}</ pre ></ details >}</ article >)}< h3 > 本项目实际应用记录 </ h3 >{ project . experience_applications . length ? project . experience_applications . map (( a : any ) => < p key = { a . id }>{ a . experience_version_id } · { a . status } · { a . check_code } · { a . reason }</ p >) : < p > 尚未应用已发布经验。 </ p >}</ section >}
2026-09-15 10:31:50 +08:00
{ tab === "time" && < section className = "xb-panel xb-wide" >< div className = "xb-time-heading" >< div >< h2 > 本项目时间轨迹 </ h2 >< p > 自动区分人员处理、排队、外部等待和系统执行,无需额外打卡。 </ p ></ div >< a className = "ra-button primary" href = { `/operations-center?project= ${ encodeURIComponent ( project . id ) } ` }> 查看全局时效看板 </ a ></ div >< div className = "xb-timeline" >{( project . timeline || []). map (( item :any ) => < article key = { item . id }>< i className = { `category- ${ item . category } ` }/>< div >< b >{ item . phase_name }</ b >< p >{ detailText ( item . detail )}</ p >< small >{ new Date ( item . started_at ). toLocaleString ()} · { item . status === "running" ? "进行中" : "已结束" }</ small ></ div >< strong >{ durationText ( item . duration_hours )}</ strong ></ article >)}</ div ></ section >}
2026-09-11 15:56:28 +08:00
</ main ></ div >;
2026-09-08 02:49:06 +08:00
}
2026-09-15 10:31:50 +08:00
function durationText ( value :any ){
const hours = Number ( value || 0 );
if ( hours < 1 ) return ` ${ Math . max ( 1 , Math . round ( hours * 60 )) } 分钟` ;
if ( hours < 24 ) return ` ${ hours . toFixed ( 1 ) } 小时` ;
return ` ${ ( hours / 24 ). toFixed ( 1 ) } 天` ;
}
function detailText ( detail : any ) {
if ( typeof detail = == "string" && detail.trim ()) return detail ;
if ( detail ? .submitted_at ) return "评审员领取后自动开始记录办理时间" ;
if ( detail ? .plan_id ) return "使用已配置的业务检查方案自动执行" ;
if ( detail ? .template_id ) return "按已配置的报告模板自动编制" ;
if ( detail ? .demo ) return "演示数据:系统自动记录" ;
return "系统自动记录" ;
}
const resultLabels : Record < string , string > = { pass : "检查通过" , issue : "发现问题" , unknown : "材料不足" , not_applicable : "不适用" , error : "执行失败" } ;
function BusinessStep ( { step , evidence , statuses } : any ) {
const checks = step.findings ||[];
const needsAttention = checks.some (( item : any )= > [ "issue" , "unknown" , "error" ] .includes ( item.result ));
return < details className = { `xb-step-business ${ needsAttention ? "attention" : "ok" } ` } open = { needsAttention }>
< summary >< span >< b >{ step.name }</ b >< small >{ checks.length ? ` ${ checks . filter (( item :any ) => [ "issue" , "unknown" , "error" ]. includes ( item . result )). length } 项需关注` : "本步骤未形成业务问题" }</ small ></ span >< strong >{ statuses [ step.status ]|| step.status }</ strong ></ summary >
< div className = "xb-step-content" >
{ step.outputs ? .requirement_response && < div className = "xb-ai-answer" >< b > 对本次要求的回应 </ b >< p >{ step.outputs.requirement_response }</ p ></ div >}
{ step.errors ? .map (( item : any , index : number )= >< p className = "xb-warning" key = { index }> 该项未完成: { item . message }</ p >)}
{ checks . length ? < div className = "xb-business-checks" >{ checks . map (( item :any , index :number ) => { const refs = ( item . evidence_ids || []). map (( id :string ) => evidence . find (( source :any ) => source . id === id )). filter ( Boolean ); return < article key = { item . code || index }>< span className = { `xb-result result- ${ item . result } ` }>{ resultLabels [ item . result ] || item . result }</ span >< div >< b >{ item . title }</ b >< p >{ item . suggested_action || "无需额外处理" }</ p >{ refs . length > 0 && < small > 来源: { refs . map (( source :any ) => ` ${ source . source || "项目材料" } ( ${ source . locator || "已定位" } ) `). join ( "; " )}</ small >}</ div ></ article >;})}</ div > : < p > 本步骤未发现需要业务人员处理的事项。 </ p >}
< details >< summary > 技术明细 </ summary >< pre >{ JSON . stringify ({ skill :step.skill , status :step.status , outputs :step.outputs , missing_inputs :step.missing_inputs }, null , 2 )}</ pre ></ details >
</ div >
</ details >;
}
function FindingCard ({ finding , evidence , isReviewer , busy , reason , onManager , onReviewer , decision } : any ){
const refs = ( finding . evidence_ids || []). map (( id :string ) => evidence . find (( source :any ) => source . id === id )). filter ( Boolean );
const impact = finding . result === "unknown" ? "缺少足够材料,当前无法确认" : finding . result === "issue" ? "可能影响项目判断,需要人工确认" : "机器检查已通过,仍可人工复核" ;
return < article className = { `xb-issue-card result- ${ finding . result } ` }>
< header >< span className = { `xb-result result- ${ finding . result } ` }>{ resultLabels [ finding . result ] || finding . status }</ span >< div >< h3 >{ finding . title }</ h3 >< small > 当前状态: { finding . status }</ small ></ div ></ header >
< div className = "xb-chain" >
< section >< small > 1 · 数据来源 </ small >{ refs . length ? refs . map (( source :any , index :number ) => < p key = { source . id || index }>< b >{ source . source || "项目材料" }</ b >< span >{ source . locator || "已定位" }{ source . value === null ? " · 未提供" : source . value !== undefined ? ` · ${ String ( source . value ) } ` : "" }</ span ></ p >) : < p className = "missing" > 尚无可验证来源 </ p >}</ section >
< section >< small > 2 · AI判断 </ small >< p >< b >{ resultLabels [ finding . result ] || finding . result }</ b >< span >{ impact }</ span ></ p ></ section >
< section >< small > 3 · 客户经理说明 </ small >< p >< b >{ finding . human_note ? "已说明" : "尚未说明" }</ b >< span >{ finding . human_note || "评审员可要求补件或进一步核实" }</ span ></ p ></ section >
</ div >
< div className = "xb-solution" >< small > 怎么解决 </ small >< b >{ finding . suggested_action || "请核实材料、数据口径及业务事实" }</ b ></ div >
{ decision && < p className = "xb-saved-decision" > 评审结论: < b >{ decision . decision }</ b > · { decision . opinion || "无补充意见" }</ p >}
< div className = "xb-actions" >{ isReviewer ? <>< button className = "ra-button" disabled = { busy } onClick = {() => onReviewer ( "认可AI判断" )}> 认可 AI判断 </ button >< button className = "ra-button" disabled = { busy } onClick = {() => onReviewer ( "不采纳AI判断" )}> 不采纳 </ button >< button className = "ra-button primary" disabled = { busy } onClick = {() => onReviewer ( "要求补件" )}> 要求补件 </ button ></> : <>< button className = "ra-button" disabled = { busy ||! reason . trim ()} onClick = {() => onManager ( "待补件" )}> 标记待补件 </ button >< button className = "ra-button" disabled = { busy ||! reason . trim ()} onClick = {() => onManager ( "待复核" )}> 提交复核 </ button >< button className = "ra-button" disabled = { busy ||! reason . trim ()} onClick = {() => onManager ( "不适用" )}> 确认不适用 </ button ></>}</ div >
< details >< summary > 查看规则与技术追溯 </ summary >< p > 检查依据: {( finding . basis_ids || []). map (( item :string ) => item . split ( "@" )[ 0 ]). join ( "、" ) || "未记录" }</ p >< pre >{ JSON . stringify ({ code :finding.code , basis_ids :finding.basis_ids , evidence_ids :finding.evidence_ids , history :finding.history }, null , 2 )}</ pre ></ details >
</ article >;
}