From c8e5e7189f3a54e0560ed182ad48cdc22d3212ad Mon Sep 17 00:00:00 2001 From: nacadmin Date: Sun, 1 Mar 2026 16:08:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(nac-admin):=20AI=E9=97=AE=E7=AD=94?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=94=B9=E7=89=88=E4=B8=BAChatGPT=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E5=AF=B9=E8=AF=9D=E7=95=8C=E9=9D=A2=20-=20NAC?= =?UTF-8?q?=E5=85=AC=E9=93=BEAI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nac-admin/client/src/pages/AIAgents.tsx | 587 ++++++++++++++++++++++++ 1 file changed, 587 insertions(+) create mode 100644 nac-admin/client/src/pages/AIAgents.tsx diff --git a/nac-admin/client/src/pages/AIAgents.tsx b/nac-admin/client/src/pages/AIAgents.tsx new file mode 100644 index 0000000..1b5e65e --- /dev/null +++ b/nac-admin/client/src/pages/AIAgents.tsx @@ -0,0 +1,587 @@ +import { useState, useEffect, useRef } from "react"; +import { trpc } from "@/lib/trpc"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "sonner"; +import { + BookOpen, Shield, Languages, ClipboardCheck, + Send, Bot, User, Loader2, Sparkles, + AlertCircle, CheckCircle2, Info, MessageSquare, + Plus, Trash2, Clock, ChevronDown, ChevronUp, + ExternalLink, Zap, SquarePen, +} from "lucide-react"; +import { useLocation } from "wouter"; + +// ─── 类型定义 ───────────────────────────────────────────────────── +type AgentType = "knowledge_qa" | "compliance" | "translation" | "approval_assist"; + +interface Message { + role: "user" | "assistant"; + content: string; + confidence?: number; + sources?: string[]; + suggestions?: string[]; + isRTL?: boolean; +} + +interface ConversationItem { + conversationId: string; + agentType: AgentType; + title: string; + messageCount: number; + updatedAt: string | Date; +} + +// ─── 图标映射 ───────────────────────────────────────────────────── +const ICON_MAP: Record = { + BookOpen, Shield, Languages, ClipboardCheck, +}; + +const AGENT_LABELS: Record = { + knowledge_qa: { name: "知识问答", desc: "NAC公链技术与规则", color: "text-blue-400", icon: "BookOpen" }, + compliance: { name: "合规审查", desc: "RWA合规与监管分析", color: "text-red-400", icon: "Shield" }, + translation: { name: "多语翻译", desc: "10种语言专业翻译", color: "text-green-400", icon: "Languages" }, + approval_assist: { name: "审批助手", desc: "资产审批流程辅助", color: "text-purple-400", icon: "ClipboardCheck" }, +}; + +// ─── 置信度徽章 ─────────────────────────────────────────────────── +function ConfidenceBadge({ confidence }: { confidence: number }) { + const pct = Math.round(confidence * 100); + const color = pct >= 80 ? "text-green-400" : pct >= 60 ? "text-yellow-400" : "text-red-400"; + return ( + + + 置信度 {pct}% + + ); +} + +// ─── 来源引用徽章 ───────────────────────────────────────────────── +function SourceBadge({ source, onClick }: { source: string; onClick: () => void }) { + return ( + + ); +} + +// ─── 消息气泡 ───────────────────────────────────────────────────── +function MessageBubble({ + msg, + onSourceClick, +}: { + msg: Message; + onSourceClick?: (source: string) => void; +}) { + const isUser = msg.role === "user"; + return ( +
+ {/* 头像 */} +
+ {isUser ? : } +
+ + {/* 内容区 */} +
+ {/* 发送者名称 */} + + {isUser ? "你" : "NAC公链AI"} + + + {/* 消息内容 */} +
+ {msg.content} +
+ + {/* AI 附加信息 */} + {!isUser && ( +
+ {msg.confidence !== undefined && } + {msg.sources && msg.sources.length > 0 && ( +
+ + + 引用来源: + + {msg.sources.slice(0, 3).map((src, i) => ( + onSourceClick?.(src)} /> + ))} + {msg.sources.length > 3 && ( + +{msg.sources.length - 3}条 + )} +
+ )} +
+ )} + + {/* 建议标签 */} + {!isUser && msg.suggestions && msg.suggestions.length > 0 && ( +
+ {msg.suggestions.map((s, i) => ( + + {s} + + ))} +
+ )} +
+
+ ); +} + +// ─── 会话列表项 ─────────────────────────────────────────────────── +function ConversationListItem({ + conv, + isActive, + onClick, + onDelete, +}: { + conv: ConversationItem; + isActive: boolean; + onClick: () => void; + onDelete: () => void; +}) { + const updatedAt = new Date(conv.updatedAt); + const timeStr = updatedAt.toLocaleDateString("zh-CN", { month: "short", day: "numeric" }); + return ( +
+ +
+

{conv.title}

+

{timeStr} · {conv.messageCount}条

+
+ +
+ ); +} + +// ─── 欢迎屏幕(无消息时) ───────────────────────────────────────── +function WelcomeScreen({ + agentType, + suggestedQuestions, + onSelect, +}: { + agentType: AgentType | null; + suggestedQuestions: string[]; + onSelect: (q: string) => void; +}) { + const info = agentType ? AGENT_LABELS[agentType] : null; + const IconComp = info ? (ICON_MAP[info.icon] || Bot) : Bot; + + return ( +
+ {/* Logo 区域 */} +
+ +
+ +

NAC公链AI

+ {info && ( +

{info.name} · {info.desc}

+ )} +

基于 NAC 原生知识库 · CBPP共识 · Charter合约

+ + {/* 建议问题 */} + {suggestedQuestions.length > 0 && ( +
+

试试这些问题:

+
+ {suggestedQuestions.map((q, i) => ( + + ))} +
+
+ )} +
+ ); +} + +// ─── 主页面 ─────────────────────────────────────────────────────── +export default function AIAgents() { + const [selectedAgent, setSelectedAgent] = useState("knowledge_qa"); + const [activeConvId, setActiveConvId] = useState(null); + const [messages, setMessages] = useState([]); + const [inputText, setInputText] = useState(""); + const [showHistory, setShowHistory] = useState(true); + const messagesEndRef = useRef(null); + const textareaRef = useRef(null); + const [, navigate] = useLocation(); + + // 来源引用跳转 + const handleSourceClick = (source: string) => { + const keyword = source.split("·").pop() || source; + navigate(`/knowledge-base?search=${encodeURIComponent(keyword)}`); + }; + + // 获取 Agent 列表和状态 + const { data: agentData, isLoading: agentLoading } = trpc.aiAgent.list.useQuery(); + const { data: statusData } = trpc.aiAgent.status.useQuery(); + + // 获取会话列表 + const { data: convData, refetch: refetchConvs } = trpc.aiAgent.listConversations.useQuery( + { agentType: selectedAgent || undefined, limit: 50 }, + { enabled: true } + ); + + // 加载历史消息 + const loadHistoryQuery = trpc.aiAgent.getConversation.useQuery( + { conversationId: activeConvId! }, + { enabled: !!activeConvId, refetchOnWindowFocus: false } + ); + + useEffect(() => { + if (loadHistoryQuery.data?.messages && activeConvId) { + setMessages( + (loadHistoryQuery.data.messages as Array<{ role: string; content: string; metadata?: Record }>).map(m => ({ + role: m.role as "user" | "assistant", + content: m.content, + confidence: m.metadata?.confidence as number | undefined, + sources: m.metadata?.sources as string[] | undefined, + suggestions: m.metadata?.suggestions as string[] | undefined, + isRTL: m.metadata?.isRTL as boolean | undefined, + })) + ); + } + }, [loadHistoryQuery.data, activeConvId]); + + // 删除会话 + const deleteConvMutation = trpc.aiAgent.deleteConversation.useMutation({ + onSuccess: () => { refetchConvs(); }, + onError: (e) => toast.error(`删除失败: ${e.message}`), + }); + + // 发送消息 + const chatMutation = trpc.aiAgent.chat.useMutation({ + onSuccess: (response) => { + setMessages(prev => [...prev, { + role: "assistant", + content: response.answer, + confidence: response.confidence, + sources: response.sources, + suggestions: response.suggestions, + isRTL: response.metadata?.isRTL as boolean, + }]); + if (!activeConvId && response.conversationId) { + setActiveConvId(response.conversationId); + } + refetchConvs(); + }, + onError: (error) => { + toast.error(`响应失败: ${error.message}`); + }, + }); + + // 自动滚动到底部 + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // 切换 Agent + const handleSelectAgent = (type: AgentType) => { + setSelectedAgent(type); + setActiveConvId(null); + setMessages([]); + setInputText(""); + }; + + // 点击历史会话 + const handleSelectConversation = (conv: ConversationItem) => { + setSelectedAgent(conv.agentType); + setActiveConvId(conv.conversationId); + setMessages([]); + }; + + // 新建会话 + const handleNewConversation = () => { + setActiveConvId(null); + setMessages([]); + setInputText(""); + setTimeout(() => textareaRef.current?.focus(), 100); + }; + + // 发送 + const handleSend = () => { + const text = inputText.trim(); + if (!text || chatMutation.isPending || !selectedAgent) return; + setMessages(prev => [...prev, { role: "user", content: text }]); + setInputText(""); + chatMutation.mutate({ + agentType: selectedAgent, + userMessage: text, + conversationId: activeConvId || undefined, + persistHistory: true, + }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const currentAgent = agentData?.agents.find(a => a.type === selectedAgent); + const conversations = (convData?.conversations || []) as ConversationItem[]; + const agentInfo = AGENT_LABELS[selectedAgent]; + + return ( +
+ + {/* ══════════════════════════════════════════ + 左侧边栏 + ══════════════════════════════════════════ */} +
+ + {/* 顶部 Logo + 新建 */} +
+
+
+ +
+ NAC公链AI +
+ +
+ + {/* AI 状态 */} + {statusData && ( +
+ {statusData.configured + ? <>AI 已就绪 + : <>AI 未配置 + } +
+ )} + + {/* Agent 切换 */} +
+

助手类型

+
+ {agentLoading ? ( +
+ +
+ ) : ( + agentData?.agents.map((agent) => { + const info = AGENT_LABELS[agent.type as AgentType]; + const IconComp = ICON_MAP[agent.icon] || Bot; + const isActive = selectedAgent === agent.type; + return ( + + ); + }) + )} +
+
+ + {/* 历史会话 */} +
+ + + {showHistory && ( +
+ {conversations.length === 0 ? ( +

暂无历史会话

+ ) : ( + conversations.map(conv => ( + handleSelectConversation(conv)} + onDelete={() => deleteConvMutation.mutate({ conversationId: conv.conversationId })} + /> + )) + )} +
+ )} +
+
+ + {/* ══════════════════════════════════════════ + 右侧主对话区 + ══════════════════════════════════════════ */} +
+ + {/* 顶部栏 */} +
+
+
+ {agentInfo && (() => { + const IconComp = ICON_MAP[agentInfo.icon] || Bot; + return ; + })()} +
+
+

NAC公链AI

+

+ {agentInfo?.name} · {agentInfo?.desc} +

+
+
+
+ {activeConvId && ( + + )} +
+
+ + {/* 消息区域 */} +
+ {/* 加载历史 */} + {loadHistoryQuery.isLoading && activeConvId && ( +
+ + 正在加载历史消息... +
+ )} + + {/* 欢迎屏幕 */} + {messages.length === 0 && !loadHistoryQuery.isLoading && ( + { + setInputText(q); + setTimeout(() => textareaRef.current?.focus(), 100); + }} + /> + )} + + {/* 消息列表 */} + {messages.map((msg, i) => ( + + ))} + + {/* AI 思考中 */} + {chatMutation.isPending && ( +
+
+ +
+
+ NAC公链AI +
+ + 正在思考... +
+
+
+ )} + +
+
+ + {/* 输入区域 */} +
+
+
+