fix: 支持逐节点填写并保存评审意见
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const component = fs.readFileSync(path.join(root, "src/components/xingbang/ReviewerWorkspace.tsx"), "utf8");
|
||||
const page = fs.readFileSync(path.join(root, "src/app/xingbang-workbench/page.tsx"), "utf8");
|
||||
|
||||
assert.match(component, /aria-label=\{`\$\{topic\.title\}评审意见`\}/, "每个评审节点应提供独立意见输入框");
|
||||
assert.match(component, /onChange=\{\(event\)=>setDraft\(event\.target\.value\)\}/, "输入框应允许修改节点意见");
|
||||
assert.match(component, /draft\.trim\(\)\|\|"已复核本章节数据来源和AI检查结果"/, "正常节点应保存人工输入或明确的默认复核说明");
|
||||
assert.match(component, /disabled=\{busy\|\|requiredOpinion\}/, "需要说明依据的评审动作不应允许空意见提交");
|
||||
assert.match(page, /setReviewOpinion\]=useState\(""\)/, "常用模板默认不应冒充人工意见");
|
||||
|
||||
console.log("Reviewer workspace input behavior checks passed");
|
||||
@@ -208,6 +208,7 @@ def main() -> int:
|
||||
checks = [
|
||||
("repository-diff", [git, "diff", "--check"], ROOT),
|
||||
("backend-unit", [project_python(), "-m", "unittest", "discover", "-s", ".", "-p", "test_*.py", "-v"], BACKEND),
|
||||
("reviewer-workspace-ui", [executable("node"), "scripts/test_reviewer_workspace.cjs"], ROOT),
|
||||
("frontend-build", [npm, "run", "build"], ROOT),
|
||||
("compose-local-config", [docker, "compose", "-f", "docker-compose.yml", "config", "--quiet"], ROOT),
|
||||
("compose-aliyun-config", [docker, "compose", "--env-file", "deploy/aliyun.env.example", "-f", "deploy/docker-compose.aliyun.yml", "config", "--quiet"], ROOT),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -19,7 +19,7 @@ export default function XingbangWorkspace() {
|
||||
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>>({});
|
||||
const [category, setCategory] = useState("rule_omission"), [feedbackNote, setFeedbackNote] = useState(""), [feedbackId, setFeedbackId] = useState("");
|
||||
const [reviewOpinion,setReviewOpinion]=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} : {})};};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"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}>={
|
||||
@@ -26,7 +27,7 @@ export default function ReviewerWorkspace({topics,reviewed,blocking,busy,opinion
|
||||
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>请补充最新材料并说明数据口径</option><option>风险已有缓释措施,整体可控</option><option>AI判断不适用于本项目,以人工核实为准</option><option>请补充现场核实记录</option><option>该事项不适用于本项目</option></select></label>
|
||||
<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>}
|
||||
@@ -38,12 +39,16 @@ 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>:<p className="xb-pending-decision">尚未形成人工结论</p>}<div className="xb-decision-actions">{topic.ai_status==="pass"?<button className="ra-button primary" disabled={busy} onClick={()=>onDecision(topic.id,"确认无异常","已复核本章节数据来源和AI检查结果")}>确认本章节</button>:<><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"确认风险可控",opinion)}>风险可控</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"要求补件",opinion)}>要求补件</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"转现场核查",opinion)}>现场核查</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"不采纳AI判断",opinion)}>不采纳AI</button><button className="ra-button" disabled={busy} onClick={()=>onDecision(topic.id,"不适用",opinion)}>不适用</button></>}</div><small className="xb-trace-note">保存后自动记录:评审员、时间、数据快照、证据编号、Skill/规则版本。</small></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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user