import React, { useEffect, useRef, useState } from 'react'; import { useTheme } from '../../contexts'; import { Content } from '../../components/layout/Content'; import { TPattern } from '../../components/common/TPattern'; import { getDocumentList, searchRegulations, uploadDocument, type RegulationSearchItem } from '../../api/docs'; import type { Doc } from '../../types'; type PipelineStatus = 'idle' | 'running' | 'completed' | 'error'; const PIPELINE_STEPS = [ { name: 'LOAD' }, { name: 'PARSE' }, { name: 'CHUNK' }, { name: 'EMBED' }, { name: 'STORE' }, ]; const STEP_DURATION_MS = 700; const INITIAL_SEARCH_QUERY = '新能源汽车电池安全要求'; function wait(ms: number) { return new Promise((resolve) => { window.setTimeout(resolve, ms); }); } export const DocsPage: React.FC = () => { const { theme, isDark } = useTheme(); const fileInputRef = useRef(null); const pipelineRunIdRef = useRef(0); const [activeStep, setActiveStep] = useState(-1); const [completedSteps, setCompletedSteps] = useState([]); const [pipelineStatus, setPipelineStatus] = useState('idle'); const [docs, setDocs] = useState([]); const [uploading, setUploading] = useState(false); const [uploadFileName, setUploadFileName] = useState(''); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(INITIAL_SEARCH_QUERY); const [searchResults, setSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [searchError, setSearchError] = useState(''); async function loadDocuments() { setLoading(true); try { const response = await getDocumentList(); const apiDocs: Doc[] = response.docs.map((doc) => ({ id: parseInt(String(doc.id).replace('doc-', ''), 10) || Math.floor(Math.random() * 10000), name: doc.name, chunks: doc.chunks, size: doc.updated_at ? new Date(doc.updated_at).toLocaleString() : 'Indexed document', status: doc.status === 'indexed' ? 'indexed' : doc.status === 'failed' ? 'failed' : 'parsing', docId: doc.id, downloadUrl: doc.download_url, updatedAt: doc.updated_at, })); setDocs(apiDocs); } catch (error) { console.error('Failed to load documents:', error); setDocs([]); } finally { setLoading(false); } } async function runSearch(query: string) { if (!query.trim()) return; setSearchLoading(true); setSearchError(''); try { const response = await searchRegulations(query.trim(), 8); setSearchResults(response.results); } catch (error) { console.error('Failed to search regulations:', error); setSearchError(error instanceof Error ? error.message : '检索失败'); setSearchResults([]); } finally { setSearchLoading(false); } } useEffect(() => { const timerId = window.setTimeout(() => { void loadDocuments(); }, 0); return () => window.clearTimeout(timerId); }, []); useEffect(() => { const timerId = window.setTimeout(() => { void runSearch(INITIAL_SEARCH_QUERY); }, 0); return () => window.clearTimeout(timerId); }, []); useEffect(() => { return () => { pipelineRunIdRef.current += 1; }; }, []); const runPipelineFlow = async (runId: number, uploadPromise: Promise>>) => { const guardedSetActiveStep = (step: number) => { if (pipelineRunIdRef.current !== runId) return false; setActiveStep(step); return true; }; const guardedCompleteStep = (step: number) => { if (pipelineRunIdRef.current !== runId) return false; setCompletedSteps((prev) => (prev.includes(step) ? prev : [...prev, step])); return true; }; for (let index = 0; index < PIPELINE_STEPS.length - 1; index += 1) { if (!guardedSetActiveStep(index)) return; await wait(STEP_DURATION_MS); if (!guardedCompleteStep(index)) return; } if (!guardedSetActiveStep(PIPELINE_STEPS.length - 1)) return; await uploadPromise; if (!guardedCompleteStep(PIPELINE_STEPS.length - 1)) return; await wait(240); if (pipelineRunIdRef.current !== runId) return; setActiveStep(-1); setPipelineStatus('completed'); }; const handleFileSelect = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file || uploading) return; const runId = pipelineRunIdRef.current + 1; pipelineRunIdRef.current = runId; setUploading(true); setUploadFileName(file.name); setActiveStep(-1); setCompletedSteps([]); setPipelineStatus('running'); const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1); const tempDocId = `pending-${Date.now()}`; const newDoc: Doc = { id: Date.now(), name: file.name, chunks: 0, size: `${fileSizeMB}MB`, status: 'parsing', docId: tempDocId, }; setDocs((prev) => [newDoc, ...prev]); const uploadPromise = uploadDocument(file); void runPipelineFlow(runId, uploadPromise); try { const uploadRes = await uploadPromise; if (pipelineRunIdRef.current !== runId) return; setDocs((prev) => prev.map((doc) => doc.id === newDoc.id ? { ...doc, status: 'indexed', docId: uploadRes.doc_id, chunks: uploadRes.num_chunks || doc.chunks, summary: uploadRes.summary, } : doc ) ); setUploading(false); setUploadFileName(''); void loadDocuments(); } catch (error) { console.error('Upload failed:', error); if (pipelineRunIdRef.current !== runId) return; setUploading(false); setUploadFileName(''); setDocs((prev) => prev.filter((doc) => doc.id !== newDoc.id)); setPipelineStatus('error'); setActiveStep(-1); setCompletedSteps([]); } finally { 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 files = event.dataTransfer.files; if (files.length === 0 || uploading) return; const droppedFile = files[0]; if (fileInputRef.current) { const dataTransfer = new DataTransfer(); dataTransfer.items.add(droppedFile); fileInputRef.current.files = dataTransfer.files; } void handleFileSelect({ target: { files: [droppedFile] as unknown as FileList }, } as React.ChangeEvent); }; const getStepStyle = (index: number) => { const isActive = activeStep === index; const isCompleted = completedSteps.includes(index); 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: theme.bgCard, 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, }; }; const getPipelineHint = () => { if (pipelineStatus === 'running') { return activeStep >= 0 ? `${PIPELINE_STEPS[activeStep].name} · ${uploadFileName}` : `LOAD · ${uploadFileName}`; } if (pipelineStatus === 'completed') { return 'PIPELINE COMPLETE'; } if (pipelineStatus === 'error') { return 'PIPELINE FAILED'; } return 'WAITING FOR UPLOAD'; }; return (

UPLOAD

{uploading ? (
) : ( )}
{uploading ? '正在上传并启动处理链路...' : '拖拽文件或点击上传'}
{uploading ? uploadFileName : 'PDF · DOCX · DOC · MAX 100MB'}

PROCESSING PIPELINE

{getPipelineHint()}
{PIPELINE_STEPS.map((step, index) => { const stepStyle = getStepStyle(index); const checkStyle = getCheckStyle(index); const arrowActive = activeStep > index || completedSteps.includes(index); const isCompleted = completedSteps.includes(index); const isActive = activeStep === index; return (
{isActive ? step.name : isCompleted ? '✓' : step.name}
{step.name}
{isCompleted ? 'DONE' : isActive ? 'RUNNING' : 'PENDING'}
{index < PIPELINE_STEPS.length - 1 && (
)}
); })}

文档管理清单 ({loading ? '...' : docs.length})

{docs.map((doc) => (
{doc.name}
{doc.updatedAt ? new Date(doc.updatedAt).toLocaleString() : doc.size} {doc.docId ? ` · ${doc.docId}` : ''}
{doc.downloadUrl && ( 下载 )}
{doc.status === 'parsing' ? '处理中...' : doc.status === 'failed' ? '处理失败' : `${doc.chunks} chunks`}
))}

文档管理内法规检索

setSearchQuery(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { void runSearch(searchQuery); } }} placeholder="输入法规关键词、条款或制度主题" style={{ flex: 1, padding: 12, fontSize: 14, background: theme.bgCard, border: `1px solid ${theme.border}`, borderRadius: 8, color: theme.text, outline: 'none', }} />
{searchError && (
{searchError}
)}
{searchResults.map((item) => (
{item.file}
{(item.score * 100).toFixed(1)}%
{item.clause} {item.tags.length > 0 ? ` · ${item.tags.join(' · ')}` : ''}
{item.content}
))} {!searchLoading && searchResults.length === 0 && (
暂无检索结果
)}
); };