feat(xingbang): 兴邦金租 AI 评审平台前端页面(深色商务风)
- 独立路由 /xingbang: 兴邦评审工作台 - 五智能体流水线可视化(状态动画: 排队/运行/成功/失败) - 评审任务表单(客户/金额/租赁物) + SSE 实时进度 - 审批结论卡片(五智能体产出) + 自进化洞察面板 - Navbar 加兴邦评审入口, 支持返回聊天 - 深色商务风 (渐变背景/玻璃拟态/金蓝主题) - api.ts 新增兴邦 API 调用(workflow/evolution/trajectories) 前端已在 next dev 验证渲染 OK, 后端 API 就绪(7条轨迹样本)
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import "../../components/xingbang/xingbang.css";
|
||||
import {
|
||||
streamXingbangWorkflow,
|
||||
getXingbangEvolution,
|
||||
} from "@/lib/api";
|
||||
|
||||
/* ── 五智能体定义(含图标) ── */
|
||||
const AGENTS = [
|
||||
{ key: "data_curator", name: "数据汇聚官", role: "多源数据 → 情报卡", icon: "🗂️", desc: "拉齐 15 类异构数据源" },
|
||||
{ key: "risk_scout", name: "预审参谋", role: "情报卡 → 尽调初稿", icon: "🔍", desc: "穿透分析风险点" },
|
||||
{ key: "case_finder", name: "经验检索", role: "案例库 → 参考意见", icon: "📚", desc: "带编号可追溯案例" },
|
||||
{ key: "approval_advisor", name: "审批顾问", role: "汇总 → 三档审批", icon: "⚖️", desc: "档位判定 + 预填流转" },
|
||||
{ key: "risk_monitor", name: "风险哨兵", role: "审批前 · 动态复核", icon: "🛡️", desc: "风控信号转变预警" },
|
||||
];
|
||||
const AGENT_MAP = Object.fromEntries(AGENTS.map((a) => [a.key, a]));
|
||||
|
||||
type AgentStatus = "pending" | "running" | "done" | "error";
|
||||
interface PipelineState {
|
||||
[key: string]: AgentStatus;
|
||||
}
|
||||
|
||||
interface AgentResult {
|
||||
success?: boolean;
|
||||
output?: string;
|
||||
file?: string | null;
|
||||
}
|
||||
|
||||
/* ── 主页面 ── */
|
||||
export default function XingbangPage() {
|
||||
const [task, setTask] = useState("");
|
||||
const [customer, setCustomer] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [asset, setAsset] = useState("");
|
||||
|
||||
const [status, setStatus] = useState<"idle" | "running" | "done" | "error">("idle");
|
||||
const [pipeline, setPipeline] = useState<PipelineState>({});
|
||||
const [results, setResults] = useState<Record<string, AgentResult>>({});
|
||||
const [finalSummary, setFinalSummary] = useState<string[]>([]);
|
||||
const [reward, setReward] = useState<number | null>(null);
|
||||
const [trajectoryId, setTrajectoryId] = useState<string | null>(null);
|
||||
const [activeOutput, setActiveOutput] = useState<string>("");
|
||||
const [evolution, setEvolution] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const [runId, setRunId] = useState(0); // 用于重跑
|
||||
|
||||
const loadEvolution = useCallback(async () => {
|
||||
try {
|
||||
const ev = await getXingbangEvolution();
|
||||
if (ev && typeof ev.insights === "string") setEvolution(ev.insights);
|
||||
} catch {
|
||||
/* 静默 */
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => { loadEvolution(); }, [loadEvolution]);
|
||||
|
||||
const runPipeline = useCallback(async () => {
|
||||
setError("");
|
||||
setResults({});
|
||||
setFinalSummary([]);
|
||||
setReward(null);
|
||||
setTrajectoryId(null);
|
||||
setActiveOutput("");
|
||||
setStatus("running");
|
||||
setPipeline({ data_curator: "pending", risk_scout: "pending", case_finder: "pending", approval_advisor: "pending", risk_monitor: "pending" });
|
||||
|
||||
const taskDesc = [
|
||||
customer || `客户`,
|
||||
amount ? `申请${amount}万` : "",
|
||||
asset ? `购置${asset}` : "",
|
||||
`融资租赁评审`,
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
try {
|
||||
for await (const ev of streamXingbangWorkflow(taskDesc, customer)) {
|
||||
if (ev.event === "agent_start") {
|
||||
setPipeline((p) => ({ ...p, [ev.data.agent as string]: "running" }));
|
||||
} else if (ev.event === "agent_end") {
|
||||
const agent = ev.data.agent as string;
|
||||
setPipeline((p) => ({ ...p, [agent]: ev.data.success ? "done" : "error" }));
|
||||
setResults((r) => ({
|
||||
...r,
|
||||
[agent]: {
|
||||
success: Boolean(ev.data.success),
|
||||
output: typeof ev.data.output === "string" ? ev.data.output : undefined,
|
||||
file: typeof ev.data.file === "string" ? ev.data.file : null,
|
||||
},
|
||||
}));
|
||||
if (typeof ev.data.output === "string") setActiveOutput(ev.data.output);
|
||||
} else if (ev.event === "done") {
|
||||
setReward(Number(ev.data.reward ?? 0));
|
||||
setTrajectoryId((ev.data.trajectory_id as string) ?? null);
|
||||
setStatus("done");
|
||||
setPipeline({ data_curator: "done", risk_scout: "done", case_finder: "done", approval_advisor: "done", risk_monitor: "done" });
|
||||
loadEvolution();
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
setStatus("error");
|
||||
setError(String(e?.message ?? e));
|
||||
}
|
||||
}, [customer, amount, asset, loadEvolution]);
|
||||
|
||||
const pipelineOrder = Object.keys(pipeline).length
|
||||
? AGENTS.filter((a) => a.key in pipeline)
|
||||
: AGENTS;
|
||||
|
||||
return (
|
||||
<div className="xb-root p-6 xb-scroll" style={{ height: "100vh", overflowY: "auto", boxSizing: "border-box" }}>
|
||||
{/* 顶部品牌 */}
|
||||
<header className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#2563eb] to-[#38bdf8] flex items-center justify-center text-lg shadow-lg shadow-blue-500/30">
|
||||
🏛️
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold xb-title-grad tracking-tight">兴邦金租 · AI 评审平台</div>
|
||||
<div className="text-xs xb-muted mt-0.5">基于 mini-OpenClaw · 五智能体协同 · 自进化决策引擎</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="xb-badge">
|
||||
<span className="w-2 h-2 rounded-full bg-[#34d399] animate-pulse" />
|
||||
引擎在线
|
||||
</span>
|
||||
<span className="xb-badge">v0.1.0</span>
|
||||
<button
|
||||
onClick={() => (window.location.href = "/")}
|
||||
className="ml-1 flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-semibold transition-all
|
||||
bg-white/5 border border-white/15 text-[#e8eefc] hover:bg-white/10"
|
||||
>
|
||||
← 返回聊天
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{/* 左栏:任务输入 + 流水线 */}
|
||||
<div className="col-span-1 flex flex-col gap-5">
|
||||
{/* 评审任务表单 */}
|
||||
<section className="xb-glass p-5 xb-glass-hover">
|
||||
<h2 className="text-sm font-bold mb-4 flex items-center gap-2">
|
||||
<span className="text-[#38bdf8]">◆</span> 新建评审任务
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="xb-label">客户名称</label>
|
||||
<input className="xb-input" value={customer} onChange={(e) => setCustomer(e.target.value)}
|
||||
placeholder="例:华南远通物流有限公司" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="xb-label">申请金额 (万元)</label>
|
||||
<input className="xb-input" value={amount} onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="5000" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="xb-label">租赁物</label>
|
||||
<input className="xb-input" value={asset} onChange={(e) => setAsset(e.target.value)}
|
||||
placeholder="50台电动重卡" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button className="xb-btn flex-1 justify-center" onClick={runPipeline} disabled={status === "running"}>
|
||||
{status === "running" ? <span className="xb-spinner" /> : "🚀"} 启动评审流水线
|
||||
</button>
|
||||
<button className="xb-btn xb-btn-ghost" onClick={() => { setTask(""); setCustomer(""); setAmount(""); setAsset(""); setStatus("idle"); setPipeline({}); setResults({}); setReward(null); }}
|
||||
disabled={status === "running"}>清空</button>
|
||||
</div>
|
||||
<p className="text-[11px] xb-muted mt-1">五智能体按序执行,全程留痕可回退,约需 3–4 分钟</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 流水线可视化 */}
|
||||
<section className="xb-glass p-5">
|
||||
<h2 className="text-sm font-bold mb-4 flex items-center gap-2">
|
||||
<span className="text-[#f5b942]">⚡</span> 智能体流水线
|
||||
</h2>
|
||||
<div className="flex items-center">
|
||||
{pipelineOrder.map((agent, i) => (
|
||||
<div key={agent.key} style={{ display: "flex", alignItems: "center", flex: 1 }}>
|
||||
<div className={`xb-node ${pipeline[agent.key] ?? "pending"}`}>
|
||||
<div className="xb-node-chip text-xl">{agent.icon}</div>
|
||||
<div className="xb-node-name" style={{ textAlign: "center" }}>{agent.name}</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{pipeline[agent.key] === "running" && <span className="xb-spinner" style={{ width: 10, height: 10 }} />}
|
||||
{pipeline[agent.key] === "done" && <span className="text-[#34d399] text-xs">✓</span>}
|
||||
{pipeline[agent.key] === "error" && <span className="text-[#f87171] text-xs">✕</span>}
|
||||
<span className="xb-node-role" style={{ whiteSpace: "nowrap" }}>{agent.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
{i < pipelineOrder.length - 1 && <div className="xb-pipe-connector" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 状态文字 */}
|
||||
<div className="mt-4 pt-3 border-t border-black/[0.06] flex items-center gap-2 text-xs xb-muted">
|
||||
{status === "running" && <span className="text-[#38bdf8]">● 智能体协同执行中…</span>}
|
||||
{status === "done" && <span className="text-[#34d399]">● 评审完成,结论已生成</span>}
|
||||
{status === "error" && <span className="text-[#f87171]">● 执行出现异常</span>}
|
||||
{status === "idle" && <span>● 等待发起评审任务</span>}
|
||||
{reward !== null && <span className="ml-auto">进化奖励分 <b className="text-[#f5b942]">{reward}</b></span>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 自进化面板 */}
|
||||
<section className="xb-glass p-5">
|
||||
<h2 className="text-sm font-bold mb-3 flex items-center gap-2">
|
||||
<span className="text-[#34d399]">🧬</span> 自进化洞察
|
||||
<span className="ml-auto text-[10px] xb-muted">Level 1 · 轨迹学习</span>
|
||||
</h2>
|
||||
<pre className="text-[11.5px] leading-relaxed xb-muted whitespace-pre-wrap xb-scroll" style={{ maxHeight: 240, overflowY: "auto" }}>
|
||||
{evolution || "尚无轨迹样本。运行审批流水线积累数据后,系统将自动进化。"}
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* 右栏:产出结论 */}
|
||||
<div className="col-span-2 flex flex-col gap-5">
|
||||
{/* 审批结论 */}
|
||||
<section className="xb-glass p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-sm font-bold flex items-center gap-2">
|
||||
<span className="text-[#38bdf8]">📋</span> 评审产出
|
||||
</h2>
|
||||
{trajectoryId && (
|
||||
<span className="xb-badge text-[10px]">轨迹 {trajectoryId}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 各智能体产出卡片 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{AGENTS.map((agent) => {
|
||||
const r = results[agent.key];
|
||||
const st = pipeline[agent.key];
|
||||
return (
|
||||
<div key={agent.key} className={`rounded-xl p-4 border transition-all ${st === "done" ? "border-[#34d399]/30 bg-white/[0.02]" : st === "error" ? "border-[#f87171]/30 bg-white/[0.02]" : "border-black/[0.06] bg-white/[0.01]"}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">{agent.icon}</span>
|
||||
<span className="font-semibold text-[13px]">{agent.name}</span>
|
||||
{r?.success && <span className="ml-auto text-[10px] text-[#34d399] font-semibold">✓ 完成</span>}
|
||||
{st === "running" && <span className="ml-auto text-[10px] text-[#38bdf8] animate-pulse">运行中…</span>}
|
||||
{r !== undefined && !r?.success && <span className="ml-auto text-[10px] text-[#f87171] font-semibold">失败</span>}
|
||||
</div>
|
||||
<div className="text-[11px] xb-muted mb-2">{agent.desc}</div>
|
||||
{r?.output ? (
|
||||
<pre className="text-[11.5px] xb-muted whitespace-pre-wrap line-clamp-4 overflow-hidden"
|
||||
style={{ maxHeight: 110, overflowY: "auto" }}>{r.output}</pre>
|
||||
) : st === "pending" ? (
|
||||
<div className="text-[11.5px] xb-muted/60">待执行</div>
|
||||
) : st === "running" ? (
|
||||
<div className="text-[11.5px] text-[#38bdf8]/70">正在分析数据…</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 rounded-lg bg-[#f87171]/10 border border-[#f87171]/30 text-[13px] text-[#f57171] break-words">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{finalSummary.length > 0 && (
|
||||
<div className="mt-4 pt-3 border-t border-black/[0.06] flex flex-wrap gap-2">
|
||||
{finalSummary.map((s, i) => <span key={i} className="xb-badge text-[10px]">{s}</span>)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 详情预览 */}
|
||||
<section className="xb-glass p-5 flex-1">
|
||||
<h2 className="text-sm font-bold mb-3 flex items-center gap-2">
|
||||
<span className="text-[#38bdf8]">📄</span> 最新产出预览
|
||||
</h2>
|
||||
{activeOutput ? (
|
||||
<pre className="text-[12px] leading-relaxed xb-muted whitespace-pre-wrap xb-scroll"
|
||||
style={{ maxHeight: 460, overflowY: "auto" }}>{activeOutput}</pre>
|
||||
) : (
|
||||
<div className="text-[12px] xb-muted/50 flex items-center justify-center py-12">
|
||||
五智能体执行完毕后,此处显示最终审批结论…
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user