feat: enhance document upload and processing features, update mock data, and improve status display
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import type { Doc } from '../types';
|
import type { Doc } from '../types';
|
||||||
|
|
||||||
export const mockDocs: Doc[] = [
|
export const mockDocs: Doc[] = [
|
||||||
{ id: 1, name: '道路交通安全法.pdf', chunks: 124, size: '1.2MB', status: 'indexed' },
|
{ id: 1, name: '道路交通安全法.pdf', chunks: 156, size: '1.8MB', status: 'indexed' },
|
||||||
{ id: 2, name: '机动车登记规定.docx', chunks: 58, size: '856KB', status: 'indexed' },
|
{ id: 2, name: '机动车登记规定.docx', chunks: 89, size: '1.1MB', status: 'indexed' },
|
||||||
{ id: 3, name: '电动自行车规范.pdf', chunks: 32, size: '245KB', 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' },
|
||||||
];
|
];
|
||||||
@@ -5,4 +5,12 @@ export const mockRetrievalData: RetrievalData[] = [
|
|||||||
{ id: 2, file: '电动自行车规范.pdf', clause: '第二条', score: 0.87, content: '最高设计车速不超过25km/h,整车质量(含电池)不超过55kg,具有脚踏骑行能力,蓄电池标称电压不超过48V。' },
|
{ id: 2, file: '电动自行车规范.pdf', clause: '第二条', score: 0.87, content: '最高设计车速不超过25km/h,整车质量(含电池)不超过55kg,具有脚踏骑行能力,蓄电池标称电压不超过48V。' },
|
||||||
{ id: 3, file: '道路交通安全法.pdf', clause: '第七十二条', score: 0.79, content: '驾驶电动自行车在道路上行驶,应当遵守下列规定:佩戴安全头盔;不得逆向行驶;不得在机动车道内行驶。' },
|
{ id: 3, file: '道路交通安全法.pdf', clause: '第七十二条', score: 0.79, content: '驾驶电动自行车在道路上行驶,应当遵守下列规定:佩戴安全头盔;不得逆向行驶;不得在机动车道内行驶。' },
|
||||||
{ id: 4, file: '机动车登记规定.pdf', clause: '第五条', score: 0.72, 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: '警车、消防车、救护车、工程救险车执行紧急任务时,可以使用警报器、标志灯具,在确保安全的前提下,不受行驶路线、行驶方向、行驶速度和信号灯的限制。' },
|
||||||
];
|
];
|
||||||
@@ -1,11 +1,180 @@
|
|||||||
import React from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { Content } from '../../components/layout/Content';
|
import { Content } from '../../components/layout/Content';
|
||||||
import { TPattern } from '../../components/common/TPattern';
|
import { TPattern } from '../../components/common/TPattern';
|
||||||
import { mockDocs } from '../../data';
|
import { mockDocs } from '../../data';
|
||||||
|
import type { Doc } from '../../types';
|
||||||
|
|
||||||
export const DocsPage: React.FC = () => {
|
export const DocsPage: React.FC = () => {
|
||||||
const { theme, isDark } = useTheme();
|
const { theme, isDark } = useTheme();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [activeStep, setActiveStep] = useState<number>(-1);
|
||||||
|
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
||||||
|
const [docs, setDocs] = useState<Doc[]>(mockDocs);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploadFileName, setUploadFileName] = useState<string>('');
|
||||||
|
|
||||||
|
const pipelineSteps = [
|
||||||
|
{ name: 'LOAD' },
|
||||||
|
{ name: 'PARSE' },
|
||||||
|
{ name: 'CHUNK' },
|
||||||
|
{ name: 'EMBED' },
|
||||||
|
{ name: 'STORE' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
return (
|
||||||
<Content>
|
<Content>
|
||||||
@@ -19,16 +188,29 @@ export const DocsPage: React.FC = () => {
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
letterSpacing: '1px',
|
letterSpacing: '1px',
|
||||||
}}>UPLOAD</h2>
|
}}>UPLOAD</h2>
|
||||||
<div style={{
|
{/* Hidden file input */}
|
||||||
border: `2px solid ${theme.border}`,
|
<input
|
||||||
borderRadius: 16,
|
ref={fileInputRef}
|
||||||
padding: 64,
|
type="file"
|
||||||
textAlign: 'center',
|
accept=".pdf,.docx,.txt"
|
||||||
background: theme.bgCard,
|
onChange={handleFileSelect}
|
||||||
transition: 'all 0.3s ease',
|
style={{ display: 'none' }}
|
||||||
cursor: 'pointer',
|
/>
|
||||||
boxShadow: !isDark ? '0 4px 16px rgba(226,0,116,0.08)' : 'none',
|
<div
|
||||||
}}>
|
onClick={triggerFileUpload}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
style={{
|
||||||
|
border: `2px solid ${uploading ? theme.accent : theme.border}`,
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 64,
|
||||||
|
textAlign: 'center',
|
||||||
|
background: theme.bgCard,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
cursor: uploading ? 'wait' : 'pointer',
|
||||||
|
boxShadow: !isDark ? '0 4px 16px rgba(226,0,116,0.08)' : 'none',
|
||||||
|
opacity: uploading ? 0.7 : 1,
|
||||||
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
@@ -39,13 +221,78 @@ export const DocsPage: React.FC = () => {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
margin: '0 auto 20px',
|
margin: '0 auto 20px',
|
||||||
}}>
|
}}>
|
||||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none">
|
{uploading ? (
|
||||||
<path d="M12 4L12 16M12 4L7 9M12 4L17 9" stroke={theme.accent} strokeWidth="2" strokeLinecap="round"/>
|
<div style={{ animation: 'spin 1s linear infinite' }}>
|
||||||
<path d="M4 18H20" stroke={theme.accent} strokeWidth="2" strokeLinecap="round"/>
|
<svg width="36" height="36" viewBox="0 0 24 24" fill="none">
|
||||||
</svg>
|
<circle cx="12" cy="12" r="10" stroke={theme.accent} strokeWidth="2" strokeDasharray="60" strokeDashoffset="20"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<svg width="36" height="36" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 4L12 16M12 4L7 9M12 4L17 9" stroke={theme.accent} strokeWidth="2" strokeLinecap="round"/>
|
||||||
|
<path d="M4 18H20" stroke={theme.accent} strokeWidth="2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8 }}>拖拽文件或点击上传</div>
|
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8 }}>
|
||||||
<div className="mono" style={{ fontSize: 12, color: theme.text3 }}>PDF · DOCX · TXT · MAX 50MB</div>
|
{uploading ? '正在上传...' : '拖拽文件或点击上传'}
|
||||||
|
</div>
|
||||||
|
<div className="mono" style={{ fontSize: 12, color: theme.text3 }}>
|
||||||
|
{uploading ? uploadFileName : 'PDF · DOCX · TXT · MAX 50MB'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Processing Pipeline */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.accent,
|
||||||
|
marginBottom: 20,
|
||||||
|
letterSpacing: '1px',
|
||||||
|
}}>PROCESSING PIPELINE</h2>
|
||||||
|
<div style={{ display: 'flex', gap: 16 }}>
|
||||||
|
{pipelineSteps.map((s, i) => {
|
||||||
|
const stepStyle = getStepStyle(i);
|
||||||
|
const checkStyle = getCheckStyle(i);
|
||||||
|
return (
|
||||||
|
<div key={i} style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
borderRadius: 12,
|
||||||
|
position: 'relative',
|
||||||
|
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
||||||
|
...stepStyle,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto 12px',
|
||||||
|
fontSize: 16,
|
||||||
|
...checkStyle,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}>
|
||||||
|
{activeStep === i ? '⋯' : '✓'}
|
||||||
|
</div>
|
||||||
|
<div className="mono" style={{ fontSize: 12, fontWeight: 600 }}>{s.name}</div>
|
||||||
|
{i < 4 && <div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: -8,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
color: completedSteps.includes(i) ? theme.green : theme.borderLight,
|
||||||
|
fontWeight: completedSteps.includes(i) ? 700 : 400,
|
||||||
|
}}>→</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -57,9 +304,9 @@ export const DocsPage: React.FC = () => {
|
|||||||
color: theme.accent,
|
color: theme.accent,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
letterSpacing: '1px',
|
letterSpacing: '1px',
|
||||||
}}>INDEXED DOCUMENTS ({mockDocs.length})</h2>
|
}}>INDEXED DOCUMENTS ({docs.length})</h2>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
{mockDocs.map(d => (
|
{docs.map(d => (
|
||||||
<div key={d.id} style={{
|
<div key={d.id} style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -67,7 +314,7 @@ export const DocsPage: React.FC = () => {
|
|||||||
padding: 20,
|
padding: 20,
|
||||||
background: theme.bgCard,
|
background: theme.bgCard,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
border: `1px solid ${theme.border}`,
|
border: `1px solid ${d.status === 'parsing' ? theme.accent : theme.border}`,
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
||||||
}}>
|
}}>
|
||||||
@@ -98,77 +345,33 @@ export const DocsPage: React.FC = () => {
|
|||||||
background: theme.bgHover,
|
background: theme.bgHover,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
color: theme.text2,
|
color: theme.text2,
|
||||||
}}>{d.chunks} chunks</div>
|
}}>
|
||||||
|
{d.status === 'parsing' ? '处理中...' : `${d.chunks} chunks`}
|
||||||
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
background: theme.green,
|
background: d.status === 'indexed' ? theme.green : theme.bgHover,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}>
|
}}>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
{d.status === 'indexed' ? (
|
||||||
<path d="M5 12L10 17L20 7" stroke="#fff" strokeWidth="2" strokeLinecap="round"/>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
</svg>
|
<path d="M5 12L10 17L20 7" stroke="#fff" strokeWidth="2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="6" stroke={theme.accent} strokeWidth="2"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Processing Pipeline */}
|
|
||||||
<section>
|
|
||||||
<h2 style={{
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: theme.accent,
|
|
||||||
marginBottom: 20,
|
|
||||||
letterSpacing: '1px',
|
|
||||||
}}>PROCESSING PIPELINE</h2>
|
|
||||||
<div style={{ display: 'flex', gap: 16 }}>
|
|
||||||
{[
|
|
||||||
{ name: 'LOAD' },
|
|
||||||
{ name: 'PARSE' },
|
|
||||||
{ name: 'CHUNK' },
|
|
||||||
{ name: 'EMBED' },
|
|
||||||
{ name: 'STORE' },
|
|
||||||
].map((s, i) => (
|
|
||||||
<div key={i} style={{
|
|
||||||
flex: 1,
|
|
||||||
padding: 20,
|
|
||||||
textAlign: 'center',
|
|
||||||
background: theme.bgCard,
|
|
||||||
borderRadius: 12,
|
|
||||||
border: `1px solid ${theme.border}`,
|
|
||||||
position: 'relative',
|
|
||||||
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: 8,
|
|
||||||
background: theme.gradientAccent,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
margin: '0 auto 12px',
|
|
||||||
fontSize: 16,
|
|
||||||
color: '#fff',
|
|
||||||
}}>✓</div>
|
|
||||||
<div className="mono" style={{ fontSize: 12, fontWeight: 600 }}>{s.name}</div>
|
|
||||||
{i < 4 && <div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: -8,
|
|
||||||
top: '50%',
|
|
||||||
transform: 'translateY(-50%)',
|
|
||||||
color: theme.borderLight,
|
|
||||||
}}>→</div>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Content>
|
</Content>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -8,25 +8,53 @@ const ragQuickQuestions = [
|
|||||||
'驾驶证如何申请?',
|
'驾驶证如何申请?',
|
||||||
'超速行驶如何处罚?',
|
'超速行驶如何处罚?',
|
||||||
'车辆年检有哪些规定?',
|
'车辆年检有哪些规定?',
|
||||||
|
'电动汽车电池安全标准?',
|
||||||
|
'正面碰撞测试要求?',
|
||||||
|
'AEB系统测试标准?',
|
||||||
|
'高速公路安全距离?',
|
||||||
];
|
];
|
||||||
|
|
||||||
const generateRagResponse = (question: string): { text: string; retrievalIds: number[] } => {
|
const generateRagResponse = (question: string): { text: string; retrievalIds: number[] } => {
|
||||||
const keywords: Record<string, { text: string; retrievalIds: number[] }> = {
|
const keywords: Record<string, { text: string; retrievalIds: number[] }> = {
|
||||||
'电动自行车': {
|
'电动自行车': {
|
||||||
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],
|
retrievalIds: [1, 2, 3],
|
||||||
},
|
},
|
||||||
'驾驶证': {
|
'驾驶证': {
|
||||||
text: '驾驶证申请流程如下:\n1. 到驾校报名并参加培训\n2. 通过科目一(理论考试)\n3. 通过科目二(场地驾驶技能考试)\n4. 通过科目三(道路驾驶技能考试)\n5. 通过科目四(安全文明驾驶常识考试)\n6. 领取驾驶证',
|
text: '驾驶证申请流程如下:\n\n1. 到驾校报名并参加培训\n2. 通过科目一(理论考试)\n3. 通过科目二(场地驾驶技能考试)\n4. 通过科目三(道路驾驶技能考试)\n5. 通过科目四(安全文明驾驶常识考试)\n6. 领取驾驶证\n\n初次申领需到住所地车辆管理所申请注册登记。',
|
||||||
retrievalIds: [1, 4],
|
retrievalIds: [1, 4],
|
||||||
},
|
},
|
||||||
'超速': {
|
'超速': {
|
||||||
text: '超速处罚标准:\n- 超速10%以下:警告\n- 超速10%-20%:罚款50-200元\n- 超速20%-50%:罚款200-500元,记3-6分\n- 超速50%以上:罚款500-2000元,记12分,可吊销驾驶证',
|
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, 2],
|
retrievalIds: [1, 7],
|
||||||
},
|
},
|
||||||
'年检': {
|
'年检': {
|
||||||
text: '车辆年检规定:\n- 小型私家车:6年内免检(每2年申领标志),6-10年每2年检验,10年以上每年检验\n- 车辆需携带行驶证、交强险保单\n- 检验项目:灯光、制动、排放等',
|
text: '车辆年检规定:\n\n- 小型私家车:6年内免检(每2年申领标志),6-10年每2年检验,10年以上每年检验\n- 车辆需携带行驶证、交强险保单\n- 检验项目:灯光、制动、排放等\n\n机动车所有人的住所迁出车辆管理所管辖区域的,需在登记证书上签注变更事项。',
|
||||||
retrievalIds: [1, 4],
|
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 {
|
return {
|
||||||
text: '抱歉,暂未找到与您问题直接相关的法规内容。请尝试更具体的问题,或联系交通管理部门获取详细信息。',
|
text: '抱歉,暂未找到与您问题直接相关的法规内容。请尝试更具体的问题,或联系交通管理部门获取详细信息。\n\n您可以尝试询问:电动自行车、驾驶证、超速处罚、年检、电池安全、碰撞测试、AEB系统、高速公路规则等话题。',
|
||||||
retrievalIds: [],
|
retrievalIds: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ const StatsCard = ({ label, value, accent = false }: {
|
|||||||
export const StatusPage: React.FC = () => {
|
export const StatusPage: React.FC = () => {
|
||||||
const { theme, isDark } = useTheme();
|
const { theme, isDark } = useTheme();
|
||||||
|
|
||||||
|
// 计算总chunks
|
||||||
|
const totalChunks = mockDocs.reduce((sum, d) => sum + d.chunks, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content>
|
<Content>
|
||||||
<TPattern />
|
<TPattern />
|
||||||
@@ -48,10 +51,10 @@ export const StatusPage: React.FC = () => {
|
|||||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
}}>
|
}}>
|
||||||
<StatsCard label="DOCUMENTS" value={3} />
|
<StatsCard label="DOCUMENTS" value={mockDocs.length} />
|
||||||
<StatsCard label="CHUNKS" value={214} />
|
<StatsCard label="CHUNKS" value={totalChunks} />
|
||||||
<StatsCard label="DIMENSIONS" value={1536} />
|
<StatsCard label="DIMENSIONS" value={1536} />
|
||||||
<StatsCard label="CLAUSES" value={214} accent />
|
<StatsCard label="CLAUSES" value={totalChunks} accent />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -64,35 +67,125 @@ export const StatusPage: React.FC = () => {
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
letterSpacing: '1px',
|
letterSpacing: '1px',
|
||||||
}}>SYSTEM CONFIGURATION</h2>
|
}}>SYSTEM CONFIGURATION</h2>
|
||||||
<div style={{
|
|
||||||
display: 'grid',
|
{/* ChromaDB Config */}
|
||||||
gridTemplateColumns: '1fr 1fr',
|
<div style={{ marginBottom: 20 }}>
|
||||||
gap: 12,
|
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>VECTOR DATABASE</div>
|
||||||
}}>
|
<div style={{
|
||||||
{[
|
display: 'grid',
|
||||||
['LLM Model', 'GPT-4o'],
|
gridTemplateColumns: '1fr 1fr 1fr',
|
||||||
['Embedding', 'text-embedding-3-small'],
|
gap: 12,
|
||||||
['Vector DB', 'ChromaDB'],
|
}}>
|
||||||
['Retrieval', 'Hybrid (Vector + BM25)'],
|
{[
|
||||||
['Top-K', '5 candidates'],
|
['Vector DB', 'Milvus'],
|
||||||
['Chunk Size', '500-1000 tokens'],
|
['Host', 'localhost'],
|
||||||
['Overlap', '100 tokens'],
|
['Port', '19530'],
|
||||||
['Temperature', '0.1'],
|
].map(([k, v]) => (
|
||||||
].map(([k, v]) => (
|
<div key={k} style={{
|
||||||
<div key={k} style={{
|
display: 'flex',
|
||||||
display: 'flex',
|
justifyContent: 'space-between',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
padding: 16,
|
||||||
padding: 16,
|
background: theme.bgCard,
|
||||||
background: theme.bgCard,
|
borderRadius: 10,
|
||||||
borderRadius: 10,
|
border: `1px solid ${theme.border}`,
|
||||||
border: `1px solid ${theme.border}`,
|
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
||||||
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
}}>
|
||||||
}}>
|
<span className="mono" style={{ fontSize: 12, color: theme.text3 }}>{k}</span>
|
||||||
<span className="mono" style={{ fontSize: 12, color: theme.text3 }}>{k}</span>
|
<span style={{ fontSize: 14, fontWeight: 500 }}>{v}</span>
|
||||||
<span style={{ fontSize: 14, fontWeight: 500 }}>{v}</span>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LLM Config */}
|
||||||
|
<div style={{ marginBottom: 20 }}>
|
||||||
|
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>LLM CONFIGURATION</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
gap: 12,
|
||||||
|
}}>
|
||||||
|
{[
|
||||||
|
['LLM Model', 'qwen-max'],
|
||||||
|
['Embedding Model', 'text-embedding-v3'],
|
||||||
|
['Embedding Dim', '1536'],
|
||||||
|
['Temperature', '0.1'],
|
||||||
|
].map(([k, v]) => (
|
||||||
|
<div key={k} style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
background: theme.bgCard,
|
||||||
|
borderRadius: 10,
|
||||||
|
border: `1px solid ${theme.border}`,
|
||||||
|
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
||||||
|
}}>
|
||||||
|
<span className="mono" style={{ fontSize: 12, color: theme.text3 }}>{k}</span>
|
||||||
|
<span style={{ fontSize: 14, fontWeight: 500 }}>{v}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Retrieval Config */}
|
||||||
|
<div style={{ marginBottom: 20 }}>
|
||||||
|
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>RETRIEVAL CONFIGURATION (HYBRID)</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr 1fr',
|
||||||
|
gap: 12,
|
||||||
|
}}>
|
||||||
|
{[
|
||||||
|
['Vector Top-K', '10'],
|
||||||
|
['BM25 Top-K', '10'],
|
||||||
|
['Final Top-K', '5'],
|
||||||
|
].map(([k, v]) => (
|
||||||
|
<div key={k} style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
background: theme.bgCard,
|
||||||
|
borderRadius: 10,
|
||||||
|
border: `1px solid ${theme.border}`,
|
||||||
|
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
||||||
|
}}>
|
||||||
|
<span className="mono" style={{ fontSize: 12, color: theme.text3 }}>{k}</span>
|
||||||
|
<span style={{ fontSize: 14, fontWeight: 500 }}>{v}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chunk Config */}
|
||||||
|
<div>
|
||||||
|
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>CHUNK CONFIGURATION</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
gap: 12,
|
||||||
|
}}>
|
||||||
|
{[
|
||||||
|
['Chunk Size', '800'],
|
||||||
|
['Chunk Overlap', '100'],
|
||||||
|
].map(([k, v]) => (
|
||||||
|
<div key={k} style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
background: theme.bgCard,
|
||||||
|
borderRadius: 10,
|
||||||
|
border: `1px solid ${theme.border}`,
|
||||||
|
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
|
||||||
|
}}>
|
||||||
|
<span className="mono" style={{ fontSize: 12, color: theme.text3 }}>{k}</span>
|
||||||
|
<span style={{ fontSize: 14, fontWeight: 500 }}>{v}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,36 @@ button, input {
|
|||||||
animation: slideOut 0.3s ease-in forwards;
|
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 */
|
/* Custom utility classes */
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.gradient-accent {
|
.gradient-accent {
|
||||||
|
|||||||
Reference in New Issue
Block a user