feat: restore risk assessment workspace and improve page scrolling
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
User,
|
||||
Bot,
|
||||
Wrench,
|
||||
CheckCircle2,
|
||||
Terminal,
|
||||
Code,
|
||||
Globe,
|
||||
FileText,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
|
||||
// 工具图标映射
|
||||
const TOOL_META: Record<string, { icon: React.ElementType; color: string; bg: string }> = {
|
||||
terminal: { icon: Terminal, color: "#6b7280", bg: "#f3f4f6" },
|
||||
python_repl: { icon: Code, color: "#2563eb", bg: "#eff6ff" },
|
||||
fetch_url: { icon: Globe, color: "#059669", bg: "#ecfdf5" },
|
||||
read_file: { icon: FileText, color: "#d97706", bg: "#fffbeb" },
|
||||
search_knowledge_base: { icon: Search, color: "#7c3aed", bg: "#f5f3ff" },
|
||||
};
|
||||
|
||||
interface Message {
|
||||
role: string;
|
||||
content: string;
|
||||
timestamp?: number;
|
||||
tool_calls?: Array<{ name: string; args: any }>;
|
||||
name?: string;
|
||||
tool_call_id?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export default function AgentLoopFlow({ messages }: Props) {
|
||||
const [expandedNodes, setExpandedNodes] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleNode = (key: string) => {
|
||||
setExpandedNodes((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{messages.map((msg, idx) => {
|
||||
const nodeKey = `msg-${idx}`;
|
||||
const isExpanded = expandedNodes[nodeKey];
|
||||
|
||||
// 人类消息
|
||||
if (msg.role === "human") {
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-4">
|
||||
<div className="relative z-10 w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-lg flex-shrink-0">
|
||||
<User className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase">用户</span>
|
||||
{msg.timestamp && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(msg.timestamp * 1000).toLocaleString("zh-CN")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-800 leading-relaxed whitespace-pre-wrap">
|
||||
{msg.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Assistant 消息
|
||||
if (msg.role === "assistant") {
|
||||
const hasToolCalls = msg.tool_calls && msg.tool_calls.length > 0;
|
||||
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-4">
|
||||
<div className="relative z-10 w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-pink-600 flex items-center justify-center shadow-lg flex-shrink-0">
|
||||
<Bot className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl border border-purple-200 shadow-sm overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleNode(nodeKey)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-white/50 transition-colors text-left"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-1">
|
||||
<span className="text-xs font-semibold text-purple-700 uppercase">
|
||||
Assistant
|
||||
</span>
|
||||
{msg.timestamp && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(msg.timestamp * 1000).toLocaleString("zh-CN")}
|
||||
</span>
|
||||
)}
|
||||
{hasToolCalls && (
|
||||
<span className="px-2 py-0.5 bg-amber-100 text-amber-700 text-xs font-medium rounded flex items-center gap-1">
|
||||
<Wrench className="w-3 h-3" />
|
||||
调用 {msg.tool_calls?.length || 0} 个工具
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{msg.content && !isExpanded && (
|
||||
<p className="text-sm text-gray-700 line-clamp-2">
|
||||
{msg.content.slice(0, 100)}
|
||||
{msg.content.length > 100 ? "..." : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 space-y-3 border-t border-purple-200 pt-3 bg-white/50">
|
||||
{msg.content && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase mb-1.5">
|
||||
回复内容
|
||||
</div>
|
||||
<div className="p-3 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 whitespace-pre-wrap max-h-60 overflow-y-auto">
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasToolCalls && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase mb-1.5">
|
||||
工具调用
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{msg.tool_calls?.map((tc, tcIdx) => {
|
||||
const toolKey = `${nodeKey}-tc-${tcIdx}`;
|
||||
const isToolExpanded = expandedNodes[toolKey];
|
||||
const meta = TOOL_META[tc.name] || TOOL_META.terminal;
|
||||
const ToolIcon = meta.icon;
|
||||
|
||||
return (
|
||||
<div key={tcIdx} className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleNode(toolKey)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
{isToolExpanded ? (
|
||||
<ChevronDown className="w-3 h-3 text-gray-400 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<div
|
||||
className="w-6 h-6 rounded flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: meta.bg }}
|
||||
>
|
||||
<ToolIcon className="w-3.5 h-3.5" style={{ color: meta.color }} />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-gray-700">
|
||||
{tc.name}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isToolExpanded && (
|
||||
<div className="px-3 pb-2 border-t border-gray-200 pt-2 bg-gray-50">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase mb-1">
|
||||
传入参数
|
||||
</div>
|
||||
<pre className="p-2 bg-white border border-gray-200 rounded text-xs overflow-x-auto font-mono text-gray-700 max-h-40 overflow-y-auto">
|
||||
{JSON.stringify(tc.args, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Tool 消息(工具返回)
|
||||
if (msg.role === "tool") {
|
||||
const meta = TOOL_META[msg.name || "terminal"] || TOOL_META.terminal;
|
||||
const ToolIcon = meta.icon;
|
||||
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-4 ml-14">
|
||||
<div
|
||||
className="relative z-10 w-10 h-10 rounded-full flex items-center justify-center shadow-lg flex-shrink-0"
|
||||
style={{ background: meta.bg }}
|
||||
>
|
||||
<ToolIcon className="w-5 h-5" style={{ color: meta.color }} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="bg-white rounded-xl border-2 border-emerald-200 shadow-sm overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleNode(nodeKey)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-emerald-50/30 transition-colors text-left"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-semibold text-emerald-700 uppercase">
|
||||
工具返回
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{msg.name}
|
||||
</span>
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
{msg.timestamp && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(msg.timestamp * 1000).toLocaleString("zh-CN")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!isExpanded && (
|
||||
<p className="text-xs text-gray-600 mt-1 line-clamp-1 font-mono">
|
||||
{msg.content.slice(0, 80)}
|
||||
{msg.content.length > 80 ? "..." : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 space-y-2 border-t border-emerald-200 pt-3 bg-emerald-50/20">
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase mb-1.5">
|
||||
返回内容
|
||||
</div>
|
||||
<pre className="p-3 bg-slate-800 text-slate-100 rounded-lg text-xs overflow-x-auto max-h-96 overflow-y-auto font-mono leading-relaxed">
|
||||
{msg.content}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 未知消息类型(兜底)
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-4">
|
||||
<div className="relative z-10 w-10 h-10 rounded-full bg-gray-300 flex items-center justify-center shadow-lg flex-shrink-0">
|
||||
<span className="text-white text-xs">?</span>
|
||||
</div>
|
||||
<div className="flex-1 bg-gray-100 rounded-xl border border-gray-300 p-4">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase mb-2">
|
||||
未知消息类型: {msg.role}
|
||||
</div>
|
||||
<pre className="text-xs text-gray-700 whitespace-pre-wrap">
|
||||
{JSON.stringify(msg, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user