chore: freeze local demo baseline

This commit is contained in:
NathanAlex7
2026-09-09 12:32:46 +08:00
parent b5073c3841
commit 299a3eb803
23 changed files with 123 additions and 5302 deletions
File diff suppressed because one or more lines are too long
+5 -4
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { Activity, ArrowLeft, ArrowRight, Boxes, Check, CheckCircle2, ChevronRight, CircleDashed, Copy, Database, FileCode2, History, Layers3, Loader2, Pencil, Play, Plus, Search, ShieldCheck, Sparkles, Trash2, Workflow, X } from "lucide-react";
import "./workspace.css";
import {ConnectionFields, AlignmentFields, RuleFields, AccessDialog, Reviews, ResultExtras} from "./Enhancements";
import AgentNav from "@/components/xingbang/AgentNav";
type Item = { id: string; name: string; description?: string; status: string; version: string; [key: string]: any };
type Kind = "connections" | "cards" | "skills" | "rules" | "plans";
@@ -29,7 +30,7 @@ export default function RiskAgentConsole() {
async function reload() { setCatalog(await api("/catalog")); }
const [access,setAccess]=useState<any>({role:"admin",preview:true}),[accessOpen,setAccessOpen]=useState(false),[reviewItems,setReviewItems]=useState<any[]>([]),[previous,setPrevious]=useState<any>(null);
const canManage=access.role!=="manager", canEdit=access.role==="admin";
async function connectIdentity(){try{const user=await api("/access/context");setAccess(user);setResult(null);setHistory([]);setEditor(null);setView(user.role==="manager"?"run":"cards");await reload();if(user.role!=="manager")setNativeSkills(await api("/native-skills/list"));setAccessOpen(false);setError("")}catch(e:any){setError(e.message);setCatalog(EMPTY);setAccessOpen(true)}finally{setLoading(false)}}
async function connectIdentity(){try{const user=await api("/access/context");const requested=typeof window!=="undefined"?new URLSearchParams(window.location.search).get("view"):null;const initialView=requested&&["run","history","rules"].includes(requested)?requested:(user.role==="manager"?"run":"cards");setAccess(user);setResult(null);setHistory([]);setEditor(null);setView(initialView as Kind|"run"|"history");await reload();if(user.role!=="manager")setNativeSkills(await api("/native-skills/list"));setAccessOpen(false);setError("")}catch(e:any){setError(e.message);setCatalog(EMPTY);setAccessOpen(true)}finally{setLoading(false)}}
useEffect(()=>{connectIdentity()},[]);
async function perform(fn:()=>Promise<void>){setBusy(true);setError("");setNotice("");try{await fn()}catch(e:any){setError(e.message)}finally{setBusy(false)}}
function open(kind:Kind,item?:Item) {
@@ -51,8 +52,8 @@ export default function RiskAgentConsole() {
const title=view==="reviews"?"发布审核":view==="run"?"项目提交前自查":view==="history"?"研判运行记录":META[view as Kind].label;
const subtitle=view==="cards"?"把业务数据封装成可复用的数据卡,连接每一次风险研判。":view==="skills"?"组合专业检查、确定性计算与语义分析能力。":view==="plans"?"选择数据、能力与规则,形成客户经理的一键自查方案。":view==="run"?"项目事实、专业检查与制度规则在同一条证据链上汇合。":view==="rules"?"管理检查条件与核查建议,保留每一个制度版本。":view==="connections"?"统一维护连接配置,由数据卡复用。":view==="reviews"?"核对提交版本,由独立审核员发布或驳回。":"回看数据快照、配置版本、执行结果与证据。";
const items=view in META?catalog[view as Kind].filter(x=>(status==="all"||x.status!=="archived")&&(x.name+" "+x.description).toLowerCase().includes(search.toLowerCase())):[];
return <div className="ra-app">
<aside className="ra-sidebar"><a href="/" className="ra-brand"><div className="ra-brand-mark"><Activity size={22}/></div><div>兴邦金租<small>RISK INTELLIGENCE</small></div></a><div className="ra-side-label">客户经理</div><button className={view==="run"?"active":""} onClick={()=>setView("run")}><ShieldCheck size={18}/>项目风险自查</button><button className={view==="history"?"active":""} onClick={()=>{setView("history");perform(async()=>setHistory(await api("/runs/history")))}}><History size={18}/>运行记录</button><div className="ra-side-label">配置管理</div>{(canManage?Object.keys(META) as Kind[]:[]).map(k=>{const Icon=META[k].icon;return <button key={k} className={view===k?"active":""} onClick={()=>{setView(k);setSearch("")}}><Icon size={18}/>{META[k].label}<span>{active(k).length}</span></button>})}{canManage&&<button className={view==="reviews"?"active":""} onClick={()=>{setView("reviews");perform(async()=>setReviewItems(await api("/reviews/list")))}}><ShieldCheck size={18}/>发布审核</button>}<div className="ra-sidebar-bottom"><div className="ra-env-dot"/>本地业务验证环境<small>示例数据 · 演示规则</small><a href="/xingbang-unified"><ArrowLeft size={14}/>返回评审工作台</a></div></aside>
return <div className="xb-agent-shell"><AgentNav active="risk" project={project}/><div className="ra-app">
<aside className="ra-sidebar"><a href="/" className="ra-brand"><div className="ra-brand-mark"><Activity size={22}/></div><div>兴邦金租<small>RISK INTELLIGENCE</small></div></a><div className="ra-side-label">客户经理</div><button className={view==="run"?"active":""} onClick={()=>setView("run")}><ShieldCheck size={18}/>项目风险自查</button><button className={view==="history"?"active":""} onClick={()=>{setView("history");perform(async()=>setHistory(await api("/runs/history")))}}><History size={18}/>运行记录</button><div className="ra-side-label">配置管理</div>{(canManage?Object.keys(META) as Kind[]:[]).map(k=>{const Icon=META[k].icon;return <button key={k} className={view===k?"active":""} onClick={()=>{setView(k);setSearch("")}}><Icon size={18}/>{META[k].label}<span>{active(k).length}</span></button>})}{canManage&&<button className={view==="reviews"?"active":""} onClick={()=>{setView("reviews");perform(async()=>setReviewItems(await api("/reviews/list")))}}><ShieldCheck size={18}/>发布审核</button>}<div className="ra-sidebar-bottom"><div className="ra-env-dot"/>本地业务验证环境<small>示例数据 · 演示规则</small><a href="/xingbang-workbench"><ArrowLeft size={14}/>返回项目总览</a></div></aside>
<div className="ra-body"><header className="ra-top"><div>风险研判 Agent <ChevronRight size={14}/><b>{title}</b></div><span className="ra-badge">业务验证版</span><button className="ra-button" onClick={()=>setAccessOpen(true)}>{access.preview?"本机预览":({admin:"配置管理员",manager:"客户经理",reviewer:"发布审核员"} as Record<string,string>)[access.role]}</button></header>
<main className="ra-main"><div className="ra-heading"><div><div className="ra-eyebrow">RISK AGENT / {view.toUpperCase()}</div><h1>{title}</h1><p>{subtitle}</p></div>{canEdit&&view in META&&<button className="ra-button primary" onClick={()=>open(view as Kind)}><Plus size={16}/>新建{META[view as Kind].label}</button>}</div>
{error&&<div role="alert" className="ra-alert error">{error}<button title="关闭错误" onClick={()=>setError("")}><X size={16}/></button></div>}{notice&&<div role="status" className="ra-alert">{notice}<button title="关闭提示" onClick={()=>setNotice("")}><X size={16}/></button></div>}
@@ -69,7 +70,7 @@ export default function RiskAgentConsole() {
</div><footer><button className="ra-button" onClick={()=>setEditor(null)}>取消</button><button disabled={busy||!editor.item.name.trim()} className="ra-button" onClick={()=>save()}>保存草稿</button><button disabled={busy||!editor.item.name.trim()} className="ra-button primary" onClick={()=>save(true)}><Check size={16}/>{access.preview?"保存并发布":"保存并提交审核"}</button></footer></section></div>}
{confirmDelete&&<div className="ra-overlay"><section className="ra-confirm" role="dialog" aria-modal="true"><Trash2 size={24}/><h2>归档「{confirmDelete.item.name}」?</h2><p>配置将停止使用,历史运行快照保留。被其他有效配置引用时需要先解除关联。</p><div><button className="ra-button" onClick={()=>setConfirmDelete(null)}>取消</button><button disabled={busy} className="ra-button primary" onClick={()=>perform(async()=>{await api(`/${confirmDelete.kind}/${confirmDelete.item.id}`,"DELETE");await reload();setConfirmDelete(null);setNotice("配置已归档")})}>确认归档</button></div>{error&&<p role="alert">{error}</p>}</section></div>}
{accessOpen&&<AccessDialog close={()=>setAccessOpen(false)} connected={connectIdentity}/>}
</div>;
</div></div>;
}
function Field({label,children}:{label:string;children:React.ReactNode}){return <label className="ra-field"><span>{label}</span>{children}</label>}
function CheckList({title,items,checked,onToggle}:{title:string;items:Item[];checked:string[];onToggle:(id:string)=>void}){return <section className="ra-checks"><h3>{title}<span>{checked.length} 已选</span></h3>{items.map(item=><label key={item.id}><input type="checkbox" checked={checked.includes(item.id)} onChange={()=>onToggle(item.id)}/><div><b>{item.name}</b><small>{item.description}</small></div><span className={`ra-status ${item.status}`}>{STATUS[item.status]}</span></label>)}</section>}
+30
View File
@@ -59,6 +59,36 @@ body {
border-radius: 10px;
min-height: 40px;
}
.xingbang-mark {
width: 28px;
height: 28px;
display: grid;
place-items: center;
flex: 0 0 auto;
border-radius: 7px;
background: #17634c;
color: #fff;
font-family: "Microsoft YaHei", sans-serif;
font-size: 15px;
font-weight: 700;
box-shadow: inset 0 -2px 0 rgba(0,0,0,.12);
}
.xingbang-chat-mark {
width: 58px;
height: 58px;
display: grid;
place-items: center;
margin-bottom: 16px;
border-radius: 16px;
background: #17634c;
color: #fff;
font-family: "Microsoft YaHei", sans-serif;
font-size: 28px;
font-weight: 700;
box-shadow: 0 8px 20px rgba(23,99,76,.18), inset 0 -3px 0 rgba(0,0,0,.12);
}
::-webkit-scrollbar-thumb:hover { background: #596f91; }
::-webkit-scrollbar-thumb:active { background: var(--accent); }
-370
View File
@@ -1,370 +0,0 @@
"use client";
import { useState } from "react";
import { Database, Search, Filter, Calendar, TrendingUp, CheckCircle, XCircle, AlertCircle } from "lucide-react";
interface HistoricalCase {
case_id: string;
customer_name: string;
project_type: string;
amount: number;
debt_ratio: number;
credit_query_count: number;
litigation_count: number;
decision: string;
decision_date: string;
processing_days: number;
mitigation_measures: string;
final_outcome: string;
risk_level: "high" | "medium" | "low";
}
export default function HistoricalDataPage() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedDecision, setSelectedDecision] = useState("all");
const [selectedRiskLevel, setSelectedRiskLevel] = useState("all");
const [dateRange, setDateRange] = useState("12");
// 模拟历史案例数据
const [cases] = useState<HistoricalCase[]>([
{
case_id: "CAS-2025-08-156",
customer_name: "华东光伏设备公司",
project_type: "新能源设备",
amount: 2.8,
debt_ratio: 0.68,
credit_query_count: 7,
litigation_count: 1,
decision: "降额通过",
decision_date: "2025-08-15",
processing_days: 4,
mitigation_measures: "降低授信额度15%,增加法人个人担保,提供政府补贴承诺函",
final_outcome: "已放款,回款正常",
risk_level: "medium"
},
{
case_id: "CAS-2025-07-089",
customer_name: "绿能公交运营公司",
project_type: "对公新能源",
amount: 3.5,
debt_ratio: 0.72,
credit_query_count: 9,
litigation_count: 0,
decision: "降额通过",
decision_date: "2025-07-22",
processing_days: 6,
mitigation_measures: "降低授信额度12%,调整还款期限,追加车辆抵押",
final_outcome: "已放款,回款正常",
risk_level: "high"
},
{
case_id: "CAS-2025-06-234",
customer_name: "中部新能源投资",
project_type: "新能源投资",
amount: 4.2,
debt_ratio: 0.65,
credit_query_count: 6,
litigation_count: 2,
decision: "附加条件通过",
decision_date: "2025-06-18",
processing_days: 5,
mitigation_measures: "要求季度现金流报告,设置财务预警指标,增加母公司担保",
final_outcome: "已放款,回款正常",
risk_level: "high"
},
{
case_id: "CAS-2025-08-201",
customer_name: "南方物流运输公司",
project_type: "物流运输",
amount: 1.8,
debt_ratio: 0.58,
credit_query_count: 4,
litigation_count: 0,
decision: "直接通过",
decision_date: "2025-08-28",
processing_days: 2,
mitigation_measures: "无需额外措施",
final_outcome: "已放款,回款正常",
risk_level: "low"
},
{
case_id: "CAS-2025-07-145",
customer_name: "东部制造业集团",
project_type: "设备制造",
amount: 5.5,
debt_ratio: 0.75,
credit_query_count: 12,
litigation_count: 3,
decision: "退回",
decision_date: "2025-07-10",
processing_days: 8,
mitigation_measures: "要求补充材料后重新申请",
final_outcome: "客户未重新申请",
risk_level: "high"
},
{
case_id: "CAS-2025-06-098",
customer_name: "西部能源开发公司",
project_type: "能源开发",
amount: 6.0,
debt_ratio: 0.62,
credit_query_count: 5,
litigation_count: 1,
decision: "附加条件通过",
decision_date: "2025-06-05",
processing_days: 7,
mitigation_measures: "增加土地使用权抵押,提供项目收益权质押",
final_outcome: "已放款,回款正常",
risk_level: "medium"
},
{
case_id: "CAS-2025-08-178",
customer_name: "北方建筑工程公司",
project_type: "建筑工程",
amount: 3.2,
debt_ratio: 0.70,
credit_query_count: 8,
litigation_count: 2,
decision: "降额通过",
decision_date: "2025-08-20",
processing_days: 5,
mitigation_measures: "降低授信额度20%,要求项目方连带担保",
final_outcome: "已放款,回款正常",
risk_level: "high"
},
{
case_id: "CAS-2025-07-167",
customer_name: "中部科技创新公司",
project_type: "科技研发",
amount: 2.0,
debt_ratio: 0.55,
credit_query_count: 3,
litigation_count: 0,
decision: "直接通过",
decision_date: "2025-07-28",
processing_days: 3,
mitigation_measures: "无需额外措施",
final_outcome: "已放款,回款正常",
risk_level: "low"
}
]);
const decisions = ["all", "直接通过", "降额通过", "附加条件通过", "退回"];
const riskLevels = ["all", "high", "medium", "low"];
const filteredCases = cases.filter(c => {
const matchesSearch = c.customer_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.case_id.toLowerCase().includes(searchTerm.toLowerCase());
const matchesDecision = selectedDecision === "all" || c.decision === selectedDecision;
const matchesRisk = selectedRiskLevel === "all" || c.risk_level === selectedRiskLevel;
return matchesSearch && matchesDecision && matchesRisk;
});
// 统计数据
const stats = {
total: cases.length,
approved: cases.filter(c => c.decision.includes("通过")).length,
rejected: cases.filter(c => c.decision === "退回").length,
avgProcessingDays: (cases.reduce((sum, c) => sum + c.processing_days, 0) / cases.length).toFixed(1)
};
const getDecisionBadge = (decision: string) => {
if (decision.includes("直接通过")) {
return <span className="px-2 py-1 bg-green-600 rounded text-xs font-bold flex items-center gap-1"><CheckCircle className="w-3 h-3" /> {decision}</span>;
} else if (decision.includes("通过")) {
return <span className="px-2 py-1 bg-blue-600 rounded text-xs font-bold flex items-center gap-1"><CheckCircle className="w-3 h-3" /> {decision}</span>;
} else if (decision === "退回") {
return <span className="px-2 py-1 bg-red-600 rounded text-xs font-bold flex items-center gap-1"><XCircle className="w-3 h-3" /> {decision}</span>;
}
return <span className="px-2 py-1 bg-slate-600 rounded text-xs font-bold">{decision}</span>;
};
const getRiskBadge = (level: string) => {
switch (level) {
case "high": return <span className="px-2 py-1 bg-red-900/50 border border-red-600 rounded text-xs">高风险</span>;
case "medium": return <span className="px-2 py-1 bg-yellow-900/50 border border-yellow-600 rounded text-xs">中风险</span>;
case "low": return <span className="px-2 py-1 bg-green-900/50 border border-green-600 rounded text-xs">低风险</span>;
default: return null;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-6">
{/* Header */}
<header className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-cyan-400 to-blue-300 bg-clip-text text-transparent">
💾 历史数据分析
</h1>
<p className="text-slate-400">案例数据查询与分析(轨道2:数据驱动)</p>
</div>
</div>
</header>
{/* 统计卡片 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-cyan-400">{stats.total}</div>
<div className="text-sm text-slate-400">历史案例总数</div>
</div>
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-green-400">{stats.approved}</div>
<div className="text-sm text-slate-400">通过案例</div>
</div>
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-red-400">{stats.rejected}</div>
<div className="text-sm text-slate-400">退回案例</div>
</div>
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-blue-400">{stats.avgProcessingDays}</div>
<div className="text-sm text-slate-400">平均处理天数</div>
</div>
</div>
{/* 搜索和筛选 */}
<div className="mb-6 p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="space-y-4">
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="搜索客户名称或案例编号..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
{/* 筛选器 */}
<div className="flex flex-wrap gap-4">
{/* 决策筛选 */}
<div className="flex items-center gap-2">
<span className="text-sm text-slate-400">决策结果:</span>
<div className="flex gap-2">
{decisions.map(dec => (
<button
key={dec}
onClick={() => setSelectedDecision(dec)}
className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${
selectedDecision === dec
? "bg-blue-600 text-white"
: "bg-slate-700 text-slate-300 hover:bg-slate-600"
}`}
>
{dec === "all" ? "全部" : dec}
</button>
))}
</div>
</div>
{/* 风险等级筛选 */}
<div className="flex items-center gap-2">
<span className="text-sm text-slate-400">风险等级:</span>
<div className="flex gap-2">
{riskLevels.map(level => (
<button
key={level}
onClick={() => setSelectedRiskLevel(level)}
className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${
selectedRiskLevel === level
? "bg-cyan-600 text-white"
: "bg-slate-700 text-slate-300 hover:bg-slate-600"
}`}
>
{level === "all" ? "全部" : level === "high" ? "高" : level === "medium" ? "中" : "低"}
</button>
))}
</div>
</div>
{/* 时间范围 */}
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-slate-400" />
<select
value={dateRange}
onChange={e => setDateRange(e.target.value)}
className="px-3 py-1 bg-slate-700 border border-slate-600 rounded-lg text-sm text-white focus:outline-none focus:border-blue-500"
>
<option value="3">近3个月</option>
<option value="6">近6个月</option>
<option value="12">近12个月</option>
<option value="24">近24个月</option>
</select>
</div>
</div>
<div className="text-sm text-slate-400">
找到 <span className="text-cyan-400 font-bold">{filteredCases.length}</span> 个匹配案例
</div>
</div>
</div>
{/* 案例列表 */}
<div className="space-y-4">
{filteredCases.length === 0 ? (
<div className="p-8 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700 text-center text-slate-400">
未找到匹配的案例
</div>
) : (
filteredCases.map(case_ => (
<div
key={case_.case_id}
className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700 hover:border-cyan-600 transition-colors"
>
{/* 标题行 */}
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-xs text-slate-500 font-mono">{case_.case_id}</span>
{getDecisionBadge(case_.decision)}
{getRiskBadge(case_.risk_level)}
</div>
<h3 className="text-xl font-bold text-white">{case_.customer_name}</h3>
<p className="text-sm text-slate-400">{case_.project_type}</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-cyan-400">{case_.amount.toFixed(1)}亿</div>
<div className="text-xs text-slate-400">{case_.decision_date}</div>
</div>
</div>
{/* 关键指标 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
<div className="p-3 bg-slate-900/50 rounded-lg">
<div className="text-xs text-slate-400">资产负债率</div>
<div className="text-lg font-bold text-white">{(case_.debt_ratio * 100).toFixed(0)}%</div>
</div>
<div className="p-3 bg-slate-900/50 rounded-lg">
<div className="text-xs text-slate-400">征信查询</div>
<div className="text-lg font-bold text-white">{case_.credit_query_count}次</div>
</div>
<div className="p-3 bg-slate-900/50 rounded-lg">
<div className="text-xs text-slate-400">涉诉记录</div>
<div className="text-lg font-bold text-white">{case_.litigation_count}起</div>
</div>
<div className="p-3 bg-slate-900/50 rounded-lg">
<div className="text-xs text-slate-400">处理时长</div>
<div className="text-lg font-bold text-white">{case_.processing_days}天</div>
</div>
</div>
{/* 风控措施 */}
<div className="mb-3 p-3 bg-blue-900/20 border border-blue-700/50 rounded-lg">
<div className="text-xs text-blue-400 mb-1">风控措施</div>
<div className="text-sm text-white">{case_.mitigation_measures}</div>
</div>
{/* 最终结果 */}
<div className="p-3 bg-slate-900/50 rounded-lg">
<div className="text-xs text-slate-400 mb-1">最终结果</div>
<div className="text-sm text-green-400">{case_.final_outcome}</div>
</div>
</div>
))
)}
</div>
</div>
);
}
+2 -2
View File
@@ -2,8 +2,8 @@ import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "mini OpenClaw",
description: "A lightweight, transparent AI Agent system",
title: "兴邦金租智能评审工作台",
description: "兴邦金租项目评审与四 Agent 协同工作台",
};
export default function RootLayout({
-706
View File
@@ -1,706 +0,0 @@
"use client";
import { useState } from "react";
import { Shield, Upload, FileText, AlertTriangle, CheckCircle, Download, Eye, File } from "lucide-react";
interface PolicyCheckResult {
policy_id: string;
customer_name: string;
check_timestamp: string;
policy_info: {
insurance_company: string;
policy_number: string;
coverage_types: string[];
coverage_amount: number;
lease_amount: number;
coverage_ratio: number;
};
compliance_items: Array<{
category: string;
item: string;
status: "pass" | "fail" | "warning";
detail: string;
}>;
exclusions: Array<{
clause: string;
risk_level: "high" | "medium" | "low";
description: string;
}>;
overall_result: "compliant" | "non_compliant" | "needs_review";
recommendations: string[];
}
interface ReportTemplate {
id: string;
name: string;
description: string;
sections: string[];
}
export default function PolicyVerificationPage() {
const [activeTab, setActiveTab] = useState<"upload" | "results" | "templates">("upload");
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<string>("TPL-POLICY-001");
const [checkForm, setCheckForm] = useState({
customer_name: "远东新能源公交公司",
lease_amount: 8000,
project_type: "新能源设备"
});
const [loading, setLoading] = useState(false);
const [checkResult, setCheckResult] = useState<PolicyCheckResult | null>(null);
const [generatedReport, setGeneratedReport] = useState<string | null>(null);
// 报告模板
const [templates] = useState<ReportTemplate[]>([
{
id: "TPL-POLICY-001",
name: "标准保单核验报告",
description: "适用于常规设备和车辆的保单核验",
sections: ["保单基本信息", "合规性检查", "除外责任分析", "风险评估", "核验结论", "整改建议"]
},
{
id: "TPL-POLICY-002",
name: "新能源专项保单核验",
description: "针对新能源设备的专项核验报告",
sections: ["保单基本信息", "新能源特殊险种", "合规性检查", "政策补贴覆盖", "除外责任分析", "核验结论"]
},
{
id: "TPL-POLICY-003",
name: "大额设备保单核验",
description: "5000万以上大额设备的详细核验",
sections: ["保单基本信息", "保额充足性分析", "多保险公司协同", "合规性检查", "除外责任分析", "风险评估", "核验结论"]
},
{
id: "TPL-POLICY-004",
name: "快速核验简报",
description: "低风险项目的快速核验简报",
sections: ["保单基本信息", "合规性检查", "核验结论"]
}
]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setSelectedFile(e.target.files[0]);
}
};
const handlePolicyCheck = async () => {
if (!selectedFile) {
alert("请先上传保单PDF文件");
return;
}
setLoading(true);
setCheckResult(null);
setGeneratedReport(null);
try {
// 模拟OCR识别和保单核验
await new Promise(resolve => setTimeout(resolve, 2500));
const mockResult: PolicyCheckResult = {
policy_id: "POL-2026-001",
customer_name: checkForm.customer_name,
check_timestamp: new Date().toLocaleString('zh-CN'),
policy_info: {
insurance_company: "中国人民财产保险股份有限公司",
policy_number: "PICC-FZL-20260801-0156",
coverage_types: ["设备损失险", "盗抢险", "第三者责任险", "营业中断险"],
coverage_amount: 9600,
lease_amount: checkForm.lease_amount,
coverage_ratio: (9600 / checkForm.lease_amount) * 100
},
compliance_items: [
{
category: "保额覆盖",
item: "保额 ≥ 授信金额 × 120%",
status: "pass",
detail: `保额9600万元 ≥ 授信${checkForm.lease_amount}万元 × 120% = ${(checkForm.lease_amount * 1.2).toFixed(0)}万元`
},
{
category: "险种完整性",
item: "必备险种齐全",
status: "pass",
detail: "设备损失险、盗抢险、第三者责任险均已投保"
},
{
category: "保险期限",
item: "保险期限覆盖租赁期",
status: "pass",
detail: "保险期限36个月,租赁期36个月,完全覆盖"
},
{
category: "第一受益人",
item: "兴邦金租为第一受益人",
status: "pass",
detail: "保单明确载明:第一受益人为兴邦融资租赁有限公司"
},
{
category: "除外责任",
item: "高频风险不得除外",
status: "fail",
detail: "保单除外责任包含「泡水」和「自然灾害」,属于高频风险,不应除外"
},
{
category: "保费支付",
item: "保费按期支付证明",
status: "warning",
detail: "仅提供首期保费缴纳凭证,后续保费需持续跟踪"
}
],
exclusions: [
{
clause: "因泡水、浸水导致的设备损失",
risk_level: "high",
description: "极端天气下泡水风险较高,尤其是户外设备。此条款将导致泡水事故无法理赔,风险敞口较大。"
},
{
clause: "因自然灾害(台风、暴雨、洪水)导致的损失",
risk_level: "high",
description: "自然灾害属于不可抗力但发生概率较高,此条款将导致重大自然灾害损失无法理赔。"
},
{
clause: "战争、核辐射、恐怖袭击",
risk_level: "low",
description: "此类风险发生概率极低,属于常规除外责任,可接受。"
},
{
clause: "故意损坏、违法使用",
risk_level: "low",
description: "此类风险属于人为因素,常规除外责任,可接受。"
}
],
overall_result: "non_compliant",
recommendations: [
"【必须整改】要求保险公司删除「泡水」除外责任条款,或增加泡水附加险",
"【必须整改】要求保险公司删除「自然灾害」除外责任条款,或增加自然灾害附加险",
"【建议跟踪】建立保费缴纳监控机制,每期保费到期前提醒客户缴纳",
"【风险提示】如客户无法调整保单条款,建议在合同中明确:保单除外责任导致的损失由客户承担"
]
};
setCheckResult(mockResult);
setActiveTab("results");
} catch (error) {
alert("保单核验失败");
} finally {
setLoading(false);
}
};
const handleGenerateReport = () => {
if (!checkResult) return;
const template = templates.find(t => t.id === selectedTemplate)!;
let reportContent = "";
reportContent += `# ${checkResult.customer_name} - 保单合规核验报告\n\n`;
reportContent += `**报告模板**: ${template.name}\n`;
reportContent += `**核验时间**: ${checkResult.check_timestamp}\n`;
reportContent += `**保单编号**: ${checkResult.policy_info.policy_number}\n`;
reportContent += `**核验结果**: ${checkResult.overall_result === "compliant" ? "✅ 合规" : checkResult.overall_result === "non_compliant" ? "❌ 不合规" : "⚠️ 需要复核"}\n\n`;
reportContent += `---\n\n`;
template.sections.forEach(section => {
switch (section) {
case "保单基本信息":
reportContent += `## 一、保单基本信息\n\n`;
reportContent += `| 项目 | 内容 |\n|------|------|\n`;
reportContent += `| 保险公司 | ${checkResult.policy_info.insurance_company} |\n`;
reportContent += `| 保单号 | ${checkResult.policy_info.policy_number} |\n`;
reportContent += `| 投保险种 | ${checkResult.policy_info.coverage_types.join("、")} |\n`;
reportContent += `| 保险金额 | ${checkResult.policy_info.coverage_amount}万元 |\n`;
reportContent += `| 授信金额 | ${checkResult.policy_info.lease_amount}万元 |\n`;
reportContent += `| 覆盖比例 | ${checkResult.policy_info.coverage_ratio.toFixed(1)}% |\n\n`;
break;
case "合规性检查":
reportContent += `## 二、合规性检查\n\n`;
checkResult.compliance_items.forEach((item, idx) => {
const icon = item.status === "pass" ? "✅" : item.status === "fail" ? "❌" : "⚠️";
reportContent += `### ${idx + 1}. ${icon} ${item.item}\n\n`;
reportContent += `**检查类别**: ${item.category}\n\n`;
reportContent += `**检查结果**: ${item.status === "pass" ? "通过" : item.status === "fail" ? "不通过" : "需关注"}\n\n`;
reportContent += `**详细说明**: ${item.detail}\n\n`;
});
break;
case "除外责任分析":
reportContent += `## 三、除外责任条款分析\n\n`;
const highRiskExclusions = checkResult.exclusions.filter(e => e.risk_level === "high");
const mediumRiskExclusions = checkResult.exclusions.filter(e => e.risk_level === "medium");
const lowRiskExclusions = checkResult.exclusions.filter(e => e.risk_level === "low");
if (highRiskExclusions.length > 0) {
reportContent += `### 🔴 高风险除外责任(${highRiskExclusions.length}项)\n\n`;
highRiskExclusions.forEach((exc, idx) => {
reportContent += `**${idx + 1}. ${exc.clause}**\n\n`;
reportContent += `${exc.description}\n\n`;
});
}
if (mediumRiskExclusions.length > 0) {
reportContent += `### 🟡 中风险除外责任(${mediumRiskExclusions.length}项)\n\n`;
mediumRiskExclusions.forEach((exc, idx) => {
reportContent += `**${idx + 1}. ${exc.clause}**\n\n`;
reportContent += `${exc.description}\n\n`;
});
}
if (lowRiskExclusions.length > 0) {
reportContent += `### 🟢 低风险除外责任(${lowRiskExclusions.length}项,可接受)\n\n`;
lowRiskExclusions.forEach((exc, idx) => {
reportContent += `- ${exc.clause}\n`;
});
reportContent += `\n`;
}
break;
case "风险评估":
reportContent += `## 四、风险评估\n\n`;
const failedItems = checkResult.compliance_items.filter(i => i.status === "fail");
const warningItems = checkResult.compliance_items.filter(i => i.status === "warning");
reportContent += `**不合规项**: ${failedItems.length}项\n\n`;
reportContent += `**关注项**: ${warningItems.length}项\n\n`;
reportContent += `**高风险除外**: ${checkResult.exclusions.filter(e => e.risk_level === "high").length}项\n\n`;
reportContent += `**综合评估**: ${checkResult.overall_result === "compliant" ? "保单符合公司要求,可以接受" : checkResult.overall_result === "non_compliant" ? "保单存在重大不合规项,建议要求客户整改" : "保单基本合规,但存在需要关注的项目"}\n\n`;
break;
case "核验结论":
reportContent += `## ${template.sections.indexOf(section) + 1}. 核验结论\n\n`;
if (checkResult.overall_result === "compliant") {
reportContent += `✅ **结论**: 保单合规,符合兴邦金租保单管理要求\n\n`;
} else if (checkResult.overall_result === "non_compliant") {
reportContent += `❌ **结论**: 保单不合规,存在以下重大问题:\n\n`;
checkResult.compliance_items.filter(i => i.status === "fail").forEach(item => {
reportContent += `- ${item.item}: ${item.detail}\n`;
});
reportContent += `\n`;
} else {
reportContent += `⚠️ **结论**: 保单基本合规,但需要关注以下事项\n\n`;
}
break;
case "整改建议":
case "核验建议":
reportContent += `## ${template.sections.indexOf(section) + 1}. 整改建议\n\n`;
checkResult.recommendations.forEach((rec, idx) => {
reportContent += `${idx + 1}. ${rec}\n\n`;
});
break;
case "新能源特殊险种":
reportContent += `## 二、新能源特殊险种核验\n\n`;
reportContent += `- 新能源设备特殊险种覆盖情况\n`;
reportContent += `- 电池、充电设施专项保险\n`;
reportContent += `- 技术更新换代风险覆盖\n\n`;
break;
case "政策补贴覆盖":
reportContent += `## 三、政策补贴覆盖情况\n\n`;
reportContent += `- 政府补贴延迟风险保险\n`;
reportContent += `- 补贴政策变更风险覆盖\n\n`;
break;
case "保额充足性分析":
reportContent += `## 二、保额充足性深度分析\n\n`;
reportContent += `- 大额设备分段保险情况\n`;
reportContent += `- 累计风险敞口分析\n`;
reportContent += `- 保额动态调整机制\n\n`;
break;
case "多保险公司协同":
reportContent += `## 三、多保险公司协同核验\n\n`;
reportContent += `- 多家保险公司分担情况\n`;
reportContent += `- 主次保险责任划分\n`;
reportContent += `- 理赔协调机制\n\n`;
break;
}
});
reportContent += `---\n\n`;
reportContent += `**核验人**: AI智能核验系统\n\n`;
reportContent += `**复核人**: [待人工复核]\n\n`;
reportContent += `**核验日期**: ${checkResult.check_timestamp}\n`;
setGeneratedReport(reportContent);
};
const getStatusBadge = (status: string) => {
switch (status) {
case "pass":
return <span className="px-2 py-1 bg-green-600 rounded text-xs font-bold flex items-center gap-1"><CheckCircle className="w-3 h-3" /> 通过</span>;
case "fail":
return <span className="px-2 py-1 bg-red-600 rounded text-xs font-bold flex items-center gap-1"><AlertTriangle className="w-3 h-3" /> 不通过</span>;
case "warning":
return <span className="px-2 py-1 bg-yellow-600 rounded text-xs font-bold flex items-center gap-1"><AlertTriangle className="w-3 h-3" /> 需关注</span>;
default:
return null;
}
};
const getRiskBadge = (level: string) => {
switch (level) {
case "high":
return <span className="px-2 py-1 bg-red-600 rounded text-xs font-bold">高风险</span>;
case "medium":
return <span className="px-2 py-1 bg-yellow-600 rounded text-xs font-bold">中风险</span>;
case "low":
return <span className="px-2 py-1 bg-green-600 rounded text-xs font-bold">低风险</span>;
default:
return null;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-6">
{/* Header */}
<header className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-green-400 to-teal-300 bg-clip-text text-transparent">
保单条款智能核验
</h1>
<p className="text-slate-400">OCR识别 + 条款解析 + 合规核验 + 报告生成</p>
</div>
</div>
</header>
{/* Tab Navigation */}
<div className="mb-6 flex gap-2">
<button
onClick={() => setActiveTab("upload")}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
activeTab === "upload"
? "bg-green-600 text-white"
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
}`}
>
上传核验
</button>
<button
onClick={() => setActiveTab("results")}
disabled={!checkResult}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
activeTab === "results"
? "bg-green-600 text-white"
: checkResult
? "bg-slate-800 text-slate-400 hover:bg-slate-700"
: "bg-slate-800 text-slate-600 cursor-not-allowed"
}`}
>
核验结果
</button>
<button
onClick={() => setActiveTab("templates")}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
activeTab === "templates"
? "bg-green-600 text-white"
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
}`}
>
报告模板
</button>
</div>
{/* Upload Tab */}
{activeTab === "upload" && (
<div className="space-y-6">
{/* Project Info */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-green-400">项目信息</h2>
<div className="grid grid-cols-3 gap-4 mb-6">
<div>
<label className="block text-sm text-slate-400 mb-2">客户名称</label>
<input
type="text"
value={checkForm.customer_name}
onChange={e => setCheckForm({ ...checkForm, customer_name: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-green-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">授信金额(万元)</label>
<input
type="number"
step="0.1"
value={checkForm.lease_amount}
onChange={e => setCheckForm({ ...checkForm, lease_amount: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-green-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">项目类型</label>
<input
type="text"
value={checkForm.project_type}
onChange={e => setCheckForm({ ...checkForm, project_type: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-green-500"
/>
</div>
</div>
</div>
{/* Upload Section */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-green-400">上传保单PDF</h2>
<div className="mb-6 p-8 border-2 border-dashed border-slate-600 rounded-lg text-center hover:border-green-500 transition-colors">
<Upload className="w-12 h-12 mx-auto mb-4 text-slate-500" />
<input
type="file"
accept=".pdf"
onChange={handleFileChange}
className="hidden"
id="policyUpload"
/>
<label
htmlFor="policyUpload"
className="cursor-pointer text-slate-300 hover:text-white transition-colors"
>
{selectedFile ? (
<div className="flex items-center justify-center gap-2">
<File className="w-5 h-5" />
<span className="font-medium">{selectedFile.name}</span>
</div>
) : (
<div>
<p className="text-lg mb-2">点击上传保单PDF文件</p>
<p className="text-sm text-slate-500">支持格式:PDF,最大50MB</p>
</div>
)}
</label>
</div>
<button
onClick={handlePolicyCheck}
disabled={loading || !selectedFile}
className={`w-full py-3 rounded-lg font-bold text-lg transition-all ${
loading || !selectedFile
? "bg-slate-700 text-slate-500 cursor-not-allowed"
: "bg-gradient-to-r from-green-600 to-teal-600 hover:shadow-xl hover:scale-[1.02]"
}`}
>
{loading ? "🔄 正在核验保单..." : "🛡️ 开始智能核验"}
</button>
</div>
</div>
)}
{/* Results Tab */}
{activeTab === "results" && checkResult && (
<div className="space-y-6">
{/* Summary Card */}
<div className={`p-6 rounded-xl border-2 ${
checkResult.overall_result === "compliant"
? "border-green-500 bg-green-900/20"
: checkResult.overall_result === "non_compliant"
? "border-red-500 bg-red-900/20"
: "border-yellow-500 bg-yellow-900/20"
}`}>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-2xl font-bold text-white mb-1">{checkResult.customer_name}</h2>
<p className="text-sm text-slate-400">核验时间: {checkResult.check_timestamp}</p>
</div>
<div className="text-center">
<div className={`text-4xl font-bold mb-1 ${
checkResult.overall_result === "compliant" ? "text-green-400" :
checkResult.overall_result === "non_compliant" ? "text-red-400" : "text-yellow-400"
}`}>
{checkResult.overall_result === "compliant" ? "✅" :
checkResult.overall_result === "non_compliant" ? "❌" : "⚠️"}
</div>
<div className="text-sm text-slate-400">
{checkResult.overall_result === "compliant" ? "合规" :
checkResult.overall_result === "non_compliant" ? "不合规" : "需复核"}
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-3 bg-slate-800/50 rounded-lg">
<div className="text-sm text-slate-400">保险金额</div>
<div className="text-xl font-bold text-white">{checkResult.policy_info.coverage_amount}万</div>
</div>
<div className="p-3 bg-slate-800/50 rounded-lg">
<div className="text-sm text-slate-400">授信金额</div>
<div className="text-xl font-bold text-white">{checkResult.policy_info.lease_amount}万</div>
</div>
<div className="p-3 bg-slate-800/50 rounded-lg">
<div className="text-sm text-slate-400">覆盖比例</div>
<div className="text-xl font-bold text-cyan-400">{checkResult.policy_info.coverage_ratio.toFixed(1)}%</div>
</div>
<div className="p-3 bg-slate-800/50 rounded-lg">
<div className="text-sm text-slate-400">高风险除外</div>
<div className="text-xl font-bold text-red-400">{checkResult.exclusions.filter(e => e.risk_level === "high").length}项</div>
</div>
</div>
</div>
{/* Compliance Items */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-green-400 flex items-center gap-2">
<CheckCircle className="w-5 h-5" />
合规性检查结果
</h2>
<div className="space-y-3">
{checkResult.compliance_items.map((item, idx) => (
<div
key={idx}
className={`p-4 rounded-lg border ${
item.status === "pass"
? "border-green-700 bg-green-900/20"
: item.status === "fail"
? "border-red-700 bg-red-900/20"
: "border-yellow-700 bg-yellow-900/20"
}`}
>
<div className="flex items-start justify-between mb-2">
<div>
<div className="flex items-center gap-2 mb-1">
{getStatusBadge(item.status)}
<span className="font-bold text-white">{item.item}</span>
</div>
<div className="text-xs text-slate-400">检查类别: {item.category}</div>
</div>
</div>
<p className="text-sm text-slate-300">{item.detail}</p>
</div>
))}
</div>
</div>
{/* Exclusions */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-green-400 flex items-center gap-2">
<AlertTriangle className="w-5 h-5" />
除外责任条款分析
</h2>
<div className="space-y-3">
{checkResult.exclusions.map((exc, idx) => (
<div
key={idx}
className={`p-4 rounded-lg border ${
exc.risk_level === "high"
? "border-red-700 bg-red-900/20"
: exc.risk_level === "medium"
? "border-yellow-700 bg-yellow-900/20"
: "border-green-700 bg-green-900/20"
}`}
>
<div className="flex items-center gap-2 mb-2">
{getRiskBadge(exc.risk_level)}
<span className="font-bold text-white">{exc.clause}</span>
</div>
<p className="text-sm text-slate-300">{exc.description}</p>
</div>
))}
</div>
</div>
{/* Recommendations */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-green-400">整改建议</h2>
<ul className="space-y-2">
{checkResult.recommendations.map((rec, idx) => (
<li key={idx} className="flex items-start gap-2 text-sm text-slate-300">
<span className="text-cyan-400 font-bold">{idx + 1}.</span>
<span>{rec}</span>
</li>
))}
</ul>
</div>
{/* Generate Report */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-green-400">生成核验报告</h2>
<div className="mb-4">
<label className="block text-sm text-slate-400 mb-2">选择报告模板</label>
<select
value={selectedTemplate}
onChange={e => setSelectedTemplate(e.target.value)}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-green-500"
>
{templates.map(template => (
<option key={template.id} value={template.id}>
{template.name} - {template.description}
</option>
))}
</select>
</div>
<button
onClick={handleGenerateReport}
className="w-full py-3 bg-gradient-to-r from-blue-600 to-cyan-600 hover:shadow-xl hover:scale-[1.02] rounded-lg font-bold text-lg transition-all"
>
📝 生成核验报告
</button>
{generatedReport && (
<div className="mt-6 p-6 bg-slate-900 rounded-lg border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h3 className="font-bold text-white">报告预览</h3>
<div className="flex gap-2">
<button className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors">
<Eye className="w-4 h-4" />
预览
</button>
<button className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors">
<Download className="w-4 h-4" />
导出
</button>
</div>
</div>
<pre className="text-sm text-slate-300 whitespace-pre-wrap font-sans leading-relaxed max-h-96 overflow-y-auto">
{generatedReport}
</pre>
</div>
)}
</div>
</div>
)}
{/* Templates Tab */}
{activeTab === "templates" && (
<div className="space-y-4">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold text-green-400">核验报告模板库</h2>
</div>
{templates.map(template => (
<div
key={template.id}
className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700"
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<FileText className="w-5 h-5 text-green-400" />
<h3 className="text-lg font-bold text-white">{template.name}</h3>
<span className="text-xs text-slate-500 font-mono">{template.id}</span>
</div>
<p className="text-sm text-slate-400">{template.description}</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{template.sections.map((section, idx) => (
<span
key={idx}
className="px-2 py-1 bg-slate-700 rounded text-xs text-slate-300"
>
{section}
</span>
))}
</div>
</div>
))}
</div>
)}
</div>
);
}
-370
View File
@@ -1,370 +0,0 @@
"use client";
import { useState } from "react";
import { FileText, Download, Eye, Edit, Plus, Trash2, CheckSquare, Square } from "lucide-react";
interface ReportTemplate {
id: string;
name: string;
description: string;
sections: string[];
created_at: string;
usage_count: number;
}
interface GeneratedReport {
template_name: string;
generated_at: string;
content: {
title: string;
sections: Array<{
name: string;
content: string;
}>;
};
}
export default function ReportAssistantPage() {
const [activeTab, setActiveTab] = useState<"generate" | "templates">("generate");
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
const [projectInfo, setProjectInfo] = useState({
customer_name: "远东新能源公交公司",
project_type: "对公新能源",
amount: 3.0,
risk_level: "medium"
});
const [loading, setLoading] = useState(false);
const [generatedReport, setGeneratedReport] = useState<GeneratedReport | null>(null);
// 模拟报告模板数据
const [templates] = useState<ReportTemplate[]>([
{
id: "TPL-001",
name: "标准风险评审报告",
description: "适用于常规项目的风险评审报告,包含完整的风险分析和建议",
sections: ["项目概况", "风险识别", "财务分析", "法律合规性", "综合评审意见", "审批建议"],
created_at: "2025-01-15",
usage_count: 156
},
{
id: "TPL-002",
name: "新能源项目专项报告",
description: "针对新能源行业的专项评审报告,关注政策补贴和行业风险",
sections: ["项目概况", "行业政策分析", "技术可行性", "财务测算", "风险评估", "审批建议"],
created_at: "2025-02-10",
usage_count: 89
},
{
id: "TPL-003",
name: "大额授信专项报告",
description: "5亿元以上大额授信项目的详细评审报告",
sections: ["项目概况", "企业背景调查", "深度财务分析", "抵押物评估", "风险缓释措施", "总部审批建议"],
created_at: "2025-01-20",
usage_count: 34
},
{
id: "TPL-004",
name: "快速审批简报",
description: "低风险项目的快速审批简报,简化流程",
sections: ["项目基本信息", "快速风险评估", "审批建议"],
created_at: "2025-02-01",
usage_count: 203
}
]);
const handleGenerateReport = async () => {
if (!selectedTemplate) {
alert("请先选择报告模板");
return;
}
setLoading(true);
setGeneratedReport(null);
try {
// 模拟生成报告
await new Promise(resolve => setTimeout(resolve, 2000));
const template = templates.find(t => t.id === selectedTemplate)!;
const mockReport: GeneratedReport = {
template_name: template.name,
generated_at: new Date().toLocaleString('zh-CN'),
content: {
title: `${projectInfo.customer_name} - ${template.name}`,
sections: template.sections.map(sectionName => ({
name: sectionName,
content: generateMockSectionContent(sectionName, projectInfo)
}))
}
};
setGeneratedReport(mockReport);
} catch (error) {
alert("生成报告失败");
} finally {
setLoading(false);
}
};
const generateMockSectionContent = (sectionName: string, info: any): string => {
switch (sectionName) {
case "项目概况":
return `客户名称:${info.customer_name}\n项目类型:${info.project_type}\n授信金额:${info.amount}亿元\n风险等级:${info.risk_level === 'high' ? '高风险' : info.risk_level === 'medium' ? '中风险' : '低风险'}\n\n本项目为${info.project_type}类型融资租赁项目,客户${info.customer_name}申请授信金额${info.amount}亿元。经初步评估,项目风险等级为${info.risk_level === 'high' ? '高风险' : info.risk_level === 'medium' ? '中风险' : '低风险'}。`;
case "风险识别":
case "风险评估":
return `基于双轨驱动分析(规则引擎+历史数据),识别出以下主要风险点:\n\n1. 财务风险:资产负债率接近行业警戒线,需关注偿债能力\n2. 信用风险:征信查询次数偏高,需核实融资需求真实性\n3. 法律风险:存在未结案涉诉记录,需评估潜在影响\n\n历史数据显示,类似项目的通过率为73%,其中60%需要附加风控措施。`;
case "财务分析":
case "深度财务分析":
return `关键财务指标分析:\n\n• 资产负债率:65%(行业平均60%)\n• 流动比率:1.2(良好)\n• 净资产收益率:8.5%(中等)\n• 现金流覆盖倍数:1.8倍\n\n综合评估:财务状况整体可控,但需关注债务结构优化。`;
case "审批建议":
case "综合评审意见":
return `综合评审意见:\n\n建议:附加条件通过\n\n理由:\n1. 客户基本面良好,行业前景明朗\n2. 财务指标整体可控,但需加强风险缓释\n3. 历史同类案例成功率较高\n\n附加条件:\n• 建议降低授信额度10-15%\n• 要求增加抵押物或第三方担保\n• 设置季度财务监控机制`;
case "行业政策分析":
return `${info.project_type}行业政策环境:\n\n• 国家政策支持力度大,补贴政策稳定\n• 地方政府配套措施完善\n• 行业准入门槛提高,竞争格局改善\n• 需关注补贴退坡风险`;
case "技术可行性":
return `技术方案评估:\n\n• 技术路线成熟,市场验证充分\n• 设备供应商资质良好\n• 运营团队经验丰富\n• 维护保养体系完善`;
default:
return `${sectionName}相关内容...\n\n[此部分内容由AI根据项目实际情况和历史数据自动生成]`;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-6">
{/* Header */}
<header className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-pink-300 bg-clip-text text-transparent">
报告助手
</h1>
<p className="text-slate-400">智能报告生成与模板管理</p>
</div>
</div>
</header>
{/* Tab Navigation */}
<div className="mb-6 flex gap-2">
<button
onClick={() => setActiveTab("generate")}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
activeTab === "generate"
? "bg-purple-600 text-white"
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
}`}
>
生成报告
</button>
<button
onClick={() => setActiveTab("templates")}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
activeTab === "templates"
? "bg-purple-600 text-white"
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
}`}
>
模板管理
</button>
</div>
{/* Generate Report Tab */}
{activeTab === "generate" && (
<div className="space-y-6">
{/* Project Info Form */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-purple-400">项目信息</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-2">客户名称</label>
<input
type="text"
value={projectInfo.customer_name}
onChange={e => setProjectInfo({ ...projectInfo, customer_name: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">项目类型</label>
<input
type="text"
value={projectInfo.project_type}
onChange={e => setProjectInfo({ ...projectInfo, project_type: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">授信金额(亿元)</label>
<input
type="number"
step="0.1"
value={projectInfo.amount}
onChange={e => setProjectInfo({ ...projectInfo, amount: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">风险等级</label>
<select
value={projectInfo.risk_level}
onChange={e => setProjectInfo({ ...projectInfo, risk_level: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-purple-500"
>
<option value="low">低风险</option>
<option value="medium">中风险</option>
<option value="high">高风险</option>
</select>
</div>
</div>
</div>
{/* Template Selection */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-purple-400">选择报告模板</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{templates.map(template => (
<div
key={template.id}
onClick={() => setSelectedTemplate(template.id)}
className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
selectedTemplate === template.id
? "border-purple-500 bg-purple-900/30"
: "border-slate-700 bg-slate-900/50 hover:border-slate-600"
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
{selectedTemplate === template.id ? (
<CheckSquare className="w-5 h-5 text-purple-400" />
) : (
<Square className="w-5 h-5 text-slate-600" />
)}
<h3 className="font-bold text-white">{template.name}</h3>
</div>
<span className="text-xs text-slate-500">{template.id}</span>
</div>
<p className="text-sm text-slate-400 mb-3">{template.description}</p>
<div className="flex items-center gap-4 text-xs text-slate-500">
<span>使用次数: {template.usage_count}</span>
<span>创建: {template.created_at}</span>
</div>
<div className="mt-2 text-xs text-slate-500">
章节: {template.sections.join(" · ")}
</div>
</div>
))}
</div>
<button
onClick={handleGenerateReport}
disabled={loading || !selectedTemplate}
className={`mt-6 w-full py-3 rounded-lg font-bold text-lg transition-all ${
loading || !selectedTemplate
? "bg-slate-700 text-slate-500 cursor-not-allowed"
: "bg-gradient-to-r from-purple-600 to-pink-600 hover:shadow-xl hover:scale-[1.02]"
}`}
>
{loading ? "🔄 正在生成报告..." : "📝 生成评审报告"}
</button>
</div>
{/* Generated Report */}
{generatedReport && (
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-purple-400">{generatedReport.content.title}</h2>
<p className="text-sm text-slate-400">生成时间: {generatedReport.generated_at}</p>
</div>
<div className="flex gap-2">
<button className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors">
<Eye className="w-4 h-4" />
预览
</button>
<button className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors">
<Download className="w-4 h-4" />
导出
</button>
</div>
</div>
<div className="space-y-6">
{generatedReport.content.sections.map((section, idx) => (
<div key={idx} className="p-4 bg-slate-900/50 rounded-lg">
<h3 className="text-lg font-bold text-white mb-3">{section.name}</h3>
<p className="text-sm text-slate-300 whitespace-pre-line leading-relaxed">
{section.content}
</p>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Templates Tab */}
{activeTab === "templates" && (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold text-purple-400">报告模板库</h2>
<button className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg font-medium flex items-center gap-2 transition-colors">
<Plus className="w-4 h-4" />
新建模板
</button>
</div>
<div className="grid grid-cols-1 gap-4">
{templates.map(template => (
<div
key={template.id}
className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700"
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<FileText className="w-5 h-5 text-purple-400" />
<h3 className="text-lg font-bold text-white">{template.name}</h3>
<span className="text-xs text-slate-500 font-mono">{template.id}</span>
</div>
<p className="text-sm text-slate-400">{template.description}</p>
</div>
<div className="flex gap-2">
<button className="p-2 text-slate-400 hover:text-blue-400 hover:bg-slate-700 rounded-lg transition-colors">
<Edit className="w-4 h-4" />
</button>
<button className="p-2 text-slate-400 hover:text-red-400 hover:bg-slate-700 rounded-lg transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-3">
{template.sections.map((section, idx) => (
<span
key={idx}
className="px-2 py-1 bg-slate-700 rounded text-xs text-slate-300"
>
{section}
</span>
))}
</div>
<div className="flex items-center gap-6 text-xs text-slate-500">
<span>使用次数: {template.usage_count}</span>
<span>创建时间: {template.created_at}</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
+5 -3
View File
@@ -1,6 +1,7 @@
"use client";
import {useEffect,useState} from "react";
import {ArrowLeft,BookOpen,Check,Download,FileText,Lock,Plus,Save,Sparkles,Unlock} from "lucide-react";
import {BookOpen,Check,Download,FileText,Lock,Plus,Save,Sparkles,Unlock} from "lucide-react";
import AgentNav,{useRequestedProject} from "@/components/xingbang/AgentNav";
import "../data-access/workspace.css";
import "./report.css";
@@ -10,6 +11,7 @@ async function call(path:string,body?:unknown){const r=await fetch(BASE+path,{me
const states:Record<string,string>={draft:"待复核草稿",final:"已定稿"};
export default function Reports(){
const [docs,setDocs]=useState<any[]>([]),[runs,setRuns]=useState<any[]>([]),[doc,setDoc]=useState<any>(null),[selectedRun,setSelectedRun]=useState(""),[section,setSection]=useState("0"),[text,setText]=useState(""),[note,setNote]=useState(""),[error,setError]=useState(""),[busy,setBusy]=useState(false),[view,setView]=useState("report"),[approval,setApproval]=useState(""),[outcome,setOutcome]=useState("");
const requested=useRequestedProject();
const update=(d:any)=>{setDoc(d);setDocs(xs=>xs.some(x=>x.id===d.id)?xs.map(x=>x.id===d.id?d:x):[d,...xs]);setText(d.sections.find((s:any)=>s.id===section)?.text||"")};
useEffect(()=>{Promise.all([call("/reports/documents"),call("/runs/history")]).then(([d,r])=>{setDocs(d);setRuns(r);setSelectedRun(r[0]?.id||"");if(d[0]){setDoc(d[0]);setText(d[0].sections[0].text)}}).catch(e=>setError(e.message))},[]);
async function perform(task:()=>Promise<void>){setBusy(true);setError("");try{await task()}catch(e:any){setError(e.message)}finally{setBusy(false)}}
@@ -19,12 +21,12 @@ export default function Reports(){
const dirty=current&&text!==current.text;
const selectDoc=(d:any)=>{if(dirty&&!window.confirm("放弃当前未保存的修改?"))return;setDoc(d);setSection("0");setText(d.sections[0].text);setApproval(d.experience?.approval_result||"");setOutcome(d.experience?.post_lease_result||"")};
async function download(){await perform(async()=>{const r=await fetch(BASE+`/reports/documents/${doc.id}/export`,{headers:headers()});if(!r.ok)throw Error("导出失败,请检查访问权限");const url=URL.createObjectURL(await r.blob());const a=document.createElement("a");a.href=url;a.download=`${doc.project_id}-评审底稿.docx`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)})}
return <div className="ra-app"><header className="ra-top"><a href="/data-access" className="ra-button"><ArrowLeft size={15}/>风险研判</a><div><FileText size={20}/><b>兴邦金租 · 报告与经验助手</b></div><span className="ra-badge">Agent 02 / 业务验证版</span></header><main className="report-main"><div className="ra-heading"><div><div className="ra-eyebrow">REPORT & EXPERIENCE</div><h1>从项目事实,到评审底稿</h1><p>源研判快照 · 章节确认 · 正式定稿 · 经验沉淀</p></div><div className="report-tabs"><button className={"ra-button "+(view==="report"?"primary":"")} onClick={()=>setView("report")}><FileText size={15}/>报告工作台</button><button className={"ra-button "+(view==="experience"?"primary":"")} onClick={()=>setView("experience")}><BookOpen size={15}/>项目经验卡</button></div></div>{error&&<div role="alert" className="ra-alert error">{error}</div>}
return <div className="xb-agent-shell"><AgentNav active="report" project={requested||undefined}/><div className="ra-app"><main className="report-main"><div className="ra-heading"><div><div className="ra-eyebrow">02 / REPORT & EXPERIENCE</div><h1>从项目事实,到评审底稿</h1><p>源研判快照 · 章节确认 · 正式定稿 · 经验沉淀</p></div><div className="report-tabs"><button className={"ra-button "+(view==="report"?"primary":"")} onClick={()=>setView("report")}><FileText size={15}/>报告工作台</button><button className={"ra-button "+(view==="experience"?"primary":"")} onClick={()=>setView("experience")}><BookOpen size={15}/>项目经验卡</button></div></div>{error&&<div role="alert" className="ra-alert error">{error}</div>}
<div className="report-layout"><aside className="report-list"><h2>项目报告 <span>{docs.length}</span></h2>{docs.map(d=><button key={d.id} className={doc?.id===d.id?"selected":""} onClick={()=>selectDoc(d)}><b>{d.project_id}</b><small>{states[d.status]} · v{d.version}</small><small>{d.source_time?.slice(0,10)}</small></button>)}<section className="report-create"><h3>从研判记录建立底稿</h3><label className="ra-field"><span>来源研判</span><select value={selectedRun} onChange={e=>setSelectedRun(e.target.value)}><option value="">请选择</option>{runs.map(r=><option key={r.id} value={r.id}>{r.project_id} · {new Date(r.time).toLocaleString()}</option>)}</select></label><label className="ra-field"><span>来源确认意见</span><textarea placeholder="确认数据来源、适用期间及未解决缺口" value={note} onChange={e=>setNote(e.target.value)}/></label><button className="ra-button primary" disabled={busy||!selectedRun||!note.trim()} onClick={()=>perform(async()=>{const d=await call("/reports/documents",{run_id:selectedRun,confirmation:note});setSection("0");update(d);setText(d.sections[0].text);setView("report")})}><Plus size={15}/>建立底稿</button><p className="ra-note">使用示例对公模板。事实章节固定引用源快照,缺失仍作为待核查事项。</p></section></aside>
{!doc?<div className="ra-empty"><FileText size={36}/><h2>选择一份研判记录开始</h2><p>可以先在风险研判助手运行示例项目,再回到这里建立报告。</p></div>:<section className="report-content"><div className="report-document-heading"><div><h2>{doc.title}</h2><p>{states[doc.status]} · v{doc.version} · {doc.template.name}</p></div><button className="ra-button" disabled={busy} onClick={download}><Download size={15}/>导出 Word</button></div>
{view==="report"?<><nav className="report-sections">{doc.sections.map((s:any)=><button key={s.id} className={section===s.id?"selected":""} onClick={()=>{if(dirty&&!window.confirm("放弃未保存的修改?"))return;setSection(s.id);setText(s.text)}}>{s.locked?<Lock size={14}/>:<FileText size={14}/>}<span>{s.title}</span></button>)}</nav><div className="report-editor-heading"><h3>{current?.title}</h3><span>{current?.locked?"已确认锁定":section==="0"||section==="1"?"来源快照,只读":"待补充判断"}</span></div><textarea aria-label="报告章节内容" className="report-editor" value={text} readOnly={doc.status==="final"||current?.locked||section==="0"||section==="1"} onChange={e=>setText(e.target.value)}/><div className="report-actions"><button className="ra-button" disabled={busy||doc.status==="final"||current?.locked||section==="0"||section==="1"} onClick={()=>action("edit")}><Save size={15}/>保存章节</button><button className="ra-button" disabled={busy||dirty||doc.status==="final"} onClick={()=>action("lock",{locked:!current?.locked})}>{current?.locked?<Unlock size={15}/>:<Lock size={15}/>} {current?.locked?"解锁修改":"确认并锁定"}</button><button className="ra-button" disabled={busy||dirty||doc.status==="final"||current?.locked||section==="0"||section==="1"} onClick={()=>perform(async()=>update(await call(`/reports/documents/${doc.id}/generate`,{kind:"generate",version:doc.version,section_id:section})))}><Sparkles size={15}/>{busy?"处理中…":"mini-openclaw 生成候选稿"}</button></div>
{proposal&&<section className="report-proposal"><h3>AI 候选稿 · 未覆盖正文</h3><p>{proposal.text}</p><button className="ra-button" disabled={doc.status==="final"||current?.locked} onClick={()=>setText(proposal.text)}>载入编辑区复核</button><details><summary>查看 Agent 工具轨迹</summary><pre>{JSON.stringify(proposal.trace.messages,null,2)}</pre></details></section>}
<section className="report-finalize"><label className="ra-field"><span>主审员定稿意见</span><textarea value={note} onChange={e=>setNote(e.target.value)} placeholder="确认事实、数字、风险判断与待落实条件"/></label><button className="ra-button primary" disabled={busy||dirty||doc.status==="final"||!note.trim()||!doc.sections.every((s:any)=>s.locked)} onClick={()=>action("finalize")}><Check size={16}/>定稿并提取经验卡</button><p className="ra-note">逐章确认后定稿。经验卡仍为草稿,不自动进入共享经验库。</p></section></>:<><div className="report-experience-status">{["草稿","主审员已确认","部门审核","可检索","已验证","已过期/归档"].map(s=><span key={s} className={doc.experience?.status===s?"selected":""}>{s}</span>)}</div>{doc.experience?<><h3>风险判断与处置经验</h3><p className="report-prose">{doc.experience.judgment}</p><h3>方案与缓释措施</h3><p className="report-prose">{doc.experience.measures}</p><label className="ra-field"><span>审批结果</span><textarea value={approval} onChange={e=>setApproval(e.target.value)}/></label><label className="ra-field"><span>租后观察结果</span><textarea value={outcome} onChange={e=>setOutcome(e.target.value)}/></label><label className="ra-field"><span>确认或审核意见</span><textarea value={note} onChange={e=>setNote(e.target.value)}/></label><button className="ra-button primary" disabled={busy||!note.trim()||doc.experience.status==="已过期/归档"} onClick={()=>action("experience")}>确认并进入下一阶段</button><details className="ra-note"><summary>经验卡来源与历史</summary><pre className="ra-result-json">{JSON.stringify(doc.experience,null,2)}</pre></details></>:<div className="ra-empty">报告定稿后生成经验卡草稿。</div>}</>}
<details className="ra-note"><summary>来源与审计记录</summary><p>源研判 {doc.source_id} · {doc.source_time}</p><p>来源确认:{doc.confirmation}</p><pre className="ra-result-json">{JSON.stringify(doc.events,null,2)}</pre></details></section>}
</div></main></div>
</div></main></div></div>
}
-698
View File
@@ -1,698 +0,0 @@
"use client";
import { useState } from "react";
import { Users, TrendingUp, AlertTriangle, Calculator, Target, History, CheckCircle } from "lucide-react";
interface Reviewer {
id: string;
name: string;
specialty: string[];
current_projects: number;
workload_capacity: number;
avg_processing_days: number;
quality_score: number;
status: "available" | "busy" | "full";
}
interface Recommendation {
reviewer: string;
score: number;
breakdown: {
history_match: number;
workload: number;
specialty: number;
quality: number;
};
estimated_days: number;
reason: string;
}
interface KeyIndicator {
name: string;
value: string;
unit: string;
warning?: string;
historical_avg?: string;
}
export default function ReviewAllocationPage() {
const [activeTab, setActiveTab] = useState<"allocation" | "calculation">("allocation");
// 智能调配表单
const [allocationForm, setAllocationForm] = useState({
project_name: "远东新能源公交项目",
project_industry: "新能源",
project_amount: 3.0,
project_complexity: 3,
risk_level: "medium"
});
const [allocationResult, setAllocationResult] = useState<{
reviewers: Reviewer[];
recommendations: Recommendation[];
} | null>(null);
const [loadingAllocation, setLoadingAllocation] = useState(false);
// 关键指标测算表单
const [calculationForm, setCalculationForm] = useState({
project_name: "远东新能源公交项目",
revenue_year1: 5000,
revenue_year2: 6000,
revenue_year3: 6500,
cost_ratio: 0.65,
depreciation: 800,
interest_rate: 0.045,
loan_amount: 25000,
irr_target: 0.08
});
const [calculationResult, setCalculationResult] = useState<{
indicators: KeyIndicator[];
warnings: string[];
historical_insights: string[];
} | null>(null);
const [loadingCalculation, setLoadingCalculation] = useState(false);
// 处理智能调配
const handleAllocation = async () => {
setLoadingAllocation(true);
setAllocationResult(null);
try {
await new Promise(resolve => setTimeout(resolve, 1500));
// 模拟评审员数据
const mockReviewers: Reviewer[] = [
{
id: "REV-001",
name: "张建国",
specialty: ["新能源", "制造业", "大额授信"],
current_projects: 3,
workload_capacity: 5,
avg_processing_days: 4.2,
quality_score: 9.2,
status: "available"
},
{
id: "REV-002",
name: "李明华",
specialty: ["新能源", "交通运输", "设备租赁"],
current_projects: 2,
workload_capacity: 5,
avg_processing_days: 3.8,
quality_score: 8.9,
status: "available"
},
{
id: "REV-003",
name: "王晓红",
specialty: ["建筑工程", "房地产", "基础设施"],
current_projects: 5,
workload_capacity: 5,
avg_processing_days: 5.5,
quality_score: 8.5,
status: "full"
},
{
id: "REV-004",
name: "赵强",
specialty: ["科技研发", "信息技术", "医疗器械"],
current_projects: 4,
workload_capacity: 5,
avg_processing_days: 4.8,
quality_score: 9.0,
status: "busy"
}
];
// 智能推荐(根据行业匹配、工作负荷、历史质量)
const mockRecommendations: Recommendation[] = [
{
reviewer: "李明华",
score: 92.5,
breakdown: {
history_match: 95,
workload: 90,
specialty: 98,
quality: 89
},
estimated_days: 4,
reason: "专长高度匹配(新能源+交通运输),当前负荷最轻,历史同类项目处理速度快"
},
{
reviewer: "张建国",
score: 88.3,
breakdown: {
history_match: 88,
workload: 80,
specialty: 95,
quality: 92
},
estimated_days: 5,
reason: "新能源领域经验丰富,质量评分最高,适合处理复杂项目"
},
{
reviewer: "赵强",
score: 65.2,
breakdown: {
history_match: 45,
workload: 60,
specialty: 40,
quality: 90
},
estimated_days: 6,
reason: "专长不完全匹配,但质量稳定,可作为备选"
}
];
setAllocationResult({
reviewers: mockReviewers,
recommendations: mockRecommendations
});
} catch (error) {
alert("智能调配失败");
} finally {
setLoadingAllocation(false);
}
};
// 处理关键指标测算
const handleCalculation = async () => {
setLoadingCalculation(true);
setCalculationResult(null);
try {
await new Promise(resolve => setTimeout(resolve, 1500));
// 模拟计算关键指标
const totalRevenue = calculationForm.revenue_year1 + calculationForm.revenue_year2 + calculationForm.revenue_year3;
const totalCost = totalRevenue * calculationForm.cost_ratio;
const interestExpense = calculationForm.loan_amount * calculationForm.interest_rate;
const netProfit = totalRevenue - totalCost - calculationForm.depreciation * 3 - interestExpense * 3;
const irr = ((netProfit / calculationForm.loan_amount) / 3);
const mockIndicators: KeyIndicator[] = [
{
name: "项目内部收益率 (IRR)",
value: (irr * 100).toFixed(2),
unit: "%",
warning: irr < calculationForm.irr_target ? `低于目标IRR ${(calculationForm.irr_target * 100).toFixed(1)}%` : undefined,
historical_avg: "8.5%"
},
{
name: "净现值 (NPV)",
value: (netProfit - calculationForm.loan_amount * 0.05).toFixed(0),
unit: "万元",
historical_avg: "3200万元"
},
{
name: "投资回收期",
value: (calculationForm.loan_amount / (totalRevenue / 3)).toFixed(1),
unit: "年",
warning: (calculationForm.loan_amount / (totalRevenue / 3)) > 5 ? "回收期较长" : undefined,
historical_avg: "4.2年"
},
{
name: "资产负债率",
value: ((calculationForm.loan_amount / (calculationForm.loan_amount * 1.5)) * 100).toFixed(1),
unit: "%",
historical_avg: "62%"
},
{
name: "利息保障倍数",
value: ((totalRevenue - totalCost) / (interestExpense * 3)).toFixed(2),
unit: "倍",
warning: ((totalRevenue - totalCost) / (interestExpense * 3)) < 2 ? "偿债能力偏弱" : undefined,
historical_avg: "3.5倍"
},
{
name: "现金流覆盖率",
value: (1.8).toFixed(2),
unit: "倍",
historical_avg: "2.1倍"
}
];
const mockWarnings: string[] = [];
const mockInsights: string[] = [];
if (irr < calculationForm.irr_target) {
mockWarnings.push("⚠️ 项目IRR低于目标值,建议调整收入预测或降低成本");
}
if ((calculationForm.loan_amount / (totalRevenue / 3)) > 5) {
mockWarnings.push("⚠️ 投资回收期超过5年,需评估长期风险");
}
mockInsights.push("📊 根据历史数据,新能源行业项目平均IRR为8.5%,当前项目处于行业中位水平");
mockInsights.push("💡 类似规模项目的成功案例中,70%采用了分期投资策略以降低风险");
mockInsights.push("🎯 建议关注:补贴政策变化、设备折旧率、运营成本控制");
setCalculationResult({
indicators: mockIndicators,
warnings: mockWarnings,
historical_insights: mockInsights
});
} catch (error) {
alert("指标测算失败");
} finally {
setLoadingCalculation(false);
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case "available":
return <span className="px-2 py-1 bg-green-600 rounded text-xs font-bold">可用</span>;
case "busy":
return <span className="px-2 py-1 bg-yellow-600 rounded text-xs font-bold">繁忙</span>;
case "full":
return <span className="px-2 py-1 bg-red-600 rounded text-xs font-bold">已满</span>;
default:
return null;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-6">
{/* Header */}
<header className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-orange-400 to-red-300 bg-clip-text text-transparent">
评审调配助手
</h1>
<p className="text-slate-400">智能评审员分配 + 关键指标测算</p>
</div>
</div>
</header>
{/* Tab Navigation */}
<div className="mb-6 flex gap-2">
<button
onClick={() => setActiveTab("allocation")}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
activeTab === "allocation"
? "bg-orange-600 text-white"
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
}`}
>
智能调配
</button>
<button
onClick={() => setActiveTab("calculation")}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
activeTab === "calculation"
? "bg-orange-600 text-white"
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
}`}
>
关键指标测算
</button>
</div>
{/* 智能调配 Tab */}
{activeTab === "allocation" && (
<div className="space-y-6">
{/* 项目信息 */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-orange-400">项目信息</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-2">项目名称</label>
<input
type="text"
value={allocationForm.project_name}
onChange={e => setAllocationForm({ ...allocationForm, project_name: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">行业类型</label>
<input
type="text"
value={allocationForm.project_industry}
onChange={e => setAllocationForm({ ...allocationForm, project_industry: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">授信金额(亿元)</label>
<input
type="number"
step="0.1"
value={allocationForm.project_amount}
onChange={e => setAllocationForm({ ...allocationForm, project_amount: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">项目复杂度(1-5)</label>
<input
type="number"
min="1"
max="5"
value={allocationForm.project_complexity}
onChange={e => setAllocationForm({ ...allocationForm, project_complexity: parseInt(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">风险等级</label>
<select
value={allocationForm.risk_level}
onChange={e => setAllocationForm({ ...allocationForm, risk_level: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
>
<option value="low">低风险</option>
<option value="medium">中风险</option>
<option value="high">高风险</option>
</select>
</div>
</div>
<button
onClick={handleAllocation}
disabled={loadingAllocation}
className={`mt-6 w-full py-3 rounded-lg font-bold text-lg transition-all ${
loadingAllocation
? "bg-slate-700 text-slate-500 cursor-not-allowed"
: "bg-gradient-to-r from-orange-600 to-red-600 hover:shadow-xl hover:scale-[1.02]"
}`}
>
{loadingAllocation ? "🔄 智能匹配中..." : "🎯 开始智能调配"}
</button>
</div>
{/* 调配结果 */}
{allocationResult && (
<>
{/* 评审员看板 */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-orange-400 flex items-center gap-2">
<Users className="w-5 h-5" />
评审员工作负荷看板
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{allocationResult.reviewers.map(reviewer => (
<div key={reviewer.id} className="p-4 bg-slate-900/50 rounded-lg border border-slate-700">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<h3 className="font-bold text-white">{reviewer.name}</h3>
<span className="text-xs text-slate-500 font-mono">{reviewer.id}</span>
</div>
{getStatusBadge(reviewer.status)}
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-400">在手项目:</span>
<span className="text-white font-medium">{reviewer.current_projects}/{reviewer.workload_capacity}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">平均处理:</span>
<span className="text-white font-medium">{reviewer.avg_processing_days}天</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">质量评分:</span>
<span className="text-cyan-400 font-medium">{reviewer.quality_score}/10</span>
</div>
<div className="mt-2">
<div className="text-xs text-slate-400 mb-1">专长领域:</div>
<div className="flex flex-wrap gap-1">
{reviewer.specialty.map((spec, idx) => (
<span key={idx} className="px-2 py-0.5 bg-slate-700 rounded text-xs">{spec}</span>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Top 3 推荐 */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-orange-400 flex items-center gap-2">
<Target className="w-5 h-5" />
Top 3 推荐评审员
</h2>
<div className="space-y-4">
{allocationResult.recommendations.map((rec, idx) => (
<div
key={idx}
className={`p-5 rounded-xl border-l-4 ${
idx === 0
? "border-yellow-500 bg-yellow-900/10"
: idx === 1
? "border-slate-400 bg-slate-900/30"
: "border-orange-700 bg-orange-900/10"
}`}
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-xl font-bold ${
idx === 0 ? "bg-yellow-600" : idx === 1 ? "bg-slate-600" : "bg-orange-700"
}`}>
{idx + 1}
</div>
<div>
<h3 className="text-xl font-bold text-white">{rec.reviewer}</h3>
<p className="text-sm text-slate-400">预计 {rec.estimated_days} 天完成</p>
</div>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-orange-400">{rec.score.toFixed(1)}</div>
<div className="text-xs text-slate-400">综合评分</div>
</div>
</div>
<div className="mb-3 p-3 bg-slate-900/50 rounded-lg">
<div className="text-sm text-slate-400 mb-2">推荐理由</div>
<div className="text-sm text-white">{rec.reason}</div>
</div>
<div className="grid grid-cols-4 gap-3">
<div className="text-center p-2 bg-slate-900/50 rounded">
<div className="text-lg font-bold text-cyan-400">{rec.breakdown.history_match}</div>
<div className="text-xs text-slate-400">历史匹配</div>
</div>
<div className="text-center p-2 bg-slate-900/50 rounded">
<div className="text-lg font-bold text-green-400">{rec.breakdown.workload}</div>
<div className="text-xs text-slate-400">负荷状态</div>
</div>
<div className="text-center p-2 bg-slate-900/50 rounded">
<div className="text-lg font-bold text-blue-400">{rec.breakdown.specialty}</div>
<div className="text-xs text-slate-400">专长匹配</div>
</div>
<div className="text-center p-2 bg-slate-900/50 rounded">
<div className="text-lg font-bold text-purple-400">{rec.breakdown.quality}</div>
<div className="text-xs text-slate-400">质量评分</div>
</div>
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
)}
{/* 关键指标测算 Tab */}
{activeTab === "calculation" && (
<div className="space-y-6">
{/* 输入表单 */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-orange-400 flex items-center gap-2">
<Calculator className="w-5 h-5" />
项目财务指标输入
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm text-slate-400 mb-2">项目名称</label>
<input
type="text"
value={calculationForm.project_name}
onChange={e => setCalculationForm({ ...calculationForm, project_name: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-2">第1年营收(万元)</label>
<input
type="number"
value={calculationForm.revenue_year1}
onChange={e => setCalculationForm({ ...calculationForm, revenue_year1: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">第2年营收(万元)</label>
<input
type="number"
value={calculationForm.revenue_year2}
onChange={e => setCalculationForm({ ...calculationForm, revenue_year2: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">第3年营收(万元)</label>
<input
type="number"
value={calculationForm.revenue_year3}
onChange={e => setCalculationForm({ ...calculationForm, revenue_year3: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-2">成本率</label>
<input
type="number"
step="0.01"
value={calculationForm.cost_ratio}
onChange={e => setCalculationForm({ ...calculationForm, cost_ratio: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">年折旧(万元)</label>
<input
type="number"
value={calculationForm.depreciation}
onChange={e => setCalculationForm({ ...calculationForm, depreciation: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">贷款利率</label>
<input
type="number"
step="0.001"
value={calculationForm.interest_rate}
onChange={e => setCalculationForm({ ...calculationForm, interest_rate: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">贷款金额(万元)</label>
<input
type="number"
value={calculationForm.loan_amount}
onChange={e => setCalculationForm({ ...calculationForm, loan_amount: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">目标IRR</label>
<input
type="number"
step="0.01"
value={calculationForm.irr_target}
onChange={e => setCalculationForm({ ...calculationForm, irr_target: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
/>
</div>
</div>
<button
onClick={handleCalculation}
disabled={loadingCalculation}
className={`mt-6 w-full py-3 rounded-lg font-bold text-lg transition-all ${
loadingCalculation
? "bg-slate-700 text-slate-500 cursor-not-allowed"
: "bg-gradient-to-r from-orange-600 to-red-600 hover:shadow-xl hover:scale-[1.02]"
}`}
>
{loadingCalculation ? "🔄 计算中..." : "📊 开始测算"}
</button>
</div>
{/* 测算结果 */}
{calculationResult && (
<>
{/* 关键指标 */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-orange-400 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
关键财务指标
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{calculationResult.indicators.map((indicator, idx) => (
<div
key={idx}
className={`p-4 rounded-lg border ${
indicator.warning
? "border-red-500 bg-red-900/20"
: "border-slate-700 bg-slate-900/50"
}`}
>
<div className="text-sm text-slate-400 mb-2">{indicator.name}</div>
<div className="text-2xl font-bold text-white mb-1">
{indicator.value} <span className="text-sm text-slate-400">{indicator.unit}</span>
</div>
{indicator.historical_avg && (
<div className="text-xs text-slate-500">
历史平均: {indicator.historical_avg}
</div>
)}
{indicator.warning && (
<div className="mt-2 text-xs text-red-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
{indicator.warning}
</div>
)}
</div>
))}
</div>
</div>
{/* 风险预警 */}
{calculationResult.warnings.length > 0 && (
<div className="p-6 bg-red-900/20 border border-red-700 rounded-xl">
<h3 className="text-lg font-bold text-red-400 mb-3 flex items-center gap-2">
<AlertTriangle className="w-5 h-5" />
风险预警
</h3>
<ul className="space-y-2">
{calculationResult.warnings.map((warning, idx) => (
<li key={idx} className="text-sm text-red-300">{warning}</li>
))}
</ul>
</div>
)}
{/* 历史洞察 */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h3 className="text-lg font-bold text-orange-400 mb-3 flex items-center gap-2">
<History className="w-5 h-5" />
基于历史数据的洞察
</h3>
<ul className="space-y-2">
{calculationResult.historical_insights.map((insight, idx) => (
<li key={idx} className="text-sm text-slate-300 flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-cyan-400 flex-shrink-0 mt-0.5" />
<span>{insight}</span>
</li>
))}
</ul>
</div>
</>
)}
</div>
)}
</div>
);
}
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,7 +1,8 @@
"use client";
import {useEffect, useState} from "react";
import {ArrowLeft, ArrowRight, Check, ClipboardList, Clock, Download, FileCheck2, GitCompareArrows, Plus, RefreshCw, Save, Search, ShieldCheck, Sparkles, Trash2, Users, X} from "lucide-react";
import {ArrowRight, Check, ClipboardList, Clock, Download, FileCheck2, GitCompareArrows, Plus, RefreshCw, Save, Search, ShieldCheck, Sparkles, Trash2, Users, X} from "lucide-react";
import AgentNav,{useRequestedProject} from "@/components/xingbang/AgentNav";
import "../data-access/workspace.css";
import "./review.css";
@@ -24,6 +25,7 @@ export default function ReviewWorkbench(){
const [runs,setRuns]=useState<any[]>([]),[selected,setSelected]=useState(""),[tab,setTab]=useState("tasks"),[search,setSearch]=useState("");
const [busy,setBusy]=useState(false),[loading,setLoading]=useState(true),[error,setError]=useState(""),[notice,setNotice]=useState("");
const [modal,setModal]=useState<any>(null),[form,setForm]=useState<any>({}),[people,setPeople]=useState<any[]>([]);
const requested=useRequestedProject();
const [plan,setPlan]=useState<any>({amount:"",term:"",rate:"",deposit:"",guarantee:"",note:""}),[dirty,setDirty]=useState(false);
const p=workspace.projects.find((x:any)=>x.id===selected);
const identity=workspace.identity,preview=identity.preview;
@@ -92,8 +94,7 @@ export default function ReviewWorkbench(){
const text=[`${p.project_id} 上会前摘要(待人工复核)`,`来源:${p.source.id} / ${stamp(p.source.time)}`,`协同版本:${p.version},负责人:${p.owner_name},节点:${p.stage}`,"\n当前方案",...Object.entries(p.plans.at(-1)?.values||{}).map(([k,v])=>`${fields[k]}:${v}`),"\n待确认风险",...p.source.findings.filter((_:any,i:number)=>!riskDone(p,i)).map((f:any)=>`${f.title}:${f.suggestion}`),"\n未关闭补件",...p.tasks.filter((t:any)=>t.status!=="已解决").map((t:any)=>`${t.title} / ${t.assignee} / ${t.due} / ${t.status}`),"\n待复核变更",...p.impacts.filter((i:any)=>!i.confirmed).map((i:any)=>`方案 v${i.plan_version}:${i.title}`),"\nAI 建议",p.summary?`${p.summary.source_version+1===p.version?"当前":"已过期"}:${p.summary.text}`:"未生成"].join("\n");
const url=URL.createObjectURL(new Blob([text],{type:"text/plain;charset=utf-8"}));const a=document.createElement("a");a.href=url;a.download=`${p.project_id}-上会前摘要.txt`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
}
return <div className="ra-app co-app">
<header className="co-top"><a href="/data-access" title="返回风险研判"><ArrowLeft size={19}/></a><div className="co-brand"><ShieldCheck size={25}/><b>兴邦金租<span>评审协同工作台</span></b></div><nav><a href="/data-access">风险研判</a><a href="/report-workbench">报告与经验</a><span>Agent 03</span></nav></header>
return <div className="xb-agent-shell"><AgentNav active="coordination" project={requested||undefined}/><div className="ra-app co-app">
<main className="co-main">
<div className="co-heading"><div><p className="co-eyebrow">REVIEW OPERATIONS</p><h1>评审协同</h1></div><div className="co-actions"><span className="co-mode">{preview?"本机预览":identity.role==="manager"?"客户经理":identity.role==="reviewer"?"审核员":"管理员"}</span><button className="ra-button" title="刷新工作台" aria-label="刷新工作台" disabled={busy} onClick={()=>{if(dirty&&!window.confirm("刷新将丢弃未保存方案,是否继续?"))return;perform(refresh);}}><RefreshCw size={16}/></button>{canManage&&<button className="ra-button" onClick={()=>{setModal({kind:"roster"});}}><Users size={16}/>人员配置</button>}{canWork&&<button className="ra-button primary" disabled={busy} onClick={()=>open("create",{values:{run_id:runs[0]?.id||"",industry:"",complexity:1}})}><Plus size={16}/>接入项目</button>}</div></div>
{error&&<div role="alert" className="ra-alert error">{error}</div>}{notice&&<div role="status" className="co-notice"><Check size={15}/>{notice}</div>}
@@ -136,5 +137,5 @@ export default function ReviewWorkbench(){
{!["create","person","delete_person"].includes(modal.kind)&&<label className="ra-field"><span>{modal.kind==="risk_review"?"处置结论、依据及保留条件":"操作 / 确认意见"}</span><textarea required maxLength={5000} value={form.note||""} onChange={e=>setForm({...form,note:e.target.value})}/></label>}
<footer><button type="button" className="ra-button" disabled={busy} onClick={()=>setModal(null)}>取消</button><button className="ra-button primary" disabled={busy}><Check size={15}/>{busy?"保存中…":"确认保存"}</button></footer></form>}
</section></div>}
</div>;
</div></div>;
}
-78
View File
@@ -1,78 +0,0 @@
"use client";
import { useState } from "react";
export default function RiskAssessPage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [report, setReport] = useState<any>(null);
const handleClick = async () => {
console.log("按钮被点击了!");
setLoading(true);
setError(null);
setReport(null);
try {
const url = "http://localhost:8002/api/risk-assess/test";
console.log("请求URL:", url);
const response = await fetch(url);
console.log("响应状态:", response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
console.log("收到数据:", data);
setReport(data);
} catch (err: any) {
console.error("错误:", err);
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-slate-900 text-white p-8">
<h1 className="text-3xl font-bold mb-8">风险研判助手</h1>
<button
onClick={handleClick}
disabled={loading}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg disabled:bg-gray-600"
>
{loading ? "加载中..." : "🔍 开始风险研判"}
</button>
{error && (
<div className="mt-4 p-4 bg-red-900 rounded-lg">
错误: {error}
</div>
)}
{report && (
<div className="mt-8 space-y-4">
<div className="p-6 bg-slate-800 rounded-lg">
<h2 className="text-xl font-bold mb-4">
📊 {report.customer_name}
</h2>
<p>高风险: {report.summary.high_count} 个</p>
<p>中风险: {report.summary.medium_count} 个</p>
<p>总计: {report.risk_items.length} 个风险项</p>
</div>
{report.risk_items.map((item: any, idx: number) => (
<div key={idx} className="p-4 bg-slate-800 rounded-lg border-l-4 border-red-500">
<h3 className="font-bold">{item.title}</h3>
<p className="text-sm text-gray-400 mt-2">{item.description}</p>
<p className="text-sm mt-2">💡 {item.suggestion}</p>
</div>
))}
</div>
)}
</div>
);
}
-706
View File
@@ -1,706 +0,0 @@
"use client";
import { useState } from "react";
interface Evidence {
rule_based: {
source: string;
page?: number;
extracted_by: string;
confidence: number;
};
data_based: {
sample_size: number;
time_range: string;
decision_stats: Array<{
decision: string;
percentage: number;
count: number;
avg_processing_days: number;
}>;
similar_cases: Array<{
case_id: string;
customer_name: string;
decision: string;
mitigation: string;
}>;
};
}
interface RiskItem {
level: "high" | "medium" | "low";
title: string;
description: string;
evidence: Evidence;
suggestion: string;
smart_recommendation: {
primary_action: string;
alternative_actions: string[];
reference_case?: {
case_id: string;
customer_name: string;
scenario: string;
outcome: string;
} | null;
};
}
interface RiskReport {
customer_name: string;
analysis_timestamp: string;
risk_items: RiskItem[];
summary: {
high_count: number;
medium_count: number;
low_count: number;
focus_areas: string[];
overall_confidence: number;
data_coverage: string;
};
}
interface ChatMessage {
role: "user" | "assistant";
content: string;
}
export default function RiskAssessPage() {
const [loading, setLoading] = useState(false);
const [report, setReport] = useState<RiskReport | null>(null);
const [error, setError] = useState<string | null>(null);
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set());
// 规则引擎状态
const [showRuleEngine, setShowRuleEngine] = useState(false);
const [newRule, setNewRule] = useState("");
const [rules, setRules] = useState<string[]>([
"资产负债率 >= 70% → 高风险",
"征信查询次数 >= 10次 → 中风险",
"涉诉金额 > 授信额度20% → 高风险"
]);
const [ruleLoading, setRuleLoading] = useState(false);
// 历史数据对话状态
const [showHistoryChat, setShowHistoryChat] = useState(false);
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
const [chatInput, setChatInput] = useState("");
const [chatLoading, setChatLoading] = useState(false);
// 报告总览状态
const [showOverview, setShowOverview] = useState(false);
const [formData, setFormData] = useState({
customer_name: "远东新能源公交公司",
project_type: "对公新能源",
amount: 3.0,
debt_ratio: 0.65,
credit_query_count: 8,
litigation_count: 2,
external_guarantee: 3.5,
});
const toggleExpand = (index: number) => {
const newExpanded = new Set(expandedItems);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedItems(newExpanded);
};
const runRiskAssess = async () => {
setLoading(true);
setError(null);
setReport(null);
setExpandedItems(new Set());
try {
const response = await fetch("http://localhost:8002/api/risk-assess/test");
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
setReport(data);
} catch (err: any) {
setError(err.message || "请求失败");
} finally {
setLoading(false);
}
};
// 添加规则(AI 总结)
const addRule = async () => {
if (!newRule.trim()) return;
setRuleLoading(true);
try {
// 调用 mini openclaw 来总结规则
const response = await fetch("http://localhost:8002/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: `请将以下自然语言描述转换为简洁的风险评估规则(格式:条件 → 风险等级):\n\n${newRule}`,
session_id: "rule-engine",
stream: false
})
});
const data = await response.json();
const summarizedRule = data.response || newRule;
setRules([...rules, summarizedRule]);
setNewRule("");
} catch (err) {
// 如果 AI 失败,直接添加原始规则
setRules([...rules, newRule]);
setNewRule("");
} finally {
setRuleLoading(false);
}
};
// 发送历史数据查询对话
const sendChatMessage = async () => {
if (!chatInput.trim() || chatLoading) return;
const userMessage = chatInput;
setChatInput("");
setChatMessages([...chatMessages, { role: "user", content: userMessage }]);
setChatLoading(true);
try {
const response = await fetch("http://localhost:8002/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: `你是风险研判历史数据助手。用户问:${userMessage}\n\n请基于历史案例数据回答(如果需要具体数据,可以说明这是模拟环境)。`,
session_id: "history-chat",
stream: false
})
});
const data = await response.json();
setChatMessages(prev => [...prev, {
role: "assistant",
content: data.response || "抱歉,查询失败。"
}]);
} catch (err) {
setChatMessages(prev => [...prev, {
role: "assistant",
content: "连接失败,请检查后端服务。"
}]);
} finally {
setChatLoading(false);
}
};
const getLevelColor = (level: string) => {
switch (level) {
case "high": return "border-red-500 bg-red-900/20";
case "medium": return "border-yellow-500 bg-yellow-900/20";
case "low": return "border-green-500 bg-green-900/20";
default: return "border-slate-500 bg-slate-900/20";
}
};
const getLevelBadge = (level: string) => {
switch (level) {
case "high": return <span className="px-3 py-1 bg-red-600 rounded-full text-xs font-bold">高风险</span>;
case "medium": return <span className="px-3 py-1 bg-yellow-600 rounded-full text-xs font-bold">中风险</span>;
case "low": return <span className="px-3 py-1 bg-green-600 rounded-full text-xs font-bold">低风险</span>;
default: return null;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-6 overflow-y-auto">
<div className="max-w-7xl mx-auto">
{/* Header */}
<header className="mb-8 sticky top-0 bg-slate-900/80 backdrop-blur-md z-10 py-4 -mx-6 px-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-cyan-300 bg-clip-text text-transparent">
风险研判助手
</h1>
<p className="text-slate-400">双轨驱动:规则引擎 + 历史数据</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setShowRuleEngine(!showRuleEngine)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
📋 规则引擎
</button>
<button
onClick={() => setShowHistoryChat(!showHistoryChat)}
className="px-4 py-2 bg-cyan-600 hover:bg-cyan-700 rounded-lg transition-colors"
>
💬 历史数据
</button>
</div>
</div>
</header>
{/* 规则引擎面板 */}
{showRuleEngine && (
<div className="mb-8 p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-blue-700 max-h-[600px] overflow-y-auto">
<h2 className="text-xl font-bold mb-4 text-blue-400 sticky top-0 bg-slate-800/90 backdrop-blur-sm py-2 -mt-2">📋 规则引擎管理</h2>
<div className="mb-4">
<label className="block text-sm text-slate-400 mb-2">用自然语言添加新规则(AI 会自动总结)</label>
<div className="flex gap-2">
<input
type="text"
value={newRule}
onChange={e => setNewRule(e.target.value)}
onKeyPress={e => e.key === "Enter" && addRule()}
placeholder="例如:如果客户的资产负债率超过80%,应该标记为高风险"
className="flex-1 px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
<button
onClick={addRule}
disabled={ruleLoading || !newRule.trim()}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg disabled:bg-slate-700 disabled:cursor-not-allowed transition-colors"
>
{ruleLoading ? "处理中..." : "添加"}
</button>
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-slate-400 mb-2">当前规则列表:</div>
{rules.map((rule, index) => (
<div key={index} className="p-3 bg-slate-900/50 rounded-lg flex items-center justify-between">
<span className="text-sm">{rule}</span>
<button
onClick={() => setRules(rules.filter((_, i) => i !== index))}
className="text-red-400 hover:text-red-300 text-sm"
>
删除
</button>
</div>
))}
</div>
</div>
)}
{/* 历史数据对话面板 */}
{showHistoryChat && (
<div className="mb-8 p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-cyan-700 max-h-[500px] flex flex-col">
<h2 className="text-xl font-bold mb-4 text-cyan-400">💬 历史数据查询助手</h2>
<div className="flex-1 overflow-y-auto space-y-3 p-4 bg-slate-900/50 rounded-lg mb-4">
{chatMessages.length === 0 && (
<div className="text-slate-400 text-sm text-center py-8">
请输入问题,例如:"历史上类似资产负债率65%的新能源客户最终是什么决策?"
</div>
)}
{chatMessages.map((msg, index) => (
<div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
<div className={`max-w-[80%] px-4 py-2 rounded-lg ${
msg.role === "user"
? "bg-blue-600 text-white"
: "bg-slate-700 text-slate-200"
}`}>
{msg.content}
</div>
</div>
))}
{chatLoading && (
<div className="flex justify-start">
<div className="px-4 py-2 bg-slate-700 rounded-lg text-slate-400">
正在查询...
</div>
</div>
)}
</div>
<div className="flex gap-2">
<input
type="text"
value={chatInput}
onChange={e => setChatInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && sendChatMessage()}
placeholder="询问历史案例..."
className="flex-1 px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-cyan-500"
/>
<button
onClick={sendChatMessage}
disabled={chatLoading || !chatInput.trim()}
className="px-6 py-2 bg-cyan-600 hover:bg-cyan-700 rounded-lg disabled:bg-slate-700 disabled:cursor-not-allowed transition-colors"
>
发送
</button>
</div>
</div>
)}
{/* 项目信息表单 */}
<div className="mb-8 p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-cyan-400">📝 项目信息</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-2">客户名称</label>
<input
type="text"
value={formData.customer_name}
onChange={e => setFormData({ ...formData, customer_name: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">项目类型</label>
<input
type="text"
value={formData.project_type}
onChange={e => setFormData({ ...formData, project_type: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">授信金额(亿元)</label>
<input
type="number"
step="0.1"
value={formData.amount}
onChange={e => setFormData({ ...formData, amount: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">资产负债率</label>
<input
type="number"
step="0.01"
value={formData.debt_ratio}
onChange={e => setFormData({ ...formData, debt_ratio: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">征信查询次数</label>
<input
type="number"
value={formData.credit_query_count}
onChange={e => setFormData({ ...formData, credit_query_count: parseInt(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">涉诉记录(起)</label>
<input
type="number"
value={formData.litigation_count}
onChange={e => setFormData({ ...formData, litigation_count: parseInt(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">对外担保(亿元)</label>
<input
type="number"
step="0.1"
value={formData.external_guarantee}
onChange={e => setFormData({ ...formData, external_guarantee: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
</div>
<button
onClick={runRiskAssess}
disabled={loading}
className={`mt-6 w-full py-3 rounded-lg font-bold text-lg transition-all ${
loading
? "bg-slate-700 text-slate-500 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-cyan-600 hover:shadow-xl hover:scale-[1.02]"
}`}
>
{loading ? "🔄 分析中..." : "🔍 开始风险研判"}
</button>
</div>
{/* 错误提示 */}
{error && (
<div className="mb-8 p-4 bg-red-900/30 border border-red-700 rounded-lg text-red-300">
❌ 错误: {error}
</div>
)}
{/* 风险研判报告 */}
{report && (
<div className="space-y-6">
{/* 汇总卡片 */}
<div className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-cyan-400">
📊 风险研判报告 - {report.customer_name}
</h2>
<div className="flex items-center gap-4">
<button
onClick={() => setShowOverview(!showOverview)}
className="px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:shadow-lg rounded-lg transition-all"
>
📑 {showOverview ? "关闭" : "查看"}报告总览
</button>
<div className="text-xs text-slate-400">
分析时间:{new Date(report.analysis_timestamp).toLocaleString('zh-CN')}
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="p-4 bg-red-900/20 border border-red-700 rounded-lg">
<div className="text-3xl font-bold text-red-400">{report.summary.high_count}</div>
<div className="text-sm text-slate-400">高风险项</div>
</div>
<div className="p-4 bg-yellow-900/20 border border-yellow-700 rounded-lg">
<div className="text-3xl font-bold text-yellow-400">{report.summary.medium_count}</div>
<div className="text-sm text-slate-400">中风险项</div>
</div>
<div className="p-4 bg-green-900/20 border border-green-700 rounded-lg">
<div className="text-3xl font-bold text-green-400">{report.summary.low_count}</div>
<div className="text-sm text-slate-400">低风险项</div>
</div>
<div className="p-4 bg-blue-900/20 border border-blue-700 rounded-lg">
<div className="text-3xl font-bold text-blue-400">{(report.summary.overall_confidence * 100).toFixed(0)}%</div>
<div className="text-sm text-slate-400">综合置信度</div>
</div>
</div>
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-2">
<span className="text-slate-400">关注领域:</span>
<div className="flex gap-2">
{report.summary.focus_areas.map((area, i) => (
<span key={i} className="px-2 py-1 bg-slate-700 rounded text-xs">{area}</span>
))}
</div>
</div>
<div className="text-slate-400">
数据覆盖:<span className="text-cyan-400">{report.summary.data_coverage}</span>
</div>
</div>
</div>
{/* 报告总览 */}
{showOverview && (
<div className="p-6 bg-gradient-to-br from-purple-900/30 to-pink-900/30 backdrop-blur-sm rounded-xl border border-purple-700">
<h3 className="text-xl font-bold mb-4 text-purple-400">📑 报告总览</h3>
<div className="space-y-3">
{report.risk_items.map((item, index) => (
<div key={index} className="p-4 bg-slate-800/50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{getLevelBadge(item.level)}
<span className="font-bold">{item.title}</span>
</div>
<button
onClick={() => {
setShowOverview(false);
toggleExpand(index);
setTimeout(() => {
document.getElementById(`risk-item-${index}`)?.scrollIntoView({ behavior: "smooth" });
}, 100);
}}
className="text-cyan-400 hover:text-cyan-300 text-sm"
>
查看详情 →
</button>
</div>
<p className="text-sm text-slate-300">{item.description}</p>
</div>
))}
</div>
</div>
)}
{/* 风险项列表 */}
{report.risk_items.map((item, index) => (
<div
key={index}
id={`risk-item-${index}`}
className={`p-6 rounded-xl border-l-4 ${getLevelColor(item.level)} backdrop-blur-sm`}
>
{/* 标题行 */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
{getLevelBadge(item.level)}
<h3 className="text-lg font-bold">{item.title}</h3>
</div>
<p className="text-slate-300">{item.description}</p>
</div>
</div>
{/* 推断过程(可溯源) */}
<div className="mb-4 p-4 bg-slate-800/70 rounded-lg border border-slate-600">
<div className="text-sm font-bold text-purple-400 mb-3">🔍 推断过程(可溯源)</div>
<div className="space-y-2 text-sm">
<div className="flex items-start gap-2">
<span className="text-cyan-400 mt-1">1.</span>
<div>
<span className="text-slate-400">规则匹配:</span>
<span className="text-white">检测到 {item.evidence.rule_based.source}</span>
{item.evidence.rule_based.page && (
<span className="text-slate-400"> (第{item.evidence.rule_based.page}页)</span>
)}
</div>
</div>
<div className="flex items-start gap-2">
<span className="text-cyan-400 mt-1">2.</span>
<div>
<span className="text-slate-400">历史数据分析:</span>
<span className="text-white">查询到 {item.evidence.data_based.sample_size} 个相似案例</span>
<span className="text-slate-400"> ({item.evidence.data_based.time_range})</span>
</div>
</div>
<div className="flex items-start gap-2">
<span className="text-cyan-400 mt-1">3.</span>
<div>
<span className="text-slate-400">置信度计算:</span>
<span className="text-white">规则置信度 {(item.evidence.rule_based.confidence * 100).toFixed(0)}%</span>
</div>
</div>
<div className="flex items-start gap-2">
<span className="text-cyan-400 mt-1">4.</span>
<div>
<span className="text-slate-400">结论:</span>
<span className="text-white">标记为 {item.level === "high" ? "高风险" : item.level === "medium" ? "中风险" : "低风险"}</span>
</div>
</div>
</div>
</div>
{/* 建议 */}
<div className="mb-4 p-3 bg-slate-800/50 rounded-lg">
<div className="text-sm text-slate-400 mb-1">💡 业务经理建议</div>
<div className="text-sm">{item.suggestion}</div>
</div>
{/* 智能推荐 */}
<div className="mb-4 p-4 bg-gradient-to-r from-blue-900/30 to-cyan-900/30 rounded-lg border border-blue-700/50">
<div className="text-sm font-bold text-cyan-400 mb-2">🎯 智能推荐(融合规则+数据)</div>
<div className="text-sm mb-2">
<span className="text-slate-400">首选方案:</span>
<span className="text-white">{item.smart_recommendation.primary_action}</span>
</div>
{item.smart_recommendation.alternative_actions.length > 0 && (
<div className="text-sm">
<span className="text-slate-400">备选方案:</span>
<ul className="list-disc list-inside ml-4 mt-1 space-y-1">
{item.smart_recommendation.alternative_actions.map((action, i) => (
<li key={i} className="text-slate-300">{action}</li>
))}
</ul>
</div>
)}
{item.smart_recommendation.reference_case && (
<div className="mt-2 p-2 bg-slate-800/50 rounded text-xs">
<div className="text-slate-400">参考案例:{item.smart_recommendation.reference_case.case_id}</div>
<div className="text-slate-300">{item.smart_recommendation.reference_case.outcome}</div>
</div>
)}
</div>
{/* 展开/收起按钮 */}
<button
onClick={() => toggleExpand(index)}
className="text-sm text-cyan-400 hover:text-cyan-300 transition-colors"
>
{expandedItems.has(index) ? "▲ 收起详细分析" : "▼ 展开详细分析"}
</button>
{/* 详细证据 */}
{expandedItems.has(index) && (
<div className="mt-4 space-y-4 border-t border-slate-700 pt-4">
{/* 轨道1: 规则引擎 */}
<div className="p-4 bg-slate-800/70 rounded-lg">
<h4 className="font-bold text-blue-400 mb-3">🔹 轨道1: 规则引擎证据</h4>
<div className="space-y-2 text-sm">
<div>
<span className="text-slate-400">来源:</span>
<span className="text-white">{item.evidence.rule_based.source}</span>
{item.evidence.rule_based.page && (
<span className="text-slate-400"> (第{item.evidence.rule_based.page}页)</span>
)}
</div>
<div>
<span className="text-slate-400">提取方式:</span>
<span className="text-white">{item.evidence.rule_based.extracted_by}</span>
</div>
<div>
<span className="text-slate-400">置信度:</span>
<span className="text-cyan-400">{(item.evidence.rule_based.confidence * 100).toFixed(0)}%</span>
</div>
</div>
</div>
{/* 轨道2: 历史数据 */}
<div className="p-4 bg-slate-800/70 rounded-lg">
<h4 className="font-bold text-cyan-400 mb-3">🔹 轨道2: 历史数据证据</h4>
<div className="mb-3 text-sm">
<span className="text-slate-400">样本规模:</span>
<span className="text-white">{item.evidence.data_based.sample_size} 个案例</span>
<span className="text-slate-400 ml-4">时间范围:</span>
<span className="text-white">{item.evidence.data_based.time_range}</span>
</div>
{/* 决策分布 */}
<div className="mb-3">
<div className="text-sm text-slate-400 mb-2">历史决策分布:</div>
<div className="space-y-2">
{item.evidence.data_based.decision_stats.map((stat, i) => (
<div key={i} className="flex items-center gap-3">
<div className="w-32 text-sm text-slate-300">{stat.decision}</div>
<div className="flex-1 bg-slate-700 rounded-full h-6 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-500 to-cyan-500 flex items-center justify-end px-2"
style={{ width: `${stat.percentage}%` }}
>
<span className="text-xs font-bold">{stat.percentage.toFixed(1)}%</span>
</div>
</div>
<div className="text-xs text-slate-400">
{stat.count}例 · 平均{stat.avg_processing_days}天
</div>
</div>
))}
</div>
</div>
{/* 典型案例 */}
{item.evidence.data_based.similar_cases.length > 0 && (
<div>
<div className="text-sm text-slate-400 mb-2">典型案例:</div>
<div className="space-y-2">
{item.evidence.data_based.similar_cases.map((case_, i) => (
<div key={i} className="p-3 bg-slate-900/50 rounded-lg text-sm">
<div className="flex items-center justify-between mb-1">
<span className="text-cyan-400 font-mono">{case_.case_id}</span>
<span className="text-slate-400">{case_.customer_name}</span>
</div>
<div className="text-slate-300 mb-1">
<span className="text-green-400">✓</span> {case_.decision}
</div>
<div className="text-slate-400 text-xs">{case_.mitigation}</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}
-273
View File
@@ -1,273 +0,0 @@
"use client";
import { useState } from "react";
import { Shield, Book, AlertTriangle, Info, Plus, Edit, Trash2, Search } from "lucide-react";
interface Rule {
id: string;
name: string;
category: string;
condition: string;
risk_level: "high" | "medium" | "low";
action: string;
source_document: string;
page?: number;
enabled: boolean;
created_at: string;
}
export default function RuleEnginePage() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState("all");
// 模拟规则数据
const [rules] = useState<Rule[]>([
{
id: "RULE-001",
name: "资产负债率红线",
category: "财务指标",
condition: "资产负债率 >= 70%",
risk_level: "high",
action: "建议退回或要求增加担保",
source_document: "《融资租赁评审手册》第12条",
page: 12,
enabled: true,
created_at: "2025-01-15"
},
{
id: "RULE-002",
name: "资产负债率预警",
category: "财务指标",
condition: "60% <= 资产负债率 < 70%",
risk_level: "medium",
action: "建议降额或增加抵押物",
source_document: "《融资租赁评审手册》第12条",
page: 12,
enabled: true,
created_at: "2025-01-15"
},
{
id: "RULE-003",
name: "征信查询异常",
category: "信用记录",
condition: "近6个月征信查询次数 >= 10",
risk_level: "high",
action: "要求说明查询原因,核查融资需求真实性",
source_document: "《信用风险管理办法》第8条",
page: 8,
enabled: true,
created_at: "2025-01-20"
},
{
id: "RULE-004",
name: "涉诉风险",
category: "法律风险",
condition: "未结案涉诉记录 >= 2起",
risk_level: "high",
action: "要求提供诉讼详情和财务影响说明",
source_document: "《法律风险评估指南》第15条",
page: 15,
enabled: true,
created_at: "2025-01-18"
},
{
id: "RULE-005",
name: "对外担保风险",
category: "担保风险",
condition: "对外担保金额 >= 净资产的50%",
risk_level: "medium",
action: "要求提供被担保方财务状况,评估代偿风险",
source_document: "《担保业务管理规定》第22条",
page: 22,
enabled: true,
created_at: "2025-02-01"
},
{
id: "RULE-006",
name: "新能源行业特殊规则",
category: "行业政策",
condition: "项目类型 = 新能源 AND 有政府补贴",
risk_level: "low",
action: "核查补贴到账情况,可适当放宽财务指标",
source_document: "《新能源项目评审补充规定》第5条",
page: 5,
enabled: true,
created_at: "2025-02-10"
},
{
id: "RULE-007",
name: "大额授信审批",
category: "授信管理",
condition: "授信金额 >= 5亿元",
risk_level: "high",
action: "必须上报总部风控委员会审批",
source_document: "《授信审批权限管理办法》第3条",
page: 3,
enabled: true,
created_at: "2025-01-10"
}
]);
const categories = ["all", "财务指标", "信用记录", "法律风险", "担保风险", "行业政策", "授信管理"];
const filteredRules = rules.filter(rule => {
const matchesSearch = rule.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
rule.condition.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === "all" || rule.category === selectedCategory;
return matchesSearch && matchesCategory;
});
const getRiskBadge = (level: string) => {
switch (level) {
case "high": return <span className="px-2 py-1 bg-red-600 rounded text-xs font-bold">高风险</span>;
case "medium": return <span className="px-2 py-1 bg-yellow-600 rounded text-xs font-bold">中风险</span>;
case "low": return <span className="px-2 py-1 bg-green-600 rounded text-xs font-bold">低风险</span>;
default: return null;
}
};
const getRiskColor = (level: string) => {
switch (level) {
case "high": return "border-red-500";
case "medium": return "border-yellow-500";
case "low": return "border-green-500";
default: return "border-slate-500";
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-6">
{/* Header */}
<header className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-cyan-300 bg-clip-text text-transparent">
📚 规则引擎管理
</h1>
<p className="text-slate-400">业务规则配置与管理(轨道1:规则驱动)</p>
</div>
<button className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium flex items-center gap-2 transition-colors">
<Plus className="w-4 h-4" />
新增规则
</button>
</div>
</header>
{/* 统计卡片 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-blue-400">{rules.length}</div>
<div className="text-sm text-slate-400">总规则数</div>
</div>
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-red-400">{rules.filter(r => r.risk_level === "high").length}</div>
<div className="text-sm text-slate-400">高风险规则</div>
</div>
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-green-400">{rules.filter(r => r.enabled).length}</div>
<div className="text-sm text-slate-400">已启用</div>
</div>
<div className="p-4 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="text-3xl font-bold text-slate-400">{categories.length - 1}</div>
<div className="text-sm text-slate-400">规则分类</div>
</div>
</div>
{/* 搜索和筛选 */}
<div className="mb-6 p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="flex flex-col md:flex-row gap-4">
{/* 搜索框 */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="搜索规则名称或条件..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
{/* 分类筛选 */}
<div className="flex gap-2 flex-wrap">
{categories.map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedCategory === cat
? "bg-blue-600 text-white"
: "bg-slate-700 text-slate-300 hover:bg-slate-600"
}`}
>
{cat === "all" ? "全部" : cat}
</button>
))}
</div>
</div>
</div>
{/* 规则列表 */}
<div className="space-y-4">
{filteredRules.length === 0 ? (
<div className="p-8 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700 text-center text-slate-400">
未找到匹配的规则
</div>
) : (
filteredRules.map(rule => (
<div
key={rule.id}
className={`p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border-l-4 ${getRiskColor(rule.risk_level)}`}
>
{/* 标题行 */}
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
{getRiskBadge(rule.risk_level)}
<span className="px-2 py-1 bg-slate-700 rounded text-xs">{rule.category}</span>
<h3 className="text-lg font-bold">{rule.name}</h3>
<span className="text-xs text-slate-500 font-mono">{rule.id}</span>
</div>
</div>
<div className="flex items-center gap-2">
<button className="p-2 text-slate-400 hover:text-blue-400 hover:bg-slate-700 rounded-lg transition-colors">
<Edit className="w-4 h-4" />
</button>
<button className="p-2 text-slate-400 hover:text-red-400 hover:bg-slate-700 rounded-lg transition-colors">
<Trash2 className="w-4 h-4" />
</button>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" checked={rule.enabled} readOnly className="sr-only peer" />
<div className="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
{/* 条件 */}
<div className="mb-3 p-3 bg-slate-900/50 rounded-lg">
<div className="text-xs text-slate-400 mb-1">触发条件</div>
<div className="text-sm font-mono text-cyan-400">{rule.condition}</div>
</div>
{/* 动作 */}
<div className="mb-3 p-3 bg-slate-900/50 rounded-lg">
<div className="text-xs text-slate-400 mb-1">执行动作</div>
<div className="text-sm text-white">{rule.action}</div>
</div>
{/* 元数据 */}
<div className="flex items-center gap-6 text-xs text-slate-400">
<div className="flex items-center gap-2">
<Book className="w-3.5 h-3.5" />
<span>{rule.source_document}</span>
{rule.page && <span className="text-slate-500">(第{rule.page}页)</span>}
</div>
<div className="text-slate-500">创建时间: {rule.created_at}</div>
</div>
</div>
))
)}
</div>
</div>
);
}
+10 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { Suspense, useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { getTrajectory } from "@/lib/api";
import { ArrowLeft, MessageSquare, GitBranch } from "lucide-react";
@@ -15,7 +15,7 @@ interface Message {
tool_call_id?: string;
}
export default function TrajectoryPage() {
function TrajectoryContent() {
const searchParams = useSearchParams();
const router = useRouter();
const sessionId = searchParams?.get("session") || "default";
@@ -105,3 +105,11 @@ export default function TrajectoryPage() {
</div>
);
}
export default function TrajectoryPage() {
return (
<Suspense fallback={<div className="min-h-screen flex items-center justify-center text-sm text-gray-600">正在加载交互轨迹...</div>}>
<TrajectoryContent />
</Suspense>
);
}
-44
View File
@@ -1,44 +0,0 @@
"use client";
import { useEffect, useState } from "react";
const API = `http://${typeof window === 'undefined' ? 'localhost' : window.location.hostname}:8002/api/xingbang/workspace`;
const labels: Record<string,string> = {name:'项目名称',client:'客户名称',amount:'授信金额(万元)',assets:'资产总额(万元)',liabilities:'负债总额(万元)',lease_value:'租赁物价值(万元)',insurance:'保额(万元)'};
export default function Page() {
const [projects,setProjects]=useState<any[]>([]), [project,setProject]=useState<any>(null);
const [error,setError]=useState(''), [busy,setBusy]=useState(false), [tab,setTab]=useState('风险研判');
const [note,setNote]=useState(''), [actor,setActor]=useState('演示主审员'), [amount,setAmount]=useState('');
const [form,setForm]=useState<Record<string,string>>({name:'演示光伏项目',client:'示例客户',amount:'5000',assets:'18000',liabilities:'13200',lease_value:'5400',insurance:''});
async function request(path:string, body?:any) {
const r=await fetch(API+path,body?{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}:undefined);
const data=await r.json(); if(!r.ok) throw new Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail)); return data;
}
useEffect(()=>{request('/projects').then(data=>{setProjects(data);setProject(data[0]||null)}).catch(e=>setError(e.message));},[]);
async function run(kind:string,target='') {
setBusy(true);setError('');
try {
let data: any;
if(kind==='create') {
const body:Record<string,any>={...form};
Object.keys(body).filter(k=>!['name','client'].includes(k)).forEach(k=>body[k]=form[k]===''?null:Number(form[k]));
data=await request('/projects',body);setProjects(xs=>[data,...xs]);
} else {data=await request(`/projects/${project.id}/actions`,{kind,target,note,actor,amount:amount?Number(amount):null});setProjects(xs=>xs.map(x=>x.id===data.id?data:x));}
setProject(data);setNote('');
}catch(e:any){setError(e.message)}finally{setBusy(false)}
}
const button=(text:string,kind:string,target='')=><button disabled={busy} className="border rounded px-3 py-2 text-sm bg-white hover:bg-gray-100 disabled:opacity-40" onClick={()=>run(kind,target)}>{text}</button>;
const snapshot=project?.snapshots?.at(-1);
return <main className="min-h-screen bg-slate-100 text-slate-900">
<header className="bg-white border-b p-5"><h1 className="text-xl font-bold">兴邦金租 · 智能评审工作台</h1><p className="text-sm text-amber-800 mt-1">业务验证原型 · 人工录入 / 演示阈值 · 未接入 ERP、本地模型及正式权限体系</p></header>
<div className="grid lg:grid-cols-[260px_minmax(0,1fr)] gap-5 p-5">
<aside className="bg-white border rounded p-4"><h2 className="font-bold mb-3">项目列表</h2>{projects.map(x=><button key={x.id} className={`block text-left w-full p-2 rounded ${project?.id===x.id?'bg-blue-50 text-blue-800':''}`} onClick={()=>{setProject(x);setError('')}}>{x.name}</button>)}<h3 className="font-bold mt-6">新建验证项目</h3>{Object.keys(form).map(k=><label key={k} className="block text-xs mt-3">{labels[k]}<input className="border rounded p-2 w-full mt-1" value={form[k]} onChange={e=>setForm({...form,[k]:e.target.value})}/></label>)}<p className="text-xs text-gray-500 my-3">数值留空表示缺失,不作为零处理。</p>{button('建立项目','create')}</aside>
<section className="min-w-0">{error&&<p role="alert" className="bg-red-50 border border-red-300 p-3 mb-4">{error}</p>}{project?<>
<div className="bg-white border rounded p-5 mb-4"><h2 className="text-xl font-semibold">{project.name}</h2><p>{project.client} · {project.stale?'当前快照待研判':'已研判'}</p><div className="text-xs text-gray-500 mt-3 break-all">快照 {snapshot.id} · {snapshot.time} · {snapshot.source}</div><details className="text-xs mt-2"><summary>查看原始数据及版本</summary><pre className="whitespace-pre-wrap">{JSON.stringify(snapshot,null,2)}</pre></details></div>
<nav className="flex flex-wrap gap-2 mb-4">{['风险研判','评审协同','租赁物与增信','报告与经验'].map(t=><button key={t} className={`px-4 py-3 rounded ${tab===t?'bg-blue-900 text-white':'bg-white border'}`} onClick={()=>setTab(t)}>{t}</button>)}</nav>
<div className="bg-white border p-4 mb-4 grid sm:grid-cols-2 gap-3"><label className="text-sm">演示操作人<input className="border p-2 w-full" value={actor} onChange={e=>setActor(e.target.value)}/></label><label className="text-sm">判断依据 / 处理说明<input className="border p-2 w-full" value={note} onChange={e=>setNote(e.target.value)}/></label></div>
{(tab==='风险研判'||tab==='租赁物与增信')&&<>{button('基于当前快照研判','assess')}{project.risks.filter((r:any)=>tab==='风险研判'||r.id!=='debt').map((r:any)=><article key={r.id} className="bg-white border rounded p-4 mt-3"><h3 className="font-bold">{r.title}</h3><p>{r.category} · {r.value===null?'数据缺失':r.value}</p><p className="text-sm mt-2">{r.status} {r.note}</p><details className="text-xs my-3"><summary>查看来源、计算公式和演示规则</summary><pre className="whitespace-pre-wrap">{JSON.stringify(r.evidence,null,2)}</pre></details><div className="flex gap-2">{button('确认','confirm',r.id)}{button('驳回','reject',r.id)}{button('转补件','task',r.id)}</div></article>)}{tab==='租赁物与增信'&&<p className="text-sm p-4">本原型仅测算价值和保额比例;权属、保险条款、担保登记和法律可执行性尚未核验。</p>}</>}
{tab==='评审协同'&&<><div className="bg-white border p-4 mb-3"><label>新授信额度(万元)<input className="border p-2 mx-2" value={amount} onChange={e=>setAmount(e.target.value)}/></label>{button('保存方案变更','change')}<ul className="mt-3">{project.impacts?.map((s:string)=><li key={s}>需复核:{s}</li>)}</ul></div>{project.tasks.map((t:any)=><article key={t.id} className="bg-white border p-4 mb-3"><b>{t.risk}</b><p>{t.requirement}</p><p className="text-sm my-2">{t.owner} · {t.status} · {t.note}</p>{t.status==='待提交'&&button('登记材料已提交','submit_task',t.id)}{t.status==='已提交待确认'&&button('确认问题已解决','close_task',t.id)}</article>)}</>}
{tab==='报告与经验'&&<>{button('生成报告草稿','report')}{project.reports.map((r:any)=><article key={r.id} className="bg-white border p-4 my-3"><b>{r.status} · {r.stale?'旧快照,需更新':'当前版本'}</b><pre className="whitespace-pre-wrap text-sm my-3">{r.body}</pre>{!r.stale&&r.status==='草稿'&&button('确认定稿并提取经验卡','finalize',r.id)}</article>)}<h3 className="font-bold mt-5">经验卡</h3>{project.cards.map((c:any)=><article key={c.id} className="bg-white border p-4 mt-3"><p className="mb-2">{c.status} · 来源报告 {c.report.slice(0,8)}</p><details><summary>查看经验事实与审核记录</summary><pre className="whitespace-pre-wrap text-xs">{JSON.stringify(c,null,2)}</pre></details>{c.status!=='已过期/归档'&&button('记录审核意见并进入下一状态','card',c.id)}</article>)}</>}
<details className="mt-5 text-xs"><summary>审计记录({project.audit.length})</summary><pre className="whitespace-pre-wrap">{JSON.stringify(project.audit,null,2)}</pre></details>
</>:<p>从左侧建立项目开始验证。</p>}</section>
</div>
</main>;
}
-370
View File
@@ -1,370 +0,0 @@
'use client';
import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';
// ============ 类型定义 ============
interface Assistant {
id: string;
name: string;
description: string;
icon: string;
pain_point: string;
value: string;
}
interface ProjectData {
project_name: string;
client_name: string;
industry: string;
credit_amount: number;
lease_object: string;
lease_value: number;
financial_data: Record<string, any>;
credit_report: Record<string, any>;
litigation_records: any[];
sentiment_info: Record<string, any>;
}
interface InsurancePolicy {
policy_number: string;
insurance_company: string;
coverage_amount: number;
policy_type: string;
exclusions: string[];
}
// 后端 API 基址(指向 mini-OpenClaw 后端 FastAPI:8002,与前端 3000 分离)
const API_BASE =
typeof window !== 'undefined' && window.location.hostname
? `http://${window.location.hostname}:8002/api/xingbang/v2`
: 'http://localhost:8002/api/xingbang/v2';
// ============ 示例数据 ============
const SAMPLE_PROJECT_1: ProjectData = {
project_name: '远东新能源公交车租赁项目',
client_name: '远东城市公交集团有限公司',
industry: '城市公共交通',
credit_amount: 8000,
lease_object: '纯电动公交车50辆',
lease_value: 10000,
financial_data: {
registered_capital: 5000,
established_years: 8,
asset_liability_ratio: 0.65,
current_ratio: 1.35,
net_profit_2024: 850,
net_profit_2025: 920,
revenue_growth_rate: 0.12,
roe: 0.085,
},
credit_report: {
overdue_count: 0,
query_count_recent_6m: 3,
credit_limit: 15000,
enforcement_records: 0,
},
litigation_records: [],
sentiment_info: {
negative_news_count: 0,
industry_risk_level: '中等',
recent_events: '政府补贴发放延迟',
},
};
const SAMPLE_PROJECT_2: ProjectData = {
project_name: '华通二手车融资租赁项目',
client_name: '张三(网约车司机)',
industry: '个人网约车',
credit_amount: 18,
lease_object: '二手轿车1辆(丰田凯美瑞2020款)',
lease_value: 22,
financial_data: {
monthly_income: 1.2,
debt_ratio: 0.4,
},
credit_report: {
overdue_count: 1,
query_count_recent_6m: 8,
credit_limit: 10,
enforcement_records: 0,
},
litigation_records: [],
sentiment_info: {
negative_news_count: 0,
industry_risk_level: '较高',
recent_events: '网约车行业竞争加剧',
},
};
const SAMPLE_POLICY: InsurancePolicy = {
policy_number: 'PICC-2026-0012345',
insurance_company: '中国人民财产保险股份有限公司',
coverage_amount: 9600,
policy_type: '车辆损失险+盗抢险+第三者责任险',
exclusions: ['战争', '核辐射', '故意损坏', '自然磨损'],
};
// ============ 主组件 ============
export default function XingbangV2Page() {
const [assistants, setAssistants] = useState<Assistant[]>([]);
const [activeAssistant, setActiveAssistant] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState('');
const [selectedProject, setSelectedProject] = useState<ProjectData>(SAMPLE_PROJECT_1);
// 加载助手列表
React.useEffect(() => {
fetch(`${API_BASE}/assistants/list`)
.then((res) => res.json())
.then((data) => setAssistants(data.assistants))
.catch((err) => console.error('加载助手列表失败:', err));
}, []);
// 调用助手(流式)
const callAssistant = async (assistantId: string) => {
setActiveAssistant(assistantId);
setLoading(true);
setResult('');
try {
let endpoint = '';
let requestBody: any = {};
switch (assistantId) {
case 'risk-check':
endpoint = `${API_BASE}/risk-check/stream`;
requestBody = { project_data: selectedProject };
break;
case 'zhongchengxin':
endpoint = `${API_BASE}/zhongchengxin/stream`;
requestBody = { project_data: selectedProject };
break;
case 'monitoring':
endpoint = `${API_BASE}/monitoring/stream`;
requestBody = {
project_id: 'PRJ-2026-001',
project_name: selectedProject.project_name,
client_name: selectedProject.client_name,
initial_data: selectedProject,
};
break;
case 'policy-check':
endpoint = `${API_BASE}/policy-check/stream`;
requestBody = {
project_name: selectedProject.project_name,
client_name: selectedProject.client_name,
credit_amount: selectedProject.credit_amount,
lease_value: selectedProject.lease_value,
policy: SAMPLE_POLICY,
};
break;
default:
throw new Error('未知助手');
}
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
});
if (!response.ok) throw new Error('请求失败');
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) throw new Error('无法读取响应流');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.type === 'content') {
setResult((prev) => prev + data.data);
} else if (data.type === 'error') {
console.error('流式错误:', data.message);
}
}
}
}
} catch (err) {
console.error('调用助手失败:', err);
setResult('❌ 调用失败:' + (err as Error).message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-slate-800 to-gray-900 text-gray-100">
{/* 顶部标题 */}
<div className="border-b border-gray-700 bg-gray-900/50 backdrop-blur">
<div className="container mx-auto px-6 py-6">
<h1 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-cyan-300">
兴邦金租智能评审助手平台 V2
</h1>
<p className="text-gray-400 mt-2">
从会议真实痛点出发,独立设计的4个专项助手
</p>
</div>
</div>
<div className="container mx-auto px-6 py-8">
{/* 项目选择 */}
<div className="mb-8 bg-gray-800/50 rounded-lg p-6 border border-gray-700">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<span>📋</span>
<span>选择测试项目</span>
</h2>
<div className="flex gap-4">
<button
onClick={() => setSelectedProject(SAMPLE_PROJECT_1)}
className={`px-6 py-3 rounded-lg font-medium transition ${
selectedProject === SAMPLE_PROJECT_1
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
对公项目:远东新能源公交车(8000万)
</button>
<button
onClick={() => setSelectedProject(SAMPLE_PROJECT_2)}
className={`px-6 py-3 rounded-lg font-medium transition ${
selectedProject === SAMPLE_PROJECT_2
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
零售项目:张三二手车(18万)
</button>
</div>
<div className="mt-4 text-sm text-gray-400">
<p>
<strong>当前项目:</strong>
{selectedProject.project_name} | <strong>客户:</strong>
{selectedProject.client_name} | <strong>授信:</strong>
{selectedProject.credit_amount}万元
</p>
</div>
</div>
{/* 助手卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{assistants.map((assistant) => (
<div
key={assistant.id}
className="bg-gray-800/50 rounded-lg p-6 border border-gray-700 hover:border-blue-500 transition cursor-pointer"
onClick={() => callAssistant(assistant.id)}
>
<div className="text-4xl mb-3">{assistant.icon}</div>
<h3 className="text-lg font-semibold mb-2">{assistant.name}</h3>
<p className="text-sm text-gray-400 mb-3">{assistant.description}</p>
<div className="text-xs space-y-1">
<div className="text-red-400">
<strong>痛点:</strong>
{assistant.pain_point}
</div>
<div className="text-green-400">
<strong>价值:</strong>
{assistant.value}
</div>
</div>
<button
disabled={loading && activeAssistant === assistant.id}
className="mt-4 w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white py-2 rounded-lg font-medium transition"
>
{loading && activeAssistant === assistant.id ? '运行中...' : '运行助手'}
</button>
</div>
))}
</div>
{/* 结果展示 */}
{result && (
<div className="bg-gray-800/50 rounded-lg p-6 border border-gray-700">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<span>📄</span>
<span>助手输出结果</span>
</h2>
<div className="prose prose-invert max-w-none">
<ReactMarkdown
components={{
h1: ({ node, ...props }) => <h1 className="text-2xl font-bold text-blue-400 mt-6 mb-4" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-xl font-semibold text-cyan-400 mt-5 mb-3" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-lg font-medium text-green-400 mt-4 mb-2" {...props} />,
p: ({ node, ...props }) => <p className="text-gray-300 leading-relaxed mb-3" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc list-inside text-gray-300 mb-3 space-y-1" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal list-inside text-gray-300 mb-3 space-y-1" {...props} />,
table: ({ node, ...props }) => (
<div className="overflow-x-auto mb-4">
<table className="min-w-full border border-gray-600" {...props} />
</div>
),
thead: ({ node, ...props }) => <thead className="bg-gray-700" {...props} />,
th: ({ node, ...props }) => <th className="border border-gray-600 px-4 py-2 text-left" {...props} />,
td: ({ node, ...props }) => <td className="border border-gray-600 px-4 py-2" {...props} />,
code: ({ node, inline, ...props }: any) =>
inline ? (
<code className="bg-gray-700 px-2 py-1 rounded text-cyan-300" {...props} />
) : (
<code className="block bg-gray-900 p-4 rounded-lg overflow-x-auto text-sm" {...props} />
),
}}
>
{result}
</ReactMarkdown>
</div>
</div>
)}
{/* 加载状态 */}
{loading && !result && (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
<p className="text-gray-400 mt-4">AI 正在工作中...</p>
</div>
)}
</div>
{/* 页脚说明 */}
<div className="border-t border-gray-700 bg-gray-900/50 mt-12">
<div className="container mx-auto px-6 py-6 text-sm text-gray-400">
<h3 className="font-semibold text-gray-300 mb-2">系统设计说明</h3>
<ul className="space-y-1">
<li>
✅ <strong>完全推翻旧五智能体框架</strong>
:不再使用"数据汇聚/预审参谋/经验检索/审批意见/风险监控"
</li>
<li>
✅ <strong>从会议独特痛点出发</strong>
:中诚信闲置、保单盲区、17天空窗期、业务评审对立
</li>
<li>
✅ <strong>4个专项助手</strong>
:立项前置风控、中诚信填报、空窗期监控、保单核验
</li>
<li>
✅ <strong>流式交互</strong>:实时展示 AI 生成过程,用户体验流畅
</li>
<li>
✅ <strong>示例数据</strong>
:对公(远东新能源公交车8000万)+ 零售(张三二手车18万)
</li>
</ul>
</div>
</div>
</div>
);
}
-330
View File
@@ -1,330 +0,0 @@
"use client";
/**
* 兴邦金租智能体工作台
* 基于2026-08-18会议纪要真实痛点设计
*
* 核心功能:
* 1. 一键运行完整五Agent流水线(SSE流式展示)
* 2. 实时展示每个Agent的进度与产出
* 3. Markdown渲染每类Agent的最终输出
* 4. 痛点面板:展示系统如何解决会议提出的10大痛点
*/
import { useState, useEffect } from "react";
import ReactMarkdown from "react-markdown";
const API_BASE = `http://${typeof window !== "undefined" ? window.location.hostname : "localhost"}:8002/api/xingbang`;
interface AgentConfig {
id: string;
name: string;
description: string;
icon: string;
}
interface AgentProgress {
agent: string;
type: string;
status: "waiting" | "running" | "completed" | "error";
output?: string;
error?: string;
}
const PAIN_POINTS = [
{ id: 1, title: "评审周期长(17工作日)", solution: "AI预处理数据+标准化报告生成,缩短至10天内" },
{ id: 2, title: "评审标准因人而异", solution: "经验检索Agent拉平标准,展示历史同类案例判断" },
{ id: 3, title: "业务vs评审对立", solution: "前置风险筛查(给风险点不给结论),业务提前准备" },
{ id: 4, title: "评审手册线下执行不力", solution: "AI内置评审逻辑,自动对照手册条款筛查" },
{ id: 5, title: "多源数据整理麻烦", solution: "数据汇聚Agent自动生成客户情报卡" },
{ id: 6, title: "中诚信评级未打通", solution: "架构预留中诚信模板接口,可自动导入" },
{ id: 7, title: "审批空窗期风险无预警", solution: "风险监控Agent实时扫描征信/涉诉/舆情" },
{ id: 8, title: "保险只查形式不查条款", solution: "AI检查保单覆盖率、条款除外责任" },
{ id: 9, title: "零售不良率高(征信追踪贵)", solution: "定期自动更新征信+低成本替代数据源" },
{ id: 10, title: "被否案例未利用", solution: "经验检索Agent纳入失败案例,提示避坑" }
];
export default function XingbangWorkbench() {
const [agents, setAgents] = useState<AgentConfig[]>([]);
const [progress, setProgress] = useState<Record<string, AgentProgress>>({});
const [isRunning, setIsRunning] = useState(false);
const [showPainPoints, setShowPainPoints] = useState(false);
// 示例项目数据
const [projectData, setProjectData] = useState({
customer_name: "远东新能源公交公司",
project_type: "对公新能源",
amount: 3.0
});
useEffect(() => {
// 加载Agent配置
fetch(`${API_BASE}/config`)
.then(res => res.json())
.then(data => {
const agentsWithIcons = data.agents.map((a: any, idx: number) => ({
...a,
icon: ["📊", "🔍", "📚", "📝", "👁️"][idx]
}));
setAgents(agentsWithIcons);
// 初始化progress
const initialProgress: Record<string, AgentProgress> = {};
agentsWithIcons.forEach((a: AgentConfig) => {
initialProgress[a.id] = {
agent: a.name,
type: a.id,
status: "waiting",
};
});
setProgress(initialProgress);
});
}, []);
const runWorkflow = async () => {
setIsRunning(true);
// 重置所有Agent状态
const resetProgress = { ...progress };
Object.keys(resetProgress).forEach(key => {
resetProgress[key].status = "waiting";
resetProgress[key].output = undefined;
resetProgress[key].error = undefined;
});
setProgress(resetProgress);
try {
const response = await fetch(`${API_BASE}/workflow/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(projectData),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const reader = response.body?.getReader();
if (!reader) throw new Error("No response body");
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
let currentEvent = "message";
for (const line of lines) {
if (line.startsWith("event:")) {
currentEvent = line.slice(6).trim();
} else if (line.startsWith("data:")) {
const dataStr = line.slice(5).trim();
if (dataStr) {
try {
const data = JSON.parse(dataStr);
if (currentEvent === "agent_start") {
setProgress(prev => ({
...prev,
[data.type]: { ...prev[data.type], status: "running" }
}));
} else if (currentEvent === "agent_end") {
setProgress(prev => ({
...prev,
[data.type]: { ...prev[data.type], status: "completed", output: data.output }
}));
} else if (currentEvent === "error") {
setProgress(prev => ({
...prev,
[data.type]: { ...prev[data.type], status: "error", error: data.error }
}));
} else if (currentEvent === "done") {
setIsRunning(false);
}
} catch (e) {
console.error("Parse error:", e);
}
}
}
if (line === "") currentEvent = "message";
}
}
} catch (error) {
console.error("Workflow error:", error);
setIsRunning(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-6">
{/* Header */}
<header className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-cyan-300 bg-clip-text text-transparent">
🏛️ 兴邦金租智能体工作台
</h1>
<p className="text-slate-400 text-sm">
基于2026-08-18会议纪要真实痛点设计 · 五Agent智能评审系统
</p>
</div>
<button
onClick={() => setShowPainPoints(!showPainPoints)}
className="px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 rounded-lg text-sm font-medium hover:shadow-lg transition-all"
>
{showPainPoints ? "隐藏痛点面板" : "查看痛点解决方案"}
</button>
</div>
</header>
{/* 痛点面板 */}
{showPainPoints && (
<div className="mb-8 p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-xl font-bold mb-4 text-cyan-400">💡 会议痛点 → AI解决方案</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{PAIN_POINTS.map(p => (
<div key={p.id} className="p-3 bg-slate-900/50 rounded-lg border border-slate-700">
<div className="flex items-start gap-2">
<span className="text-red-400 font-bold text-sm">{p.id}.</span>
<div className="flex-1">
<div className="text-sm font-semibold text-slate-300 mb-1">{p.title}</div>
<div className="text-xs text-green-400">✓ {p.solution}</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* 项目输入表单 */}
<div className="mb-8 p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<h2 className="text-lg font-bold mb-4 text-blue-400">📋 模拟项目数据</h2>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-2">客户名称</label>
<input
type="text"
value={projectData.customer_name}
onChange={e => setProjectData({ ...projectData, customer_name: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">项目类型</label>
<select
value={projectData.project_type}
onChange={e => setProjectData({ ...projectData, project_type: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
>
<option>对公新能源</option>
<option>对公传统</option>
<option>零售乘用车</option>
<option>零售二手车</option>
</select>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">申请金额(亿元)</label>
<input
type="number"
step="0.1"
value={projectData.amount}
onChange={e => setProjectData({ ...projectData, amount: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
</div>
<button
onClick={runWorkflow}
disabled={isRunning}
className={`mt-4 w-full py-3 rounded-lg font-bold text-lg transition-all ${
isRunning
? "bg-slate-700 text-slate-500 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-cyan-600 hover:shadow-xl hover:scale-[1.02]"
}`}
>
{isRunning ? "🔄 运行中..." : "▶️ 一键运行完整流水线"}
</button>
</div>
{/* Agent进度面板 */}
<div className="grid grid-cols-5 gap-4 mb-8">
{agents.map(agent => {
const p = progress[agent.id];
const statusColor = {
waiting: "border-slate-700 bg-slate-800/30",
running: "border-yellow-500 bg-yellow-900/20 animate-pulse",
completed: "border-green-500 bg-green-900/20",
error: "border-red-500 bg-red-900/20"
}[p?.status || "waiting"];
return (
<div key={agent.id} className={`p-4 rounded-xl border-2 ${statusColor} transition-all`}>
<div className="text-3xl mb-2 text-center">{agent.icon}</div>
<div className="text-sm font-bold text-center mb-1">{agent.name}</div>
<div className="text-xs text-slate-400 text-center mb-2">{agent.description}</div>
<div className="text-xs text-center font-semibold">
{p?.status === "waiting" && "⏳ 等待中"}
{p?.status === "running" && "⚙️ 执行中"}
{p?.status === "completed" && "✅ 已完成"}
{p?.status === "error" && "❌ 出错"}
</div>
</div>
);
})}
</div>
{/* Agent输出展示 */}
<div className="space-y-6">
{agents.map(agent => {
const p = progress[agent.id];
if (!p?.output && !p?.error) return null;
return (
<div key={agent.id} className="p-6 bg-slate-800/50 backdrop-blur-sm rounded-xl border border-slate-700">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl">{agent.icon}</span>
<div>
<h3 className="text-lg font-bold text-blue-400">{agent.name}</h3>
<p className="text-xs text-slate-400">{agent.description}</p>
</div>
</div>
{p.error && (
<div className="p-4 bg-red-900/30 border border-red-700 rounded-lg text-red-300">
❌ 错误: {p.error}
</div>
)}
{p.output && (
<div className="prose prose-invert prose-sm max-w-none">
<ReactMarkdown
components={{
h1: ({ node, ...props }) => <h1 className="text-2xl font-bold text-cyan-300 mb-3" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-xl font-bold text-blue-400 mt-4 mb-2" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-lg font-bold text-slate-300 mt-3 mb-2" {...props} />,
p: ({ node, ...props }) => <p className="text-slate-300 mb-2 leading-relaxed" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc list-inside text-slate-300 space-y-1 mb-3" {...props} />,
li: ({ node, ...props }) => <li className="text-slate-300" {...props} />,
strong: ({ node, ...props }) => <strong className="text-white font-semibold" {...props} />,
code: ({ node, ...props }) => <code className="bg-slate-900 px-1 py-0.5 rounded text-cyan-400" {...props} />,
blockquote: ({ node, ...props }) => (
<blockquote className="border-l-4 border-yellow-500 pl-4 my-3 text-yellow-200 italic" {...props} />
),
}}
>
{p.output}
</ReactMarkdown>
</div>
)}
</div>
);
})}
</div>
</div>
);
}