Files
AIRegulation-Demo-Test/src/pages/Compliance/CompliancePage.tsx
Yuemin.Mao 1bb7151abe 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.
2026-05-11 11:32:02 +08:00

1916 lines
82 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
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();
// Upload & Analysis States
const [uploadedDoc, setUploadedDoc] = useState<UploadedDoc | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
const [analyzeStep, setAnalyzeStep] = useState<number>(0);
const [analyzePercent, setAnalyzePercent] = useState<number>(0);
const [analyzeAction, setAnalyzeAction] = useState<string>('');
const [chunks, setChunks] = useState<ComplianceChunk[]>([]);
// Interaction States
const [activeChunkId, setActiveChunkId] = useState<number | null>(null);
const [expandedRegulationId, setExpandedRegulationId] = useState<number | null>(null);
const [chatPanelOpen, setChatPanelOpen] = useState<boolean>(false);
const [chatMessages, setChatMessages] = useState<Record<number, Array<{ id: number; role: 'user' | 'assistant'; content: string }>>>({});
const [chatInput, setChatInput] = useState<string>('');
const [chatLoading, setChatLoading] = useState<boolean>(false);
const [dashboardExpanded, setDashboardExpanded] = useState<boolean>(false);
// Handlers
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploadedDoc({
name: file.name,
size: `${(file.size / 1024 / 1024).toFixed(2)}MB`,
});
setChunks([]);
}
};
const startAnalysis = async () => {
if (!uploadedDoc) 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);
// 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);
// Fallback to mock data
setChunks(mockComplianceChunks);
}
setAnalyzePercent(100);
setAnalyzeAction('分析完成');
setIsAnalyzing(false);
}, 4500);
} catch (error) {
console.error('Failed to analyze document:', error);
setIsAnalyzing(false);
// Fallback to mock data after delay
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: Date.now(),
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: Date.now(), 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: 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);
}
);
};
const dashboard = calculateRiskDashboard(chunks);
const segmentRiskMap = dashboard?.segmentRisks?.reduce((map: Record<number, SegmentRisk>, 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(
<div key={`text-${idx}`} style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
marginBottom: 16,
}}>
{renderStructuredDocument(fullDocumentContent.slice(lastPos, chunk.startPos))}
</div>
);
}
// Add segment block
result.push(
<div
key={`chunk-${chunk.id}`}
onClick={() => 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 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 10,
marginBottom: 12,
paddingLeft: 8,
}}>
<div style={{
width: 20,
height: 20,
borderRadius: '50%',
background: isActive ? theme.gradientAccent : theme.bgElevated,
border: isActive ? 'none' : `2px solid ${riskColor}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<span className="mono" style={{
fontSize: 10,
fontWeight: 600,
color: isActive ? '#fff' : riskColor,
}}>{chunk.index}</span>
</div>
<span style={{
fontSize: 13,
fontWeight: 500,
color: isActive ? theme.accent : theme.text,
letterSpacing: '0.02em',
}}>{chunk.intent}</span>
{/* Risk level badge */}
{riskLevel !== 'low' && (
<div style={{
padding: '3px 8px',
background: riskLevel === 'high'
? 'linear-gradient(135deg, rgba(255,68,68,0.2), rgba(255,68,68,0.1))'
: 'linear-gradient(135deg, rgba(255,136,0,0.2), rgba(255,136,0,0.1))',
borderRadius: 4,
display: 'flex',
alignItems: 'center',
gap: 4,
}}>
<div style={{
width: 4,
height: 4,
borderRadius: '50%',
background: riskColor,
}} />
<span style={{
fontSize: 10,
fontWeight: 500,
color: riskColor,
}}>{riskLevel === 'high' ? '高风险' : '中风险'}</span>
</div>
)}
<span className="mono" style={{
fontSize: 11,
color: theme.text3,
marginLeft: 'auto',
padding: '3px 8px',
background: theme.bgElevated,
borderRadius: 4,
}}>
{chunk.regulations.length}
</span>
</div>
{/* Segment content */}
<div style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
paddingLeft: 8,
textIndent: '2em',
textAlign: 'justify',
}}>
{fullDocumentContent.slice(chunk.startPos, chunk.endPos)}
</div>
{/* Regulation distribution mini bar */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginTop: 14,
paddingLeft: 8,
}}>
{(() => {
const regs = getRegsByCategory(chunk.regulations);
return (
<>
{regs.high.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: theme.green }} />
<span className="mono" style={{ fontSize: 11, color: theme.green }}>{regs.high.length}</span>
</div>
)}
{regs.medium.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: theme.orange }} />
<span className="mono" style={{ fontSize: 11, color: theme.orange }}>{regs.medium.length}</span>
</div>
)}
{regs.low.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: theme.text3 }} />
<span className="mono" style={{ fontSize: 11, color: theme.text3 }}>{regs.low.length}</span>
</div>
)}
</>
);
})()}
</div>
</div>
);
lastPos = chunk.endPos;
});
// Add remaining text after all chunks
if (lastPos < fullDocumentContent.length) {
result.push(
<div key="text-end" style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
}}>
{renderStructuredDocument(fullDocumentContent.slice(lastPos))}
</div>
);
}
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(/^([一二三四五六七八九十]+[、..]|第[一二三四五六七八九十]+[章节部篇]|[-]+[、..])/);
if (sectionMatch || (line.trim() && currentParagraph.length > 0 && line.trim().length < 20 && !line.trim().endsWith('。'))) {
if (currentParagraph.length > 0) {
elements.push(
<p key={`p-${elements.length}`} style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
marginBottom: 16,
textIndent: '2em',
textAlign: 'justify',
}}>{currentParagraph.join('')}</p>
);
currentParagraph = [];
}
elements.push(
<h2 key={`h-${elements.length}`} style={{
fontSize: 16,
fontWeight: 600,
color: theme.text,
marginTop: 28,
marginBottom: 12,
paddingBottom: 8,
borderBottom: `1px solid ${theme.border}`,
letterSpacing: '0.05em',
}}>{line.trim()}</h2>
);
} else if (line.trim()) {
currentParagraph.push(line.trim());
} else {
if (currentParagraph.length > 0) {
elements.push(
<p key={`p-${elements.length}`} style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
marginBottom: 16,
textIndent: '2em',
textAlign: 'justify',
}}>{currentParagraph.join('')}</p>
);
currentParagraph = [];
}
}
});
if (currentParagraph.length > 0) {
elements.push(
<p key={`p-${elements.length}`} style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
marginBottom: 16,
textIndent: '2em',
textAlign: 'justify',
}}>{currentParagraph.join('')}</p>
);
}
return elements;
};
return (
<div style={{
flex: 1,
display: 'flex',
height: '100%',
minHeight: 'calc(100vh - 128px)',
position: 'relative',
}}>
{/* Main Content Area */}
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
padding: '32px 48px',
overflowY: 'auto',
marginRight: chatPanelOpen ? '420px' : '0',
transition: 'margin-right 0.3s ease',
}}>
<TPattern />
{/* Upload Section */}
{!uploadedDoc && (
<section style={{ marginBottom: 40 }}>
<h2 style={{
fontSize: 14,
fontWeight: 600,
color: theme.accent,
marginBottom: 20,
letterSpacing: '1px',
}}>UPLOAD DESIGN DOCUMENT</h2>
<div style={{
border: `2px solid ${theme.border}`,
borderRadius: 16,
padding: 64,
textAlign: 'center',
background: theme.bgCard,
cursor: 'pointer',
position: 'relative',
boxShadow: !isDark ? '0 4px 16px rgba(226,0,116,0.08)' : 'none',
}}>
<input
type="file"
accept=".pdf,.docx,.doc,.txt"
onChange={handleUpload}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: 0,
cursor: 'pointer',
}}
/>
<div style={{
width: 80,
height: 80,
borderRadius: 20,
background: theme.bgHover,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 20px',
}}>
<svg width="36" height="36" viewBox="0 0 24 24" fill="none">
<path d="M12 4L12 16M12 4L7 9M12 4L17 9" stroke={theme.accent} strokeWidth="2" strokeLinecap="round"/>
<path d="M4 18H20" stroke={theme.accent} strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8, color: theme.text }}></div>
<div className="mono" style={{ fontSize: 12, color: theme.text3 }}>PDF · DOCX · TXT · MAX 50MB</div>
</div>
</section>
)}
{/* Document Preview Section */}
{uploadedDoc && !isAnalyzing && chunks.length === 0 && (
<section style={{ marginBottom: 40 }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 20,
}}>
<h2 style={{
fontSize: 14,
fontWeight: 600,
color: theme.accent,
letterSpacing: '1px',
}}>DOCUMENT PREVIEW</h2>
<button
onClick={() => { setUploadedDoc(null); }}
style={{
padding: '8px 16px',
fontSize: 13,
background: theme.bgCard,
border: `1px solid ${theme.border}`,
borderRadius: 8,
color: theme.text2,
cursor: 'pointer',
}}
></button>
</div>
<div style={{
padding: 20,
background: theme.bgCard,
borderRadius: 12,
border: `1px solid ${theme.border}`,
marginBottom: 24,
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M14 2H6C5 2 4 3 4 4V20C4 21 5 22 6 22H18C19 22 20 21 20 20V8L14 2Z" stroke={theme.accent} strokeWidth="1.5"/>
<path d="M14 2V8H20" stroke={theme.accent} strokeWidth="1.5"/>
</svg>
<span style={{ fontSize: 15, fontWeight: 500, color: theme.text }}>{uploadedDoc.name}</span>
<span className="mono" style={{ fontSize: 12, color: theme.text3 }}>{uploadedDoc.size}</span>
</div>
</div>
<div style={{
padding: 0,
background: theme.bgCard,
borderRadius: 12,
border: `1px solid ${theme.border}`,
marginBottom: 24,
maxHeight: 'none',
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 24px',
borderBottom: `1px solid ${theme.border}`,
background: theme.bgElevated,
borderRadius: '12px 12px 0 0',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M14 2H6C5 2 4 3 4 4V20C4 21 5 22 6 22H18C19 22 20 21 20 20V8L14 2Z" stroke={theme.accent} strokeWidth="1.5"/>
<path d="M14 2V8H20" stroke={theme.accent} strokeWidth="1.5"/>
</svg>
<span style={{ fontSize: 14, fontWeight: 600, color: theme.text }}>{uploadedDoc.name}</span>
<span className="mono" style={{ fontSize: 11, color: theme.text3 }}>{uploadedDoc.size}</span>
</div>
</div>
<div style={{
padding: '24px 32px',
overflowY: 'auto',
maxHeight: 'calc(100vh - 500px)',
minHeight: 200,
}}>
<div style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
fontFamily: "'TeleNeo', 'Segoe UI', system-ui, sans-serif",
textAlign: 'justify',
}}>{renderStructuredDocument(fullDocumentContent)}</div>
</div>
</div>
<button
onClick={startAnalysis}
className="t-btn"
type="button"
style={{
padding: '20px 48px',
fontSize: 16,
fontWeight: 600,
color: '#fff',
border: 'none',
borderRadius: 12,
cursor: 'pointer',
pointerEvents: 'auto',
zIndex: 10,
}}
></button>
</section>
)}
{/* Analysis Progress Section */}
{isAnalyzing && (
<section style={{ marginBottom: 40 }}>
<h2 style={{
fontSize: 14,
fontWeight: 600,
color: theme.accent,
marginBottom: 20,
letterSpacing: '1px',
}}>ANALYZING...</h2>
<div style={{
padding: 32,
background: theme.bgCard,
borderRadius: 12,
border: `1px solid ${theme.border}`,
boxShadow: !isDark ? '0 4px 16px rgba(226,0,116,0.08)' : 'none',
}}>
{/* Steps */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 24 }}>
{[
{ name: '文档解析', desc: '提取文档内容' },
{ name: 'AI语义分段', desc: '识别设计意图' },
{ name: '法规匹配标注', desc: '关联法规条款' },
].map((step, i) => (
<div key={i} style={{
display: 'flex',
alignItems: 'center',
gap: 16,
}}>
<div style={{
width: 36,
height: 36,
borderRadius: 8,
background: analyzeStep > i + 1 ? theme.green : (analyzeStep === i + 1 ? theme.gradientAccent : theme.bgHover),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{analyzeStep > i + 1 ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M5 12L10 17L20 7" stroke="#fff" strokeWidth="2" strokeLinecap="round"/>
</svg>
) : analyzeStep === i + 1 ? (
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#fff', animation: 'pulse 1s infinite' }} />
) : (
<span className="mono" style={{ fontSize: 12, color: theme.text3 }}>{i + 1}</span>
)}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 500, color: theme.text }}>{step.name}</div>
<div style={{ fontSize: 12, color: theme.text3 }}>{step.desc}</div>
</div>
{analyzeStep === i + 1 && (
<span className="mono" style={{ fontSize: 12, color: theme.accent }}></span>
)}
</div>
))}
</div>
{/* Progress Bar */}
<div style={{ marginBottom: 16 }}>
<div style={{
height: 8,
borderRadius: 4,
background: theme.bgHover,
overflow: 'hidden',
}}>
<div style={{
height: '100%',
width: `${analyzePercent}%`,
background: theme.gradientAccent,
borderRadius: 4,
transition: 'width 0.3s ease',
}} />
</div>
</div>
<div className="mono" style={{ fontSize: 12, color: theme.text2 }}>{analyzeAction}</div>
</div>
</section>
)}
{/* Analysis Results - Dashboard + Split Layout */}
{chunks.length > 0 && dashboard && (
<>
{/* Risk Dashboard Section — Collapsible */}
<section style={{
marginBottom: 32,
padding: dashboardExpanded ? '24px 32px' : '16px 24px',
background: theme.bgCard,
borderRadius: 12,
border: `1px solid ${theme.border}`,
boxShadow: !isDark ? '0 4px 16px rgba(226,0,116,0.08)' : 'none',
transition: 'padding 0.4s ease',
}}>
{/* Collapsed: Compact Summary */}
{!dashboardExpanded && (
<div
onClick={() => setDashboardExpanded(true)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke={theme.accent} strokeWidth="1.5"/>
<path d="M12 6V12L16 14" stroke={theme.accent} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: '0.05em', color: theme.text }}></span>
<div style={{
padding: '4px 12px',
background: dashboard.status === 'pass'
? 'linear-gradient(135deg, rgba(0,212,170,0.2), rgba(0,212,170,0.1))'
: dashboard.status === 'warning'
? 'linear-gradient(135deg, rgba(255,136,0,0.2), rgba(255,136,0,0.1))'
: 'linear-gradient(135deg, rgba(255,68,68,0.2), rgba(255,68,68,0.1))',
borderRadius: 6,
display: 'flex',
alignItems: 'center',
gap: 6,
}}>
<div style={{
width: 6,
height: 6,
borderRadius: '50%',
background: dashboard.status === 'pass' ? theme.green : dashboard.status === 'warning' ? theme.orange : '#ff4444',
}} />
<span style={{
fontSize: 12,
fontWeight: 500,
color: dashboard.status === 'pass' ? theme.green : dashboard.status === 'warning' ? theme.orange : '#ff4444',
}}>{dashboard.statusLabel}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="mono" style={{
fontSize: 24,
fontWeight: 700,
color: dashboard.status === 'pass' ? theme.green : dashboard.status === 'warning' ? theme.orange : '#ff4444',
}}>{dashboard.score}</span>
<span style={{ fontSize: 12, color: theme.text3 }}></span>
</div>
{dashboard.highRiskCount > 0 && (
<div style={{
padding: '3px 10px',
background: 'rgba(255,68,68,0.15)',
borderRadius: 4,
display: 'flex',
alignItems: 'center',
gap: 4,
}}>
<div style={{ width: 4, height: 4, borderRadius: '50%', background: '#ff4444' }} />
<span style={{ fontSize: 11, color: '#ff4444', fontWeight: 500 }}>{dashboard.highRiskCount} </span>
</div>
)}
{dashboard.needFixSegments > 0 && (
<div style={{
padding: '3px 10px',
background: 'rgba(255,136,0,0.12)',
borderRadius: 4,
display: 'flex',
alignItems: 'center',
gap: 4,
}}>
<div style={{ width: 4, height: 4, borderRadius: '50%', background: theme.orange }} />
<span style={{ fontSize: 11, color: theme.orange, fontWeight: 500 }}>{dashboard.needFixSegments} </span>
</div>
)}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 11, color: theme.text3 }}></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ transition: 'transform 0.2s ease' }}>
<path d="M6 9L12 15L18 9" stroke={theme.text3} strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
</div>
)}
{/* Expanded: Full Dashboard */}
{dashboardExpanded && (
<div className="animate-slide-up">
{/* Dashboard Header with Collapse */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 24,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke={theme.accent} strokeWidth="1.5"/>
<path d="M12 6V12L16 14" stroke={theme.accent} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: '0.05em', color: theme.text }}></span>
</div>
<div
onClick={() => setDashboardExpanded(false)}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '6px 14px',
background: theme.bgHover,
borderRadius: 6,
border: `1px solid ${theme.border}`,
cursor: 'pointer',
}}
>
<span style={{ fontSize: 11, color: theme.text3 }}></span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ transform: 'rotate(180deg)' }}>
<path d="M6 9L12 15L18 9" stroke={theme.text3} strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
</div>
{/* Metrics Grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 16,
marginBottom: 24,
}}>
{/* Compliance Score */}
<div style={{
padding: '20px 24px',
background: theme.bgElevated,
borderRadius: 10,
border: `1px solid ${theme.border}`,
textAlign: 'center',
}}>
<div style={{
fontSize: 11,
color: theme.text3,
marginBottom: 8,
letterSpacing: '0.03em',
}}></div>
<div style={{
fontSize: 36,
fontWeight: 700,
color: dashboard.status === 'pass' ? theme.green
: dashboard.status === 'warning' ? theme.orange
: '#ff4444',
marginBottom: 8,
}}>
{dashboard.score}
</div>
<div style={{
height: 6,
borderRadius: 3,
background: theme.bgHover,
overflow: 'hidden',
}}>
<div style={{
height: '100%',
width: `${dashboard.score}%`,
background: dashboard.status === 'pass'
? `linear-gradient(90deg, ${theme.green}, #00ff88)`
: dashboard.status === 'warning'
? `linear-gradient(90deg, ${theme.orange}, #ffaa00)`
: '#ff4444',
borderRadius: 3,
}} />
</div>
</div>
{/* High Risk Items */}
<div style={{
padding: '20px 24px',
background: dashboard.highRiskCount > 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',
}}>
<div style={{
fontSize: 11,
color: theme.text3,
marginBottom: 8,
}}></div>
<div style={{
fontSize: 36,
fontWeight: 700,
color: dashboard.highRiskCount > 0 ? '#ff4444' : theme.green,
marginBottom: 4,
}}>
{dashboard.highRiskCount}
</div>
<div style={{
fontSize: 12,
color: dashboard.highRiskCount > 0 ? '#ff6666' : theme.text3,
}}>
{dashboard.highRiskCount > 0 ? '需立即处理' : '无高风险'}
</div>
</div>
{/* Need Fix Segments */}
<div style={{
padding: '20px 24px',
background: dashboard.needFixSegments > 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',
}}>
<div style={{
fontSize: 11,
color: theme.text3,
marginBottom: 8,
}}></div>
<div style={{
fontSize: 36,
fontWeight: 700,
color: dashboard.needFixSegments > 0 ? theme.orange : theme.green,
marginBottom: 4,
}}>
{dashboard.needFixSegments}
</div>
<div style={{
fontSize: 12,
color: dashboard.needFixSegments > 0 ? '#ff9944' : theme.text3,
}}>
{dashboard.needFixSegments > 0 ? '需补充材料' : '无需修改'}
</div>
</div>
{/* Overall Status */}
<div style={{
padding: '20px 24px',
background: dashboard.status === 'pass'
? 'linear-gradient(135deg, rgba(0,212,170,0.12), rgba(0,212,170,0.04))'
: dashboard.status === 'warning'
? 'linear-gradient(135deg, rgba(255,136,0,0.12), rgba(255,136,0,0.04))'
: 'linear-gradient(135deg, rgba(255,68,68,0.12), rgba(255,68,68,0.04))',
borderRadius: 10,
border: `1px solid ${
dashboard.status === 'pass' ? theme.green
: dashboard.status === 'warning' ? theme.orange
: '#ff4444'
}`,
textAlign: 'center',
}}>
<div style={{
fontSize: 11,
color: theme.text3,
marginBottom: 8,
}}></div>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
marginTop: 12,
}}>
{dashboard.status === 'pass' ? (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" fill={theme.green} fillOpacity="0.15"/>
<path d="M8 12L11 15L16 9" stroke={theme.green} strokeWidth="2" strokeLinecap="round"/>
</svg>
) : dashboard.status === 'warning' ? (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" fill={theme.orange} fillOpacity="0.15"/>
<path d="M12 8V12M12 16H12.01" stroke={theme.orange} strokeWidth="2" strokeLinecap="round"/>
</svg>
) : (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" fill="#ff4444" fillOpacity="0.15"/>
<path d="M15 9L9 15M9 9L15 15" stroke="#ff4444" strokeWidth="2" strokeLinecap="round"/>
</svg>
)}
<span style={{
fontSize: 16,
fontWeight: 600,
color: dashboard.status === 'pass' ? theme.green
: dashboard.status === 'warning' ? theme.orange
: '#ff4444',
}}>{dashboard.statusLabel}</span>
</div>
</div>
</div>
{/* Priority Actions */}
<div style={{
border: `1px solid ${theme.border}`,
borderRadius: 10,
background: theme.bgElevated,
}}>
<div style={{
padding: '14px 20px',
borderBottom: `1px solid ${theme.border}`,
display: 'flex',
alignItems: 'center',
gap: 10,
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke={theme.accent} strokeWidth="1.5"/>
</svg>
<span style={{ fontSize: 13, fontWeight: 600, color: theme.text }}></span>
<span className="mono" style={{
fontSize: 11,
padding: '3px 10px',
background: theme.accent,
borderRadius: 4,
color: '#fff',
}}>{mockPriorityActions.length} </span>
</div>
<div style={{ padding: '12px 20px' }}>
{mockPriorityActions.map((action, idx) => (
<div
key={action.id}
onClick={() => 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',
}}
>
<div style={{
width: 24,
height: 24,
borderRadius: 6,
background: action.severity === 'high' ? '#ff4444' : theme.orange,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}>
<span style={{
fontSize: 12,
fontWeight: 600,
color: '#fff',
}}>{idx + 1}</span>
</div>
<div style={{ flex: 1 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 10,
marginBottom: 6,
}}>
<span style={{
fontSize: 13,
fontWeight: 600,
color: action.severity === 'high' ? '#ff4444' : theme.orange,
}}>{action.regulation}</span>
<span style={{
fontSize: 11,
color: theme.text3,
padding: '2px 8px',
background: theme.bgElevated,
borderRadius: 4,
}}>{action.issue}</span>
</div>
<div style={{
fontSize: 12,
color: theme.text2,
lineHeight: 1.5,
}}>{action.suggestion}</div>
</div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ marginTop: 4 }}>
<path d="M9 6L15 12L9 18" stroke={theme.text3} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</div>
))}
</div>
</div>
</div>
)}
</section>
{/* Split Layout: Document Left, Regulations Right */}
<section style={{
display: 'flex',
gap: 0,
minHeight: 'calc(100vh - 260px)',
background: theme.bgCard,
borderRadius: 12,
border: `1px solid ${theme.border}`,
boxShadow: !isDark ? '0 4px 16px rgba(226,0,116,0.08)' : 'none',
overflow: 'hidden',
}}>
{/* Left: Full Document with Marginalia Markers */}
<div style={{
flex: '1 1 55%',
display: 'flex',
overflowY: 'auto',
borderRight: `1px solid ${theme.border}`,
}}>
{/* Marginalia Column - Segment Markers */}
<div style={{
width: 48,
background: theme.bgElevated,
borderRight: `1px solid ${theme.border}`,
display: 'flex',
flexDirection: 'column',
paddingTop: 80,
position: 'relative',
}}>
{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 (
<div
key={chunk.id}
onClick={() => 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,
}}
>
<span className="mono" style={{
fontSize: 10,
fontWeight: 600,
color: isActive ? '#fff' : riskColor,
}}>{chunk.index}</span>
</div>
);
})}
</div>
{/* Document Content Column */}
<div style={{
flex: 1,
padding: '32px 40px',
overflowY: 'auto',
}}>
<h3 style={{
fontSize: 13,
fontWeight: 600,
color: theme.accent,
marginBottom: 24,
display: 'flex',
alignItems: 'center',
gap: 8,
letterSpacing: '0.05em',
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M14 2H6C5 2 4 3 4 4V20C4 21 5 22 6 22H18C19 22 20 21 20 20V8L14 2Z" stroke={theme.accent} strokeWidth="1.5"/>
<path d="M14 2V8H20" stroke={theme.accent} strokeWidth="1.5"/>
</svg>
</h3>
{/* Render document with segment blocks — document-style layout */}
<div style={{
fontSize: 15,
lineHeight: 2,
color: theme.text2,
}}>
{renderDocumentWithSegmentBlocks()}
</div>
</div>
</div>
{/* Right: Regulations Panel */}
<div style={{
flex: '1 1 45%',
minWidth: 340,
display: 'flex',
flexDirection: 'column',
background: theme.bgElevated,
}}>
{/* Header */}
<div style={{
padding: '24px 32px 16px',
borderBottom: `1px solid ${theme.border}`,
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 8,
}}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke={theme.accent} strokeWidth="1.5"/>
<path d="M12 6V12L16 14" stroke={theme.accent} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: '0.05em', color: theme.text }}></span>
</div>
{activeChunkId && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: 10,
}}>
<div style={{
padding: '3px 10px',
background: theme.gradientAccent,
borderRadius: 4,
}}>
<span className="mono" style={{ fontSize: 11, fontWeight: 600, color: '#fff' }}>
#{chunks.find(c => c.id === activeChunkId)?.index}
</span>
</div>
<span style={{ fontSize: 12, color: theme.text2 }}>
{chunks.find(c => c.id === activeChunkId)?.intent}
</span>
</div>
)}
</div>
{/* Content */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '20px 32px',
}}>
{activeChunkId === null ? (
/* Default: All segments with regulation counts */
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{chunks.map(chunk => {
const regs = getRegsByCategory(chunk.regulations);
const totalRegs = chunk.regulations.length;
return (
<div
key={chunk.id}
onClick={() => setActiveChunkId(chunk.id)}
style={{
padding: '16px 20px',
background: theme.bgHover,
borderRadius: 10,
border: `1px solid ${theme.border}`,
cursor: 'pointer',
transition: 'all 0.25s ease',
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 10,
}}>
<div style={{
width: 28,
height: 28,
borderRadius: '50%',
background: theme.gradientAccent,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<span className="mono" style={{ fontSize: 11, fontWeight: 600, color: '#fff' }}>
{chunk.index}
</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 500, marginBottom: 4, color: theme.text }}>
{chunk.intent}
</div>
<div style={{
fontSize: 12,
color: theme.text3,
lineHeight: 1.4,
maxHeight: 34,
overflow: 'hidden',
}}>
{chunk.content.substring(0, 50)}...
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div className="mono" style={{
fontSize: 14,
fontWeight: 600,
color: theme.accent,
}}>{totalRegs}</div>
<div style={{ fontSize: 10, color: theme.text3 }}></div>
</div>
</div>
{/* Mini regulation distribution bar */}
<div style={{
display: 'flex',
height: 4,
borderRadius: 2,
background: theme.bgElevated,
overflow: 'hidden',
gap: 1,
}}>
{regs.high.length > 0 && (
<div style={{
width: `${(regs.high.length / totalRegs) * 100}%`,
background: theme.green,
}} />
)}
{regs.medium.length > 0 && (
<div style={{
width: `${(regs.medium.length / totalRegs) * 100}%`,
background: theme.orange,
}} />
)}
{regs.low.length > 0 && (
<div style={{
width: `${(regs.low.length / totalRegs) * 100}%`,
background: theme.text3,
}} />
)}
</div>
</div>
);
})}
</div>
) : (
/* Active: Detailed regulations with expand/collapse */
<div>
{/* Active chunk brief */}
<div style={{
padding: '14px 18px',
background: 'linear-gradient(135deg, rgba(226,0,116,0.08), rgba(190,0,96,0.04))',
borderRadius: 8,
marginBottom: 24,
borderLeft: `3px solid ${theme.accent}`,
}}>
<div style={{
fontSize: 13,
color: theme.text2,
lineHeight: 1.5,
maxHeight: 80,
overflow: 'hidden',
}}>
{chunks.find(c => c.id === activeChunkId)?.content.substring(0, 120)}...
</div>
<button
onClick={() => setActiveChunkId(null)}
style={{
marginTop: 10,
padding: '5px 12px',
background: 'transparent',
border: `1px solid ${theme.border}`,
borderRadius: 4,
fontSize: 11,
color: theme.text3,
cursor: 'pointer',
}}
>
</button>
</div>
{/* Regulations by category */}
{(() => {
const activeChunk = chunks.find(c => c.id === activeChunkId);
if (!activeChunk) return null;
const regs = getRegsByCategory(activeChunk.regulations);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* High Relevance */}
{regs.high.length > 0 && (
<div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 12,
}}>
<div style={{
width: 8,
height: 8,
borderRadius: '50%',
background: theme.green,
boxShadow: `0 0 6px ${theme.green}`,
}} />
<span style={{ fontSize: 12, fontWeight: 600, letterSpacing: '0.03em', color: theme.text }}></span>
<span className="mono" style={{
fontSize: 11,
padding: '2px 8px',
background: theme.green,
borderRadius: 4,
color: '#fff',
}}>{regs.high.length}</span>
</div>
{regs.high.map((reg) => {
const isExpanded = expandedRegulationId === reg.id;
return (
<div
key={reg.id}
onClick={() => 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',
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 8,
height: 8,
borderRadius: '50%',
background: theme.green,
boxShadow: `0 0 6px ${theme.green}`,
}} />
<span style={{ fontSize: 14, fontWeight: 600, color: theme.text }}>{reg.name}</span>
<span className="mono" style={{
fontSize: 11,
color: theme.text3,
}}>{reg.clause}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span className="mono" style={{
fontSize: 12,
fontWeight: 500,
color: theme.green,
}}>{Math.round(reg.score * 100)}%</span>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
style={{
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
}}
>
<path d="M6 9L12 15L18 9" stroke={theme.text3} strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
</div>
{isExpanded && (
<div style={{
marginTop: 14,
padding: '14px 16px',
background: theme.bgElevated,
borderRadius: 8,
borderLeft: `3px solid ${theme.green}`,
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 10,
}}>
<span style={{ fontSize: 12, fontWeight: 600, color: theme.green }}></span>
<span className="mono" style={{
fontSize: 11,
padding: '3px 10px',
background: theme.green,
borderRadius: 4,
color: '#fff',
}}>{reg.matchKeyword}</span>
</div>
<div style={{
fontSize: 13,
color: theme.text2,
lineHeight: 1.6,
}}>
{reg.fullContent}
</div>
</div>
)}
</div>
);
})}
</div>
)}
{/* Medium Relevance */}
{regs.medium.length > 0 && (
<div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 12,
}}>
<div style={{
width: 8,
height: 8,
borderRadius: '50%',
background: theme.orange,
}} />
<span style={{ fontSize: 12, fontWeight: 600, color: theme.text }}></span>
<span className="mono" style={{
fontSize: 11,
padding: '2px 8px',
background: theme.orange,
borderRadius: 4,
color: '#fff',
}}>{regs.medium.length}</span>
</div>
{regs.medium.map((reg) => {
const isExpanded = expandedRegulationId === reg.id;
return (
<div
key={reg.id}
onClick={() => 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',
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 6,
height: 6,
borderRadius: '50%',
background: theme.orange,
}} />
<span style={{ fontSize: 13, fontWeight: 500, color: theme.text }}>{reg.name}</span>
<span className="mono" style={{
fontSize: 10,
color: theme.text3,
}}>{reg.clause}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span className="mono" style={{
fontSize: 11,
fontWeight: 500,
color: theme.orange,
}}>{Math.round(reg.score * 100)}%</span>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
style={{
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
}}
>
<path d="M6 9L12 15L18 9" stroke={theme.text3} strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
</div>
{isExpanded && (
<div style={{
marginTop: 12,
padding: '12px 14px',
background: theme.bgElevated,
borderRadius: 8,
borderLeft: `3px solid ${theme.orange}`,
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 8,
}}>
<span style={{ fontSize: 11, fontWeight: 600, color: theme.orange }}></span>
<span className="mono" style={{
fontSize: 10,
padding: '2px 8px',
background: theme.orange,
borderRadius: 4,
color: '#fff',
}}>{reg.matchKeyword}</span>
</div>
<div style={{
fontSize: 12,
color: theme.text2,
lineHeight: 1.5,
}}>
{reg.fullContent}
</div>
</div>
)}
</div>
);
})}
</div>
)}
{/* Low Relevance */}
{regs.low.length > 0 && (
<div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 12,
}}>
<div style={{
width: 8,
height: 8,
borderRadius: '50%',
background: theme.text3,
}} />
<span style={{ fontSize: 12, fontWeight: 600, color: theme.text }}></span>
<span className="mono" style={{
fontSize: 11,
padding: '2px 8px',
background: theme.text3,
borderRadius: 4,
color: '#fff',
}}>{regs.low.length}</span>
</div>
<div style={{
display: 'flex',
gap: 8,
flexWrap: 'wrap',
}}>
{regs.low.map(reg => {
const isExpanded = expandedRegulationId === reg.id;
return (
<div
key={reg.id}
onClick={() => 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',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span>{reg.name}</span>
{isExpanded && (
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" style={{ transform: 'rotate(180deg)' }}>
<path d="M6 9L12 15L18 9" stroke={theme.text3} strokeWidth="2" strokeLinecap="round"/>
</svg>
)}
</div>
{isExpanded && (
<div style={{
marginTop: 8,
fontSize: 11,
color: theme.text3,
lineHeight: 1.4,
}}>
<div className="mono" style={{ marginBottom: 4 }}>{reg.clause}</div>
<div style={{ marginBottom: 4 }}>{reg.matchKeyword}</div>
<div>{reg.fullContent.substring(0, 60)}...</div>
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* Chat Button */}
<button
onClick={() => openChat(activeChunkId)}
className="t-btn"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
marginTop: 24,
padding: '14px 24px',
border: 'none',
borderRadius: 10,
color: '#fff',
cursor: 'pointer',
fontSize: 14,
fontWeight: 500,
width: '100%',
}}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</button>
</div>
);
})()}
</div>
)}
</div>
</div>
</section>
</>
)}
</div>
{/* Chat Panel */}
{chatPanelOpen && activeChunkId && (
<ChatPanel
activeChunkId={activeChunkId}
chunks={chunks}
messages={chatMessages[activeChunkId] || []}
chatInput={chatInput}
setChatInput={setChatInput}
chatLoading={chatLoading}
sendChatMessage={sendChatMessage}
closeChat={closeChat}
quickQuestions={quickQuestions}
/>
)}
</div>
);
};