import React, { useEffect, useRef, useState } from 'react'; import { useTheme } from '../../contexts'; import type { ChatMessage, RetrievalData } from '../../types'; import { getQuickQuestions, ragChat } from '../../api/rag'; import { CitedAnswer } from './CitedAnswer'; const ragQuickQuestionsDefault = [ '电动自行车上路需要什么条件?', '驾驶证如何申请?', '超速行驶如何处罚?', '车辆年检有哪些规定?', '电动汽车电池安全标准?', '正面碰撞测试要求?', 'AEB系统测试标准?', '高速公路安全距离?', ]; export const RagChatPage: React.FC = () => { const { theme } = useTheme(); const nextMessageIdRef = useRef(1); const [messages, setMessages] = useState([]); const [retrievals, setRetrievals] = useState([]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [showClearConfirm, setShowClearConfirm] = useState(false); const [selectedRetrieval, setSelectedRetrieval] = useState(null); const [quickQuestions, setQuickQuestions] = useState(ragQuickQuestionsDefault); const [filterRegulationType, setFilterRegulationType] = useState(''); const [highlightedSourceIdx, setHighlightedSourceIdx] = useState(null); function nextMessageId() { const currentId = nextMessageIdRef.current; nextMessageIdRef.current += 1; return currentId; } async function loadQuickQuestions() { try { const response = await getQuickQuestions(); setQuickQuestions(response.questions.map(q => q.question)); } catch (error) { console.error('Failed to load quick questions:', error); } } useEffect(() => { const timerId = window.setTimeout(() => { void loadQuickQuestions(); }, 0); return () => window.clearTimeout(timerId); }, []); const sendMessage = (text: string) => { if (!text.trim()) return; const userMsg = { id: nextMessageId(), role: 'user' as const, content: text }; setMessages((prev) => [...prev, userMsg]); setInput(''); setLoading(true); setRetrievals([]); setHighlightedSourceIdx(null); let currentResponse = ''; const activeFilters = filterRegulationType.trim() || undefined; void ragChat( text, 5, (data: unknown) => { const sseData = data as { type: string; text?: string; docs?: Array<{ id: string; score: number; preview: string; doc_name: string; clause: string; doc_id?: string; download_url?: string; }>; }; if (sseData.type === 'retrieved' && sseData.docs) { const retrievedDocs: RetrievalData[] = sseData.docs.map(d => ({ id: parseInt(d.id.replace('chunk-', ''), 10) || 1, file: d.doc_name, clause: d.clause, score: d.score, content: d.preview, docId: d.doc_id, downloadUrl: d.download_url, })); setRetrievals(retrievedDocs); } else if (sseData.type === 'chunk' && sseData.text) { currentResponse += sseData.text; setMessages((prev) => { const lastMsg = prev[prev.length - 1]; if (lastMsg?.role === 'assistant') { return [...prev.slice(0, -1), { ...lastMsg, content: currentResponse }]; } return [...prev, { id: nextMessageId(), role: 'assistant' as const, content: currentResponse }]; }); } else if (sseData.type === 'done') { setLoading(false); } else if (sseData.type === 'error') { setLoading(false); } }, (error: Error) => { console.error('RAG chat error:', error); setLoading(false); setMessages((prev) => [ ...prev, { id: nextMessageId(), role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' } ]); }, () => { setLoading(false); }, activeFilters ); }; const clearMessages = () => { setMessages([]); setRetrievals([]); setShowClearConfirm(false); }; const regenerateLastAnswer = () => { if (messages.length < 2) return; const lastUserMsg = messages.filter((m) => m.role === 'user').pop(); if (!lastUserMsg) return; setLoading(true); setMessages((prev) => [...prev.slice(0, -1)]); setRetrievals([]); setHighlightedSourceIdx(null); let currentResponse = ''; const activeFilters = filterRegulationType.trim() || undefined; void ragChat( lastUserMsg.content, 5, (data: unknown) => { const sseData = data as { type: string; text?: string; docs?: Array<{ id: string; score: number; preview: string; doc_name: string; clause: string; doc_id?: string; download_url?: string; }>; }; if (sseData.type === 'retrieved' && sseData.docs) { const retrievedDocs: RetrievalData[] = sseData.docs.map(d => ({ id: parseInt(d.id.replace('chunk-', ''), 10) || 1, file: d.doc_name, clause: d.clause, score: d.score, content: d.preview, docId: d.doc_id, downloadUrl: d.download_url, })); setRetrievals(retrievedDocs); } else if (sseData.type === 'chunk' && sseData.text) { currentResponse += sseData.text; setMessages((prev) => { const lastMsg = prev[prev.length - 1]; if (lastMsg?.role === 'assistant') { return [...prev.slice(0, -1), { ...lastMsg, content: currentResponse }]; } return [...prev, { id: nextMessageId(), role: 'assistant' as const, content: currentResponse }]; }); } else if (sseData.type === 'done') { setLoading(false); } }, (error: Error) => { console.error('RAG chat error:', error); setLoading(false); setMessages((prev) => [ ...prev, { id: nextMessageId(), role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' } ]); }, () => { setLoading(false); }, activeFilters ); }; return (
{messages.length === 0 ? (
开始法规对话
选择快捷问题或输入您的问题
) : ( messages.map(msg => (
{msg.role === 'assistant' && (
)}
{msg.role === 'assistant' ? ( { setHighlightedSourceIdx(idx); const el = document.getElementById(`source-${idx}`); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }} /> ) : msg.content} {msg.role === 'assistant' && msg.retrievalIds && msg.retrievalIds.length > 0 && (
{msg.retrievalIds.length} 个法规引用
)}
)) )} {loading && (
检索中...
)}
法规类型 setFilterRegulationType(e.target.value)} placeholder="如: GB / UN-ECE / IATF(留空不过滤)" style={{ flex: 1, maxWidth: 280, padding: '5px 10px', fontSize: 12, background: theme.bgHover, border: `1px solid ${theme.border}`, borderRadius: 6, color: theme.text, outline: 'none', }} />
{quickQuestions.map(q => ( ))}
setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && sendMessage(input)} placeholder="输入法规问题..." style={{ flex: 1, padding: 12, fontSize: 14, background: theme.bgCard, border: `1px solid ${theme.border}`, borderRadius: 8, color: theme.text, outline: 'none', }} /> {messages.length > 0 && ( )} {messages.filter(m => m.role === 'assistant').length > 0 && ( )}
RETRIEVED FRAGMENTS {retrievals.length > 0 && ( {retrievals.length} )}
{retrievals.length > 0 ? (
{retrievals.map((r, i) => (
setSelectedRetrieval(r)} style={{ padding: 16, background: highlightedSourceIdx === i + 1 ? theme.bgElevated : theme.bgHover, borderRadius: 10, border: `1px solid ${highlightedSourceIdx === i + 1 ? theme.accent : theme.border}`, cursor: 'pointer', position: 'relative', transition: 'border-color 0.2s, background 0.2s', }} >
{r.file}
{r.clause}{r.docId ? ` · ${r.docId}` : ''}
{r.content}
))}
) : (
对话后显示相关法规
)}
{showClearConfirm && (
确定清空对话?
此操作不可恢复
)} {selectedRetrieval && (
setSelectedRetrieval(null)} style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, }} >
e.stopPropagation()} style={{ width: 520, maxWidth: '90%', maxHeight: '80%', padding: 24, background: theme.bgCard, borderRadius: 16, border: `1px solid ${theme.accent}`, boxShadow: '0 8px 32px rgba(0,0,0,0.3)', }} >
{(selectedRetrieval.score * 100).toFixed(0)}%
{selectedRetrieval.file} {selectedRetrieval.downloadUrl && ( 下载关联文档 )}
{selectedRetrieval.clause} {selectedRetrieval.docId && ( {selectedRetrieval.docId} )}
{selectedRetrieval.content}
)}
); };