Files
AIPortPilot/frontend/src/components/shared/InsightRail.tsx
T
selfrelease f4ddcab2ca test(frontend): UIUX E2E 测试 — 新增路由导航/创始人端/Admin端/工作台/移动端适配
新增 6 个 E2E 测试文件,覆盖 2-task-uiux.md 全部 50 项任务:
- uiux-navigation.spec.ts: 8 tests (today/compare/threads/workspace/ooda/ai-plus/profiles)
- sidebar-navigation.spec.ts: 6 tests (投资人 Sidebar 6 业务域)
- founder-uiux.spec.ts: 7 tests (创始人端 6 域导航)
- admin-uiux.spec.ts: 3 tests (Admin 6 管理域 + 商业秘密保护)
- workbench-uiux.spec.ts: 5 tests (Highlights/WorkMode/Tab/InsightRail)
- mobile-uiux.spec.ts: 5 tests (移动端抽屉导航 + 创始人底部导航)

同时修复全部 no-explicit-any 警告,替换为 TypeScript 接口定义。

测试结果: 41 E2E passed, 405 backend passed
2026-07-19 20:37:25 +08:00

209 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Insight Rail 组件 — 右侧 AI 洞察面板,可展开/折叠。 */
"use client";
import { useState } from "react";
import {
ChevronRight, ChevronLeft, Bot, Sparkles, FileText, AlertTriangle,
Users, ScrollText, Network, UserCircle, TrendingUp, Cpu, ShieldCheck,
type LucideIcon,
} from "lucide-react";
/** Agent 类型定义。 */
interface AgentDef {
key: string;
name: string;
icon: LucideIcon;
trigger: string;
output: string;
action: string;
}
/** 12 个 Agent 配置。 */
const AGENTS: AgentDef[] = [
{ key: "report", name: "报表分析", icon: FileText, trigger: "查看月报/季报", output: "指标提取卡片 + 异常高亮", action: "生成投后摘要草稿" },
{ key: "validator", name: "数据校验", icon: ShieldCheck, trigger: "指标出现矛盾", output: "校验维度矩阵 + 可信度评分", action: "标记可疑数据" },
{ key: "risk", name: "风险预警", icon: AlertTriangle, trigger: "健康度下降/弱信号", output: "风险等级 + 证据链", action: "创建决策线程" },
{ key: "board", name: "董事会", icon: Users, trigger: "董事会前后", output: "决议追踪 + 提问清单", action: "加入董事会议题" },
{ key: "agreement", name: "投资协议", icon: ScrollText, trigger: "协议条款即将触发", output: "条款原文 + 触发条件", action: "发送披露提醒" },
{ key: "synergy", name: "协同匹配", icon: Network, trigger: "查看协同中心", output: "匹配度评分 + 资源互补图", action: "生成协同方案草稿" },
{ key: "talent", name: "人才分析", icon: UserCircle, trigger: "核心团队变动", output: "稳定性评分 + 9-Box 矩阵", action: "启动人才搜索" },
{ key: "financing", name: "融资支持", icon: TrendingUp, trigger: "融资阶段/Runway", output: "融资准备度 + 投资人画像", action: "生成融资材料清单" },
{ key: "research", name: "行业研究", icon: Bot, trigger: "竞品/行业动态", output: "行业风险提示 + 对标分析", action: "加入洞察域报告" },
{ key: "ai_commercial", name: "AI+商业化", icon: Sparkles, trigger: "PoC/转化率指标", output: "转化漏斗 + 证据链", action: "生成董事会问题清单" },
{ key: "ai_cost", name: "AI+模型成本", icon: Cpu, trigger: "推理成本/毛利指标", output: "成本趋势 + 单位经济模型", action: "建议模型调用优化" },
{ key: "ai_compliance", name: "AI+数据合规", icon: ShieldCheck, trigger: "合规指标变化", output: "合规风险等级 + 整改建议", action: "生成合规报告草稿" },
];
/** Insight Rail Props。 */
interface InsightRailProps {
/** 当前激活的 Agent key,默认 "risk"。 */
defaultAgent?: string;
}
/**
* Insight Rail 组件 — 桌面端右侧可折叠 AI 洞察面板。
*
* 展开宽度 w-80320px),折叠宽度 w-1040px)。
* 包含 12 个 Agent 差异化卡片,支持 Tab 切换。
*/
export function InsightRail({ defaultAgent = "risk" }: InsightRailProps) {
const [expanded, setExpanded] = useState(false);
const [activeAgent, setActiveAgent] = useState(defaultAgent);
const [activeTabs, setActiveTabs] = useState<string[]>([defaultAgent]);
const agent = AGENTS.find((a) => a.key === activeAgent) ?? AGENTS[0];
/** 切换 Agent(最多 3 个 Tab)。 */
function switchAgent(key: string) {
setActiveAgent(key);
setActiveTabs((prev) => {
if (prev.includes(key)) return prev;
if (prev.length >= 3) return [...prev.slice(1), key];
return [...prev, key];
});
}
if (!expanded) {
return (
<aside
className="sticky top-0 hidden h-screen w-10 shrink-0 items-center justify-center border-l bg-white md:flex"
>
<button
type="button"
onClick={() => setExpanded(true)}
className="flex flex-col items-center gap-1 text-gray-400 transition-colors hover:text-indigo-600"
aria-label="展开 Insight Rail"
>
<ChevronLeft size={18} aria-hidden="true" />
<Bot size={16} aria-hidden="true" />
<span className="text-xs [writing-mode:vertical-rl]">Insight</span>
</button>
</aside>
);
}
return (
<aside
className="sticky top-0 hidden h-screen w-80 shrink-0 flex-col border-l bg-white md:flex"
>
{/* 顶部:折叠按钮 + 当前上下文 */}
<div className="flex items-center justify-between border-b px-3 py-2">
<span className="text-sm font-medium">AI </span>
<button
type="button"
onClick={() => setExpanded(false)}
className="text-gray-400 transition-colors hover:text-gray-600"
aria-label="折叠 Insight Rail"
>
<ChevronRight size={18} aria-hidden="true" />
</button>
</div>
{/* 当前上下文摘要 */}
<div className="border-b px-3 py-2 text-xs text-muted-foreground">
<div> A · </div>
<div> 90 · 3 </div>
</div>
{/* Agent Tab 切换 */}
<div className="flex gap-1 border-b px-2 py-1.5">
{activeTabs.map((key) => {
const a = AGENTS.find((ag) => ag.key === key)!;
return (
<button
key={key}
type="button"
onClick={() => setActiveAgent(key)}
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
key === activeAgent
? "bg-indigo-50 text-indigo-600"
: "text-gray-500 hover:bg-gray-50"
}`}
>
<a.icon size={12} aria-hidden="true" />
{a.name}
</button>
);
})}
</div>
{/* Agent 列表 */}
<div className="flex-1 overflow-y-auto px-3 py-2">
{/* 当前 Agent 输出 */}
<div className="mb-3 rounded-lg border bg-gray-50 p-3">
<div className="mb-2 flex items-center gap-2">
<agent.icon size={16} className="text-indigo-500" aria-hidden="true" />
<span className="text-sm font-medium">{agent.name}</span>
</div>
<div className="space-y-1.5 text-xs text-gray-600">
<p><span className="text-gray-400"></span>{agent.trigger}</p>
<p><span className="text-gray-400"></span>{agent.output}</p>
<p><span className="text-gray-400"></span>{agent.action}</p>
</div>
</div>
{/* AI 输出规范字段 */}
<div className="mb-3 space-y-1.5 rounded-lg border p-3 text-xs">
<div className="flex justify-between">
<span className="text-gray-400"></span>
<span className="font-medium text-indigo-600">0.85</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400"></span>
<span className="text-gray-600">3 </span>
</div>
<div className="flex justify-between">
<span className="text-gray-400"></span>
<span className="text-gray-600"> 2 </span>
</div>
<div className="flex justify-between">
<span className="text-gray-400"></span>
<span className="text-gray-600">使</span>
</div>
</div>
{/* 操作按钮 */}
<div className="space-y-2">
<button
type="button"
className="w-full rounded-md bg-indigo-50 px-3 py-2 text-sm text-indigo-600 transition-colors hover:bg-indigo-100"
>
稿
</button>
<button
type="button"
className="w-full rounded-md border px-3 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-50"
>
</button>
</div>
{/* 全部 Agent 列表 */}
<div className="mt-4">
<div className="mb-2 text-xs font-medium text-gray-400"> Agent</div>
<div className="grid grid-cols-2 gap-1">
{AGENTS.map((a) => (
<button
key={a.key}
type="button"
onClick={() => switchAgent(a.key)}
className={`flex items-center gap-1.5 rounded px-2 py-1.5 text-xs transition-colors ${
a.key === activeAgent
? "bg-indigo-50 text-indigo-600"
: "text-gray-600 hover:bg-gray-50"
}`}
>
<a.icon size={12} aria-hidden="true" />
{a.name}
</button>
))}
</div>
</div>
</div>
</aside>
);
}