feat: restore risk assessment workspace and improve page scrolling

This commit is contained in:
NathanAlex7
2026-09-07 02:28:37 +08:00
parent db5d6763f1
commit f2439ee5a3
65 changed files with 13612 additions and 2649 deletions
+16 -5
View File
@@ -11,7 +11,8 @@
--accent-orange: #ff6723;
}
html, body { height: 100%; overflow: hidden; }
html { height: 100%; overflow-y: auto; }
body { min-height: 100%; }
body {
color: var(--text-primary);
@@ -50,10 +51,20 @@ body {
}
/* ── Scrollbar ──────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.10); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.18); }
::-webkit-scrollbar { width: 14px; height: 14px; }
::-webkit-scrollbar-track { background: #e8ecf2; border-radius: 10px; }
::-webkit-scrollbar-thumb {
background: #8796ad;
border: 3px solid #e8ecf2;
border-radius: 10px;
min-height: 40px;
}
::-webkit-scrollbar-thumb:hover { background: #596f91; }
::-webkit-scrollbar-thumb:active { background: var(--accent); }
@supports not selector(::-webkit-scrollbar) {
* { scrollbar-width: auto; scrollbar-color: #8796ad #e8ecf2; }
}
/* ── Scroll shadow overlay (top/bottom) ─────────────── */
.scroll-shadow {
+370
View File
@@ -0,0 +1,370 @@
"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>
);
}
+1 -1
View File
@@ -41,7 +41,7 @@ function MainLayout() {
<Navbar />
{/* Content area — flexbox layout with resizable panels */}
<div className="flex-1 flex overflow-hidden">
<div className="flex-1 min-h-0 flex overflow-hidden">
{/* Left sidebar */}
<div
className="glass-panel border-r border-black/[0.06] shrink-0 panel-transition overflow-hidden"
+706
View File
@@ -0,0 +1,706 @@
"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
@@ -0,0 +1,370 @@
"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>
);
}
+698
View File
@@ -0,0 +1,698 @@
"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
+78
View File
@@ -0,0 +1,78 @@
"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
@@ -0,0 +1,706 @@
"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
@@ -0,0 +1,273 @@
"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>
);
}
+107
View File
@@ -0,0 +1,107 @@
"use client";
import { useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { getTrajectory } from "@/lib/api";
import { ArrowLeft, MessageSquare, GitBranch } from "lucide-react";
import AgentLoopFlow from "@/components/trajectory/AgentLoopFlow";
interface Message {
role: string;
content: string;
timestamp?: number;
tool_calls?: Array<{ name: string; args: any }>;
name?: string;
tool_call_id?: string;
}
export default function TrajectoryPage() {
const searchParams = useSearchParams();
const router = useRouter();
const sessionId = searchParams?.get("session") || "default";
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadTrajectory() {
try {
setLoading(true);
const data = await getTrajectory(sessionId);
// 优先使用新的 messages 格式(包含 tool 消息)
setMessages(data.messages || []);
} catch (err: any) {
setError(err.message || "加载轨迹失败");
} finally {
setLoading(false);
}
}
loadTrajectory();
}, [sessionId]);
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-sm text-gray-600">加载 Agent Loop 轨迹...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center">
<div className="text-center">
<p className="text-red-600 mb-4">❌ {error}</p>
<button
onClick={() => router.push("/")}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
返回聊天
</button>
</div>
</div>
);
}
return (
<div className="h-screen overflow-y-auto bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50">
{/* 顶部导航 */}
<div className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b border-gray-200">
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<button
onClick={() => router.push("/")}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
title="返回聊天"
>
<ArrowLeft className="w-5 h-5 text-gray-600" />
</button>
<div>
<h1 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<GitBranch className="w-5 h-5 text-indigo-600" />
Agent Loop 交互轨迹
</h1>
<p className="text-xs text-gray-500">会话 ID: {sessionId}</p>
</div>
</div>
</div>
</div>
{/* 主体内容 */}
<div className="max-w-5xl mx-auto px-6 py-8">
{messages.length === 0 ? (
<div className="text-center py-16">
<MessageSquare className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">该会话暂无交互记录</p>
</div>
) : (
<AgentLoopFlow messages={messages} />
)}
</div>
</div>
);
}
+186
View File
@@ -0,0 +1,186 @@
'use client'
import React from 'react'
import { Shield, FileSearch, TrendingUp, CheckCircle } from 'lucide-react'
const AGENTS = [
{
id: 1,
name: '风险研判助手',
icon: '🎯',
description: '双轨驱动:规则引擎 + 历史数据分析',
features: ['智能风险识别', '历史案例匹配', '智能决策推荐'],
status: 'active',
route: '/risk-assess',
color: 'from-blue-600 to-cyan-600'
},
{
id: 2,
name: '报告助手',
icon: '📝',
description: '智能报告生成与模板管理',
features: ['报告模板设计', '一键生成评审报告', '多模板快速切换'],
status: 'active',
route: '/report-assistant',
color: 'from-purple-600 to-pink-600'
},
{
id: 3,
name: '评审调配助手',
icon: '🎯',
description: '智能评审员分配 + 关键指标测算',
features: ['智能评审员匹配', '工作负荷平衡', '财务指标测算'],
status: 'active',
route: '/review-allocation',
color: 'from-orange-600 to-red-600'
},
{
id: 4,
name: '保单条款智能核验',
icon: '🛡️',
description: 'OCR识别 + 条款解析 + 合规核验',
features: ['保单PDF智能识别', '除外责任风险分析', '多模板核验报告'],
status: 'active',
route: '/policy-verification',
color: 'from-green-600 to-teal-600'
}
]
export default function XingbangUnifiedPage() {
const handleAgentClick = (agent: typeof AGENTS[0]) => {
if (agent.route) {
window.location.href = agent.route
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
{/* 导航栏 */}
<nav className="sticky top-0 z-50 backdrop-blur-md bg-slate-900/80 border-b border-slate-700">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-600 to-cyan-600 flex items-center justify-center shadow-lg">
<span className="text-white text-lg font-bold">兴</span>
</div>
<div>
<h1 className="text-lg font-bold text-white">兴邦金租智能评审工作台</h1>
<p className="text-xs text-slate-400">AI-Powered Risk Assessment Platform</p>
</div>
</div>
<a
href="/"
className="px-4 py-2 text-sm text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors"
>
返回主页
</a>
</div>
</nav>
{/* 主内容区 */}
<div className="max-w-7xl mx-auto px-6 py-12">
{/* 页面标题 */}
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-white mb-4">
四大智能助手
</h2>
<p className="text-xl text-slate-400">
AI驱动的金融风控决策系统
</p>
</div>
{/* Agent 卡片网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{AGENTS.map(agent => (
<div
key={agent.id}
onClick={() => handleAgentClick(agent)}
className={`group relative overflow-hidden rounded-2xl border border-slate-700 bg-slate-800/50 backdrop-blur-sm transition-all duration-300 ${
agent.status === 'active'
? 'hover:scale-[1.02] hover:shadow-2xl hover:border-cyan-500 cursor-pointer'
: 'opacity-60 cursor-not-allowed'
}`}
>
{/* 渐变背景装饰 */}
<div className={`absolute top-0 right-0 w-64 h-64 bg-gradient-to-br ${agent.color} opacity-10 blur-3xl group-hover:opacity-20 transition-opacity`} />
<div className="relative p-8">
{/* 图标和状态 */}
<div className="flex items-start justify-between mb-6">
<div className="flex items-center gap-4">
<div className={`w-16 h-16 rounded-xl bg-gradient-to-br ${agent.color} flex items-center justify-center text-3xl shadow-lg`}>
{agent.icon}
</div>
<div>
<h3 className="text-2xl font-bold text-white mb-1">
{agent.name}
</h3>
<p className="text-sm text-slate-400">
{agent.description}
</p>
</div>
</div>
{agent.status === 'active' && (
<div className="px-3 py-1 bg-green-500/20 border border-green-500 rounded-full text-xs font-bold text-green-400">
已启用
</div>
)}
{agent.status === 'planned' && (
<div className="px-3 py-1 bg-slate-700 border border-slate-600 rounded-full text-xs font-bold text-slate-400">
规划中
</div>
)}
</div>
{/* 功能特性 */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-slate-400 uppercase tracking-wider">核心功能</h4>
<ul className="space-y-2">
{agent.features.map((feature, idx) => (
<li key={idx} className="flex items-center gap-2 text-slate-300">
<CheckCircle className="w-4 h-4 text-cyan-400 flex-shrink-0" />
<span className="text-sm">{feature}</span>
</li>
))}
</ul>
</div>
{/* 操作按钮 */}
{agent.status === 'active' ? (
<div className="mt-6 pt-6 border-t border-slate-700">
<button className={`w-full py-3 rounded-lg font-bold text-white bg-gradient-to-r ${agent.color} hover:shadow-lg transition-all`}>
进入助手 →
</button>
</div>
) : (
<div className="mt-6 pt-6 border-t border-slate-700">
<div className="w-full py-3 rounded-lg font-bold text-center text-slate-500 bg-slate-700/50">
即将推出
</div>
</div>
)}
</div>
</div>
))}
</div>
{/* 底部说明 */}
<div className="mt-12 p-6 rounded-xl bg-slate-800/30 border border-slate-700">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-blue-600/20 flex items-center justify-center flex-shrink-0">
<Shield className="w-5 h-5 text-blue-400" />
</div>
<div>
<h3 className="text-lg font-bold text-white mb-2">关于智能评审工作台</h3>
<p className="text-sm text-slate-400 leading-relaxed">
兴邦金租智能评审工作台由四大AI助手组成,覆盖从风险研判到决策支持的完整流程。
每个助手都采用深度学习和规则引擎相结合的方式,确保决策的准确性和可解释性。
Agent 1 风险研判助手已正式上线,其他助手正在紧张开发中。
</p>
</div>
</div>
</div>
</div>
</div>
)
}
+370
View File
@@ -0,0 +1,370 @@
'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>
);
}
+291 -254
View File
@@ -1,292 +1,329 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import "../../components/xingbang/xingbang.css";
import {
streamXingbangWorkflow,
getXingbangEvolution,
} from "@/lib/api";
/**
* 兴邦金租智能体工作台
* 基于2026-08-18会议纪要真实痛点设计
*
* 核心功能:
* 1. 一键运行完整五Agent流水线(SSE流式展示)
* 2. 实时展示每个Agent的进度与产出
* 3. Markdown渲染每类Agent的最终输出
* 4. 痛点面板:展示系统如何解决会议提出的10大痛点
*/
/* ── 五智能体定义(含图标) ── */
const AGENTS = [
{ key: "data_curator", name: "数据汇聚官", role: "多源数据 → 情报卡", icon: "🗂️", desc: "拉齐 15 类异构数据源" },
{ key: "risk_scout", name: "预审参谋", role: "情报卡 → 尽调初稿", icon: "🔍", desc: "穿透分析风险点" },
{ key: "case_finder", name: "经验检索", role: "案例库 → 参考意见", icon: "📚", desc: "带编号可追溯案例" },
{ key: "approval_advisor", name: "审批顾问", role: "汇总 → 三档审批", icon: "⚖️", desc: "档位判定 + 预填流转" },
{ key: "risk_monitor", name: "风险哨兵", role: "审批前 · 动态复核", icon: "🛡️", desc: "风控信号转变预警" },
];
const AGENT_MAP = Object.fromEntries(AGENTS.map((a) => [a.key, a]));
import { useState, useEffect } from "react";
import ReactMarkdown from "react-markdown";
type AgentStatus = "pending" | "running" | "done" | "error";
interface PipelineState {
[key: string]: AgentStatus;
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 AgentResult {
success?: boolean;
interface AgentProgress {
agent: string;
type: string;
status: "waiting" | "running" | "completed" | "error";
output?: string;
file?: string | null;
error?: string;
}
/* ── 主页面 ── */
export default function XingbangPage() {
const [task, setTask] = useState("");
const [customer, setCustomer] = useState("");
const [amount, setAmount] = useState("");
const [asset, setAsset] = useState("");
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纳入失败案例,提示避坑" }
];
const [status, setStatus] = useState<"idle" | "running" | "done" | "error">("idle");
const [pipeline, setPipeline] = useState<PipelineState>({});
const [results, setResults] = useState<Record<string, AgentResult>>({});
const [finalSummary, setFinalSummary] = useState<string[]>([]);
const [reward, setReward] = useState<number | null>(null);
const [trajectoryId, setTrajectoryId] = useState<string | null>(null);
const [activeOutput, setActiveOutput] = useState<string>("");
const [evolution, setEvolution] = useState<string>("");
const [error, setError] = useState<string>("");
const [runId, setRunId] = useState(0); // 用于重跑
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 loadEvolution = useCallback(async () => {
try {
const ev = await getXingbangEvolution();
if (ev && typeof ev.insights === "string") setEvolution(ev.insights);
} catch {
/* 静默 */
}
// 示例项目数据
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);
});
}, []);
useEffect(() => { loadEvolution(); }, [loadEvolution]);
const runPipeline = useCallback(async () => {
setError("");
setResults({});
setFinalSummary([]);
setReward(null);
setTrajectoryId(null);
setActiveOutput("");
setStatus("running");
setPipeline({ data_curator: "pending", risk_scout: "pending", case_finder: "pending", approval_advisor: "pending", risk_monitor: "pending" });
const runWorkflow = async () => {
setIsRunning(true);
const taskDesc = [
customer || `客户`,
amount ? `申请${amount}万` : "",
asset ? `购置${asset}` : "",
`融资租赁评审`,
].filter(Boolean).join(" ");
// 重置所有Agent状态
const resetProgress = { ...progress };
Object.keys(resetProgress).forEach(key => {
resetProgress[key].status = "waiting";
resetProgress[key].output = undefined;
resetProgress[key].error = undefined;
});
setProgress(resetProgress);
try {
for await (const ev of streamXingbangWorkflow(taskDesc, customer)) {
if (ev.event === "agent_start") {
setPipeline((p) => ({ ...p, [ev.data.agent as string]: "running" }));
} else if (ev.event === "agent_end") {
const agent = ev.data.agent as string;
setPipeline((p) => ({ ...p, [agent]: ev.data.success ? "done" : "error" }));
setResults((r) => ({
...r,
[agent]: {
success: Boolean(ev.data.success),
output: typeof ev.data.output === "string" ? ev.data.output : undefined,
file: typeof ev.data.file === "string" ? ev.data.file : null,
},
}));
if (typeof ev.data.output === "string") setActiveOutput(ev.data.output);
} else if (ev.event === "done") {
setReward(Number(ev.data.reward ?? 0));
setTrajectoryId((ev.data.trajectory_id as string) ?? null);
setStatus("done");
setPipeline({ data_curator: "done", risk_scout: "done", case_finder: "done", approval_advisor: "done", risk_monitor: "done" });
loadEvolution();
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 (e: any) {
setStatus("error");
setError(String(e?.message ?? e));
} catch (error) {
console.error("Workflow error:", error);
setIsRunning(false);
}
}, [customer, amount, asset, loadEvolution]);
const pipelineOrder = Object.keys(pipeline).length
? AGENTS.filter((a) => a.key in pipeline)
: AGENTS;
};
return (
<div className="xb-root p-6 xb-scroll" style={{ height: "100vh", overflowY: "auto", boxSizing: "border-box" }}>
{/* 顶部品牌 */}
<header className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#2563eb] to-[#38bdf8] flex items-center justify-center text-lg shadow-lg shadow-blue-500/30">
🏛️
</div>
<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>
<div className="text-2xl font-bold xb-title-grad tracking-tight">兴邦金租 · AI 评审平台</div>
<div className="text-xs xb-muted mt-0.5">基于 mini-OpenClaw · 五智能体协同 · 自进化决策引擎</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>
</div>
<div className="flex items-center gap-2">
<span className="xb-badge">
<span className="w-2 h-2 rounded-full bg-[#34d399] animate-pulse" />
引擎在线
</span>
<span className="xb-badge">v0.1.0</span>
<button
onClick={() => (window.location.href = "/")}
className="ml-1 flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-semibold transition-all
bg-white/5 border border-white/15 text-[#e8eefc] hover:bg-white/10"
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>
<div className="grid grid-cols-3 gap-5">
{/* 左栏:任务输入 + 流水线 */}
<div className="col-span-1 flex flex-col gap-5">
{/* 评审任务表单 */}
<section className="xb-glass p-5 xb-glass-hover">
<h2 className="text-sm font-bold mb-4 flex items-center gap-2">
<span className="text-[#38bdf8]">◆</span> 新建评审任务
</h2>
<div className="space-y-3">
<div>
<label className="xb-label">客户名称</label>
<input className="xb-input" value={customer} onChange={(e) => setCustomer(e.target.value)}
placeholder="例:华南远通物流有限公司" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="xb-label">申请金额 (万元)</label>
<input className="xb-input" value={amount} onChange={(e) => setAmount(e.target.value)}
placeholder="5000" />
</div>
<div>
<label className="xb-label">租赁物</label>
<input className="xb-input" value={asset} onChange={(e) => setAsset(e.target.value)}
placeholder="50台电动重卡" />
</div>
</div>
<div className="flex gap-2 pt-2">
<button className="xb-btn flex-1 justify-center" onClick={runPipeline} disabled={status === "running"}>
{status === "running" ? <span className="xb-spinner" /> : "🚀"} 启动评审流水线
</button>
<button className="xb-btn xb-btn-ghost" onClick={() => { setTask(""); setCustomer(""); setAmount(""); setAsset(""); setStatus("idle"); setPipeline({}); setResults({}); setReward(null); }}
disabled={status === "running"}>清空</button>
</div>
<p className="text-[11px] xb-muted mt-1">五智能体按序执行,全程留痕可回退,约需 3–4 分钟</p>
</div>
</section>
{/* 流水线可视化 */}
<section className="xb-glass p-5">
<h2 className="text-sm font-bold mb-4 flex items-center gap-2">
<span className="text-[#f5b942]">⚡</span> 智能体流水线
</h2>
<div className="flex items-center">
{pipelineOrder.map((agent, i) => (
<div key={agent.key} style={{ display: "flex", alignItems: "center", flex: 1 }}>
<div className={`xb-node ${pipeline[agent.key] ?? "pending"}`}>
<div className="xb-node-chip text-xl">{agent.icon}</div>
<div className="xb-node-name" style={{ textAlign: "center" }}>{agent.name}</div>
<div className="flex items-center gap-1.5">
{pipeline[agent.key] === "running" && <span className="xb-spinner" style={{ width: 10, height: 10 }} />}
{pipeline[agent.key] === "done" && <span className="text-[#34d399] text-xs">✓</span>}
{pipeline[agent.key] === "error" && <span className="text-[#f87171] text-xs">✕</span>}
<span className="xb-node-role" style={{ whiteSpace: "nowrap" }}>{agent.role}</span>
</div>
{/* 痛点面板 */}
{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>
{i < pipelineOrder.length - 1 && <div className="xb-pipe-connector" />}
</div>
))}
</div>
{/* 状态文字 */}
<div className="mt-4 pt-3 border-t border-black/[0.06] flex items-center gap-2 text-xs xb-muted">
{status === "running" && <span className="text-[#38bdf8]">● 智能体协同执行中…</span>}
{status === "done" && <span className="text-[#34d399]">● 评审完成,结论已生成</span>}
{status === "error" && <span className="text-[#f87171]">● 执行出现异常</span>}
{status === "idle" && <span>● 等待发起评审任务</span>}
{reward !== null && <span className="ml-auto">进化奖励分 <b className="text-[#f5b942]">{reward}</b></span>}
</div>
</section>
{/* 自进化面板 */}
<section className="xb-glass p-5">
<h2 className="text-sm font-bold mb-3 flex items-center gap-2">
<span className="text-[#34d399]">🧬</span> 自进化洞察
<span className="ml-auto text-[10px] xb-muted">Level 1 · 轨迹学习</span>
</h2>
<pre className="text-[11.5px] leading-relaxed xb-muted whitespace-pre-wrap xb-scroll" style={{ maxHeight: 240, overflowY: "auto" }}>
{evolution || "尚无轨迹样本。运行审批流水线积累数据后,系统将自动进化。"}
</pre>
</section>
</div>
))}
</div>
</div>
)}
{/* 右栏:产出结论 */}
<div className="col-span-2 flex flex-col gap-5">
{/* 审批结论 */}
<section className="xb-glass p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-bold flex items-center gap-2">
<span className="text-[#38bdf8]">📋</span> 评审产出
</h2>
{trajectoryId && (
<span className="xb-badge text-[10px]">轨迹 {trajectoryId}</span>
{/* 项目输入表单 */}
<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 className="grid grid-cols-2 gap-3">
{AGENTS.map((agent) => {
const r = results[agent.key];
const st = pipeline[agent.key];
return (
<div key={agent.key} className={`rounded-xl p-4 border transition-all ${st === "done" ? "border-[#34d399]/30 bg-white/[0.02]" : st === "error" ? "border-[#f87171]/30 bg-white/[0.02]" : "border-black/[0.06] bg-white/[0.01]"}`}>
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">{agent.icon}</span>
<span className="font-semibold text-[13px]">{agent.name}</span>
{r?.success && <span className="ml-auto text-[10px] text-[#34d399] font-semibold">✓ 完成</span>}
{st === "running" && <span className="ml-auto text-[10px] text-[#38bdf8] animate-pulse">运行中…</span>}
{r !== undefined && !r?.success && <span className="ml-auto text-[10px] text-[#f87171] font-semibold">失败</span>}
</div>
<div className="text-[11px] xb-muted mb-2">{agent.desc}</div>
{r?.output ? (
<pre className="text-[11.5px] xb-muted whitespace-pre-wrap line-clamp-4 overflow-hidden"
style={{ maxHeight: 110, overflowY: "auto" }}>{r.output}</pre>
) : st === "pending" ? (
<div className="text-[11.5px] xb-muted/60">待执行</div>
) : st === "running" ? (
<div className="text-[11.5px] text-[#38bdf8]/70">正在分析数据…</div>
) : null}
</div>
);
})}
</div>
{error && (
<div className="mt-4 p-3 rounded-lg bg-[#f87171]/10 border border-[#f87171]/30 text-[13px] text-[#f57171] break-words">
⚠️ {error}
</div>
)}
{finalSummary.length > 0 && (
<div className="mt-4 pt-3 border-t border-black/[0.06] flex flex-wrap gap-2">
{finalSummary.map((s, i) => <span key={i} className="xb-badge text-[10px]">{s}</span>)}
</div>
)}
</section>
{/* 详情预览 */}
<section className="xb-glass p-5 flex-1">
<h2 className="text-sm font-bold mb-3 flex items-center gap-2">
<span className="text-[#38bdf8]">📄</span> 最新产出预览
</h2>
{activeOutput ? (
<pre className="text-[12px] leading-relaxed xb-muted whitespace-pre-wrap xb-scroll"
style={{ maxHeight: 460, overflowY: "auto" }}>{activeOutput}</pre>
) : (
<div className="text-[12px] xb-muted/50 flex items-center justify-center py-12">
五智能体执行完毕后,此处显示最终审批结论…
</div>
)}
</section>
</div>
);
})}
</div>
</div>
);