- Created StatusPage component with system stats, configuration, and indexed documents overview. - Added RagChatPage component for chat functionality. - Introduced global CSS styles for light and dark themes, including utility classes and animations. - Defined TypeScript types for compliance, documents, and themes. - Configured Tailwind CSS for dynamic theming and custom animations. - Set up TypeScript configuration for app and node environments. - Initialized Vite configuration for React project.
1816 lines
79 KiB
TypeScript
1816 lines
79 KiB
TypeScript
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 { 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 = () => {
|
||
setIsAnalyzing(true);
|
||
setAnalyzeStep(1);
|
||
setAnalyzePercent(0);
|
||
|
||
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);
|
||
|
||
setTimeout(() => {
|
||
setAnalyzePercent(100);
|
||
setAnalyzeAction('分析完成');
|
||
setIsAnalyzing(false);
|
||
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);
|
||
|
||
setTimeout(() => {
|
||
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);
|
||
}, 1000);
|
||
};
|
||
|
||
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(/^([一二三四五六七八九十]+[、..]|第[一二三四五六七八九十]+[章节部篇]|[0-9]+[、..])/);
|
||
|
||
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"
|
||
style={{
|
||
padding: '20px 48px',
|
||
fontSize: 16,
|
||
fontWeight: 600,
|
||
color: '#fff',
|
||
border: 'none',
|
||
borderRadius: 12,
|
||
cursor: 'pointer',
|
||
}}
|
||
>开始分析</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>
|
||
);
|
||
}; |