import { useState, useEffect, useRef, useCallback } 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 { Separator } from "@/components/ui/separator"; import { toast } from "sonner"; import { BookOpen, Shield, Languages, ClipboardCheck, Send, Bot, User, Loader2, ChevronRight, Sparkles, AlertCircle, CheckCircle2, Info, MessageSquare, Plus, Trash2, Clock, ChevronDown, ChevronUp, ExternalLink, } 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_COLORS: Record = { knowledge_qa: "text-blue-500", compliance: "text-red-500", translation: "text-green-500", approval_assist: "text-purple-500", }; // ─── 置信度徽章 ─────────────────────────────────────────────────── function ConfidenceBadge({ confidence }: { confidence: number }) { const pct = Math.round(confidence * 100); const color = pct >= 80 ? "text-green-600" : pct >= 60 ? "text-yellow-600" : "text-red-500"; 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 ? : }
{msg.content}
{!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 IconComp = ICON_MAP[conv.agentType === "knowledge_qa" ? "BookOpen" : conv.agentType === "compliance" ? "Shield" : conv.agentType === "translation" ? "Languages" : "ClipboardCheck"] || Bot; const colorClass = AGENT_COLORS[conv.agentType] || "text-muted-foreground"; const updatedAt = new Date(conv.updatedAt); const timeStr = updatedAt.toLocaleDateString("zh-CN", { month: "short", day: "numeric" }); return (

{conv.title}

{timeStr} · {conv.messageCount}条
); } // ─── 主页面 ─────────────────────────────────────────────────────── export default function AIAgents() { const [selectedAgent, setSelectedAgent] = useState(null); const [activeConvId, setActiveConvId] = useState(null); const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(""); const [showHistory, setShowHistory] = useState(true); const messagesEndRef = useRef(null); const utils = trpc.useUtils(); const [, navigate] = useLocation(); // 处理来源引用点击 - 跳转到知识库并搜索对应条目 const handleSourceClick = (source: string) => { // 解析来源格式:"CN·RWA·房地产登记规则" -> 提取关键词 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: 30 }, { enabled: true } ); // 加载历史消息 const loadHistoryQuery = trpc.aiAgent.loadHistory.useQuery( { conversationId: activeConvId!, limit: 50 }, { enabled: !!activeConvId } ); // 当加载历史成功时,将消息填充到界面 useEffect(() => { if (loadHistoryQuery.data?.messages && activeConvId) { const msgs: Message[] = loadHistoryQuery.data.messages.map(m => ({ role: m.role as "user" | "assistant", content: m.content, confidence: m.confidence, sources: m.sources, suggestions: m.suggestions, })); setMessages(msgs); } }, [loadHistoryQuery.data, activeConvId]); // 删除会话 const deleteConvMutation = trpc.aiAgent.deleteConversation.useMutation({ onSuccess: () => { toast.success("会话已删除"); refetchConvs(); }, onError: (e) => toast.error(`删除失败: ${e.message}`), }); // Agent对话mutation const chatMutation = trpc.aiAgent.chat.useMutation({ onSuccess: (response) => { setMessages(prev => [...prev, { role: "assistant", content: response.message, confidence: response.confidence, sources: response.sources, suggestions: response.suggestions, isRTL: response.metadata?.isRTL as boolean, }]); // 如果是新会话,更新activeConvId if (!activeConvId && response.conversationId) { setActiveConvId(response.conversationId); } // 刷新会话列表 refetchConvs(); }, onError: (error) => { toast.error(`Agent响应失败: ${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([]); // 先清空,等loadHistory加载 }; // 新建会话 const handleNewConversation = () => { setActiveConvId(null); setMessages([]); setInputText(""); }; // 发送消息 const handleSend = () => { const text = inputText.trim(); if (!text || chatMutation.isPending || !selectedAgent) return; const userMsg: Message = { role: "user", content: text }; setMessages(prev => [...prev, userMsg]); 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[]; return (
{/* ── 左侧:Agent选择 + 历史会话 ── */}
{/* 顶部状态 */}

AI智能体

{statusData && (
{statusData.configured ? <>AI服务已就绪 · {statusData.model} : <>AI服务未配置 }
)}
{/* Agent列表 */}
{agentLoading ? (
) : ( agentData?.agents.map((agent) => { const IconComp = ICON_MAP[agent.icon] || Bot; const isActive = selectedAgent === agent.type && !activeConvId; return ( ); }) )}
{/* 历史会话列表 */}
{showHistory && (
{conversations.length === 0 ? (

暂无历史会话

) : ( conversations.map(conv => ( handleSelectConversation(conv)} onDelete={() => deleteConvMutation.mutate({ conversationId: conv.conversationId })} /> )) )}
)}
{/* 未配置提示 */} {agentData && !agentData.configured && (
{agentData.configHint}
)}
{/* ── 右侧:对话区域 ── */}
{!selectedAgent ? ( /* 欢迎界面 */

NAC AI智能体系统

选择左侧的智能体开始对话。对话历史将自动保存到数据库,支持跨会话续接。

{agentData?.agents.map((agent) => { const IconComp = ICON_MAP[agent.icon] || Bot; return ( ); })}
) : ( <> {/* 对话头部 */}
{currentAgent && (() => { const IconComp = ICON_MAP[currentAgent.icon] || Bot; return ( <>

{currentAgent.name}

{activeConvId ? `续接历史会话 · ${messages.length}条消息` : "新对话 · 历史将自动保存" }

); })()}
{activeConvId && ( )}
{/* 加载历史中 */} {loadHistoryQuery.isLoading && activeConvId && (
正在加载历史消息...
)} {/* 消息列表 */}
{messages.length === 0 && !loadHistoryQuery.isLoading && currentAgent && (

建议提问:

{currentAgent.suggestedQuestions.map((q, i) => ( ))}
)} {messages.map((msg, i) => ( ))} {chatMutation.isPending && (
)}
{/* 输入区域 */}