Fix SSE route dependency and align architecture docs

This commit is contained in:
ash66
2026-05-18 16:32:42 +08:00
parent 86b9ac806a
commit 3f69cad404
149 changed files with 4786 additions and 5957 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useTheme } from '../../contexts/ThemeContext';
import { useTheme } from '../../contexts';
import type { ComplianceChunk } from '../../types';
interface ChatPanelProps {
@@ -243,4 +243,4 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
</div>
</div>
);
};
};

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
import React, { useRef, useState } from 'react';
import { useTheme } from '../../contexts';
import type { UploadedDoc, ComplianceChunk, Regulation, SegmentRisk, RiskDashboardData } from '../../types';
import {
mockComplianceChunks,
@@ -82,9 +82,11 @@ const getRegsByCategory = (regulations: Regulation[]) => {
export const CompliancePage: React.FC = () => {
const { theme, isDark } = useTheme();
const nextMessageIdRef = useRef(1);
// Upload & Analysis States
const [uploadedDoc, setUploadedDoc] = useState<UploadedDoc | null>(null);
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
const [analyzeStep, setAnalyzeStep] = useState<number>(0);
const [analyzePercent, setAnalyzePercent] = useState<number>(0);
@@ -100,10 +102,17 @@ export const CompliancePage: React.FC = () => {
const [chatLoading, setChatLoading] = useState<boolean>(false);
const [dashboardExpanded, setDashboardExpanded] = useState<boolean>(false);
const nextMessageId = () => {
const currentId = nextMessageIdRef.current;
nextMessageIdRef.current += 1;
return currentId;
};
// Handlers
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploadedFile(file);
setUploadedDoc({
name: file.name,
size: `${(file.size / 1024 / 1024).toFixed(2)}MB`,
@@ -113,27 +122,14 @@ export const CompliancePage: React.FC = () => {
};
const startAnalysis = async () => {
if (!uploadedDoc) return;
if (!uploadedDoc || !uploadedFile) return;
setIsAnalyzing(true);
setAnalyzeStep(1);
setAnalyzePercent(0);
try {
// Get file from upload input
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput?.files?.[0];
console.log("123")
// if (!file) {
// // setIsAnalyzing(false);
// // return;
// }
console.log("456")
// Upload and get task ID
const uploadRes = await analyzeDocument(file);
const uploadRes = await analyzeDocument(uploadedFile);
// Simulate progress
setTimeout(() => {
@@ -174,7 +170,7 @@ export const CompliancePage: React.FC = () => {
regulations: s.regulations.map(r => ({
id: r.id,
name: r.name,
clause: r.clause,
clause: r.clause || '',
score: r.score,
matchKeyword: r.match_keyword,
category: r.category as 'high' | 'medium' | 'low',
@@ -204,7 +200,6 @@ export const CompliancePage: React.FC = () => {
}
} catch (error) {
console.error('Failed to get compliance result:', error);
// Fallback to mock data
setChunks(mockComplianceChunks);
}
@@ -215,7 +210,6 @@ export const CompliancePage: React.FC = () => {
} catch (error) {
console.error('Failed to analyze document:', error);
setIsAnalyzing(false);
// Fallback to mock data after delay
setTimeout(() => {
setChunks(mockComplianceChunks);
}, 4500);
@@ -230,7 +224,7 @@ export const CompliancePage: React.FC = () => {
setChatMessages(prev => ({
...prev,
[chunkId]: [{
id: Date.now(),
id: nextMessageId(),
role: 'assistant',
content: `您好!我是法规合规分析助手。当前段落涉及 ${chunk?.regulations.length} 条相关法规,您可以询问合规性评估、法规解读或修改建议。`,
}]
@@ -248,7 +242,7 @@ export const CompliancePage: React.FC = () => {
const chunk = chunks.find(c => c.id === activeChunkId);
if (!chunk) return;
const userMsg = { id: Date.now(), role: 'user' as const, content: chatInput };
const userMsg = { id: nextMessageId(), role: 'user' as const, content: chatInput };
setChatMessages(prev => ({
...prev,
[activeChunkId]: [...(prev[activeChunkId] || []), userMsg],
@@ -267,7 +261,7 @@ export const CompliancePage: React.FC = () => {
currentResponse += sseData.text;
setChatMessages(prev => ({
...prev,
[activeChunkId]: [...(prev[activeChunkId] || []).slice(0, -1), { id: Date.now() + 1, role: 'assistant', content: currentResponse }],
[activeChunkId]: [...(prev[activeChunkId] || []).slice(0, -1), { id: nextMessageId(), role: 'assistant', content: currentResponse }],
}));
} else if (sseData.type === 'done') {
setChatLoading(false);
@@ -291,7 +285,7 @@ export const CompliancePage: React.FC = () => {
}
setChatMessages(prev => ({
...prev,
[activeChunkId]: [...(prev[activeChunkId] || []), { id: Date.now() + 1, role: 'assistant', content: response }],
[activeChunkId]: [...(prev[activeChunkId] || []), { id: nextMessageId(), role: 'assistant', content: response }],
}));
},
() => {
@@ -1913,4 +1907,4 @@ export const CompliancePage: React.FC = () => {
)}
</div>
);
};
};

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
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';
@@ -16,6 +16,7 @@ const PIPELINE_STEPS = [
];
const STEP_DURATION_MS = 700;
const INITIAL_SEARCH_QUERY = '新能源汽车电池安全要求';
function wait(ms: number) {
return new Promise<void>((resolve) => {
@@ -35,33 +36,12 @@ export const DocsPage: React.FC = () => {
const [uploading, setUploading] = useState(false);
const [uploadFileName, setUploadFileName] = useState('');
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('新能源汽车电池安全要求');
const [searchQuery, setSearchQuery] = useState(INITIAL_SEARCH_QUERY);
const [searchResults, setSearchResults] = useState<RegulationSearchItem[]>([]);
const [searchLoading, setSearchLoading] = useState(false);
const [searchError, setSearchError] = useState('');
useEffect(() => {
void loadDocuments();
}, []);
useEffect(() => {
void runSearch(searchQuery);
}, []);
useEffect(() => {
return () => {
pipelineRunIdRef.current += 1;
};
}, []);
const resetPipeline = (status: PipelineStatus = 'idle') => {
pipelineRunIdRef.current += 1;
setActiveStep(-1);
setCompletedSteps([]);
setPipelineStatus(status);
};
const loadDocuments = async () => {
async function loadDocuments() {
setLoading(true);
try {
const response = await getDocumentList();
@@ -69,10 +49,11 @@ export const DocsPage: React.FC = () => {
id: parseInt(String(doc.id).replace('doc-', ''), 10) || Math.floor(Math.random() * 10000),
name: doc.name,
chunks: doc.chunks,
size: doc.size_text || `${((doc.chunks * 8) / 1024).toFixed(1)}MB`,
status: doc.status === 'indexed' ? 'indexed' : 'parsing',
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) {
@@ -81,7 +62,43 @@ export const DocsPage: React.FC = () => {
} 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<Awaited<ReturnType<typeof uploadDocument>>>) => {
const guardedSetActiveStep = (step: number) => {
@@ -209,22 +226,6 @@ export const DocsPage: React.FC = () => {
} as React.ChangeEvent<HTMLInputElement>);
};
const runSearch = async (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);
}
};
const getStepStyle = (index: number) => {
const isActive = activeStep === index;
const isCompleted = completedSteps.includes(index);
@@ -525,7 +526,7 @@ export const DocsPage: React.FC = () => {
<div>
<div style={{ fontSize: 15, fontWeight: 500 }}>{doc.name}</div>
<div className="mono" style={{ fontSize: 11, color: theme.text3 }}>
{doc.size}
{doc.updatedAt ? new Date(doc.updatedAt).toLocaleString() : doc.size}
{doc.docId ? ` · ${doc.docId}` : ''}
</div>
</div>
@@ -549,10 +550,14 @@ export const DocsPage: React.FC = () => {
padding: '6px 12px',
background: theme.bgHover,
borderRadius: 6,
color: theme.text2,
color: doc.status === 'failed' ? '#d64545' : theme.text2,
}}
>
{doc.status === 'parsing' ? '处理中...' : `${doc.chunks} chunks`}
{doc.status === 'parsing'
? '处理中...'
: doc.status === 'failed'
? '处理失败'
: `${doc.chunks} chunks`}
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
import React, { useEffect, useRef, useState } from 'react';
import { useTheme } from '../../contexts';
import type { ChatMessage, RetrievalData } from '../../types';
import { getQuickQuestions, ragChat } from '../../api/rag';
@@ -16,6 +16,7 @@ const ragQuickQuestionsDefault = [
export const RagChatPage: React.FC = () => {
const { theme } = useTheme();
const nextMessageIdRef = useRef(1);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [retrievals, setRetrievals] = useState<RetrievalData[]>([]);
const [input, setInput] = useState<string>('');
@@ -24,23 +25,32 @@ export const RagChatPage: React.FC = () => {
const [selectedRetrieval, setSelectedRetrieval] = useState<RetrievalData | null>(null);
const [quickQuestions, setQuickQuestions] = useState<string[]>(ragQuickQuestionsDefault);
useEffect(() => {
void loadQuickQuestions();
}, []);
function nextMessageId() {
const currentId = nextMessageIdRef.current;
nextMessageIdRef.current += 1;
return currentId;
}
const loadQuickQuestions = async () => {
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: Date.now(), role: 'user' as const, content: text };
const userMsg = { id: nextMessageId(), role: 'user' as const, content: text };
setMessages((prev) => [...prev, userMsg]);
setInput('');
setLoading(true);
@@ -84,7 +94,7 @@ export const RagChatPage: React.FC = () => {
if (lastMsg?.role === 'assistant') {
return [...prev.slice(0, -1), { ...lastMsg, content: currentResponse }];
}
return [...prev, { id: Date.now() + 1, role: 'assistant' as const, content: currentResponse }];
return [...prev, { id: nextMessageId(), role: 'assistant' as const, content: currentResponse }];
});
} else if (sseData.type === 'done') {
setLoading(false);
@@ -97,7 +107,7 @@ export const RagChatPage: React.FC = () => {
setLoading(false);
setMessages((prev) => [
...prev,
{ id: Date.now() + 1, role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' }
{ id: nextMessageId(), role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' }
]);
},
() => {
@@ -159,7 +169,7 @@ export const RagChatPage: React.FC = () => {
if (lastMsg?.role === 'assistant') {
return [...prev.slice(0, -1), { ...lastMsg, content: currentResponse }];
}
return [...prev, { id: Date.now() + 1, role: 'assistant' as const, content: currentResponse }];
return [...prev, { id: nextMessageId(), role: 'assistant' as const, content: currentResponse }];
});
} else if (sseData.type === 'done') {
setLoading(false);
@@ -170,7 +180,7 @@ export const RagChatPage: React.FC = () => {
setLoading(false);
setMessages((prev) => [
...prev,
{ id: Date.now() + 1, role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' }
{ id: nextMessageId(), role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' }
]);
},
() => {

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
import React, { useEffect, useState } from 'react';
import { useTheme } from '../../contexts';
import { Content } from '../../components/layout/Content';
import { TPattern } from '../../components/common/TPattern';
import { getSystemStats, getSystemConfig, type SystemStats, type SystemConfig } from '../../api/status';
@@ -38,17 +38,16 @@ const StatsCard = ({ label, value, accent = false }: {
export const StatusPage: React.FC = () => {
const { theme, isDark } = useTheme();
const [stats, setStats] = useState<SystemStats>({ docs: 0, chunks: 0, vectors: 0, segments: 0 });
const [stats, setStats] = useState<SystemStats>({
documents_total: 0,
documents_indexed: 0,
documents_failed: 0,
chunks_total: 0,
});
const [config, setConfig] = useState<SystemConfig | null>(null);
const [docs, setDocs] = useState<DocInfo[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
async function loadData() {
try {
const [statsRes, configRes, docsRes] = await Promise.all([
getSystemStats(),
@@ -61,30 +60,31 @@ export const StatusPage: React.FC = () => {
} catch (error) {
console.error('Failed to load status data:', error);
}
setLoading(false);
};
}
// 计算总chunks
const totalChunks = docs.reduce((sum, d) => sum + d.chunks, 0);
useEffect(() => {
const timerId = window.setTimeout(() => {
void loadData();
}, 0);
return () => window.clearTimeout(timerId);
}, []);
return (
<Content>
<TPattern />
{/* System Stats */}
<section style={{ marginBottom: 48 }}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 16,
}}>
<StatsCard label="DOCUMENTS" value={stats.docs} />
<StatsCard label="CHUNKS" value={stats.chunks} />
<StatsCard label="DIMENSIONS" value={config?.embedding.dimension || 1536} />
<StatsCard label="CLAUSES" value={stats.vectors} accent />
<StatsCard label="DOCUMENTS" value={stats.documents_total} />
<StatsCard label="INDEXED" value={stats.documents_indexed} />
<StatsCard label="FAILED" value={stats.documents_failed} />
<StatsCard label="CHUNKS" value={stats.chunks_total} accent />
</div>
</section>
{/* Configuration */}
<section style={{ marginBottom: 48 }}>
<h2 style={{
fontSize: 14,
@@ -94,49 +94,18 @@ export const StatusPage: React.FC = () => {
letterSpacing: '1px',
}}>SYSTEM CONFIGURATION</h2>
{/* ChromaDB Config */}
<div style={{ marginBottom: 20 }}>
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>VECTOR DATABASE</div>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
gap: 12,
}}>
{[
['Vector DB', 'Milvus'],
['Host', config?.milvus.host || 'localhost'],
['Port', String(config?.milvus.port || 19530)],
].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>
{/* LLM Config */}
<div style={{ marginBottom: 20 }}>
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>LLM CONFIGURATION</div>
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>MODELS</div>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 12,
}}>
{[
['LLM Model', config?.llm.model || 'qwen-max'],
['Embedding Model', config?.embedding.model || 'text-embedding-v3'],
['Embedding Dim', String(config?.embedding.dimension || 1536)],
['Temperature', '0.1'],
['LLM Provider', config?.llm_provider || '-'],
['LLM Model', config?.llm_model || '-'],
['Embedding Model', config?.embedding_model || '-'],
['Embedding Dim', String(config?.embedding_dim || 0)],
].map(([k, v]) => (
<div key={k} style={{
display: 'flex',
@@ -155,47 +124,17 @@ export const StatusPage: React.FC = () => {
</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', String(config?.retrieval.vector_top_k || 10)],
['BM25 Top-K', '10'],
['Final Top-K', String(config?.retrieval.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 className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>STORAGE AND PATHS</div>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 12,
}}>
{[
['Chunk Size', '800'],
['Chunk Overlap', '100'],
['Milvus Collection', config?.milvus_collection || '-'],
['Metadata Path', config?.document_metadata_path || '-'],
['Embedding Base URL', config?.embedding_base_url || '-'],
].map(([k, v]) => (
<div key={k} style={{
display: 'flex',
@@ -215,7 +154,6 @@ export const StatusPage: React.FC = () => {
</div>
</section>
{/* Indexed Docs Overview */}
<section>
<h2 style={{
fontSize: 14,
@@ -237,16 +175,20 @@ export const StatusPage: React.FC = () => {
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 14 }}>{d.name}</span>
<span className="mono" style={{ fontSize: 11, color: theme.text3 }}>{(d.chunks * 8 / 1024).toFixed(1)}MB</span>
<span className="mono" style={{ fontSize: 11, color: theme.text3 }}>
{d.updated_at ? new Date(d.updated_at).toLocaleString() : d.status}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span className="mono" style={{ fontSize: 12, color: theme.text2 }}>{d.chunks} chunks</span>
<div style={{
padding: '4px 12px',
background: theme.green,
background: d.status === 'failed' ? '#d64545' : theme.green,
borderRadius: 6,
}}>
<span className="mono" style={{ fontSize: 10, fontWeight: 600, color: '#fff' }}>INDEXED</span>
<span className="mono" style={{ fontSize: 10, fontWeight: 600, color: '#fff' }}>
{d.status.toUpperCase()}
</span>
</div>
</div>
</div>
@@ -254,4 +196,4 @@ export const StatusPage: React.FC = () => {
</section>
</Content>
);
};
};