feat: restore risk assessment workspace and improve page scrolling
This commit is contained in:
+16
-5
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user