Files
mini-openclaw/src/components/xingbang/ReviewerWorkspace.tsx
T

56 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {useEffect, useState} from "react";
import {AlertTriangle, CheckCircle2, CircleDashed, Database, MapPin, ShieldAlert} from "lucide-react";
const AI_STATUS:Record<string,{label:string;note:string}>={
pass:{label:"未发现异常",note:"AI检查正常,仍需评审员按本章节确认"},
issue:{label:"发现异常",note:"请结合原始数据作出独立判断"},
missing:{label:"资料不足",note:"缺少数据或口径,不能视为检查通过"},
not_run:{label:"尚未检查",note:"请先发起独立评审"},
};
const CHECK_LABEL:Record<string,string>={pass:"正常",issue:"异常",unknown:"资料不足",error:"执行失败",not_applicable:"不适用"};
function valueText(value:any){
if(value===null||value===undefined||value==="")return "未提供";
if(typeof value==="object")return Object.entries(value).map(([key,item])=>`${key}:${String(item)}`).join(";");
return String(value);
}
export default function ReviewerWorkspace({topics,reviewed,blocking,busy,opinion,setOpinion,onDecision,onComplete,hasRun}:any){
const total=topics.length;
const [returnReason,setReturnReason]=useState("");
const issues=topics.filter((item:any)=>item.ai_status==="issue").length;
const incomplete=topics.filter((item:any)=>["missing","not_run"].includes(item.ai_status)).length;
const pending=topics.filter((item:any)=>!item.decision).map((item:any)=>item.title);
const canApprove=hasRun&&total>0&&reviewed===total&&!blocking.length;
return <section className="xb-panel xb-wide xb-full-review" id="full-review">
<header className="xb-review-heading"><div><span>评审员独立判断</span><h2>全链路评审</h2><p>每个结论自动绑定本次数据快照、材料定位和检查规则;AI只提示,最终结论由评审员选择。</p></div><div className="xb-review-progress"><strong>{reviewed}/{total}</strong><small>必审领域已确认</small></div></header>
<div className="xb-review-kpis"><article><b>{total}</b><span>必审领域</span></article><article className="danger"><b>{issues}</b><span>AI发现异常</span></article><article className="warning"><b>{incomplete}</b><span>资料不足/未检查</span></article><article><b>{blocking.length}</b><span>尚未闭环</span></article></div>
<label className="ra-field xb-review-opinion"><span>常用意见模板(选择后填入尚未确认的章节)</span><select value={opinion} onChange={(event)=>setOpinion(event.target.value)}><option value="">请选择,或直接在章节内填写</option><option>请补充最新材料并说明数据口径</option><option>风险已有缓释措施,整体可控</option><option>AI判断不适用于本项目,以人工核实为准</option><option>请补充现场核实记录</option><option>该事项不适用于本项目</option></select></label>
<div className="xb-review-lanes" aria-hidden="true"><span><Database size={15}/>数据与来源</span><span><ShieldAlert size={15}/>AI辅助判断</span><span><CheckCircle2 size={15}/>评审员结论</span></div>
<div className="xb-topic-list">{topics.map((topic:any)=><ReviewTopic key={topic.id} topic={topic} busy={busy} opinion={opinion} onDecision={onDecision}/>)}</div>
{!hasRun&&<div className="xb-review-hint"><CircleDashed size={18}/><span>请先在页面上方发起一次独立评审,系统才会冻结本次数据与规则版本。</span></div>}
<div className="xb-review-final"><div><CheckCircle2 size={21}/><span><b>完成全链路核查</b><small>{pending.length?`待确认:${pending.join("、")}`:blocking.length?`尚未闭环:${blocking.map((item:any)=>item.title).join("、")}`:"所有必审领域均已形成可追溯结论"}</small></span></div><label className="xb-return-reason"><span>退回原因(退回时必填)</span><textarea aria-label="退回补充材料原因" value={returnReason} maxLength={2000} onChange={(event)=>setReturnReason(event.target.value)} placeholder="例如:请补充最新银行流水并说明回款口径"/></label><button className="ra-button" title={!hasRun?"请先完成独立评审":!returnReason.trim()?"请先填写退回原因":"退回客户经理补充材料"} disabled={busy||!hasRun||!returnReason.trim()} onClick={()=>onComplete("退回",returnReason.trim())}>退回补充材料</button><button className="ra-button primary" disabled={busy||!canApprove} onClick={()=>onComplete("批准")}>批准并生成评审报告</button></div>
</section>;
}
function ReviewTopic({topic,busy,opinion,onDecision}:any){
const status=AI_STATUS[topic.ai_status]||AI_STATUS.not_run;
const needsAttention=topic.ai_status!=="pass";
const decision=topic.decision;
const [draft,setDraft]=useState(decision?.opinion||"");
useEffect(()=>{setDraft(decision?.opinion||"");},[decision?.id,decision?.opinion]);
useEffect(()=>{if(!decision&&opinion)setDraft(opinion);},[opinion,decision]);
const requiredOpinion=!draft.trim();
return <details className={`xb-review-topic status-${topic.ai_status}`} open={needsAttention||!decision}>
<summary><span className="xb-topic-state">{topic.ai_status==="pass"?<CheckCircle2/>:<AlertTriangle/>}</span><span><b>{topic.title}</b><small>{status.label} · {topic.sources.length} 条数据来源 · {topic.checks.length} 项AI检查</small></span><strong>{decision?`已确认:${decision.decision}`:"待评审员确认"}</strong></summary>
<div className="xb-topic-lanes">
<section><h3>数据与来源</h3>{topic.sources.length?topic.sources.map((source:any)=><details className="xb-source" key={source.id}><summary><span><b>{source.source||"项目数据"}</b><small><MapPin size={12}/>{source.locator||"已定位"}</small></span><em>查看来源</em></summary><p><b>数据项</b>{source.field||"未标记"}</p><p><b>原始值</b>{valueText(source.value)}</p><p><b>证据编号</b>{source.id}</p><p><b>冻结快照</b>{topic.snapshot_id||"尚未形成"}</p></details>):<div className="xb-empty-source"><AlertTriangle size={17}/>没有可验证的数据来源</div>}</section>
<section><h3>AI辅助判断</h3><div className={`xb-ai-state status-${topic.ai_status}`}><b>{status.label}</b><p>{status.note}</p></div>{topic.checks.map((check:any,index:number)=><article className="xb-ai-check" key={check.code||index}><span>{CHECK_LABEL[check.result]||check.result}</span><div><b>{check.title}</b><p>{check.suggested_action||"无需额外处理"}</p><small>依据:{(check.basis_ids||[]).join("、")||"人工核查项"}</small></div></article>)}</section>
<section><h3>评审员结论</h3>{decision&&<div className="xb-current-decision"><b>{decision.decision}</b><p>{decision.opinion||"已复核本章节数据和AI结果"}</p><small>{new Date(decision.updated_at).toLocaleString()} · 已绑定 {decision.evidence_ids?.length||0} 条证据</small></div>}<label className="xb-decision-editor"><span>{decision?"修改本章节意见":"填写本章节意见"}</span><textarea aria-label={`${topic.title}评审意见`} value={draft} maxLength={2000} onChange={(event)=>setDraft(event.target.value)} placeholder={topic.ai_status==="pass"?"可填写补充判断;无补充可直接确认":"请说明判断依据、补件要求或处理意见"}/><small>{draft.length}/2000</small></label><div className="xb-decision-actions">{topic.ai_status==="pass"?<button className="ra-button primary" disabled={busy} onClick={()=>onDecision(topic.id,"确认无异常",draft.trim()||"已复核本章节数据来源和AI检查结果")}>{decision?"保存本章节":"确认本章节"}</button>:<><button className="ra-button" disabled={busy||requiredOpinion} onClick={()=>onDecision(topic.id,"确认风险可控",draft)}>风险可控</button><button className="ra-button" disabled={busy||requiredOpinion} onClick={()=>onDecision(topic.id,"要求补件",draft)}>要求补件</button><button className="ra-button" disabled={busy||requiredOpinion} onClick={()=>onDecision(topic.id,"转现场核查",draft)}>现场核查</button><button className="ra-button" disabled={busy||requiredOpinion} onClick={()=>onDecision(topic.id,"不采纳AI判断",draft)}>不采纳AI</button><button className="ra-button" disabled={busy||requiredOpinion} onClick={()=>onDecision(topic.id,"不适用",draft)}>不适用</button></>}</div><small className="xb-trace-note">保存后自动记录:评审员、时间、数据快照、证据编号、Skill/规则版本。</small></section>
</div>
</details>;
}