import React, { useState, useRef, useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router'; import { Send, Plus, ChevronRight, Clock, ExternalLink, Info } from 'lucide-react'; import { toast } from 'sonner'; import { useAppState, type ChatMessage, type ToolCall } from '../../store'; import { api, ApiError } from '../../api'; import { TYPE_COLORS } from '../../mock-data'; export function QAChat() { const { messages, setMessages, chatHistory, suggestedPrompts, nodes, refreshHistory } = useAppState(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [input, setInput] = useState(''); const [isThinking, setIsThinking] = useState(false); const [activeHistoryId, setActiveHistoryId] = useState(null); const [conversationHistory, setConversationHistory] = useState<{ question: string; answer: string }[]>([]); const messagesEndRef = useRef(null); const inputRef = useRef(null); useEffect(() => { const q = searchParams.get('q'); if (q) { setInput(q); inputRef.current?.focus(); } }, [searchParams]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, isThinking]); // Build cited node objects from node IDs using local KG function resolveCitedNodes(ids: string[]) { return ids .map(id => { const n = nodes.find(n => n.id === id); return n ? { id: n.id, name: n.name, type: n.type } : null; }) .filter(Boolean) as { id: string; name: string; type: string }[]; } const handleSend = async () => { if (!input.trim() || isThinking) return; const question = input.trim(); setInput(''); setIsThinking(true); const userMsg: ChatMessage = { id: `m${Date.now()}`, role: 'human', content: question, timestamp: new Date().toISOString(), }; setMessages(prev => [...prev, userMsg]); try { const result = await api.query(question, conversationHistory); const aiMsg: ChatMessage = { id: result.id ?? `m${Date.now() + 1}`, role: 'ai', content: result.answer, timestamp: result.timestamp ?? new Date().toISOString(), toolCalls: result.tool_calls.map((tc, i) => ({ step: tc.step ?? i + 1, tool: tc.tool_name, input: tc.tool_input, output: tc.tool_output, })), citedNodes: resolveCitedNodes(result.cited_nodes ?? []), duration: result.duration_seconds, }; setMessages(prev => [...prev, aiMsg]); setConversationHistory(prev => [...prev, { question, answer: result.answer }]); // Refresh history sidebar refreshHistory(); } catch (err) { const msg = err instanceof ApiError ? err.message : '问答服务异常'; toast.error(msg); setMessages(prev => [...prev, { id: `err${Date.now()}`, role: 'ai', content: `⚠️ 请求失败:${msg}\n\n请确认:\n1. 后端服务已启动\n2. 知识图谱已有数据(请先上传并索引文档)\n3. DeepSeek API Key 已配置`, timestamp: new Date().toISOString(), }]); } finally { setIsThinking(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleNewChat = () => { setMessages([]); setInput(''); setActiveHistoryId(null); setConversationHistory([]); }; // Load a history item as a single Q&A session const handleLoadHistory = (h: typeof chatHistory[0]) => { setActiveHistoryId(h.id); const msgs: ChatMessage[] = [ { id: `${h.id}-q`, role: 'human', content: h.question, timestamp: h.timestamp }, { id: `${h.id}-a`, role: 'ai', content: h.answer, timestamp: h.timestamp, toolCalls: h.toolCalls, citedNodes: resolveCitedNodes(h.citedNodeIds ?? []), duration: h.duration, }, ]; setMessages(msgs); setConversationHistory([{ question: h.question, answer: h.answer }]); }; const groupedHistory = { '今天': chatHistory.filter(h => h.group === '今天'), '昨天': chatHistory.filter(h => h.group === '昨天'), '更早': chatHistory.filter(h => h.group === '更早'), }; return (
{/* History Sidebar */}
{/* 历史会话管理说明 */}
点击历史记录查看单条问答;多轮对话会话管理 未开发
{Object.entries(groupedHistory).map(([group, items]) => items.length > 0 && (
{group}
{items.map(h => ( ))}
))} {chatHistory.length === 0 && (
暂无历史记录
)}
{/* Chat Area */}
{/* Messages */}
{messages.length === 0 ? (
GraphRAG{' '} Studio

向知识图谱提问。我将使用多步推理从已索引的文档中为您找到准确答案。

{suggestedPrompts.map((p, i) => ( ))}
) : (
{messages.map(msg => (
{msg.role === 'human' ? (
{msg.content}
) : (
{msg.toolCalls && msg.toolCalls.length > 0 && ( )} {msg.citedNodes && msg.citedNodes.length > 0 && (
{msg.citedNodes.map(cn => ( ))}
)} {msg.duration !== undefined && (
{msg.duration.toFixed(1)}s
)}
)}
))} {isThinking && (
)}
)}
{/* Input Area */}