Files
AIRegulation-Demo-Test/src/pages/Compliance/CompliancePage.tsx

1916 lines
82 KiB
TypeScript
Raw Normal View History

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>
);
};