import React, { useRef, useState } from 'react'; import { useTheme } from '../../contexts'; import type { UploadedDoc, ComplianceChunk, Regulation, SegmentRisk, RiskDashboardData } from '../../types'; import { mockComplianceChunks, mockAIResponses, mockPriorityActions, fullDocumentContent, } from '../../data'; import { analyzeDocument, getComplianceResult, complianceChat } from '../../api/compliance'; import { ChatPanel } from './ChatPanel'; import { TPattern } from '../../components/common/TPattern'; // Risk calculation function const calculateRiskDashboard = (chunks: ComplianceChunk[]): RiskDashboardData | null => { if (!chunks || chunks.length === 0) return null; const segmentRisks: SegmentRisk[] = chunks.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; const lowScoreHighRegs = highRegs.filter(r => r.score < 0.9).length; let level: 'high' | 'medium' | 'low' = 'low'; let score = Math.round(avgHighScore * 100); if (lowScoreHighRegs >= 1 || avgHighScore < 0.85) { level = 'high'; score = Math.min(score, 72); } else if (avgHighScore < 0.92 || regs.filter(r => r.category === 'medium').length >= 2) { level = 'medium'; score = Math.min(score, 85); } return { chunkId: chunk.id, level, score, highRegsCount: highRegs.length, riskRegs: lowScoreHighRegs, }; }); const totalScore = Math.round( segmentRisks.reduce((sum, s) => sum + s.score, 0) / segmentRisks.length ); const highRiskCount = segmentRisks.filter(s => s.level === 'high').length; const mediumRiskCount = segmentRisks.filter(s => s.level === 'medium').length; const needFixSegments = segmentRisks.filter(s => s.riskRegs > 0).length; let status: 'pass' | 'warning' | 'fail' = 'pass'; let statusLabel = '合规通过'; if (totalScore < 70) { status = 'fail'; statusLabel = '不合规'; } else if (totalScore < 85 || highRiskCount > 0) { status = 'warning'; statusLabel = '需优化'; } return { score: totalScore, highRiskCount, mediumRiskCount, lowRiskCount: segmentRisks.filter(s => s.level === 'low').length, needFixSegments, status, statusLabel, segmentRisks, }; }; // Get regulations by category const getRegsByCategory = (regulations: Regulation[]) => { const high = regulations.filter(r => r.category === 'high'); const medium = regulations.filter(r => r.category === 'medium'); const low = regulations.filter(r => r.category === 'low'); return { high, medium, low }; }; export const CompliancePage: React.FC = () => { const { theme, isDark } = useTheme(); const nextMessageIdRef = useRef(1); // Upload & Analysis States const [uploadedDoc, setUploadedDoc] = useState(null); const [uploadedFile, setUploadedFile] = useState(null); const [isAnalyzing, setIsAnalyzing] = useState(false); const [analyzeStep, setAnalyzeStep] = useState(0); const [analyzePercent, setAnalyzePercent] = useState(0); const [analyzeAction, setAnalyzeAction] = useState(''); const [chunks, setChunks] = useState([]); // Interaction States const [activeChunkId, setActiveChunkId] = useState(null); const [expandedRegulationId, setExpandedRegulationId] = useState(null); const [chatPanelOpen, setChatPanelOpen] = useState(false); const [chatMessages, setChatMessages] = useState>>({}); const [chatInput, setChatInput] = useState(''); const [chatLoading, setChatLoading] = useState(false); const [dashboardExpanded, setDashboardExpanded] = useState(false); const nextMessageId = () => { const currentId = nextMessageIdRef.current; nextMessageIdRef.current += 1; return currentId; }; // Handlers const handleUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setUploadedFile(file); setUploadedDoc({ name: file.name, size: `${(file.size / 1024 / 1024).toFixed(2)}MB`, }); setChunks([]); } }; const startAnalysis = async () => { if (!uploadedDoc || !uploadedFile) return; setIsAnalyzing(true); setAnalyzeStep(1); setAnalyzePercent(0); try { const uploadRes = await analyzeDocument(uploadedFile); // Simulate progress setTimeout(() => { setAnalyzePercent(30); setAnalyzeAction('正在解析文档结构...'); }, 500); setTimeout(() => { setAnalyzeStep(2); 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); setChunks(mockComplianceChunks); } setAnalyzePercent(100); setAnalyzeAction('分析完成'); setIsAnalyzing(false); }, 4500); } catch (error) { console.error('Failed to analyze document:', error); setIsAnalyzing(false); setTimeout(() => { setChunks(mockComplianceChunks); }, 4500); } }; const openChat = (chunkId: number) => { setActiveChunkId(chunkId); setChatPanelOpen(true); if (!chatMessages[chunkId]) { const chunk = chunks.find(c => c.id === chunkId); setChatMessages(prev => ({ ...prev, [chunkId]: [{ id: nextMessageId(), role: 'assistant', content: `您好!我是法规合规分析助手。当前段落涉及 ${chunk?.regulations.length} 条相关法规,您可以询问合规性评估、法规解读或修改建议。`, }] })); } }; const closeChat = () => { setChatPanelOpen(false); }; const sendChatMessage = () => { if (!chatInput.trim() || !activeChunkId) return; const chunk = chunks.find(c => c.id === activeChunkId); if (!chunk) return; const userMsg = { id: nextMessageId(), role: 'user' as const, content: chatInput }; setChatMessages(prev => ({ ...prev, [activeChunkId]: [...(prev[activeChunkId] || []), userMsg], })); setChatInput(''); setChatLoading(true); let currentResponse = ''; complianceChat( activeChunkId, chatInput, (data: unknown) => { const sseData = data as { type: string; text?: string }; if (sseData.type === 'chunk' && sseData.text) { currentResponse += sseData.text; setChatMessages(prev => ({ ...prev, [activeChunkId]: [...(prev[activeChunkId] || []).slice(0, -1), { id: nextMessageId(), 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: nextMessageId(), role: 'assistant', content: response }], })); }, () => { setChatLoading(false); } ); }; const dashboard = calculateRiskDashboard(chunks); const segmentRiskMap = dashboard?.segmentRisks?.reduce((map: Record, s) => { map[s.chunkId] = s; return map; }, {}) || {}; // Render document with segment blocks const renderDocumentWithSegmentBlocks = () => { if (!fullDocumentContent || chunks.length === 0) return null; const sortedChunks = [...chunks].sort((a, b) => a.startPos - b.startPos); const result: React.ReactNode[] = []; let lastPos = 0; sortedChunks.forEach((chunk, idx) => { const isActive = activeChunkId === chunk.id; const segmentRisk = segmentRiskMap[chunk.id]; const riskLevel = segmentRisk?.level || 'low'; const riskColor = riskLevel === 'high' ? '#ff4444' : riskLevel === 'medium' ? theme.orange : theme.green; // Add normal text before this chunk if (chunk.startPos > lastPos) { result.push(
{renderStructuredDocument(fullDocumentContent.slice(lastPos, chunk.startPos))}
); } // Add segment block result.push(
setActiveChunkId(isActive ? null : chunk.id)} style={{ padding: '20px 24px', marginBottom: 20, background: isActive ? 'linear-gradient(135deg, rgba(226,0,116,0.12), rgba(190,0,96,0.08))' : riskLevel === 'high' ? 'linear-gradient(135deg, rgba(255,68,68,0.06), rgba(255,68,68,0.02))' : riskLevel === 'medium' ? 'linear-gradient(135deg, rgba(255,136,0,0.05), rgba(255,136,0,0.02))' : theme.bgHover, borderRadius: 12, border: isActive ? `2px solid ${theme.accent}` : riskLevel === 'high' ? '2px solid rgba(255,68,68,0.4)' : riskLevel === 'medium' ? '2px solid rgba(255,136,0,0.3)' : 'transparent', cursor: 'pointer', position: 'relative', transition: 'all 0.3s ease', boxShadow: isActive ? '0 4px 20px rgba(226,0,116,0.15), inset 0 1px 0 rgba(226,0,116,0.1)' : riskLevel === 'high' ? '0 2px 10px rgba(255,68,68,0.1)' : 'none', }} > {/* Intent label with risk indicator */}
{chunk.index}
{chunk.intent} {/* Risk level badge */} {riskLevel !== 'low' && (
{riskLevel === 'high' ? '高风险' : '中风险'}
)} {chunk.regulations.length} 条法规
{/* Segment content */}
{fullDocumentContent.slice(chunk.startPos, chunk.endPos)}
{/* Regulation distribution mini bar */}
{(() => { const regs = getRegsByCategory(chunk.regulations); return ( <> {regs.high.length > 0 && (
{regs.high.length}
)} {regs.medium.length > 0 && (
{regs.medium.length}
)} {regs.low.length > 0 && (
{regs.low.length}
)} ); })()}
); lastPos = chunk.endPos; }); // Add remaining text after all chunks if (lastPos < fullDocumentContent.length) { result.push(
{renderStructuredDocument(fullDocumentContent.slice(lastPos))}
); } return result; }; const quickQuestions = [ '这个设计是否合规?', '需要修改哪些内容?', '法规的具体要求是什么?', ]; // Render structured document — parses section headers and formats them const renderStructuredDocument = (content: string) => { if (!content) return null; const lines = content.split('\n'); const elements: React.ReactNode[] = []; let currentParagraph: string[] = []; lines.forEach((line) => { const sectionMatch = line.match(/^([一二三四五六七八九十]+[、..]|第[一二三四五六七八九十]+[章节部篇]|[0-9]+[、..])/); if (sectionMatch || (line.trim() && currentParagraph.length > 0 && line.trim().length < 20 && !line.trim().endsWith('。'))) { if (currentParagraph.length > 0) { elements.push(

{currentParagraph.join('')}

); currentParagraph = []; } elements.push(

{line.trim()}

); } else if (line.trim()) { currentParagraph.push(line.trim()); } else { if (currentParagraph.length > 0) { elements.push(

{currentParagraph.join('')}

); currentParagraph = []; } } }); if (currentParagraph.length > 0) { elements.push(

{currentParagraph.join('')}

); } return elements; }; return (
{/* Main Content Area */}
{/* Upload Section */} {!uploadedDoc && (

UPLOAD DESIGN DOCUMENT

拖拽文件或点击上传
PDF · DOCX · TXT · MAX 50MB
)} {/* Document Preview Section */} {uploadedDoc && !isAnalyzing && chunks.length === 0 && (

DOCUMENT PREVIEW

{uploadedDoc.name} {uploadedDoc.size}
{uploadedDoc.name} {uploadedDoc.size}
{renderStructuredDocument(fullDocumentContent)}
)} {/* Analysis Progress Section */} {isAnalyzing && (

ANALYZING...

{/* Steps */}
{[ { name: '文档解析', desc: '提取文档内容' }, { name: 'AI语义分段', desc: '识别设计意图' }, { name: '法规匹配标注', desc: '关联法规条款' }, ].map((step, i) => (
i + 1 ? theme.green : (analyzeStep === i + 1 ? theme.gradientAccent : theme.bgHover), display: 'flex', alignItems: 'center', justifyContent: 'center', }}> {analyzeStep > i + 1 ? ( ) : analyzeStep === i + 1 ? (
) : ( {i + 1} )}
{step.name}
{step.desc}
{analyzeStep === i + 1 && ( 进行中 )}
))}
{/* Progress Bar */}
{analyzeAction}
)} {/* Analysis Results - Dashboard + Split Layout */} {chunks.length > 0 && dashboard && ( <> {/* Risk Dashboard Section — Collapsible */}
{/* Collapsed: Compact Summary */} {!dashboardExpanded && (
setDashboardExpanded(true)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', }} >
合规风险仪表盘
{dashboard.statusLabel}
{dashboard.score}
{dashboard.highRiskCount > 0 && (
{dashboard.highRiskCount} 高风险
)} {dashboard.needFixSegments > 0 && (
{dashboard.needFixSegments} 待修改
)}
展开详情
)} {/* Expanded: Full Dashboard */} {dashboardExpanded && (
{/* Dashboard Header with Collapse */}
合规风险仪表盘
setDashboardExpanded(false)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 14px', background: theme.bgHover, borderRadius: 6, border: `1px solid ${theme.border}`, cursor: 'pointer', }} > 收起
{/* Metrics Grid */}
{/* Compliance Score */}
合规评分
{dashboard.score}
{/* High Risk Items */}
0 ? 'linear-gradient(135deg, rgba(255,68,68,0.12), rgba(255,68,68,0.04))' : theme.bgElevated, borderRadius: 10, border: `1px solid ${dashboard.highRiskCount > 0 ? '#ff4444' : theme.border}`, textAlign: 'center', }}>
高风险项
0 ? '#ff4444' : theme.green, marginBottom: 4, }}> {dashboard.highRiskCount}
0 ? '#ff6666' : theme.text3, }}> {dashboard.highRiskCount > 0 ? '需立即处理' : '无高风险'}
{/* Need Fix Segments */}
0 ? 'linear-gradient(135deg, rgba(255,136,0,0.12), rgba(255,136,0,0.04))' : theme.bgElevated, borderRadius: 10, border: `1px solid ${dashboard.needFixSegments > 0 ? theme.orange : theme.border}`, textAlign: 'center', }}>
待修改段落
0 ? theme.orange : theme.green, marginBottom: 4, }}> {dashboard.needFixSegments}
0 ? '#ff9944' : theme.text3, }}> {dashboard.needFixSegments > 0 ? '需补充材料' : '无需修改'}
{/* Overall Status */}
整体状态
{dashboard.status === 'pass' ? ( ) : dashboard.status === 'warning' ? ( ) : ( )} {dashboard.statusLabel}
{/* Priority Actions */}
优先行动建议 {mockPriorityActions.length} 条
{mockPriorityActions.map((action, idx) => (
setActiveChunkId(action.chunkId)} style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 16px', marginBottom: idx < mockPriorityActions.length - 1 ? 8 : 0, background: theme.bgHover, borderRadius: 8, border: `1px solid ${theme.border}`, cursor: 'pointer', transition: 'all 0.2s ease', }} >
{idx + 1}
{action.regulation} {action.issue}
{action.suggestion}
))}
)}
{/* Split Layout: Document Left, Regulations Right */}
{/* Left: Full Document with Marginalia Markers */}
{/* Marginalia Column - Segment Markers */}
{chunks.map((chunk, idx) => { const isActive = activeChunkId === chunk.id; const segmentRisk = segmentRiskMap[chunk.id]; const riskLevel = segmentRisk?.level || 'low'; const riskColor = riskLevel === 'high' ? '#ff4444' : riskLevel === 'medium' ? theme.orange : theme.green; const topPos = 60 + idx * 280; return (
setActiveChunkId(chunk.id)} style={{ position: 'absolute', top: topPos, left: 12, width: 24, height: 24, borderRadius: '50%', background: isActive ? theme.gradientAccent : theme.bgHover, border: isActive ? 'none' : `2px solid ${riskColor}`, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', boxShadow: isActive ? '0 0 12px rgba(226,0,116,0.5), 0 0 24px rgba(226,0,116,0.2)' : riskLevel === 'high' ? '0 0 8px rgba(255,68,68,0.3)' : riskLevel === 'medium' ? '0 0 6px rgba(255,136,0,0.2)' : '0 0 4px rgba(0,212,170,0.15)', transition: 'all 0.3s ease', animation: isActive ? 'pulse-glow 2s infinite' : 'none', zIndex: 10, }} > {chunk.index}
); })}
{/* Document Content Column */}

原文文档

{/* Render document with segment blocks — document-style layout */}
{renderDocumentWithSegmentBlocks()}
{/* Right: Regulations Panel */}
{/* Header */}
法规标注
{activeChunkId && (
#{chunks.find(c => c.id === activeChunkId)?.index}
{chunks.find(c => c.id === activeChunkId)?.intent}
)}
{/* Content */}
{activeChunkId === null ? ( /* Default: All segments with regulation counts */
{chunks.map(chunk => { const regs = getRegsByCategory(chunk.regulations); const totalRegs = chunk.regulations.length; return (
setActiveChunkId(chunk.id)} style={{ padding: '16px 20px', background: theme.bgHover, borderRadius: 10, border: `1px solid ${theme.border}`, cursor: 'pointer', transition: 'all 0.25s ease', }} >
{chunk.index}
{chunk.intent}
{chunk.content.substring(0, 50)}...
{totalRegs}
条法规
{/* Mini regulation distribution bar */}
{regs.high.length > 0 && (
)} {regs.medium.length > 0 && (
)} {regs.low.length > 0 && (
)}
); })}
) : ( /* Active: Detailed regulations with expand/collapse */
{/* Active chunk brief */}
{chunks.find(c => c.id === activeChunkId)?.content.substring(0, 120)}...
{/* Regulations by category */} {(() => { const activeChunk = chunks.find(c => c.id === activeChunkId); if (!activeChunk) return null; const regs = getRegsByCategory(activeChunk.regulations); return (
{/* High Relevance */} {regs.high.length > 0 && (
高度相关 {regs.high.length}
{regs.high.map((reg) => { const isExpanded = expandedRegulationId === reg.id; return (
setExpandedRegulationId(isExpanded ? null : reg.id)} style={{ padding: isExpanded ? '18px 20px' : '12px 16px', background: isExpanded ? 'linear-gradient(135deg, rgba(0,212,170,0.08), rgba(0,212,170,0.02))' : theme.bgHover, borderRadius: 8, marginBottom: 8, border: `1px solid ${isExpanded ? theme.green : theme.border}`, cursor: 'pointer', transition: 'all 0.25s ease', }} >
{reg.name} {reg.clause}
{Math.round(reg.score * 100)}%
{isExpanded && (
匹配关键词 {reg.matchKeyword}
{reg.fullContent}
)}
); })}
)} {/* Medium Relevance */} {regs.medium.length > 0 && (
中度相关 {regs.medium.length}
{regs.medium.map((reg) => { const isExpanded = expandedRegulationId === reg.id; return (
setExpandedRegulationId(isExpanded ? null : reg.id)} style={{ padding: isExpanded ? '16px 18px' : '12px 16px', background: isExpanded ? 'linear-gradient(135deg, rgba(255,136,0,0.08), rgba(255,136,0,0.02))' : theme.bgHover, borderRadius: 8, marginBottom: 6, border: `1px solid ${isExpanded ? theme.orange : theme.border}`, cursor: 'pointer', transition: 'all 0.25s ease', }} >
{reg.name} {reg.clause}
{Math.round(reg.score * 100)}%
{isExpanded && (
匹配关键词 {reg.matchKeyword}
{reg.fullContent}
)}
); })}
)} {/* Low Relevance */} {regs.low.length > 0 && (
低度相关 {regs.low.length}
{regs.low.map(reg => { const isExpanded = expandedRegulationId === reg.id; return (
setExpandedRegulationId(isExpanded ? null : reg.id)} style={{ padding: isExpanded ? '10px 14px' : '6px 12px', background: isExpanded ? theme.bgElevated : theme.bgHover, borderRadius: 6, fontSize: 12, color: theme.text2, border: `1px solid ${isExpanded ? theme.borderLight : theme.border}`, cursor: 'pointer', transition: 'all 0.2s ease', }} >
{reg.name} {isExpanded && ( )}
{isExpanded && (
{reg.clause}
{reg.matchKeyword}
{reg.fullContent.substring(0, 60)}...
)}
); })}
)} {/* Chat Button */}
); })()}
)}
)}
{/* Chat Panel */} {chatPanelOpen && activeChunkId && ( )}
); };