feat: Implement compliance document analysis and chat functionality
- Added API functions for document analysis, compliance result retrieval, and chat streaming. - Integrated document upload and analysis in CompliancePage with progress simulation. - Enhanced DocsPage to load documents from the API and handle file uploads with parsing and embedding. - Updated RagChatPage to fetch quick questions from the API and handle chat responses with streaming. - Improved StatusPage to display system statistics and configuration from the API. - Configured Vite to proxy API requests to the backend server.
This commit is contained in:
43
src/api/compliance.ts
Normal file
43
src/api/compliance.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { streamSSE, type ComplianceResult, type SSEMessage } from './index';
|
||||||
|
|
||||||
|
// Upload and analyze a design document
|
||||||
|
export async function analyzeDocument(file: File): Promise<{ task_id: string; status: string }> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch('/api/compliance/analyze', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Upload failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get analysis result
|
||||||
|
export async function getComplianceResult(taskId: string): Promise<ComplianceResult | { status: string; message: string }> {
|
||||||
|
const response = await fetch(`/api/compliance/result/${taskId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Get result failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compliance chat with SSE streaming
|
||||||
|
export function complianceChat(
|
||||||
|
segmentId: number,
|
||||||
|
query: string,
|
||||||
|
onMessage: (data: SSEMessage) => void,
|
||||||
|
onError?: (error: Error) => void,
|
||||||
|
onComplete?: () => void
|
||||||
|
): void {
|
||||||
|
streamSSE(`/compliance/chat/${segmentId}`, { query }, onMessage, onError, onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { ComplianceResult, SSEMessage };
|
||||||
41
src/api/docs.ts
Normal file
41
src/api/docs.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { fetchAPI, streamSSE, type DocInfo, type DocListResponse, type DocUploadResponse } from './index';
|
||||||
|
|
||||||
|
// Upload a document
|
||||||
|
export async function uploadDocument(file: File): Promise<DocUploadResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch('/api/docs/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Upload failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of indexed documents
|
||||||
|
export async function getDocumentList(): Promise<DocListResponse> {
|
||||||
|
return fetchAPI<DocListResponse>('/docs/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a document
|
||||||
|
export async function parseDocument(docId: string): Promise<{ doc_id: string; chunks: number; status: string }> {
|
||||||
|
return fetchAPI(`/docs/parse/${docId}`, { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed a document
|
||||||
|
export async function embedDocument(docId: string): Promise<{ doc_id: string; vectors: number; status: string }> {
|
||||||
|
return fetchAPI(`/docs/embed/${docId}`, { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a document
|
||||||
|
export async function deleteDocument(docId: string): Promise<{ success: boolean }> {
|
||||||
|
return fetchAPI(`/docs/delete/${docId}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export types for use in components
|
||||||
|
export type { DocInfo, DocListResponse, DocUploadResponse };
|
||||||
207
src/api/index.ts
Normal file
207
src/api/index.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// API configuration - 使用相对路径,通过 Vite proxy 转发
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// Helper function for fetch requests
|
||||||
|
async function fetchAPI<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE helper for streaming responses
|
||||||
|
function createSSEConnection(endpoint: string, body: unknown): EventSource {
|
||||||
|
// For POST requests with SSE, we need to use fetch with ReadableStream
|
||||||
|
// since EventSource only supports GET requests
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
|
return new EventSource(url); // This won't work for POST, we'll handle it differently
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE streaming helper for POST requests
|
||||||
|
async function streamSSE(
|
||||||
|
endpoint: string,
|
||||||
|
body: unknown,
|
||||||
|
onMessage: (data: unknown) => void,
|
||||||
|
onError?: (error: Error) => void,
|
||||||
|
onComplete?: () => void
|
||||||
|
): Promise<void> {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'text/event-stream',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (onError) {
|
||||||
|
onError(new Error(`HTTP error! status: ${response.status}`));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
if (onError) {
|
||||||
|
onError(new Error('No response body'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process SSE events
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data:')) {
|
||||||
|
const data = line.slice(5).trim();
|
||||||
|
if (data) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
onMessage(parsed);
|
||||||
|
} catch {
|
||||||
|
// Handle non-JSON data
|
||||||
|
onMessage({ type: 'raw', text: data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export interface DocInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
chunks: number;
|
||||||
|
status: string;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocListResponse {
|
||||||
|
docs: DocInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocUploadResponse {
|
||||||
|
doc_id: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickQuestion {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickQuestionsResponse {
|
||||||
|
questions: QuickQuestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RetrievedDoc {
|
||||||
|
id: string;
|
||||||
|
score: number;
|
||||||
|
preview: string;
|
||||||
|
doc_name: string;
|
||||||
|
clause: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SSEMessage {
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
docs?: RetrievedDoc[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Regulation {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
clause: string;
|
||||||
|
score: number;
|
||||||
|
match_keyword: string;
|
||||||
|
category: string;
|
||||||
|
full_content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceSegment {
|
||||||
|
id: number;
|
||||||
|
index: number;
|
||||||
|
intent: string;
|
||||||
|
start_pos: number;
|
||||||
|
end_pos: number;
|
||||||
|
content: string;
|
||||||
|
risk_level: string;
|
||||||
|
regulations: Regulation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskDashboard {
|
||||||
|
score: number;
|
||||||
|
high_risk_count: number;
|
||||||
|
medium_risk_count: number;
|
||||||
|
low_risk_count: number;
|
||||||
|
need_fix_segments: number;
|
||||||
|
status: string;
|
||||||
|
status_label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PriorityAction {
|
||||||
|
regulation: string;
|
||||||
|
issue: string;
|
||||||
|
suggestion: string;
|
||||||
|
severity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceResult {
|
||||||
|
task_id: string;
|
||||||
|
dashboard: RiskDashboard;
|
||||||
|
segments: ComplianceSegment[];
|
||||||
|
priority_actions: PriorityAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStats {
|
||||||
|
docs: number;
|
||||||
|
chunks: number;
|
||||||
|
vectors: number;
|
||||||
|
segments: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemConfig {
|
||||||
|
llm: { model: string };
|
||||||
|
embedding: { model: string; dimension: number };
|
||||||
|
milvus: { host: string; port: number };
|
||||||
|
retrieval: { vector_top_k: number; final_top_k: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchAPI, streamSSE, API_BASE_URL };
|
||||||
20
src/api/rag.ts
Normal file
20
src/api/rag.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { fetchAPI, streamSSE, type QuickQuestionsResponse, type SSEMessage } from './index';
|
||||||
|
|
||||||
|
// Get quick questions
|
||||||
|
export async function getQuickQuestions(): Promise<QuickQuestionsResponse> {
|
||||||
|
return fetchAPI<QuickQuestionsResponse>('/rag/quick-questions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAG chat with SSE streaming
|
||||||
|
export function ragChat(
|
||||||
|
query: string,
|
||||||
|
topK: number = 5,
|
||||||
|
onMessage: (data: SSEMessage) => void,
|
||||||
|
onError?: (error: Error) => void,
|
||||||
|
onComplete?: () => void
|
||||||
|
): void {
|
||||||
|
streamSSE('/rag/chat', { query, top_k: topK }, onMessage, onError, onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { QuickQuestionsResponse, SSEMessage };
|
||||||
19
src/api/status.ts
Normal file
19
src/api/status.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { fetchAPI, type SystemStats, type SystemConfig } from './index';
|
||||||
|
|
||||||
|
// Get system statistics
|
||||||
|
export async function getSystemStats(): Promise<SystemStats> {
|
||||||
|
return fetchAPI<SystemStats>('/status/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get system configuration
|
||||||
|
export async function getSystemConfig(): Promise<SystemConfig> {
|
||||||
|
return fetchAPI<SystemConfig>('/status/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Milvus health status
|
||||||
|
export async function getMilvusHealth(): Promise<{ connected: boolean; collections: string[] }> {
|
||||||
|
return fetchAPI('/status/milvus/health');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { SystemStats, SystemConfig };
|
||||||
@@ -22,7 +22,7 @@ export const Header: React.FC = () => {
|
|||||||
T-Systems
|
T-Systems
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontWeight: 300, fontSize: 16, color: theme.text2 }}>
|
<span style={{ fontWeight: 300, fontSize: 16, color: theme.text2 }}>
|
||||||
Regulation RAG
|
Regulation
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const tabs = [
|
|||||||
{ id: 'docs', label: '文档管理' },
|
{ id: 'docs', label: '文档管理' },
|
||||||
{ id: 'compliance', label: '合规分析' },
|
{ id: 'compliance', label: '合规分析' },
|
||||||
{ id: 'status', label: '系统状态' },
|
{ id: 'status', label: '系统状态' },
|
||||||
{ id: 'rag', label: 'RAG对话' },
|
{ id: 'rag', label: '法规对话' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Tabs: React.FC = () => {
|
export const Tabs: React.FC = () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
mockPriorityActions,
|
mockPriorityActions,
|
||||||
fullDocumentContent,
|
fullDocumentContent,
|
||||||
} from '../../data';
|
} from '../../data';
|
||||||
|
import { analyzeDocument, getComplianceResult, complianceChat } from '../../api/compliance';
|
||||||
import { ChatPanel } from './ChatPanel';
|
import { ChatPanel } from './ChatPanel';
|
||||||
import { TPattern } from '../../components/common/TPattern';
|
import { TPattern } from '../../components/common/TPattern';
|
||||||
|
|
||||||
@@ -111,39 +112,114 @@ export const CompliancePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startAnalysis = () => {
|
const startAnalysis = async () => {
|
||||||
|
if (!uploadedDoc) return;
|
||||||
|
|
||||||
|
|
||||||
setIsAnalyzing(true);
|
setIsAnalyzing(true);
|
||||||
setAnalyzeStep(1);
|
setAnalyzeStep(1);
|
||||||
setAnalyzePercent(0);
|
setAnalyzePercent(0);
|
||||||
|
|
||||||
setTimeout(() => {
|
try {
|
||||||
setAnalyzePercent(30);
|
// Get file from upload input
|
||||||
setAnalyzeAction('正在解析文档结构...');
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
}, 500);
|
const file = fileInput?.files?.[0];
|
||||||
|
console.log("123")
|
||||||
|
|
||||||
setTimeout(() => {
|
// if (!file) {
|
||||||
setAnalyzeStep(2);
|
// // setIsAnalyzing(false);
|
||||||
setAnalyzePercent(50);
|
// // return;
|
||||||
setAnalyzeAction('正在识别语义段落...');
|
// }
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
console.log("456")
|
||||||
setAnalyzePercent(70);
|
// Upload and get task ID
|
||||||
setAnalyzeAction('正在识别第 2/3 个语义段落...');
|
const uploadRes = await analyzeDocument(file);
|
||||||
}, 2500);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
// Simulate progress
|
||||||
setAnalyzeStep(3);
|
setTimeout(() => {
|
||||||
setAnalyzePercent(85);
|
setAnalyzePercent(30);
|
||||||
setAnalyzeAction('正在匹配法规条款...');
|
setAnalyzeAction('正在解析文档结构...');
|
||||||
}, 3500);
|
}, 500);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setAnalyzePercent(100);
|
setAnalyzeStep(2);
|
||||||
setAnalyzeAction('分析完成');
|
setAnalyzePercent(50);
|
||||||
|
setAnalyzeAction('正在识别语义段落...');
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnalyzePercent(70);
|
||||||
|
setAnalyzeAction('正在识别第 2/3 个语义段落...');
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnalyzeStep(3);
|
||||||
|
setAnalyzePercent(85);
|
||||||
|
setAnalyzeAction('正在匹配法规条款...');
|
||||||
|
}, 3500);
|
||||||
|
|
||||||
|
// Get result after analysis completes
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await getComplianceResult(uploadRes.task_id);
|
||||||
|
if ('segments' in result) {
|
||||||
|
// Convert API response to frontend format
|
||||||
|
const apiChunks: ComplianceChunk[] = result.segments.map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
index: s.index,
|
||||||
|
intent: s.intent,
|
||||||
|
startPos: s.start_pos,
|
||||||
|
endPos: s.end_pos,
|
||||||
|
content: s.content,
|
||||||
|
regulations: s.regulations.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
clause: r.clause,
|
||||||
|
score: r.score,
|
||||||
|
matchKeyword: r.match_keyword,
|
||||||
|
category: r.category as 'high' | 'medium' | 'low',
|
||||||
|
fullContent: r.full_content,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Calculate risk levels for each segment
|
||||||
|
const chunksWithRisk = apiChunks.map(chunk => {
|
||||||
|
const regs = chunk.regulations;
|
||||||
|
const highRegs = regs.filter(r => r.category === 'high');
|
||||||
|
const avgHighScore = highRegs.length > 0
|
||||||
|
? highRegs.reduce((sum, r) => sum + r.score, 0) / highRegs.length
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
let riskLevel: 'high' | 'medium' | 'low' = 'low';
|
||||||
|
if (avgHighScore < 0.85 || highRegs.filter(r => r.score < 0.9).length >= 1) {
|
||||||
|
riskLevel = 'high';
|
||||||
|
} else if (avgHighScore < 0.92 || regs.filter(r => r.category === 'medium').length >= 2) {
|
||||||
|
riskLevel = 'medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...chunk, riskLevel } as ComplianceChunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
setChunks(chunksWithRisk);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get compliance result:', error);
|
||||||
|
// Fallback to mock data
|
||||||
|
setChunks(mockComplianceChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnalyzePercent(100);
|
||||||
|
setAnalyzeAction('分析完成');
|
||||||
|
setIsAnalyzing(false);
|
||||||
|
}, 4500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to analyze document:', error);
|
||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
setChunks(mockComplianceChunks);
|
// Fallback to mock data after delay
|
||||||
}, 4500);
|
setTimeout(() => {
|
||||||
|
setChunks(mockComplianceChunks);
|
||||||
|
}, 4500);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openChat = (chunkId: number) => {
|
const openChat = (chunkId: number) => {
|
||||||
@@ -180,27 +256,48 @@ export const CompliancePage: React.FC = () => {
|
|||||||
setChatInput('');
|
setChatInput('');
|
||||||
setChatLoading(true);
|
setChatLoading(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
let currentResponse = '';
|
||||||
let response = '';
|
|
||||||
const intent = chunk.intent;
|
|
||||||
const mockResps = mockAIResponses[intent];
|
|
||||||
|
|
||||||
if (chatInput.includes('合规') || chatInput.includes('符合')) {
|
complianceChat(
|
||||||
response = mockResps?.compliance || '根据相关法规分析,该段落的合规性需进一步评估。';
|
activeChunkId,
|
||||||
} else if (chatInput.includes('解读') || chatInput.includes('什么') || chatInput.includes('如何')) {
|
chatInput,
|
||||||
response = mockResps?.interpretation || '法规要求详细解读如下...';
|
(data: unknown) => {
|
||||||
} else if (chatInput.includes('修改') || chatInput.includes('建议') || chatInput.includes('完善')) {
|
const sseData = data as { type: string; text?: string };
|
||||||
response = mockResps?.suggestion || '建议进行以下修改以提升合规性...';
|
if (sseData.type === 'chunk' && sseData.text) {
|
||||||
} else {
|
currentResponse += sseData.text;
|
||||||
response = `关于您的问题,${chunk.intent}部分涉及以下法规要点:\n\n${chunk.regulations.slice(0, 2).map(r => `• ${r.name} ${r.clause}(相关性 ${Math.round(r.score * 100)}%)`).join('\n')}\n\n您可以进一步询问合规性评估或修改建议。`;
|
setChatMessages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[activeChunkId]: [...(prev[activeChunkId] || []).slice(0, -1), { id: Date.now() + 1, role: 'assistant', content: currentResponse }],
|
||||||
|
}));
|
||||||
|
} else if (sseData.type === 'done') {
|
||||||
|
setChatLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error: Error) => {
|
||||||
|
console.error('Compliance chat error:', error);
|
||||||
|
setChatLoading(false);
|
||||||
|
// Fallback to mock response
|
||||||
|
let response = '';
|
||||||
|
const intent = chunk.intent;
|
||||||
|
const mockResps = mockAIResponses[intent];
|
||||||
|
if (chatInput.includes('合规') || chatInput.includes('符合')) {
|
||||||
|
response = mockResps?.compliance || '根据相关法规分析,该段落的合规性需进一步评估。';
|
||||||
|
} else if (chatInput.includes('解读') || chatInput.includes('什么') || chatInput.includes('如何')) {
|
||||||
|
response = mockResps?.interpretation || '法规要求详细解读如下...';
|
||||||
|
} else if (chatInput.includes('修改') || chatInput.includes('建议') || chatInput.includes('完善')) {
|
||||||
|
response = mockResps?.suggestion || '建议进行以下修改以提升合规性...';
|
||||||
|
} else {
|
||||||
|
response = `关于您的问题,${chunk.intent}部分涉及以下法规要点:\n\n${chunk.regulations.slice(0, 2).map(r => `• ${r.name} ${r.clause}(相关性 ${Math.round(r.score * 100)}%)`).join('\n')}\n\n您可以进一步询问合规性评估或修改建议。`;
|
||||||
|
}
|
||||||
|
setChatMessages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[activeChunkId]: [...(prev[activeChunkId] || []), { id: Date.now() + 1, role: 'assistant', content: response }],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setChatLoading(false);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
setChatMessages(prev => ({
|
|
||||||
...prev,
|
|
||||||
[activeChunkId]: [...(prev[activeChunkId] || []), { id: Date.now() + 1, role: 'assistant', content: response }],
|
|
||||||
}));
|
|
||||||
setChatLoading(false);
|
|
||||||
}, 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dashboard = calculateRiskDashboard(chunks);
|
const dashboard = calculateRiskDashboard(chunks);
|
||||||
@@ -650,6 +747,7 @@ export const CompliancePage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={startAnalysis}
|
onClick={startAnalysis}
|
||||||
className="t-btn"
|
className="t-btn"
|
||||||
|
type="button"
|
||||||
style={{
|
style={{
|
||||||
padding: '20px 48px',
|
padding: '20px 48px',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -658,6 +756,8 @@ export const CompliancePage: React.FC = () => {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
>开始分析</button>
|
>开始分析</button>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef, useEffect } 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 { getDocumentList, uploadDocument, parseDocument, embedDocument, type DocInfo } from '../../api/docs';
|
||||||
import type { Doc } from '../../types';
|
import type { Doc } from '../../types';
|
||||||
|
|
||||||
export const DocsPage: React.FC = () => {
|
export const DocsPage: React.FC = () => {
|
||||||
@@ -10,9 +10,35 @@ export const DocsPage: React.FC = () => {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [activeStep, setActiveStep] = useState<number>(-1);
|
const [activeStep, setActiveStep] = useState<number>(-1);
|
||||||
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
||||||
const [docs, setDocs] = useState<Doc[]>(mockDocs);
|
const [docs, setDocs] = useState<Doc[]>([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [uploadFileName, setUploadFileName] = useState<string>('');
|
const [uploadFileName, setUploadFileName] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Load documents from API on mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadDocuments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadDocuments = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getDocumentList();
|
||||||
|
const apiDocs: Doc[] = response.docs.map(d => ({
|
||||||
|
id: parseInt(d.id.replace('doc-', ''), 10) || Math.floor(Math.random() * 10000),
|
||||||
|
name: d.name,
|
||||||
|
chunks: d.chunks,
|
||||||
|
size: `${(d.chunks * 8 / 1024).toFixed(1)}MB`,
|
||||||
|
status: d.status === 'indexed' ? 'indexed' : 'parsing',
|
||||||
|
}));
|
||||||
|
setDocs(apiDocs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load documents:', error);
|
||||||
|
// Keep empty list on error
|
||||||
|
setDocs([]);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const pipelineSteps = [
|
const pipelineSteps = [
|
||||||
{ name: 'LOAD' },
|
{ name: 'LOAD' },
|
||||||
@@ -22,7 +48,7 @@ export const DocsPage: React.FC = () => {
|
|||||||
{ name: 'STORE' },
|
{ name: 'STORE' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file || uploading) return;
|
if (!file || uploading) return;
|
||||||
|
|
||||||
@@ -31,37 +57,67 @@ export const DocsPage: React.FC = () => {
|
|||||||
setActiveStep(0);
|
setActiveStep(0);
|
||||||
setCompletedSteps([]);
|
setCompletedSteps([]);
|
||||||
|
|
||||||
// Add new doc in "parsing" state
|
try {
|
||||||
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);
|
// Upload file via API
|
||||||
const newDoc: Doc = {
|
const uploadRes = await uploadDocument(file);
|
||||||
id: Date.now(),
|
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);
|
||||||
name: file.name,
|
|
||||||
chunks: 0,
|
const newDoc: Doc = {
|
||||||
size: `${fileSizeMB}MB`,
|
id: parseInt(uploadRes.doc_id.replace('doc-', ''), 10) || Date.now(),
|
||||||
status: 'parsing',
|
name: file.name,
|
||||||
};
|
chunks: 0,
|
||||||
setDocs(prev => [...prev, newDoc]);
|
size: `${fileSizeMB}MB`,
|
||||||
|
status: 'parsing',
|
||||||
|
};
|
||||||
|
setDocs(prev => [...prev, newDoc]);
|
||||||
|
|
||||||
|
// Simulate pipeline steps
|
||||||
|
const stepDuration = 600;
|
||||||
|
pipelineSteps.forEach((_, index) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
setActiveStep(index);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCompletedSteps(prev => [...prev, index]);
|
||||||
|
}, stepDuration - 100);
|
||||||
|
|
||||||
|
// Call parse and embed APIs
|
||||||
|
if (index === 1) {
|
||||||
|
try {
|
||||||
|
const parseRes = await parseDocument(uploadRes.doc_id);
|
||||||
|
setDocs(prev => prev.map(d =>
|
||||||
|
d.id === newDoc.id ? { ...d, chunks: parseRes.chunks } : d
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Parse failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 3) {
|
||||||
|
try {
|
||||||
|
const embedRes = await embedDocument(uploadRes.doc_id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Embed failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simulate each pipeline step
|
|
||||||
const stepDuration = 600;
|
|
||||||
pipelineSteps.forEach((_, index) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setActiveStep(index);
|
|
||||||
setTimeout(() => {
|
|
||||||
setCompletedSteps(prev => [...prev, index]);
|
|
||||||
if (index === pipelineSteps.length - 1) {
|
if (index === pipelineSteps.length - 1) {
|
||||||
// Final step completed
|
|
||||||
setActiveStep(-1);
|
setActiveStep(-1);
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
setUploadFileName('');
|
setUploadFileName('');
|
||||||
// Update doc to indexed state with mock chunks
|
|
||||||
setDocs(prev => prev.map(d =>
|
setDocs(prev => prev.map(d =>
|
||||||
d.id === newDoc.id ? { ...d, chunks: Math.floor(file.size / 8000) + 20, status: 'indexed' } : d
|
d.id === newDoc.id ? { ...d, status: 'indexed' } : d
|
||||||
));
|
));
|
||||||
|
// Refresh list from API
|
||||||
|
loadDocuments();
|
||||||
}
|
}
|
||||||
}, stepDuration - 100);
|
}, index * stepDuration);
|
||||||
}, index * stepDuration);
|
});
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error);
|
||||||
|
setUploading(false);
|
||||||
|
setActiveStep(-1);
|
||||||
|
setUploadFileName('');
|
||||||
|
}
|
||||||
|
|
||||||
// Clear input for next upload
|
// Clear input for next upload
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
@@ -82,51 +138,16 @@ export const DocsPage: React.FC = () => {
|
|||||||
const handleDrop = (event: React.DragEvent) => {
|
const handleDrop = (event: React.DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
const files = event.dataTransfer.files;
|
||||||
const file = event.dataTransfer.files?.[0];
|
if (files.length > 0) {
|
||||||
if (!file || uploading) return;
|
// Simulate file input change
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
// Check file type
|
dataTransfer.items.add(files[0]);
|
||||||
const validTypes = ['.pdf', '.docx', '.txt'];
|
if (fileInputRef.current) {
|
||||||
const fileExt = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
|
fileInputRef.current.files = dataTransfer.files;
|
||||||
if (!validTypes.includes(fileExt)) {
|
handleFileSelect({ target: { files: files } } as React.ChangeEvent<HTMLInputElement>);
|
||||||
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 getStepStyle = (index: number) => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import type { ChatMessage, RetrievalData } from '../../types';
|
import type { ChatMessage, RetrievalData } from '../../types';
|
||||||
import { mockRetrievalData } from '../../data';
|
import { getQuickQuestions } from '../../api/rag';
|
||||||
|
import { ragChat } from '../../api/rag';
|
||||||
|
|
||||||
const ragQuickQuestions = [
|
const ragQuickQuestionsDefault = [
|
||||||
'电动自行车上路需要什么条件?',
|
'电动自行车上路需要什么条件?',
|
||||||
'驾驶证如何申请?',
|
'驾驶证如何申请?',
|
||||||
'超速行驶如何处罚?',
|
'超速行驶如何处罚?',
|
||||||
@@ -14,62 +15,6 @@ const ragQuickQuestions = [
|
|||||||
'高速公路安全距离?',
|
'高速公路安全距离?',
|
||||||
];
|
];
|
||||||
|
|
||||||
const generateRagResponse = (question: string): { text: string; retrievalIds: number[] } => {
|
|
||||||
const keywords: Record<string, { text: string; retrievalIds: number[] }> = {
|
|
||||||
'电动自行车': {
|
|
||||||
text: '根据《道路交通安全法》及相关规范,电动自行车上路需满足以下条件:\n\n1. 符合国家标准 GB17761-2018\n2. 经公安机关交通管理部门登记\n3. 最高设计车速不超过 25km/h\n4. 整车质量不超过 55kg\n5. 具有脚踏骑行能力\n6. 蓄电池标称电压不超过 48V\n\n行驶时还需佩戴安全头盔,不得逆向行驶或在机动车道内行驶。',
|
|
||||||
retrievalIds: [1, 2, 3],
|
|
||||||
},
|
|
||||||
'驾驶证': {
|
|
||||||
text: '驾驶证申请流程如下:\n\n1. 到驾校报名并参加培训\n2. 通过科目一(理论考试)\n3. 通过科目二(场地驾驶技能考试)\n4. 通过科目三(道路驾驶技能考试)\n5. 通过科目四(安全文明驾驶常识考试)\n6. 领取驾驶证\n\n初次申领需到住所地车辆管理所申请注册登记。',
|
|
||||||
retrievalIds: [1, 4],
|
|
||||||
},
|
|
||||||
'超速': {
|
|
||||||
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\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],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(keywords)) {
|
|
||||||
if (question.includes(key)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: '抱歉,暂未找到与您问题直接相关的法规内容。请尝试更具体的问题,或联系交通管理部门获取详细信息。\n\n您可以尝试询问:电动自行车、驾驶证、超速处罚、年检、电池安全、碰撞测试、AEB系统、高速公路规则等话题。',
|
|
||||||
retrievalIds: [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RagChatPage: React.FC = () => {
|
export const RagChatPage: React.FC = () => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
@@ -78,6 +23,22 @@ export const RagChatPage: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [showClearConfirm, setShowClearConfirm] = useState<boolean>(false);
|
const [showClearConfirm, setShowClearConfirm] = useState<boolean>(false);
|
||||||
const [selectedRetrieval, setSelectedRetrieval] = useState<RetrievalData | null>(null);
|
const [selectedRetrieval, setSelectedRetrieval] = useState<RetrievalData | null>(null);
|
||||||
|
const [quickQuestions, setQuickQuestions] = useState<string[]>(ragQuickQuestionsDefault);
|
||||||
|
|
||||||
|
// Load quick questions from API
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuickQuestions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadQuickQuestions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getQuickQuestions();
|
||||||
|
setQuickQuestions(response.questions.map(q => q.question));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load quick questions:', error);
|
||||||
|
// Keep default questions on error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const sendMessage = (text: string) => {
|
const sendMessage = (text: string) => {
|
||||||
if (!text.trim()) return;
|
if (!text.trim()) return;
|
||||||
@@ -86,19 +47,52 @@ export const RagChatPage: React.FC = () => {
|
|||||||
setMessages((prev) => [...prev, userMsg]);
|
setMessages((prev) => [...prev, userMsg]);
|
||||||
setInput('');
|
setInput('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setRetrievals([]);
|
||||||
|
|
||||||
setTimeout(() => {
|
let currentResponse = '';
|
||||||
const response = generateRagResponse(text);
|
|
||||||
const aiMsg = {
|
ragChat(
|
||||||
id: Date.now() + 1,
|
text,
|
||||||
role: 'assistant' as const,
|
5,
|
||||||
content: response.text,
|
(data: unknown) => {
|
||||||
retrievalIds: response.retrievalIds,
|
const sseData = data as { type: string; text?: string; docs?: Array<{ id: string; score: number; preview: string; doc_name: string; clause: string }> };
|
||||||
};
|
|
||||||
setMessages((prev) => [...prev, aiMsg]);
|
if (sseData.type === 'retrieved' && sseData.docs) {
|
||||||
setRetrievals(mockRetrievalData.filter((r) => response.retrievalIds.includes(r.id)));
|
const retrievedDocs: RetrievalData[] = sseData.docs.map(d => ({
|
||||||
setLoading(false);
|
id: parseInt(d.id.replace('chunk-', ''), 10) || 1,
|
||||||
}, 800);
|
file: d.doc_name,
|
||||||
|
clause: d.clause,
|
||||||
|
score: d.score,
|
||||||
|
content: d.preview,
|
||||||
|
}));
|
||||||
|
setRetrievals(retrievedDocs);
|
||||||
|
} else if (sseData.type === 'chunk' && sseData.text) {
|
||||||
|
currentResponse += sseData.text;
|
||||||
|
// Update the last assistant message
|
||||||
|
setMessages((prev) => {
|
||||||
|
const lastMsg = prev[prev.length - 1];
|
||||||
|
if (lastMsg?.role === 'assistant') {
|
||||||
|
return [...prev.slice(0, -1), { ...lastMsg, content: currentResponse }];
|
||||||
|
}
|
||||||
|
// Add new assistant message if none exists
|
||||||
|
return [...prev, { id: Date.now() + 1, 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: Date.now() + 1, role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' }
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearMessages = () => {
|
const clearMessages = () => {
|
||||||
@@ -113,18 +107,51 @@ export const RagChatPage: React.FC = () => {
|
|||||||
if (!lastUserMsg) return;
|
if (!lastUserMsg) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setTimeout(() => {
|
setMessages((prev) => [...prev.slice(0, -1)]);
|
||||||
const response = generateRagResponse(lastUserMsg.content);
|
setRetrievals([]);
|
||||||
const aiMsg = {
|
|
||||||
id: Date.now(),
|
let currentResponse = '';
|
||||||
role: 'assistant' as const,
|
|
||||||
content: response.text,
|
ragChat(
|
||||||
retrievalIds: response.retrievalIds,
|
lastUserMsg.content,
|
||||||
};
|
5,
|
||||||
setMessages((prev) => [...prev.slice(0, -1), aiMsg]);
|
(data: unknown) => {
|
||||||
setRetrievals(mockRetrievalData.filter((r) => response.retrievalIds.includes(r.id)));
|
const sseData = data as { type: string; text?: string; docs?: Array<{ id: string; score: number; preview: string; doc_name: string; clause: string }> };
|
||||||
setLoading(false);
|
|
||||||
}, 800);
|
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,
|
||||||
|
}));
|
||||||
|
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: Date.now() + 1, 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: Date.now() + 1, role: 'assistant' as const, content: '抱歉,连接服务器时出错,请稍后再试。' }
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -279,7 +306,7 @@ export const RagChatPage: React.FC = () => {
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
}}>
|
}}>
|
||||||
{ragQuickQuestions.map(q => (
|
{quickQuestions.map(q => (
|
||||||
<button
|
<button
|
||||||
key={q}
|
key={q}
|
||||||
onClick={() => sendMessage(q)}
|
onClick={() => sendMessage(q)}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } 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 { getSystemStats, getSystemConfig, type SystemStats, type SystemConfig } from '../../api/status';
|
||||||
|
import { getDocumentList, type DocInfo } from '../../api/docs';
|
||||||
|
|
||||||
const StatsCard = ({ label, value, accent = false }: {
|
const StatsCard = ({ label, value, accent = false }: {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -37,9 +38,34 @@ const StatsCard = ({ label, value, accent = false }: {
|
|||||||
|
|
||||||
export const StatusPage: React.FC = () => {
|
export const StatusPage: React.FC = () => {
|
||||||
const { theme, isDark } = useTheme();
|
const { theme, isDark } = useTheme();
|
||||||
|
const [stats, setStats] = useState<SystemStats>({ docs: 0, chunks: 0, vectors: 0, segments: 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);
|
||||||
|
try {
|
||||||
|
const [statsRes, configRes, docsRes] = await Promise.all([
|
||||||
|
getSystemStats(),
|
||||||
|
getSystemConfig(),
|
||||||
|
getDocumentList(),
|
||||||
|
]);
|
||||||
|
setStats(statsRes);
|
||||||
|
setConfig(configRes);
|
||||||
|
setDocs(docsRes.docs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load status data:', error);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
// 计算总chunks
|
// 计算总chunks
|
||||||
const totalChunks = mockDocs.reduce((sum, d) => sum + d.chunks, 0);
|
const totalChunks = docs.reduce((sum, d) => sum + d.chunks, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content>
|
<Content>
|
||||||
@@ -51,10 +77,10 @@ export const StatusPage: React.FC = () => {
|
|||||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
}}>
|
}}>
|
||||||
<StatsCard label="DOCUMENTS" value={mockDocs.length} />
|
<StatsCard label="DOCUMENTS" value={stats.docs} />
|
||||||
<StatsCard label="CHUNKS" value={totalChunks} />
|
<StatsCard label="CHUNKS" value={stats.chunks} />
|
||||||
<StatsCard label="DIMENSIONS" value={1536} />
|
<StatsCard label="DIMENSIONS" value={config?.embedding.dimension || 1536} />
|
||||||
<StatsCard label="CLAUSES" value={totalChunks} accent />
|
<StatsCard label="CLAUSES" value={stats.vectors} accent />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -78,8 +104,8 @@ export const StatusPage: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
{[
|
{[
|
||||||
['Vector DB', 'Milvus'],
|
['Vector DB', 'Milvus'],
|
||||||
['Host', 'localhost'],
|
['Host', config?.milvus.host || 'localhost'],
|
||||||
['Port', '19530'],
|
['Port', String(config?.milvus.port || 19530)],
|
||||||
].map(([k, v]) => (
|
].map(([k, v]) => (
|
||||||
<div key={k} style={{
|
<div key={k} style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -107,9 +133,9 @@ export const StatusPage: React.FC = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}>
|
}}>
|
||||||
{[
|
{[
|
||||||
['LLM Model', 'qwen-max'],
|
['LLM Model', config?.llm.model || 'qwen-max'],
|
||||||
['Embedding Model', 'text-embedding-v3'],
|
['Embedding Model', config?.embedding.model || 'text-embedding-v3'],
|
||||||
['Embedding Dim', '1536'],
|
['Embedding Dim', String(config?.embedding.dimension || 1536)],
|
||||||
['Temperature', '0.1'],
|
['Temperature', '0.1'],
|
||||||
].map(([k, v]) => (
|
].map(([k, v]) => (
|
||||||
<div key={k} style={{
|
<div key={k} style={{
|
||||||
@@ -138,9 +164,9 @@ export const StatusPage: React.FC = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}>
|
}}>
|
||||||
{[
|
{[
|
||||||
['Vector Top-K', '10'],
|
['Vector Top-K', String(config?.retrieval.vector_top_k || 10)],
|
||||||
['BM25 Top-K', '10'],
|
['BM25 Top-K', '10'],
|
||||||
['Final Top-K', '5'],
|
['Final Top-K', String(config?.retrieval.final_top_k || 5)],
|
||||||
].map(([k, v]) => (
|
].map(([k, v]) => (
|
||||||
<div key={k} style={{
|
<div key={k} style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -198,7 +224,7 @@ export const StatusPage: React.FC = () => {
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
letterSpacing: '1px',
|
letterSpacing: '1px',
|
||||||
}}>DOCUMENT INDEX</h2>
|
}}>DOCUMENT INDEX</h2>
|
||||||
{mockDocs.map(d => (
|
{docs.map(d => (
|
||||||
<div key={d.id} style={{
|
<div key={d.id} style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -211,7 +237,7 @@ export const StatusPage: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
<span style={{ fontSize: 14 }}>{d.name}</span>
|
<span style={{ fontSize: 14 }}>{d.name}</span>
|
||||||
<span className="mono" style={{ fontSize: 11, color: theme.text3 }}>{d.size}</span>
|
<span className="mono" style={{ fontSize: 11, color: theme.text3 }}>{(d.chunks * 8 / 1024).toFixed(1)}MB</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
<span className="mono" style={{ fontSize: 12, color: theme.text2 }}>{d.chunks} chunks</span>
|
<span className="mono" style={{ fontSize: 12, color: theme.text2 }}>{d.chunks} chunks</span>
|
||||||
|
|||||||
@@ -4,4 +4,14 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user