diff --git a/frontend/src/pages/Docs/DocsPage.tsx b/frontend/src/pages/Docs/DocsPage.tsx index 1db5c29..a960d31 100644 --- a/frontend/src/pages/Docs/DocsPage.tsx +++ b/frontend/src/pages/Docs/DocsPage.tsx @@ -1,3 +1,141 @@ -export function DocsPage() { - return

Documents

; +import { useState, useEffect } from 'react'; +import { Topbar } from '../../components/layout/Topbar'; +import { Upload, Search } from 'lucide-react'; + +interface Doc { + id: string; + name: string; + status: 'ok' | 'warn' | 'risk' | 'info'; + uploadedAt: string; + chunks: number; + type: string; +} + +const STATUS_FILTERS = ['All', 'Ready', 'Embedding', 'Failed', 'Pending']; +const TYPE_OPTS = ['All types', 'EU Regulation', 'ISO Standard', 'National Draft', 'Internal Policy']; + +const MOCK_DOCS: Doc[] = [ + { id: '1', name: 'EU AI Act — Full text (EN)', status: 'ok', uploadedAt: '2025-11-10', chunks: 842, type: 'EU Regulation' }, + { id: '2', name: 'MIIT Draft 2025-08 (ZH)', status: 'ok', uploadedAt: '2025-11-01', chunks: 320, type: 'National Draft' }, + { id: '3', name: 'ISO/SAE 21434:2021', status: 'ok', uploadedAt: '2025-10-15', chunks: 614, type: 'ISO Standard' }, + { id: '4', name: 'Vehicle AI Safety Manual v3.2', status: 'ok', uploadedAt: '2025-10-08', chunks: 198, type: 'Internal Policy' }, + { id: '5', name: 'ADAS System Requirements', status: 'warn', uploadedAt: '2025-09-22', chunks: 0, type: 'Internal Policy' }, + { id: '6', name: 'UNECE R155 Corrigendum', status: 'info', uploadedAt: '2025-09-12', chunks: 87, type: 'EU Regulation' }, + { id: '7', name: 'GB/T 42118-2022', status: 'risk', uploadedAt: '2025-08-30', chunks: 0, type: 'National Draft' }, +]; + +const STATUS_LABEL: Record = { ok: 'Ready', warn: 'Embedding', risk: 'Failed', info: 'Pending' }; +const STATUS_MAP: Record = { All: 'All', Ready: 'ok', Embedding: 'warn', Failed: 'risk', Pending: 'info' }; + +export function DocsPage() { + const [search, setSearch] = useState(''); + const [statusF, setStatusF] = useState('All'); + const [typeF, setTypeF] = useState('All types'); + const [selected, setSelected] = useState>(new Set()); + const [docs, setDocs] = useState(MOCK_DOCS); + + useEffect(() => { + fetch('/api/v1/documents') + .then(r => r.json()) + .then(d => { if (Array.isArray(d?.documents)) setDocs(d.documents); }) + .catch(() => {}); + }, []); + + const filtered = docs.filter(d => { + const matchSearch = !search || d.name.toLowerCase().includes(search.toLowerCase()); + const matchStatus = statusF === 'All' || d.status === STATUS_MAP[statusF]; + const matchType = typeF === 'All types' || d.type === typeF; + return matchSearch && matchStatus && matchType; + }); + + function toggleAll() { + if (selected.size === filtered.length) setSelected(new Set()); + else setSelected(new Set(filtered.map(d => d.id))); + } + + function toggleOne(id: string) { + const s = new Set(selected); + s.has(id) ? s.delete(id) : s.add(id); + setSelected(s); + } + + return ( +
+ +
+ + setSearch(e.target.value)} + /> +
+ + + } + /> +
+
+
+ {STATUS_FILTERS.map(f => ( + + ))} +
+ +
+ + {selected.size > 0 && ( +
+ {selected.size} document{selected.size > 1 ? 's' : ''} selected + + +
+ )} + +
+
+ 0} + onChange={toggleAll} + /> + Document name + Status + Uploaded + Chunks + Type + Actions +
+ {filtered.map(d => ( +
+ toggleOne(d.id)} + /> + {d.name} + {STATUS_LABEL[d.status]} + {d.uploadedAt} + {d.chunks || '—'} + {d.type} + + + + {d.status === 'risk' && } + +
+ ))} +
+
+
+ ); } diff --git a/frontend/src/pages/Perception/AnalysisPanel.tsx b/frontend/src/pages/Perception/AnalysisPanel.tsx deleted file mode 100644 index a6fa528..0000000 --- a/frontend/src/pages/Perception/AnalysisPanel.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React, { useRef } from 'react'; -import { useTheme } from '../../contexts'; -import type { RegulationEvent, AffectedDoc } from '../../api/perception'; - -interface AnalysisPanelProps { - event: RegulationEvent | null; - analyzing: boolean; - analysisText: string; - affectedDocs: AffectedDoc[]; - onAnalyze: () => void; - onAbort: () => void; -} - -// Minimal markdown renderer — handles ##/### headings, **bold**, bullet lists -function MarkdownText({ text, textColor, accent }: { text: string; textColor: string; accent: string }) { - const lines = text.split('\n'); - return ( -
- {lines.map((line, i) => { - if (line.startsWith('## ')) { - return
{line.slice(3)}
; - } - if (line.startsWith('### ')) { - return
{line.slice(4)}
; - } - if (line.startsWith('- ') || line.startsWith('* ')) { - const content = line.slice(2); - return ( -
- · - $1') }} /> -
- ); - } - if (/^\d+\./.test(line)) { - return ( -
- $1') }} /> -
- ); - } - if (!line.trim()) return
; - return ( -
- $1') }} /> -
- ); - })} -
- ); -} - -const IMPACT_COLORS = { high: '#d64545', medium: '#ff8800', low: '#00d4aa' }; -const SOURCE_COLORS: Record = { - MIIT: '#e20074', 'UN-ECE': '#4a90d9', ISO: '#7b68ee', - '国标委': '#00b89c', 'EUR-Lex': '#f5a623', IATF: '#9b59b6', -}; -const STATUS_LABEL: Record = { enacted: '已生效', draft: '征求意见', consultation: '公众咨询' }; - -export const AnalysisPanel: React.FC = ({ - event, analyzing, analysisText, affectedDocs, onAnalyze, onAbort, -}) => { - const { theme, isDark } = useTheme(); - const analysisRef = useRef(null); - - if (!event) { - return ( -
-
-
选择左侧法规动态以查看智能影响分析
-
- ); - } - - const impactColor = IMPACT_COLORS[event.impact_level]; - const srcColor = SOURCE_COLORS[event.source] || theme.accent; - - return ( -
- {/* Event header */} -
- {/* Source + status */} -
- {event.source} - {event.standard_code} - - {STATUS_LABEL[event.status] ?? event.status} - -
- - {/* Title */} -
- {event.title} -
- - {/* Summary */} -
- {event.summary} -
- - {/* Tags */} -
- {event.tags.map(tag => ( - - {tag} - - ))} -
- - {/* Dates + Analyze button */} -
-
- 发布:{event.published_at} - {event.effective_at && 生效:{event.effective_at}} -
- {analyzing ? ( - - ) : ( - - )} -
-
- - {/* Affected documents */} - {affectedDocs.length > 0 && ( -
-
- 关联文档({affectedDocs.length}) -
-
- {affectedDocs.map(doc => ( -
-
-
- {doc.doc_name} -
- {doc.snippet && ( -
- {doc.snippet} -
- )} -
- - {Math.round(doc.score * 100)}% - -
- ))} -
-
- )} - - {/* Streaming analysis output */} - {(analysisText || analyzing) && ( -
-
- ANALYSIS {analyzing && } -
- {analysisText && ( - - )} - {analyzing && !analysisText && ( -
正在分析法规影响...
- )} -
- )} - - {/* Empty analysis state */} - {!analysisText && !analyzing && ( -
-
- 点击「触发智能分析」查看 AI 影响评估 -
-
- )} -
- ); -}; diff --git a/frontend/src/pages/Perception/EventFeed.tsx b/frontend/src/pages/Perception/EventFeed.tsx deleted file mode 100644 index fae08f1..0000000 --- a/frontend/src/pages/Perception/EventFeed.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react'; -import { useTheme } from '../../contexts'; -import type { RegulationEvent, ImpactLevel, EventSource } from '../../api/perception'; - -const IMPACT_CONFIG: Record = { - high: { color: '#d64545', label: '高影响', dot: '●' }, - medium: { color: '#ff8800', label: '中影响', dot: '●' }, - low: { color: '#00d4aa', label: '低影响', dot: '●' }, -}; - -const STATUS_LABEL: Record = { - enacted: '已生效', - draft: '征求意见', - consultation: '公众咨询', -}; - -const SOURCE_COLORS: Record = { - MIIT: '#e20074', - 'UN-ECE': '#4a90d9', - ISO: '#7b68ee', - '国标委': '#00b89c', - 'EUR-Lex': '#f5a623', - IATF: '#9b59b6', -}; - -interface EventFeedProps { - events: RegulationEvent[]; - selectedId: string | null; - onSelect: (id: string) => void; - filterSource: string; - filterImpact: string; - onFilterSource: (v: string) => void; - onFilterImpact: (v: string) => void; - stats: { total: number; high_impact: number; medium_impact: number; low_impact: number; recent_90d: number } | null; - loading: boolean; -} - -export const EventFeed: React.FC = ({ - events, selectedId, onSelect, - filterSource, filterImpact, onFilterSource, onFilterImpact, - stats, loading, -}) => { - const { theme, isDark } = useTheme(); - - const sources: EventSource[] = ['MIIT', 'UN-ECE', 'ISO', '国标委', 'EUR-Lex', 'IATF']; - const impacts: ImpactLevel[] = ['high', 'medium', 'low']; - - return ( -
- - {/* KPI mini-cards */} - {stats && ( -
- {[ - { label: '总计', value: stats.total, color: theme.text }, - { label: '高影响', value: stats.high_impact, color: '#d64545' }, - { label: '中影响', value: stats.medium_impact, color: '#ff8800' }, - { label: '近90天', value: stats.recent_90d, color: theme.accent }, - ].map(({ label, value, color }) => ( -
-
-
{label}
-
{value}
-
- ))} -
- )} - - {/* Filter row */} -
- - {sources.map(s => ( - - ))} -
-
- - {impacts.map(lvl => ( - - ))} -
- - {/* Event list */} -
- {loading && ( -
加载中...
- )} - {!loading && events.length === 0 && ( -
暂无法规动态
- )} - {events.map(evt => { - const cfg = IMPACT_CONFIG[evt.impact_level]; - const isSelected = evt.id === selectedId; - const srcColor = SOURCE_COLORS[evt.source] || theme.accent; - return ( -
onSelect(evt.id)} - style={{ - padding: '14px 16px', - background: isSelected ? theme.bgHover : theme.bgCard, - borderRadius: 10, - border: `1px solid ${isSelected ? theme.accent : theme.border}`, - borderLeft: `4px solid ${cfg.color}`, - cursor: 'pointer', - transition: 'all 0.15s ease', - boxShadow: isSelected ? `0 0 0 1px ${theme.accent}40` : 'none', - }} - > - {/* Source + Status row */} -
- {evt.source} - {evt.standard_code} - - {STATUS_LABEL[evt.status] ?? evt.status} - -
- - {/* Title */} -
- {evt.title} -
- - {/* Date + impact */} -
- - {evt.published_at}{evt.effective_at ? ` → ${evt.effective_at}` : ''} - - {cfg.dot} {cfg.label} -
-
- ); - })} -
-
- ); -}; diff --git a/frontend/src/pages/RagChat/CitedAnswer.tsx b/frontend/src/pages/RagChat/CitedAnswer.tsx deleted file mode 100644 index 1f33b5e..0000000 --- a/frontend/src/pages/RagChat/CitedAnswer.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { useTheme } from '../../contexts'; -import type { RetrievalData } from '../../types'; - -interface CitedAnswerProps { - text: string; - sources: RetrievalData[]; - onCiteClick: (index: number) => void; -} - -export const CitedAnswer: React.FC = ({ text, sources, onCiteClick }) => { - const { theme } = useTheme(); - - if (!text) return null; - - // Split on [N] patterns, preserving delimiters - const parts = text.split(/(\[\d+\])/g); - - return ( - - {parts.map((part, i) => { - const match = part.match(/^\[(\d+)\]$/); - if (!match) return {part}; - - const idx = parseInt(match[1], 10); - const source = sources[idx - 1]; - - return ( - onCiteClick(idx)} - style={{ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - minWidth: 18, - height: 18, - padding: '0 4px', - marginLeft: 1, - fontSize: 10, - fontWeight: 700, - background: theme.gradientAccent, - color: '#fff', - borderRadius: 4, - cursor: source ? 'pointer' : 'default', - verticalAlign: 'super', - lineHeight: 1, - userSelect: 'none', - }} - > - {idx} - - ); - })} - - ); -};