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