From 41f9c122a9b73e51ebf10ea66317ef9c3aa4359a Mon Sep 17 00:00:00 2001 From: "Yuemin.Mao" Date: Fri, 8 May 2026 19:09:00 +0800 Subject: [PATCH] feat: enhance document upload and processing features, update mock data, and improve status display --- src/data/mockDocs.ts | 8 +- src/data/mockRetrievalData.ts | 8 + src/pages/Docs/DocsPage.tsx | 357 +++++++++++++++++++++++------- src/pages/RagChat/RagChatPage.tsx | 42 +++- src/pages/Status/StatusPage.tsx | 157 ++++++++++--- src/styles/globals.css | 30 +++ start.sh | 10 + 7 files changed, 493 insertions(+), 119 deletions(-) create mode 100755 start.sh diff --git a/src/data/mockDocs.ts b/src/data/mockDocs.ts index ae09c1e..ff6ffcd 100644 --- a/src/data/mockDocs.ts +++ b/src/data/mockDocs.ts @@ -1,7 +1,9 @@ import type { Doc } from '../types'; export const mockDocs: Doc[] = [ - { id: 1, name: '道路交通安全法.pdf', chunks: 124, size: '1.2MB', status: 'indexed' }, - { id: 2, name: '机动车登记规定.docx', chunks: 58, size: '856KB', status: 'indexed' }, - { id: 3, name: '电动自行车规范.pdf', chunks: 32, size: '245KB', status: 'indexed' }, + { id: 1, name: '道路交通安全法.pdf', chunks: 156, size: '1.8MB', status: 'indexed' }, + { id: 2, name: '机动车登记规定.docx', chunks: 89, size: '1.1MB', status: 'indexed' }, + { id: 3, name: '电动自行车规范.pdf', chunks: 42, size: '345KB', status: 'indexed' }, + { id: 4, name: 'GB 38031-2020 电动汽车安全要求.pdf', chunks: 128, size: '2.2MB', status: 'indexed' }, + { id: 5, name: 'C-NCAP管理规则(2021版).pdf', chunks: 95, size: '1.5MB', status: 'indexed' }, ]; \ No newline at end of file diff --git a/src/data/mockRetrievalData.ts b/src/data/mockRetrievalData.ts index f3f23d1..130ace4 100644 --- a/src/data/mockRetrievalData.ts +++ b/src/data/mockRetrievalData.ts @@ -5,4 +5,12 @@ export const mockRetrievalData: RetrievalData[] = [ { id: 2, file: '电动自行车规范.pdf', clause: '第二条', score: 0.87, content: '最高设计车速不超过25km/h,整车质量(含电池)不超过55kg,具有脚踏骑行能力,蓄电池标称电压不超过48V。' }, { id: 3, file: '道路交通安全法.pdf', clause: '第七十二条', score: 0.79, content: '驾驶电动自行车在道路上行驶,应当遵守下列规定:佩戴安全头盔;不得逆向行驶;不得在机动车道内行驶。' }, { id: 4, file: '机动车登记规定.pdf', clause: '第五条', score: 0.72, content: '初次申领机动车号牌、行驶证的,应当向住所地的车辆管理所申请注册登记,填写申请表,交验机动车。' }, + { id: 5, file: 'GB 38031-2020 电动汽车安全要求.pdf', clause: '5.1 热失控要求', score: 0.95, content: '电池系统发生热失控后,应在5分钟内不起火不爆炸,为乘员预留逃生时间。电池包需通过针刺、过充、短路等安全测试。' }, + { id: 6, file: 'C-NCAP管理规则(2021版).pdf', clause: '4.2 正面碰撞', score: 0.88, content: '正面100%重叠刚性壁障碰撞试验,碰撞速度50km/h。试验后车门应能打开,燃油系统无泄漏,座椅及安全带功能正常。' }, + { id: 7, file: '道路交通安全法.pdf', clause: '第九十条', score: 0.76, content: '机动车驾驶人违反道路交通安全法律、法规关于道路通行规定的,处警告或者二十元以上二百元以下罚款。' }, + { id: 8, file: '机动车登记规定.pdf', clause: '第十二条', score: 0.65, content: '机动车所有人的住所迁出车辆管理所管辖区域的,车辆管理所应当自受理之日起三日内,在机动车登记证书上签注变更事项。' }, + { id: 9, file: 'GB 38031-2020 电动汽车安全要求.pdf', clause: '6.2 充电安全', score: 0.91, content: '充电系统应具备过充保护功能,当电池SOC达到100%时应自动停止充电。充电接口应符合GB/T 18487.1标准要求。' }, + { id: 10, file: 'C-NCAP管理规则(2021版).pdf', clause: '5.3.1 AEB功能', score: 0.84, content: '自动紧急制动系统(AEB)应在检测到前方障碍物时主动减速或停车。AEB系统测试分为目标车静止、移动、制动三种场景。' }, + { id: 11, file: '道路交通安全法.pdf', clause: '第六十七条', score: 0.70, content: '机动车在高速公路上行驶,车速超过100km/h时,应当与同车道前车保持100米以上的距离;车速低于100km/h时,距离可适当缩短。' }, + { id: 12, file: '道路交通安全法.pdf', clause: '第五十三条', score: 0.68, content: '警车、消防车、救护车、工程救险车执行紧急任务时,可以使用警报器、标志灯具,在确保安全的前提下,不受行驶路线、行驶方向、行驶速度和信号灯的限制。' }, ]; \ No newline at end of file diff --git a/src/pages/Docs/DocsPage.tsx b/src/pages/Docs/DocsPage.tsx index fcfc595..f431e2c 100644 --- a/src/pages/Docs/DocsPage.tsx +++ b/src/pages/Docs/DocsPage.tsx @@ -1,11 +1,180 @@ -import React from 'react'; +import React, { useState, useRef } from 'react'; import { useTheme } from '../../contexts/ThemeContext'; import { Content } from '../../components/layout/Content'; import { TPattern } from '../../components/common/TPattern'; import { mockDocs } from '../../data'; +import type { Doc } from '../../types'; export const DocsPage: React.FC = () => { const { theme, isDark } = useTheme(); + const fileInputRef = useRef(null); + const [activeStep, setActiveStep] = useState(-1); + const [completedSteps, setCompletedSteps] = useState([]); + const [docs, setDocs] = useState(mockDocs); + const [uploading, setUploading] = useState(false); + const [uploadFileName, setUploadFileName] = useState(''); + + const pipelineSteps = [ + { name: 'LOAD' }, + { name: 'PARSE' }, + { name: 'CHUNK' }, + { name: 'EMBED' }, + { name: 'STORE' }, + ]; + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file || uploading) return; + + setUploading(true); + setUploadFileName(file.name); + setActiveStep(0); + setCompletedSteps([]); + + // Add new doc in "parsing" state + const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1); + const newDoc: Doc = { + id: Date.now(), + name: file.name, + chunks: 0, + size: `${fileSizeMB}MB`, + status: 'parsing', + }; + setDocs(prev => [...prev, newDoc]); + + // Simulate each pipeline step + const stepDuration = 600; + pipelineSteps.forEach((_, index) => { + setTimeout(() => { + setActiveStep(index); + setTimeout(() => { + setCompletedSteps(prev => [...prev, index]); + if (index === pipelineSteps.length - 1) { + // Final step completed + setActiveStep(-1); + setUploading(false); + setUploadFileName(''); + // Update doc to indexed state with mock chunks + setDocs(prev => prev.map(d => + d.id === newDoc.id ? { ...d, chunks: Math.floor(file.size / 8000) + 20, status: 'indexed' } : d + )); + } + }, stepDuration - 100); + }, index * stepDuration); + }); + + // Clear input for next upload + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const triggerFileUpload = () => { + if (uploading) return; + fileInputRef.current?.click(); + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const file = event.dataTransfer.files?.[0]; + if (!file || uploading) return; + + // Check file type + const validTypes = ['.pdf', '.docx', '.txt']; + const fileExt = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); + if (!validTypes.includes(fileExt)) { + alert('请上传 PDF、DOCX 或 TXT 文件'); + return; + } + + // Simulate upload process + setUploading(true); + setUploadFileName(file.name); + setActiveStep(0); + setCompletedSteps([]); + + const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1); + const newDoc: Doc = { + id: Date.now(), + name: file.name, + chunks: 0, + size: `${fileSizeMB}MB`, + status: 'parsing', + }; + setDocs(prev => [...prev, newDoc]); + + const stepDuration = 600; + pipelineSteps.forEach((_, index) => { + setTimeout(() => { + setActiveStep(index); + setTimeout(() => { + setCompletedSteps(prev => [...prev, index]); + if (index === pipelineSteps.length - 1) { + setActiveStep(-1); + setUploading(false); + setUploadFileName(''); + setDocs(prev => prev.map(d => + d.id === newDoc.id ? { ...d, chunks: Math.floor(file.size / 8000) + 20, status: 'indexed' } : d + )); + } + }, stepDuration - 100); + }, index * stepDuration); + }); + }; + + const getStepStyle = (index: number) => { + const isActive = activeStep === index; + const isCompleted = completedSteps.includes(index); + const baseBg = theme.bgCard; + + if (isActive) { + return { + background: theme.bgCard, + border: `2px solid ${theme.accent}`, + boxShadow: `0 0 12px ${theme.accent}40`, + }; + } + if (isCompleted) { + return { + background: theme.bgCard, + border: `1px solid ${theme.green}`, + }; + } + return { + background: baseBg, + border: `1px solid ${theme.border}`, + }; + }; + + const getCheckStyle = (index: number) => { + const isActive = activeStep === index; + const isCompleted = completedSteps.includes(index); + + if (isActive) { + return { + background: theme.gradientAccent, + color: '#fff', + animation: 'pulse 0.6s infinite', + }; + } + if (isCompleted) { + return { + background: theme.green, + color: '#fff', + }; + } + return { + background: theme.bgHover, + color: theme.text3, + }; + }; return ( @@ -19,16 +188,29 @@ export const DocsPage: React.FC = () => { marginBottom: 20, letterSpacing: '1px', }}>UPLOAD -
+ {/* Hidden file input */} + +
{ justifyContent: 'center', margin: '0 auto 20px', }}> - - - - + {uploading ? ( +
+ + + +
+ ) : ( + + + + + )}
-
拖拽文件或点击上传
-
PDF · DOCX · TXT · MAX 50MB
+
+ {uploading ? '正在上传...' : '拖拽文件或点击上传'} +
+
+ {uploading ? uploadFileName : 'PDF · DOCX · TXT · MAX 50MB'} +
+
+ + + {/* Processing Pipeline */} +
+

PROCESSING PIPELINE

+
+ {pipelineSteps.map((s, i) => { + const stepStyle = getStepStyle(i); + const checkStyle = getCheckStyle(i); + return ( +
+
+ {activeStep === i ? '⋯' : '✓'} +
+
{s.name}
+ {i < 4 &&
} +
+ ); + })}
@@ -57,9 +304,9 @@ export const DocsPage: React.FC = () => { color: theme.accent, marginBottom: 20, letterSpacing: '1px', - }}>INDEXED DOCUMENTS ({mockDocs.length}) + }}>INDEXED DOCUMENTS ({docs.length})
- {mockDocs.map(d => ( + {docs.map(d => (
{ padding: 20, background: theme.bgCard, borderRadius: 12, - border: `1px solid ${theme.border}`, + border: `1px solid ${d.status === 'parsing' ? theme.accent : theme.border}`, transition: 'all 0.2s ease', boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none', }}> @@ -98,77 +345,33 @@ export const DocsPage: React.FC = () => { background: theme.bgHover, borderRadius: 6, color: theme.text2, - }}>{d.chunks} chunks
+ }}> + {d.status === 'parsing' ? '处理中...' : `${d.chunks} chunks`} +
- - - + {d.status === 'indexed' ? ( + + + + ) : ( + + + + )}
))} - - {/* Processing Pipeline */} -
-

PROCESSING PIPELINE

-
- {[ - { name: 'LOAD' }, - { name: 'PARSE' }, - { name: 'CHUNK' }, - { name: 'EMBED' }, - { name: 'STORE' }, - ].map((s, i) => ( -
-
-
{s.name}
- {i < 4 &&
} -
- ))} -
-
); }; \ No newline at end of file diff --git a/src/pages/RagChat/RagChatPage.tsx b/src/pages/RagChat/RagChatPage.tsx index eea672a..f65e15d 100644 --- a/src/pages/RagChat/RagChatPage.tsx +++ b/src/pages/RagChat/RagChatPage.tsx @@ -8,25 +8,53 @@ const ragQuickQuestions = [ '驾驶证如何申请?', '超速行驶如何处罚?', '车辆年检有哪些规定?', + '电动汽车电池安全标准?', + '正面碰撞测试要求?', + 'AEB系统测试标准?', + '高速公路安全距离?', ]; const generateRagResponse = (question: string): { text: string; retrievalIds: number[] } => { const keywords: Record = { '电动自行车': { - text: '根据《道路交通安全法》及相关规范,电动自行车上路需满足以下条件:\n1. 符合国家标准 GB17761-2018\n2. 经公安机关交通管理部门登记\n3. 最高设计车速不超过 25km/h\n4. 整车质量不超过 55kg\n5. 具有脚踏骑行能力', + text: '根据《道路交通安全法》及相关规范,电动自行车上路需满足以下条件:\n\n1. 符合国家标准 GB17761-2018\n2. 经公安机关交通管理部门登记\n3. 最高设计车速不超过 25km/h\n4. 整车质量不超过 55kg\n5. 具有脚踏骑行能力\n6. 蓄电池标称电压不超过 48V\n\n行驶时还需佩戴安全头盔,不得逆向行驶或在机动车道内行驶。', retrievalIds: [1, 2, 3], }, '驾驶证': { - text: '驾驶证申请流程如下:\n1. 到驾校报名并参加培训\n2. 通过科目一(理论考试)\n3. 通过科目二(场地驾驶技能考试)\n4. 通过科目三(道路驾驶技能考试)\n5. 通过科目四(安全文明驾驶常识考试)\n6. 领取驾驶证', + text: '驾驶证申请流程如下:\n\n1. 到驾校报名并参加培训\n2. 通过科目一(理论考试)\n3. 通过科目二(场地驾驶技能考试)\n4. 通过科目三(道路驾驶技能考试)\n5. 通过科目四(安全文明驾驶常识考试)\n6. 领取驾驶证\n\n初次申领需到住所地车辆管理所申请注册登记。', retrievalIds: [1, 4], }, '超速': { - text: '超速处罚标准:\n- 超速10%以下:警告\n- 超速10%-20%:罚款50-200元\n- 超速20%-50%:罚款200-500元,记3-6分\n- 超速50%以上:罚款500-2000元,记12分,可吊销驾驶证', - retrievalIds: [1, 2], + text: '超速处罚标准(根据《道路交通安全法》):\n\n- 超速10%以下:警告\n- 超速10%-20%:罚款50-200元\n- 超速20%-50%:罚款200-500元,记3-6分\n- 超速50%以上:罚款500-2000元,记12分,可吊销驾驶证\n\n机动车驾驶人违反道路交通安全法律、法规将处警告或二十元以上二百元以下罚款。', + retrievalIds: [1, 7], }, '年检': { - text: '车辆年检规定:\n- 小型私家车:6年内免检(每2年申领标志),6-10年每2年检验,10年以上每年检验\n- 车辆需携带行驶证、交强险保单\n- 检验项目:灯光、制动、排放等', - retrievalIds: [1, 4], + text: '车辆年检规定:\n\n- 小型私家车:6年内免检(每2年申领标志),6-10年每2年检验,10年以上每年检验\n- 车辆需携带行驶证、交强险保单\n- 检验项目:灯光、制动、排放等\n\n机动车所有人的住所迁出车辆管理所管辖区域的,需在登记证书上签注变更事项。', + retrievalIds: [1, 4, 8], + }, + '电池': { + text: '电动汽车电池安全标准(GB 38031-2020):\n\n1. 热失控要求:电池系统发生热失控后,应在5分钟内不起火不爆炸,为乘员预留逃生时间\n2. 电池包需通过针刺、过充、短路等安全测试\n3. 充电系统应具备过充保护功能,当电池SOC达到100%时应自动停止充电\n4. 充电接口应符合GB/T 18487.1标准要求\n\n以上要求确保电动汽车的整车安全性。', + retrievalIds: [5, 9], + }, + '碰撞': { + text: '正面碰撞测试要求(C-NCAP管理规则):\n\n1. 正面100%重叠刚性壁障碰撞试验\n2. 碰撞速度:50km/h\n3. 试验后要求:\n - 车门应能打开\n - 燃油系统无泄漏\n - 座椅及安全带功能正常\n\n此测试用于评估车辆在正面碰撞事故中对乘员的保护能力。', + retrievalIds: [6, 10], + }, + 'AEB': { + text: 'AEB(自动紧急制动系统)测试标准:\n\n1. 系统应在检测到前方障碍物时主动减速或停车\n2. 测试场景分为三种:\n - 目标车静止\n - 目标车移动\n - 目标车制动\n3. AEB功能是C-NCAP评分的重要加分项\n\n该系统对提升车辆主动安全性能具有重要意义。', + retrievalIds: [10], + }, + '高速公路': { + text: '高速公路安全距离规定:\n\n1. 车速超过100km/h时,与同车道前车保持100米以上距离\n2. 车速低于100km/h时,距离可适当缩短\n3. 执行紧急任务的警车、消防车、救护车、工程救险车不受行驶速度限制\n\n保持安全距离是预防追尾事故的关键措施。', + retrievalIds: [11, 12], + }, + '充电': { + text: '电动汽车充电安全要求:\n\n1. 充电系统应具备过充保护功能\n2. 电池SOC达到100%时应自动停止充电\n3. 充电接口应符合GB/T 18487.1标准\n4. 需具备温度监控和通信协议(符合GB/T 27930)\n\n快充30分钟充至80%符合行业标准,但需确保循环寿命测试数据完整。', + retrievalIds: [9, 5], + }, + '安全': { + text: '车辆安全配置要求:\n\n1. 电池安全:热失控后5分钟内不起火不爆炸(GB 38031-2020)\n2. 碰撞安全:正面碰撞后车门能打开,燃油无泄漏(C-NCAP)\n3. 主动安全:AEB系统应能检测障碍物并主动减速\n4. 安全气囊:乘用车需配置符合GB 27887-2011的安全气囊\n\n以上配置确保车辆在各类场景下的乘员安全。', + retrievalIds: [5, 6, 10], }, }; @@ -37,7 +65,7 @@ const generateRagResponse = (question: string): { text: string; retrievalIds: nu } return { - text: '抱歉,暂未找到与您问题直接相关的法规内容。请尝试更具体的问题,或联系交通管理部门获取详细信息。', + text: '抱歉,暂未找到与您问题直接相关的法规内容。请尝试更具体的问题,或联系交通管理部门获取详细信息。\n\n您可以尝试询问:电动自行车、驾驶证、超速处罚、年检、电池安全、碰撞测试、AEB系统、高速公路规则等话题。', retrievalIds: [], }; }; diff --git a/src/pages/Status/StatusPage.tsx b/src/pages/Status/StatusPage.tsx index 31e0936..734034d 100644 --- a/src/pages/Status/StatusPage.tsx +++ b/src/pages/Status/StatusPage.tsx @@ -38,6 +38,9 @@ const StatsCard = ({ label, value, accent = false }: { export const StatusPage: React.FC = () => { const { theme, isDark } = useTheme(); + // 计算总chunks + const totalChunks = mockDocs.reduce((sum, d) => sum + d.chunks, 0); + return ( @@ -48,10 +51,10 @@ export const StatusPage: React.FC = () => { gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, }}> - - + + - + @@ -64,35 +67,125 @@ export const StatusPage: React.FC = () => { marginBottom: 20, letterSpacing: '1px', }}>SYSTEM CONFIGURATION -
- {[ - ['LLM Model', 'GPT-4o'], - ['Embedding', 'text-embedding-3-small'], - ['Vector DB', 'ChromaDB'], - ['Retrieval', 'Hybrid (Vector + BM25)'], - ['Top-K', '5 candidates'], - ['Chunk Size', '500-1000 tokens'], - ['Overlap', '100 tokens'], - ['Temperature', '0.1'], - ].map(([k, v]) => ( -
- {k} - {v} -
- ))} + + {/* ChromaDB Config */} +
+
VECTOR DATABASE
+
+ {[ + ['Vector DB', 'Milvus'], + ['Host', 'localhost'], + ['Port', '19530'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+ + {/* LLM Config */} +
+
LLM CONFIGURATION
+
+ {[ + ['LLM Model', 'qwen-max'], + ['Embedding Model', 'text-embedding-v3'], + ['Embedding Dim', '1536'], + ['Temperature', '0.1'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+ + {/* Retrieval Config */} +
+
RETRIEVAL CONFIGURATION (HYBRID)
+
+ {[ + ['Vector Top-K', '10'], + ['BM25 Top-K', '10'], + ['Final Top-K', '5'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+ + {/* Chunk Config */} +
+
CHUNK CONFIGURATION
+
+ {[ + ['Chunk Size', '800'], + ['Chunk Overlap', '100'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
diff --git a/src/styles/globals.css b/src/styles/globals.css index 824e130..cdb7df9 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -232,6 +232,36 @@ button, input { animation: slideOut 0.3s ease-in forwards; } +/* Pulse animation for processing steps */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.8; + transform: scale(0.95); + } +} + +.animate-pulse { + animation: pulse 0.6s ease-in-out infinite; +} + +/* Spin animation for loading icons */ +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + /* Custom utility classes */ @layer utilities { .gradient-accent { diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..c70ff74 --- /dev/null +++ b/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# 启动开发服务器 +# 端口: 8000 +# 监听: 0.0.0.0 + +cd "$(dirname "$0")" + +echo "Starting dev server on http://0.0.0.0:8001" +npx vite --host 0.0.0.0 --port 8001 \ No newline at end of file