Files
AIRegulation-DocAnalysis/docs/superpowers/plans/2026-06-03-frontend-redesign.md
wangwei dcda7e0423 @
chore: delete old layout/common/tabs components before redesign
@
2026-06-03 16:58:35 +08:00

2016 lines
83 KiB
Markdown

# Frontend Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Completely replace the existing frontend UI layer with a new design strictly following the HTML prototypes in `Prototype/`, building a 6-page app with a permanent 232px left sidebar.
**Architecture:** Delete all existing layout/page components and CSS. Rebuild with React 19 + TypeScript using a CSS-first approach (design tokens in globals.css), a permanent Sidebar component, and 6 dedicated page components. Preserve api/, data/, types/ unchanged.
**Tech Stack:** React 19, TypeScript, Vite, TailwindCSS 4, React Router 7, Lucide React icons.
---
## File Structure Map
**Delete (existing UI layer):**
- `frontend/src/styles/globals.css` — rewrite entirely
- `frontend/src/components/layout/AppShell.tsx` — rewrite
- `frontend/src/components/layout/FooterLayout.tsx` — delete
- `frontend/src/components/layout/HeaderBrand.tsx` — delete
- `frontend/src/components/layout/HeaderLayout.tsx` — delete
- `frontend/src/components/layout/KeepAliveViewport.tsx` — delete
- `frontend/src/components/layout/TabNav.tsx` — delete
- `frontend/src/components/layout/ContentLayout.tsx` — delete
- `frontend/src/components/common/TLogo.tsx` — delete
- `frontend/src/components/common/TPattern.tsx` — delete
- `frontend/src/components/common/ThemeToggle.tsx` — delete
- `frontend/src/router/tabs.tsx` — delete
- `frontend/src/pages/Status/StatusPage.tsx` — rewrite
- `frontend/src/pages/Perception/PerceptionPage.tsx` — rewrite
- `frontend/src/pages/Perception/EventFeed.tsx` — rewrite
- `frontend/src/pages/Perception/AnalysisPanel.tsx` — rewrite (if exists)
- `frontend/src/pages/Docs/DocsPage.tsx` — rewrite
- `frontend/src/pages/Compliance/CompliancePage.tsx` — rewrite
- `frontend/src/pages/RagChat/RagChatPage.tsx` — rewrite
- `frontend/src/pages/RagChat/CitedAnswer.tsx` — delete (inline into RagChatPage)
**Create (new UI layer):**
- `frontend/src/styles/globals.css` — design tokens + base reset
- `frontend/src/components/layout/AppShell.tsx` — CSS grid: sidebar(232px) + content-area
- `frontend/src/components/layout/Sidebar.tsx` — brand + nav groups + footer + theme toggle
- `frontend/src/components/layout/Topbar.tsx` — sticky topbar per page
- `frontend/src/router/AppRouter.tsx` — 6 routes with AppShell layout
- `frontend/src/pages/Overview/OverviewPage.tsx` — launcher index
- `frontend/src/pages/Status/StatusPage.tsx` — system status dashboard
- `frontend/src/pages/Perception/PerceptionPage.tsx` — regulatory signals two-pane
- `frontend/src/pages/Docs/DocsPage.tsx` — document management table
- `frontend/src/pages/Compliance/CompliancePage.tsx` — three-column workspace
- `frontend/src/pages/RagChat/RagChatPage.tsx` — three-column chat
**Preserve (no changes):**
- `frontend/src/api/` — all files
- `frontend/src/data/` — all files
- `frontend/src/types/` — all files
- `frontend/src/App.tsx` — keep as-is (ThemeProvider wrapper)
- `frontend/src/contexts/ThemeContext.tsx` — update to use `data-theme` on `<html>`
---
## Task 1: Rewrite globals.css with design tokens
**Files:**
- Modify: `frontend/src/styles/globals.css`
- [ ] **Step 1: Delete all existing CSS content and write new design tokens**
Replace the entire file content:
```css
/* ── Design Tokens ──────────────────────────────── */
:root {
--rail-bg: #ffffff;
--rail-surface: #f7f8fa;
--rail-fg: #111827;
--rail-muted: #8b929e;
--rail-border: #e8eaed;
--rail-hover: rgba(0,0,0,.04);
--rail-active: rgba(226,0,116,.07);
--bg: #f2f4f7;
--surface: #ffffff;
--fg: #111827;
--muted: #6b7280;
--border: #e5e7eb;
--border-strong: #d1d5db;
--accent: #e20074;
--accent-dim: rgba(226,0,116,.10);
--accent-hover: #c8006a;
--success: #16a34a;
--success-bg: rgba(22,163,74,.08);
--warn: #d97706;
--warn-bg: rgba(217,119,6,.08);
--danger: #dc2626;
--danger-bg: rgba(220,38,38,.08);
--info: #2563eb;
--info-bg: rgba(37,99,235,.08);
--font-display: "TeleNeoWeb-Bold", "Inter", -apple-system, sans-serif;
--font-body: "TeleNeoWeb-Regular", "Inter", -apple-system, sans-serif;
--font-mono: ui-monospace, "JetBrains Mono", Menlo, monospace;
--sidebar-w: 232px;
--topbar-h: 54px;
--radius-sm: 6px;
--radius-md: 10px;
--radius-pill: 9999px;
--shadow-card: 0 1px 4px rgba(0,0,0,.06), 0 0 0 1px rgba(0,0,0,.04);
}
[data-theme="dark"] {
--rail-bg: #1a1a2e;
--rail-surface: #16213e;
--rail-fg: #f0f0f0;
--rail-muted: #8b929e;
--rail-border: #2d3748;
--rail-hover: rgba(255,255,255,.06);
--rail-active: rgba(226,0,116,.15);
--bg: #0f0f1a;
--surface: #1a1a2e;
--fg: #f0f0f0;
--muted: #9ca3af;
--border: #2d3748;
--border-strong: #4a5568;
}
/* ── Base Reset ─────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { height: 100%; }
body {
height: 100%;
font-family: var(--font-body);
background: var(--bg);
color: var(--fg);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
#root { height: 100%; }
/* ── Utility Classes ────────────────────────────── */
.status {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: var(--radius-pill);
}
.status::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.status.ok { color: var(--success); background: var(--success-bg); }
.status.ok::before { background: var(--success); }
.status.warn { color: var(--warn); background: var(--warn-bg); }
.status.warn::before { background: var(--warn); }
.status.risk { color: var(--danger); background: var(--danger-bg); }
.status.risk::before { background: var(--danger); }
.status.info { color: var(--info); background: var(--info-bg); }
.status.info::before { background: var(--info); }
.card {
background: var(--surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-card);
padding: 16px 18px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border-radius: var(--radius-sm);
font-size: 13px;
font-family: var(--font-body);
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
background: var(--surface);
color: var(--fg);
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.btn:hover { background: var(--bg); }
.btn.primary {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn.sm { height: 28px; font-size: 12px; padding: 0 10px; }
```
- [ ] **Step 2: Verify CSS loads without errors**
Run: `cd frontend && npm run build 2>&1 | head -30`
Expected: No CSS parse errors.
- [ ] **Step 3: Commit**
```bash
git add frontend/src/styles/globals.css
git commit -m "feat: rewrite globals.css with prototype design tokens and base utilities"
```
---
## Task 2: Update ThemeContext to use data-theme on html element
**Files:**
- Modify: `frontend/src/contexts/ThemeContext.tsx`
- [ ] **Step 1: Read the current ThemeContext**
Read `frontend/src/contexts/ThemeContext.tsx` to understand its current implementation.
- [ ] **Step 2: Update theme application to use data-theme attribute**
Replace the theme application logic so that toggling dark mode sets `document.documentElement.setAttribute('data-theme', 'dark')` instead of adding a CSS class. The full replacement:
```tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue>({
theme: 'light',
toggleTheme: () => {},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
const saved = localStorage.getItem('theme');
return (saved === 'dark' || saved === 'light') ? saved : 'light';
});
useEffect(() => {
if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/contexts/ThemeContext.tsx
git commit -m "feat: update ThemeContext to use data-theme attribute on html element"
```
---
## Task 3: Delete old layout/common components and router tabs
**Files:**
- Delete: `frontend/src/components/layout/FooterLayout.tsx`
- Delete: `frontend/src/components/layout/HeaderBrand.tsx`
- Delete: `frontend/src/components/layout/HeaderLayout.tsx`
- Delete: `frontend/src/components/layout/KeepAliveViewport.tsx`
- Delete: `frontend/src/components/layout/TabNav.tsx`
- Delete: `frontend/src/components/layout/ContentLayout.tsx`
- Delete: `frontend/src/components/common/TLogo.tsx`
- Delete: `frontend/src/components/common/TPattern.tsx`
- Delete: `frontend/src/components/common/ThemeToggle.tsx`
- Delete: `frontend/src/router/tabs.tsx`
- [ ] **Step 1: Delete old component files**
```bash
cd frontend
rm src/components/layout/FooterLayout.tsx
rm src/components/layout/HeaderBrand.tsx
rm src/components/layout/HeaderLayout.tsx
rm src/components/layout/KeepAliveViewport.tsx
rm src/components/layout/TabNav.tsx
rm src/components/layout/ContentLayout.tsx
rm src/components/common/TLogo.tsx
rm src/components/common/TPattern.tsx
rm src/components/common/ThemeToggle.tsx
rm src/router/tabs.tsx
```
- [ ] **Step 2: Commit**
```bash
git add -A
git commit -m "chore: delete old layout/common/tabs components before redesign"
```
---
## Task 4: Build Sidebar component
**Files:**
- Create: `frontend/src/components/layout/Sidebar.tsx`
- [ ] **Step 1: Create Sidebar.tsx**
```tsx
import { NavLink, useLocation } from 'react-router-dom';
import {
LayoutDashboard, Radio, Monitor, FileText,
Shield, MessageSquare, Sun, Moon
} from 'lucide-react';
import { useTheme } from '../../contexts/ThemeContext';
interface NavItem {
to: string;
icon: React.ReactNode;
label: string;
badge?: number;
}
const mainNav: NavItem[] = [
{ to: '/', icon: <LayoutDashboard size={16} />, label: 'Overview' },
{ to: '/signals', icon: <Radio size={16} />, label: 'Regulatory Signals' },
{ to: '/status', icon: <Monitor size={16} />, label: 'System Status' },
];
const workbenchNav: NavItem[] = [
{ to: '/documents', icon: <FileText size={16} />, label: 'Documents' },
{ to: '/compliance', icon: <Shield size={16} />, label: 'Compliance Analysis' },
];
const chatNav: NavItem[] = [
{ to: '/chat', icon: <MessageSquare size={16} />, label: 'Regulation Q&A' },
];
function NavGroup({ title, items }: { title: string; items: NavItem[] }) {
return (
<div className="nav-group">
<div className="nav-group-label">{title}</div>
{items.map(item => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
className={({ isActive }) => `nav-item${isActive ? ' active' : ''}`}
>
<span className="nav-icon">{item.icon}</span>
<span className="nav-label">{item.label}</span>
{item.badge !== undefined && item.badge > 0 && (
<span className="nav-badge">{item.badge}</span>
)}
</NavLink>
))}
</div>
);
}
export function Sidebar() {
const { theme, toggleTheme } = useTheme();
return (
<aside className="sidebar">
<div className="sidebar-brand">
<div className="brand-mark">TS</div>
<div className="brand-text">
<div className="brand-name">Regulation Hub</div>
<div className="brand-sub">T-Systems AI</div>
</div>
</div>
<nav className="sidebar-nav">
<NavGroup title="Main" items={mainNav} />
<NavGroup title="Workbench" items={workbenchNav} />
<NavGroup title="Chat" items={chatNav} />
</nav>
<div className="sidebar-footer">
<div className="sidebar-user">
<div className="user-avatar">TS</div>
<div className="user-info">
<div className="user-name">Analyst</div>
<div className="user-role">T-Systems</div>
</div>
</div>
<button className="theme-btn" onClick={toggleTheme} title="Toggle theme">
{theme === 'dark' ? <Sun size={14} /> : <Moon size={14} />}
</button>
</div>
</aside>
);
}
```
- [ ] **Step 2: Add sidebar CSS to globals.css**
Append to `frontend/src/styles/globals.css`:
```css
/* ── Sidebar ────────────────────────────────────── */
.sidebar {
width: var(--sidebar-w);
min-height: 100vh;
background: var(--rail-bg);
border-right: 1px solid var(--rail-border);
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
flex-shrink: 0;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 10px;
padding: 18px 16px 14px;
border-bottom: 1px solid var(--rail-border);
}
.brand-mark {
width: 32px; height: 32px;
background: var(--accent);
color: #fff;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; font-family: var(--font-display);
flex-shrink: 0;
}
.brand-name { font-size: 13px; font-weight: 700; font-family: var(--font-display); color: var(--rail-fg); }
.brand-sub { font-size: 10px; color: var(--rail-muted); }
.sidebar-nav { flex: 1; overflow-y: auto; padding: 12px 0; }
.nav-group { margin-bottom: 4px; }
.nav-group-label {
font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--rail-muted);
padding: 8px 16px 4px;
}
.nav-item {
display: flex; align-items: center; gap: 9px;
height: 36px; padding: 0 14px 0 16px;
text-decoration: none;
color: var(--rail-fg);
font-size: 13px;
border-left: 3px solid transparent;
transition: background 0.12s, color 0.12s;
position: relative;
}
.nav-item:hover { background: var(--rail-hover); }
.nav-item.active {
background: var(--rail-active);
color: var(--accent);
border-left-color: var(--accent);
font-weight: 600;
}
.nav-icon { display: flex; align-items: center; color: inherit; flex-shrink: 0; }
.nav-label { flex: 1; }
.nav-badge {
font-size: 10px; font-weight: 700; font-family: var(--font-mono);
background: var(--accent-dim); color: var(--accent);
padding: 1px 6px; border-radius: var(--radius-pill);
}
.sidebar-footer {
display: flex; align-items: center; gap: 8px;
padding: 12px 14px;
border-top: 1px solid var(--rail-border);
}
.sidebar-user { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.user-avatar {
width: 28px; height: 28px;
background: var(--accent-dim); color: var(--accent);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 700; flex-shrink: 0;
}
.user-name { font-size: 12px; font-weight: 600; color: var(--rail-fg); }
.user-role { font-size: 10px; color: var(--rail-muted); }
.theme-btn {
width: 28px; height: 28px;
border: 1px solid var(--rail-border);
background: var(--rail-surface);
color: var(--rail-muted);
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
cursor: pointer; flex-shrink: 0;
transition: background 0.12s;
}
.theme-btn:hover { background: var(--rail-hover); color: var(--rail-fg); }
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/components/layout/Sidebar.tsx frontend/src/styles/globals.css
git commit -m "feat: add Sidebar component with nav groups, badges, and theme toggle"
```
---
## Task 5: Build AppShell + Topbar + rewrite AppRouter
**Files:**
- Modify: `frontend/src/components/layout/AppShell.tsx`
- Create: `frontend/src/components/layout/Topbar.tsx`
- Modify: `frontend/src/router/AppRouter.tsx`
- [ ] **Step 1: Rewrite AppShell.tsx**
```tsx
import { Outlet } from 'react-router-dom';
import { Sidebar } from './Sidebar';
export function AppShell() {
return (
<div className="app-shell">
<Sidebar />
<div className="content-area">
<Outlet />
</div>
</div>
);
}
```
Append to `frontend/src/styles/globals.css`:
```css
/* ── App Shell ──────────────────────────────────── */
.app-shell {
display: flex;
height: 100vh;
overflow: hidden;
}
.content-area {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
```
- [ ] **Step 2: Create Topbar.tsx**
```tsx
interface TopbarProps {
title: string;
subtitle?: string;
actions?: React.ReactNode;
}
export function Topbar({ title, subtitle, actions }: TopbarProps) {
return (
<header className="topbar">
<div className="topbar-left">
<h1 className="topbar-title">{title}</h1>
{subtitle && <span className="topbar-sub">{subtitle}</span>}
</div>
{actions && <div className="topbar-actions">{actions}</div>}
</header>
);
}
```
Append to `frontend/src/styles/globals.css`:
```css
/* ── Topbar ─────────────────────────────────────── */
.topbar {
height: var(--topbar-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
border-bottom: 1px solid var(--border);
background: var(--surface);
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 10;
}
.topbar-left { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.topbar-title { font-size: 15px; font-weight: 700; font-family: var(--font-display); color: var(--fg); }
.topbar-sub { font-size: 11px; color: var(--muted); font-family: var(--font-mono); }
.topbar-actions { display: flex; align-items: center; gap: 8px; }
```
- [ ] **Step 3: Rewrite AppRouter.tsx with 6 routes**
```tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AppShell } from '../components/layout/AppShell';
import { OverviewPage } from '../pages/Overview/OverviewPage';
import { StatusPage } from '../pages/Status/StatusPage';
import { PerceptionPage } from '../pages/Perception/PerceptionPage';
import { DocsPage } from '../pages/Docs/DocsPage';
import { CompliancePage } from '../pages/Compliance/CompliancePage';
import { RagChatPage } from '../pages/RagChat/RagChatPage';
export function AppRouter() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<AppShell />}>
<Route index element={<OverviewPage />} />
<Route path="status" element={<StatusPage />} />
<Route path="signals" element={<PerceptionPage />} />
<Route path="documents" element={<DocsPage />} />
<Route path="compliance" element={<CompliancePage />} />
<Route path="chat" element={<RagChatPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
```
- [ ] **Step 4: Create stub page files so the build doesn't fail**
Create each page as a minimal placeholder:
`frontend/src/pages/Overview/OverviewPage.tsx`:
```tsx
export function OverviewPage() { return <div className="page-content"><p>Overview</p></div>; }
```
`frontend/src/pages/Status/StatusPage.tsx` (overwrite):
```tsx
export function StatusPage() { return <div className="page-content"><p>Status</p></div>; }
```
`frontend/src/pages/Perception/PerceptionPage.tsx` (overwrite):
```tsx
export function PerceptionPage() { return <div className="page-content"><p>Signals</p></div>; }
```
`frontend/src/pages/Docs/DocsPage.tsx` (overwrite):
```tsx
export function DocsPage() { return <div className="page-content"><p>Documents</p></div>; }
```
`frontend/src/pages/Compliance/CompliancePage.tsx` (overwrite):
```tsx
export function CompliancePage() { return <div className="page-content"><p>Compliance</p></div>; }
```
`frontend/src/pages/RagChat/RagChatPage.tsx` (overwrite):
```tsx
export function RagChatPage() { return <div className="page-content"><p>Chat</p></div>; }
```
Also delete now-unused files:
```bash
cd frontend
rm -f src/pages/Perception/EventFeed.tsx src/pages/Perception/AnalysisPanel.tsx
rm -f src/pages/RagChat/CitedAnswer.tsx
```
Append to `frontend/src/styles/globals.css`:
```css
/* ── Page Content ───────────────────────────────── */
.page-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
```
- [ ] **Step 5: Verify build passes**
```bash
cd frontend && npm run build 2>&1 | tail -20
```
Expected: Build succeeds with no TypeScript or import errors.
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "feat: add AppShell + Topbar + 6-route AppRouter with stub pages"
```
---
## Task 6: Implement Overview page
**Files:**
- Modify: `frontend/src/pages/Overview/OverviewPage.tsx`
- [ ] **Step 1: Write OverviewPage with hero, summary card, workflow steps, screen grid**
```tsx
import { useNavigate } from 'react-router-dom';
import { ArrowRight, BarChart2, Eye, FileText, Shield, MessageSquare, Monitor } from 'lucide-react';
const SCREENS = [
{ id: 'status', label: 'System Status', icon: <Monitor size={20} />, to: '/status', desc: 'Live health and workflow queue' },
{ id: 'signals', label: 'Regulatory Signals', icon: <Eye size={20} />, to: '/signals', desc: 'AI-detected regulatory changes' },
{ id: 'documents', label: 'Document Management', icon: <FileText size={20} />, to: '/documents', desc: 'Upload and inspect documents' },
{ id: 'compliance', label: 'Compliance Analysis', icon: <Shield size={20} />, to: '/compliance', desc: 'Three-column compliance workspace' },
{ id: 'chat', label: 'Regulation Q&A', icon: <MessageSquare size={20} />, to: '/chat', desc: 'Chat with cited regulation sources' },
{ id: 'analytics', label: 'Analytics', icon: <BarChart2 size={20} />, to: '/status', desc: 'KPIs and coverage metrics' },
];
const STEPS = [
{ num: '01', label: 'Upload', desc: 'Ingest regulation documents' },
{ num: '02', label: 'Process', desc: 'Embed and chunk via vector DB' },
{ num: '03', label: 'Monitor', desc: 'Watch regulatory signal feed' },
{ num: '04', label: 'Analyze', desc: 'Run compliance gap analysis' },
{ num: '05', label: 'Review', desc: 'Inspect findings with AI assist' },
{ num: '06', label: 'Chat', desc: 'Ask questions with cited answers' },
];
export function OverviewPage() {
const navigate = useNavigate();
return (
<div className="overview-page">
<section className="overview-hero">
<p className="hero-eyebrow">T-Systems · AI Regulation Hub</p>
<h1 className="hero-title">AI Compliance,<br />Automated end-to-end</h1>
<p className="hero-desc">
Monitor global AI regulations, analyze document compliance gaps,
and get cited answers all in one platform.
</p>
<div className="hero-actions">
<button className="btn primary" onClick={() => navigate('/status')}>
Open dashboard <ArrowRight size={14} />
</button>
<button className="btn" onClick={() => navigate('/chat')}>
Jump to regulation chat
</button>
</div>
</section>
<div className="overview-summary card">
<div className="summary-item">
<span className="summary-num">6</span>
<span className="summary-label">Screens</span>
</div>
<div className="summary-divider" />
<div className="summary-item">
<span className="summary-num">5</span>
<span className="summary-label">Backend-aware flows</span>
</div>
<div className="summary-divider" />
<div className="summary-item">
<span className="summary-num">AI</span>
<span className="summary-label">Review posture</span>
</div>
</div>
<section className="overview-workflow">
<h2 className="section-title">How it works</h2>
<div className="workflow-steps">
{STEPS.map(s => (
<div key={s.num} className="workflow-step">
<div className="step-num">{s.num}</div>
<div className="step-label">{s.label}</div>
<div className="step-desc">{s.desc}</div>
</div>
))}
</div>
</section>
<section className="overview-screens">
<h2 className="section-title">Screens</h2>
<div className="screen-grid">
{SCREENS.map(s => (
<button key={s.id} className="screen-card card" onClick={() => navigate(s.to)}>
<div className="screen-icon">{s.icon}</div>
<div className="screen-label">{s.label}</div>
<div className="screen-desc">{s.desc}</div>
</button>
))}
</div>
</section>
</div>
);
}
```
- [ ] **Step 2: Add Overview page CSS to globals.css**
Append:
```css
/* ── Overview Page ──────────────────────────────── */
.overview-page { padding: 32px; max-width: 900px; display: flex; flex-direction: column; gap: 32px; }
.overview-hero { display: flex; flex-direction: column; gap: 12px; }
.hero-eyebrow { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); }
.hero-title { font-size: 32px; font-weight: 700; font-family: var(--font-display); line-height: 1.15; }
.hero-desc { font-size: 14px; color: var(--muted); max-width: 480px; line-height: 1.6; }
.hero-actions { display: flex; gap: 10px; padding-top: 4px; }
.overview-summary { display: flex; align-items: center; gap: 0; }
.summary-item { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1; padding: 14px; }
.summary-num { font-size: 22px; font-weight: 700; font-family: var(--font-display); color: var(--accent); }
.summary-label { font-size: 11px; color: var(--muted); }
.summary-divider { width: 1px; height: 40px; background: var(--border); flex-shrink: 0; }
.section-title { font-size: 13px; font-weight: 700; font-family: var(--font-display); color: var(--fg); margin-bottom: 14px; }
.workflow-steps { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }
.workflow-step { display: flex; flex-direction: column; gap: 4px; padding: 12px; background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); }
.step-num { font-size: 10px; font-weight: 700; font-family: var(--font-mono); color: var(--accent); }
.step-label { font-size: 13px; font-weight: 700; font-family: var(--font-display); }
.step-desc { font-size: 11px; color: var(--muted); line-height: 1.4; }
.screen-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.screen-card { text-align: left; cursor: pointer; border: none; transition: box-shadow 0.15s; display: flex; flex-direction: column; gap: 6px; }
.screen-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.10), 0 0 0 2px var(--accent-dim); }
.screen-icon { width: 36px; height: 36px; background: var(--accent-dim); color: var(--accent); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; margin-bottom: 4px; }
.screen-label { font-size: 13px; font-weight: 700; font-family: var(--font-display); }
.screen-desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/pages/Overview/OverviewPage.tsx frontend/src/styles/globals.css
git commit -m "feat: implement Overview launcher page with hero, workflow steps, screen grid"
```
---
## Task 7: Implement System Status page
**Files:**
- Modify: `frontend/src/pages/Status/StatusPage.tsx`
Refer to `Prototype/dashboard-sidebar.html` for layout details.
- [ ] **Step 1: Read the prototype to confirm grid/row structure**
Read `Prototype/dashboard-sidebar.html` lines 1-100 to confirm `.stats-grid`, `.panel-grid`, task row, and KPI strip structure.
- [ ] **Step 2: Write StatusPage.tsx**
```tsx
import { useState, useEffect } from 'react';
import { Topbar } from '../../components/layout/Topbar';
import { Search, Upload, Download } from 'lucide-react';
interface Stats { total_documents: number; vector_chunks: number; high_impact: number; last_90_days: number; }
const TASKS = [
{ name: 'EU AI Act — Article 13 check', status: 'ok', progress: 88, cta: 'View report' },
{ name: 'GB/T 42118 compliance scan', status: 'warn', progress: 54, cta: 'Continue' },
{ name: 'MIIT Draft — automotive AI embedding', status: 'info', progress: 12, cta: 'Start' },
];
const PROGRAMS = [
{ name: 'EU AI Act Readiness', status: 'ok', coverage: 88 },
{ name: 'China MIIT Compliance', status: 'warn', coverage: 54 },
{ name: 'ISO/SAE 21434 Audit', status: 'info', coverage: 32 },
];
const KPIS = [
{ label: 'Retrieval hit rate', value: 94, unit: '%' },
{ label: 'Evidence coverage', value: 78, unit: '%' },
{ label: 'Reviewer SLA', value: 91, unit: '%' },
];
const SERVICES = [
{ name: 'Vector store (Chroma)', status: 'ok' },
{ name: 'LLM gateway (Claude)', status: 'ok' },
{ name: 'Document parser', status: 'ok' },
{ name: 'SSE stream endpoint', status: 'ok' },
{ name: 'Regulation feed sync', status: 'warn' },
];
const EVENTS = [
{ date: '2025-11-18', title: 'EU AI Act — Delegated acts published', summary: 'European Commission releases implementing rules for high-risk AI classification under Annex III.' },
{ date: '2025-10-30', title: 'MIIT Draft — automotive AI', summary: 'New draft regulation covers in-vehicle AI training data provenance and OTA update governance.' },
{ date: '2025-10-05', title: 'ISO/SAE 21434 amendment', summary: 'Amendment 1 clarifies cybersecurity management system scope for software-only updates.' },
];
export function StatusPage() {
const [stats, setStats] = useState<Stats | null>(null);
useEffect(() => {
fetch('/api/v1/perception/stats')
.then(r => r.json())
.then(d => setStats(d))
.catch(() => setStats({ total_documents: 42, vector_chunks: 3841, high_impact: 7, last_90_days: 14 }));
}, []);
return (
<div className="status-page">
<Topbar
title="System Status"
actions={
<>
<div className="search-box"><Search size={13} /><input placeholder="Search..." /></div>
<button className="btn sm"><Download size={13} />Export status</button>
<button className="btn sm primary"><Upload size={13} />New upload</button>
</>
}
/>
<div className="page-content">
<div className="stats-grid">
<div className="stat-cell">
<div className="stat-value">{stats?.total_documents ?? '—'}</div>
<div className="stat-label">Documents indexed</div>
</div>
<div className="stat-cell">
<div className="stat-value">{stats?.vector_chunks?.toLocaleString() ?? '—'}</div>
<div className="stat-label">Vector chunks</div>
</div>
<div className="stat-cell danger">
<div className="stat-value">{stats?.high_impact ?? '—'}</div>
<div className="stat-label">High-impact signals</div>
</div>
<div className="stat-cell">
<div className="stat-value">{stats?.last_90_days ?? '—'}</div>
<div className="stat-label">Last 90 days</div>
</div>
</div>
<div className="panel-grid">
<div className="panel-left">
<div className="card mb-16">
<div className="card-header">Workflow queue</div>
{TASKS.map(t => (
<div key={t.name} className="task-row">
<div className="task-info">
<div className="task-name">{t.name}</div>
<div className="task-progress-bar">
<div className="task-progress-fill" style={{ width: `${t.progress}%` }} />
</div>
</div>
<span className={`status ${t.status}`}>{t.status === 'ok' ? 'Complete' : t.status === 'warn' ? 'In progress' : 'Pending'}</span>
<button className="btn sm">{t.cta}</button>
</div>
))}
</div>
<div className="card">
<div className="card-header">Active compliance programs</div>
{PROGRAMS.map(p => (
<div key={p.name} className="program-row">
<span className={`status ${p.status}`} style={{ marginRight: 'auto' }}>{p.name}</span>
<span className="program-pct">{p.coverage}%</span>
</div>
))}
<div className="kpi-strip">
{KPIS.map(k => (
<div key={k.label} className="kpi-item">
<div className="kpi-label">{k.label}</div>
<div className="kpi-bar"><div className="kpi-fill" style={{ width: `${k.value}%` }} /></div>
<div className="kpi-value">{k.value}{k.unit}</div>
</div>
))}
</div>
</div>
</div>
<div className="panel-right">
<div className="card mb-16">
<div className="card-header">System health</div>
{SERVICES.map(s => (
<div key={s.name} className="service-row">
<span className="service-name">{s.name}</span>
<span className={`status ${s.status}`}>{s.status === 'ok' ? 'Online' : 'Degraded'}</span>
</div>
))}
</div>
<div className="card">
<div className="card-header">Regulatory watch</div>
{EVENTS.map(e => (
<div key={e.title} className="event-row">
<div className="event-date">{e.date}</div>
<div className="event-title">{e.title}</div>
<div className="event-summary">{e.summary}</div>
</div>
))}
</div>
</div>
</div>
</div>
<footer className="page-footer">
<div className="live-dot" />
<span>Regulation Hub · T-Systems AI · Online</span>
</footer>
</div>
);
}
```
- [ ] **Step 3: Add Status page CSS to globals.css**
Append:
```css
/* ── Status Page ────────────────────────────────── */
.status-page { display: flex; flex-direction: column; height: 100%; }
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
background: var(--surface);
box-shadow: var(--shadow-card);
margin-bottom: 20px;
}
.stat-cell {
padding: 18px 22px;
border-right: 1px solid var(--border);
display: flex; flex-direction: column; gap: 4px;
}
.stat-cell:last-child { border-right: none; }
.stat-value { font-size: 26px; font-weight: 700; font-family: var(--font-display); }
.stat-label { font-size: 11px; color: var(--muted); }
.stat-cell.danger .stat-value { color: var(--danger); }
.panel-grid { display: grid; grid-template-columns: 1.4fr 0.9fr; gap: 16px; }
.panel-left, .panel-right { display: flex; flex-direction: column; gap: 16px; }
.mb-16 { margin-bottom: 0; }
.card-header { font-size: 12px; font-weight: 700; font-family: var(--font-display); color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 12px; }
.task-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-top: 1px solid var(--border); }
.task-info { flex: 1; min-width: 0; }
.task-name { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
.task-progress-bar { height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
.task-progress-fill { height: 100%; background: var(--accent); border-radius: 2px; }
.program-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-top: 1px solid var(--border); }
.program-pct { font-size: 12px; font-weight: 700; font-family: var(--font-mono); color: var(--muted); }
.kpi-strip { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
.kpi-item { display: flex; align-items: center; gap: 10px; }
.kpi-label { font-size: 11px; color: var(--muted); width: 130px; flex-shrink: 0; }
.kpi-bar { flex: 1; height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
.kpi-fill { height: 100%; background: var(--accent); border-radius: 2px; }
.kpi-value { font-size: 11px; font-weight: 700; font-family: var(--font-mono); color: var(--fg); width: 36px; text-align: right; flex-shrink: 0; }
.service-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-top: 1px solid var(--border); }
.service-name { font-size: 13px; }
.event-row { padding: 10px 0; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 3px; }
.event-date { font-size: 10px; font-family: var(--font-mono); color: var(--muted); }
.event-title { font-size: 13px; font-weight: 600; }
.event-summary { font-size: 12px; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.page-footer { padding: 12px 22px; border-top: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--muted); background: var(--surface); flex-shrink: 0; }
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 2px var(--success-bg); animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.search-box { display: flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); font-size: 13px; color: var(--muted); }
.search-box input { border: none; background: transparent; outline: none; font-size: 13px; color: var(--fg); width: 160px; }
```
- [ ] **Step 4: Commit**
```bash
git add frontend/src/pages/Status/StatusPage.tsx frontend/src/styles/globals.css
git commit -m "feat: implement System Status page with stats grid, panel grid, KPI strip"
```
---
## Task 8: Implement Regulatory Signals page
**Files:**
- Modify: `frontend/src/pages/Perception/PerceptionPage.tsx`
Refer to `Prototype/perception.html` for exact stats-bar, filter chips, feed cards, and analysis pane.
- [ ] **Step 1: Write PerceptionPage.tsx**
```tsx
import { useState, useEffect, useRef } from 'react';
import { Topbar } from '../../components/layout/Topbar';
import { RefreshCw, Play, Square, ExternalLink } from 'lucide-react';
interface Signal { id: string; source: string; standard: string; status: 'ok'|'warn'|'risk'|'info'; title: string; summary: string; date: string; tags: string[]; impact: 'High'|'Medium'|'Low'; }
interface Stats { total: number; high_impact: number; medium_impact: number; last_90_days: number; }
interface DocResult { score: number; name: string; clause: string; snippet: string; }
const SOURCES = ['All', 'MIIT', 'UN-ECE', 'ISO', 'GB Comm.', 'EUR-Lex', 'IATF'];
const IMPACTS = ['All', 'High', 'Medium', 'Low'];
const MOCK_SIGNALS: Signal[] = [
{ id: '1', source: 'EUR-Lex', standard: 'EU/2024/1689', status: 'risk', title: 'EU AI Act — High-risk AI in vehicles', summary: 'Article 9 mandates risk management systems for automotive AI classifying as high-risk under Annex III point 3.', date: '2025-11-18', tags: ['automotive', 'GDPR', 'certification'], impact: 'High' },
{ id: '2', source: 'MIIT', standard: 'Draft-2025-08', status: 'warn', title: 'MIIT Draft — in-vehicle AI training data', summary: 'Draft regulation requires OEM data provenance documentation and OTA audit trails for AI systems.', date: '2025-10-30', tags: ['OTA', 'data-governance', 'China'], impact: 'High' },
{ id: '3', source: 'ISO', standard: 'ISO/SAE 21434:2021/Amd1', status: 'info', title: 'ISO/SAE 21434 Amendment 1', summary: 'Amendment clarifies CSMS scope for software-only updates and vulnerability disclosure timelines.', date: '2025-10-05', tags: ['cybersecurity', 'CSMS', 'ISO'], impact: 'Medium' },
{ id: '4', source: 'UN-ECE', standard: 'UNECE WP.29 R155', status: 'ok', title: 'UNECE R155 Corrigendum', summary: 'Editorial corrections to cybersecurity management system requirements. No substantive changes.', date: '2025-09-12', tags: ['type-approval', 'UNECE'], impact: 'Low' },
];
const MOCK_DOCS: DocResult[] = [
{ score: 94, name: 'Vehicle AI Safety Manual v3.2', clause: '§4.2.1', snippet: 'The risk management process shall identify and evaluate risks arising from AI system decisions in safety-critical scenarios...' },
{ score: 87, name: 'ADAS System Requirements', clause: '§7.1', snippet: 'Automated driving functions must document training data lineage and model performance envelopes prior to deployment.' },
{ score: 71, name: 'Type Approval Documentation', clause: 'Annex B', snippet: 'Cybersecurity management system certification requires third-party audit of AI decision audit logs retention policy.' },
];
export function PerceptionPage() {
const [stats, setStats] = useState<Stats | null>(null);
const [sourceFilter, setSourceFilter] = useState('All');
const [impactFilter, setImpactFilter] = useState('All');
const [selected, setSelected] = useState<Signal | null>(null);
const [streaming, setStreaming] = useState(false);
const [aiOutput, setAiOutput] = useState('');
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
fetch('/api/v1/perception/stats')
.then(r => r.json())
.then(setStats)
.catch(() => setStats({ total: 47, high_impact: 7, medium_impact: 18, last_90_days: 14 }));
}, []);
const filtered = MOCK_SIGNALS.filter(s =>
(sourceFilter === 'All' || s.source === sourceFilter) &&
(impactFilter === 'All' || s.impact === impactFilter)
);
function runAnalysis() {
if (!selected) return;
setStreaming(true);
setAiOutput('');
const ctrl = new AbortController();
abortRef.current = ctrl;
const url = `/api/v1/perception/analyze?signal_id=${selected.id}`;
fetch(url, { signal: ctrl.signal })
.then(async res => {
if (!res.body) { setAiOutput('No stream available.'); setStreaming(false); return; }
const reader = res.body.getReader();
const dec = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = dec.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') break;
try { const j = JSON.parse(data); setAiOutput(p => p + (j.text || '')); } catch { setAiOutput(p => p + data); }
}
}
}
setStreaming(false);
})
.catch(e => { if (e.name !== 'AbortError') { setAiOutput('Analysis failed. Check API connection.'); } setStreaming(false); });
}
function stopAnalysis() {
abortRef.current?.abort();
setStreaming(false);
}
return (
<div className="perception-page">
<Topbar
title="Regulatory Signals"
subtitle="ai-powered · live feed"
actions={
<>
<div className="search-box"><input placeholder="Search signals..." /></div>
<button className="btn sm"><RefreshCw size={13} />Refresh</button>
</>
}
/>
<div className="stats-bar">
<div className="sbar-cell"><span className="sbar-val">{stats?.total ?? '—'}</span><span className="sbar-lbl">Total signals</span></div>
<div className="sbar-cell danger"><span className="sbar-val">{stats?.high_impact ?? '—'}</span><span className="sbar-lbl">High impact</span></div>
<div className="sbar-cell warn"><span className="sbar-val">{stats?.medium_impact ?? '—'}</span><span className="sbar-lbl">Medium impact</span></div>
<div className="sbar-cell accent"><span className="sbar-val">{stats?.last_90_days ?? '—'}</span><span className="sbar-lbl">Last 90 days</span></div>
</div>
<div className="filter-bar">
<div className="chip-group">
{SOURCES.map(s => (
<button key={s} className={`chip${sourceFilter === s ? ' active' : ''}`} onClick={() => setSourceFilter(s)}>{s}</button>
))}
</div>
<div className="filter-sep" />
<div className="chip-group">
{IMPACTS.map(i => (
<button key={i} className={`chip${impactFilter === i ? ' active' : ''}`} onClick={() => setImpactFilter(i)}>{i}</button>
))}
</div>
</div>
<div className="perception-split">
<div className="feed-pane">
{filtered.map(sig => (
<div key={sig.id} className={`ev-card${selected?.id === sig.id ? ' selected' : ''}`} onClick={() => { setSelected(sig); setAiOutput(''); }}>
<div className="ev-top">
<span className={`source-tag src-${sig.source.toLowerCase().replace(/[^a-z]/g,'')}`}>{sig.source}</span>
<span className="ev-std">{sig.standard}</span>
<span className={`status ${sig.status}`}>{sig.status === 'ok' ? 'Final' : sig.status === 'warn' ? 'Draft' : sig.status === 'risk' ? 'Urgent' : 'Published'}</span>
</div>
<div className="ev-title">{sig.title}</div>
<div className="ev-summary">{sig.summary}</div>
<div className="ev-bottom">
<span className="ev-date">{sig.date}</span>
<div className="ev-tags">{sig.tags.map(t => <span key={t} className="ev-tag">{t}</span>)}</div>
<span className={`impact-dot impact-${sig.impact.toLowerCase()}`}>{sig.impact}</span>
</div>
</div>
))}
</div>
<div className="analysis-pane">
{!selected ? (
<div className="analysis-empty">
<div className="empty-ring" />
<p>Select a signal to run impact analysis</p>
</div>
) : (
<>
<div className="card detail-card">
<div className="detail-header">
<span className={`source-tag src-${selected.source.toLowerCase().replace(/[^a-z]/g,'')}`}>{selected.source}</span>
<span className="ev-std">{selected.standard}</span>
<span className={`status ${selected.status}`}>{selected.status === 'risk' ? 'Urgent' : 'Published'}</span>
</div>
<div className="detail-title">{selected.title}</div>
<p className="detail-summary">{selected.summary}</p>
<div className="detail-actions">
{!streaming
? <button className="btn sm primary" onClick={runAnalysis}><Play size={12} />Run impact analysis</button>
: <button className="btn sm" onClick={stopAnalysis}><Square size={12} />Stop</button>
}
<button className="btn sm"><ExternalLink size={12} />Source</button>
</div>
</div>
<div className="card docs-card">
<div className="card-header">Affected documents</div>
{MOCK_DOCS.map(d => (
<div key={d.name} className="doc-row">
<span className="doc-score">{d.score}%</span>
<div className="doc-info">
<div className="doc-name">{d.name} <span className="doc-clause">{d.clause}</span></div>
<div className="doc-snippet">{d.snippet}</div>
</div>
</div>
))}
</div>
{(aiOutput || streaming) && (
<div className="card ai-card">
<div className="card-header">AI Impact Analysis</div>
<div className="ai-output">
{aiOutput}
{streaming && <span className="blink-cursor"></span>}
</div>
</div>
)}
</>
)}
</div>
</div>
<footer className="page-footer">
<div className="live-dot" />
<span>Live feed · Regulation Hub</span>
</footer>
</div>
);
}
```
- [ ] **Step 2: Add Perception page CSS to globals.css**
Append:
```css
/* ── Perception Page ────────────────────────────── */
.perception-page { display: flex; flex-direction: column; height: 100%; }
.stats-bar { display: flex; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
.sbar-cell { flex: 1; padding: 14px 22px; border-right: 1px solid var(--border); display: flex; flex-direction: column; gap: 3px; }
.sbar-cell:last-child { border-right: none; }
.sbar-val { font-size: 22px; font-weight: 700; font-family: var(--font-display); }
.sbar-lbl { font-size: 11px; color: var(--muted); }
.sbar-cell.danger .sbar-val { color: var(--danger); }
.sbar-cell.warn .sbar-val { color: var(--warn); }
.sbar-cell.accent .sbar-val { color: var(--accent); }
.filter-bar { display: flex; align-items: center; gap: 12px; padding: 10px 22px; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
.chip-group { display: flex; gap: 6px; flex-wrap: wrap; }
.chip { height: 26px; padding: 0 10px; border-radius: var(--radius-pill); border: 1px solid var(--border); background: transparent; font-size: 11px; font-weight: 500; cursor: pointer; color: var(--muted); transition: all 0.12s; }
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); font-weight: 600; }
.filter-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
.perception-split { display: grid; grid-template-columns: 360px 1fr; flex: 1; overflow: hidden; }
.feed-pane { overflow-y: auto; border-right: 1px solid var(--border); display: flex; flex-direction: column; gap: 0; }
.ev-card { padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.12s; border-left: 3px solid transparent; }
.ev-card:hover { background: var(--bg); }
.ev-card.selected { border-left-color: var(--accent); background: var(--accent-dim); box-shadow: inset 0 0 0 1px var(--accent-dim); }
.ev-top { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; }
.ev-std { font-size: 10px; font-family: var(--font-mono); color: var(--muted); }
.ev-title { font-size: 13px; font-weight: 600; line-height: 1.35; margin-bottom: 5px; }
.ev-summary { font-size: 12px; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 8px; }
.ev-bottom { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ev-date { font-size: 10px; font-family: var(--font-mono); color: var(--muted); }
.ev-tags { display: flex; gap: 4px; flex-wrap: wrap; }
.ev-tag { font-size: 10px; padding: 1px 6px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-pill); color: var(--muted); }
.impact-dot { font-size: 10px; font-weight: 700; margin-left: auto; }
.impact-high { color: var(--danger); }
.impact-medium { color: var(--warn); }
.impact-low { color: var(--success); }
.source-tag { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: var(--radius-pill); background: var(--accent-dim); color: var(--accent); }
.analysis-pane { overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.analysis-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; color: var(--muted); font-size: 13px; }
.empty-ring { width: 56px; height: 56px; border-radius: 50%; border: 2px dashed var(--border); }
.detail-card { display: flex; flex-direction: column; gap: 10px; }
.detail-header { display: flex; align-items: center; gap: 8px; }
.detail-title { font-size: 15px; font-weight: 700; font-family: var(--font-display); }
.detail-summary { font-size: 13px; color: var(--muted); line-height: 1.6; }
.detail-actions { display: flex; gap: 8px; padding-top: 4px; }
.docs-card { display: flex; flex-direction: column; gap: 0; }
.doc-row { display: flex; gap: 10px; padding: 10px 0; border-top: 1px solid var(--border); }
.doc-score { font-size: 11px; font-weight: 700; font-family: var(--font-mono); color: var(--success); width: 34px; flex-shrink: 0; padding-top: 1px; }
.doc-name { font-size: 12px; font-weight: 600; margin-bottom: 3px; }
.doc-clause { font-size: 10px; font-family: var(--font-mono); color: var(--muted); margin-left: 5px; }
.doc-snippet { font-size: 11px; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ai-card .ai-output { font-size: 13px; line-height: 1.7; white-space: pre-wrap; font-family: var(--font-mono); }
.blink-cursor { animation: blink 1s step-end infinite; }
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/pages/Perception/PerceptionPage.tsx frontend/src/styles/globals.css
git commit -m "feat: implement Regulatory Signals page with two-pane split and SSE streaming"
```
---
## Task 9: Implement Documents page
**Files:**
- Modify: `frontend/src/pages/Docs/DocsPage.tsx`
Refer to `Prototype/cc29bcb0-.../document-management.html` for table columns and filter bar.
- [ ] **Step 1: Write DocsPage.tsx**
```tsx
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<string, string> = { ok: 'Ready', warn: 'Embedding', risk: 'Failed', info: 'Pending' };
const STATUS_MAP: Record<string, string> = { 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<Set<string>>(new Set());
const [docs, setDocs] = useState<Doc[]>(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)));
}
return (
<div className="docs-page">
<Topbar
title="Document Management"
actions={
<>
<div className="search-box"><Search size={13} /><input placeholder="Search documents..." value={search} onChange={e => setSearch(e.target.value)} /></div>
<button className="btn sm primary"><Upload size={13} />Upload document</button>
</>
}
/>
<div className="page-content">
<div className="docs-controls">
<div className="chip-group">
{STATUS_FILTERS.map(f => (
<button key={f} className={`chip${statusF === f ? ' active' : ''}`} onClick={() => setStatusF(f)}>{f}</button>
))}
</div>
<select className="select-input" value={typeF} onChange={e => setTypeF(e.target.value)}>
{TYPE_OPTS.map(o => <option key={o}>{o}</option>)}
</select>
</div>
{selected.size > 0 && (
<div className="batch-bar">
<span>{selected.size} document{selected.size > 1 ? 's' : ''} selected</span>
<button className="btn sm">Analyze selected</button>
<button className="btn sm risk-btn">Delete selected</button>
</div>
)}
<div className="docs-table">
<div className="table-header">
<input type="checkbox" checked={selected.size === filtered.length && filtered.length > 0} onChange={toggleAll} />
<span>Document name</span>
<span>Status</span>
<span>Uploaded</span>
<span>Chunks</span>
<span>Type</span>
<span>Actions</span>
</div>
{filtered.map(d => (
<div key={d.id} className={`table-row${selected.has(d.id) ? ' row-selected' : ''}`}>
<input type="checkbox" checked={selected.has(d.id)} onChange={() => {
const s = new Set(selected);
s.has(d.id) ? s.delete(d.id) : s.add(d.id);
setSelected(s);
}} />
<span className="doc-name-cell">{d.name}</span>
<span><span className={`status ${d.status}`}>{STATUS_LABEL[d.status]}</span></span>
<span className="cell-mono">{d.uploadedAt}</span>
<span className="cell-mono">{d.chunks || '—'}</span>
<span className="cell-muted">{d.type}</span>
<span className="row-actions">
<button className="text-link">Inspect</button>
<button className="text-link">Analyze</button>
{d.status === 'risk' && <button className="text-link danger-link">Resolve</button>}
</span>
</div>
))}
</div>
</div>
</div>
);
}
```
- [ ] **Step 2: Add Documents page CSS to globals.css**
Append:
```css
/* ── Documents Page ─────────────────────────────── */
.docs-page { display: flex; flex-direction: column; height: 100%; }
.docs-controls { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.select-input { height: 28px; padding: 0 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); font-size: 12px; color: var(--fg); outline: none; cursor: pointer; }
.batch-bar { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--accent-dim); border: 1px solid var(--accent); border-radius: var(--radius-sm); margin-bottom: 12px; font-size: 13px; color: var(--accent); font-weight: 600; }
.risk-btn { color: var(--danger); border-color: var(--danger-bg); }
.docs-table { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); overflow: hidden; }
.table-header {
display: grid;
grid-template-columns: 28px 1.4fr 0.8fr 0.85fr 0.85fr 0.75fr 0.75fr;
gap: 12px;
padding: 10px 14px;
background: var(--bg);
border-bottom: 1px solid var(--border);
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted);
align-items: center;
}
.table-row {
display: grid;
grid-template-columns: 28px 1.4fr 0.8fr 0.85fr 0.85fr 0.75fr 0.75fr;
gap: 12px;
padding: 11px 14px;
border-bottom: 1px solid var(--border);
font-size: 13px;
align-items: center;
transition: background 0.1s;
}
.table-row:last-child { border-bottom: none; }
.table-row:hover { background: var(--bg); }
.table-row.row-selected { background: var(--accent-dim); }
.doc-name-cell { font-weight: 500; }
.cell-mono { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.cell-muted { font-size: 12px; color: var(--muted); }
.row-actions { display: flex; gap: 10px; }
.text-link { background: none; border: none; font-size: 12px; color: var(--accent); cursor: pointer; font-weight: 500; padding: 0; }
.text-link:hover { text-decoration: underline; }
.danger-link { color: var(--danger); }
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/pages/Docs/DocsPage.tsx frontend/src/styles/globals.css
git commit -m "feat: implement Document Management page with filterable table and batch actions"
```
---
## Task 10: Implement Compliance Analysis page
**Files:**
- Modify: `frontend/src/pages/Compliance/CompliancePage.tsx`
Refer to `Prototype/cc29bcb0-.../compliance-analysis.html` for three-column layout.
- [ ] **Step 1: Write CompliancePage.tsx**
```tsx
import { Topbar } from '../../components/layout/Topbar';
import { Search, Plus } from 'lucide-react';
const SOURCES = [
{ standard: 'EU AI Act', helper: 'Art. 9 — Risk management', scores: ['Art. 9.1', 'Art. 9.2'], status: 'risk' },
{ standard: 'MIIT Draft 2025-08', helper: '§3 — Training data provenance', scores: ['§3.1', '§3.4'], status: 'warn' },
{ standard: 'ISO/SAE 21434:2021', helper: 'Clause 9 — CSMS', scores: ['9.3', '9.4'], status: 'ok' },
];
const STAGES = [
{ label: 'Clause retrieval', pct: 100, status: 'ok' },
{ label: 'Requirement extraction', pct: 100, status: 'ok' },
{ label: 'Gap analysis', pct: 78, status: 'warn' },
{ label: 'Recommendation synthesis', pct: 30, status: 'info' },
];
const FINDINGS = [
{ title: 'Missing risk management documentation', desc: 'No formal risk management system found for the described AI system scope under Art. 9.', status: 'risk' },
{ title: 'Training data lineage incomplete', desc: 'MIIT §3.1 requires traceable provenance for training datasets. Current documentation lacks data source registry.', status: 'warn' },
{ title: 'CSMS audit trail present', desc: 'ISO 21434 audit log requirements are met. Retention policy documented in Annex B.', status: 'ok' },
];
const PARA = `The AI system described in Section 4.2.1 of the Vehicle AI Safety Manual performs real-time classification of driving scenarios to support Level 3 automated driving decisions. The system ingests sensor fusion data from cameras, LIDAR, and radar arrays, processes it through a deep neural network trained on 2.4M annotated driving scenarios, and outputs driving mode recommendations with associated confidence scores. The model was trained using data collected between 2022 and 2024 across European and Chinese road environments.`;
export function CompliancePage() {
return (
<div className="compliance-page">
<Topbar
title="Compliance Analysis"
actions={
<>
<div className="search-box"><Search size={13} /><input placeholder="Search analyses..." /></div>
<button className="btn sm primary"><Plus size={13} />New analysis</button>
</>
}
/>
<div className="compliance-hero">
<p className="hero-eyebrow">Compliance Workspace</p>
<h2 className="compliance-title">Document Paragraph Review</h2>
<p className="compliance-desc">Three-column AI-assisted compliance gap analysis with regulation retrieval, paragraph review, and findings synthesis.</p>
</div>
<div className="compliance-workspace">
<div className="comp-col source-col">
<div className="col-header">Retrieved Regulations</div>
{SOURCES.map(s => (
<div key={s.standard} className="source-item card">
<div className="source-top">
<span className="source-std">{s.standard}</span>
<span className={`status ${s.status}`}>{s.status === 'ok' ? 'Covered' : s.status === 'warn' ? 'Gap' : 'Critical'}</span>
</div>
<div className="source-helper">{s.helper}</div>
<div className="source-scores">
{s.scores.map(sc => <span key={sc} className="score-pill">{sc}</span>)}
</div>
</div>
))}
</div>
<div className="comp-col review-col">
<div className="col-header">Paragraph Under Review</div>
<div className="card para-card">
<p className="para-text">
{PARA.split('AI system').map((part, i, arr) =>
i < arr.length - 1
? <span key={i}>{part}<mark>AI system</mark></span>
: <span key={i}>{part}</span>
)}
</p>
</div>
<div className="card stages-card">
<div className="card-header">Analysis stages</div>
{STAGES.map(st => (
<div key={st.label} className="stage-row">
<div className="stage-label-row">
<span className="stage-label">{st.label}</span>
<span className="stage-pct">{st.pct}%</span>
</div>
<div className="stage-bar">
<div className={`stage-fill stage-${st.status}`} style={{ width: `${st.pct}%` }} />
</div>
</div>
))}
</div>
</div>
<div className="comp-col findings-col">
<div className="col-header">Findings</div>
{FINDINGS.map(f => (
<div key={f.title} className="finding-item card">
<div className="finding-top">
<span className="finding-title">{f.title}</span>
<span className={`status ${f.status}`}>{f.status === 'ok' ? 'OK' : f.status === 'warn' ? 'Gap' : 'Critical'}</span>
</div>
<p className="finding-desc">{f.desc}</p>
</div>
))}
<div className="card conclusion-box">
<div className="card-header">Conclusion</div>
<p className="conclusion-text">The document requires a formal risk management section documenting the AI system classification, risk identification methodology, and mitigation measures per EU AI Act Art. 9 before compliance can be certified.</p>
<div className="action-items">
<div className="action-item">
<span className="action-label">Next action</span>
<span className="action-value">Draft risk management annex</span>
</div>
<div className="action-item">
<span className="action-label">Escalation</span>
<span className="action-value risk-text">Legal review required</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
```
- [ ] **Step 2: Add Compliance page CSS to globals.css**
Append:
```css
/* ── Compliance Page ────────────────────────────── */
.compliance-page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.compliance-hero { padding: 20px 24px 0; flex-shrink: 0; }
.compliance-title { font-size: 18px; font-weight: 700; font-family: var(--font-display); margin-bottom: 4px; }
.compliance-desc { font-size: 13px; color: var(--muted); }
.compliance-workspace { display: grid; grid-template-columns: 0.95fr 1.25fr 0.9fr; gap: 14px; padding: 16px 24px 24px; flex: 1; overflow: hidden; min-height: 0; }
.comp-col { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }
.col-header { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); padding: 0 2px 4px; flex-shrink: 0; }
.source-item { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
.source-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.source-std { font-size: 13px; font-weight: 700; font-family: var(--font-display); }
.source-helper { font-size: 11px; color: var(--muted); }
.source-scores { display: flex; gap: 5px; flex-wrap: wrap; }
.score-pill { font-size: 10px; font-family: var(--font-mono); padding: 2px 6px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-pill); color: var(--muted); }
.para-card { flex-shrink: 0; }
.para-text { font-size: 13px; line-height: 1.7; color: var(--fg); }
.para-text mark { background: rgba(226,0,116,.15); color: var(--accent); padding: 0 2px; border-radius: 2px; }
.stages-card { flex-shrink: 0; }
.stage-row { padding: 8px 0; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 5px; }
.stage-label-row { display: flex; justify-content: space-between; align-items: center; }
.stage-label { font-size: 12px; }
.stage-pct { font-size: 11px; font-family: var(--font-mono); color: var(--muted); }
.stage-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
.stage-fill { height: 100%; border-radius: 2px; }
.stage-fill.stage-ok { background: var(--success); }
.stage-fill.stage-warn { background: var(--warn); }
.stage-fill.stage-info { background: var(--info); }
.finding-item { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
.finding-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.finding-title { font-size: 13px; font-weight: 600; line-height: 1.3; }
.finding-desc { font-size: 12px; color: var(--muted); line-height: 1.5; }
.conclusion-box { display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; }
.conclusion-text { font-size: 12px; line-height: 1.6; color: var(--fg); }
.action-items { display: flex; flex-direction: column; gap: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.action-item { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 12px; }
.action-label { color: var(--muted); }
.action-value { font-weight: 600; }
.risk-text { color: var(--danger); }
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/pages/Compliance/CompliancePage.tsx frontend/src/styles/globals.css
git commit -m "feat: implement Compliance Analysis three-column workspace with findings and stages"
```
---
## Task 11: Implement Regulation Q&A chat page
**Files:**
- Modify: `frontend/src/pages/RagChat/RagChatPage.tsx`
Refer to `Prototype/cc29bcb0-.../regulation-chat.html` for chat layout.
- [ ] **Step 1: Write RagChatPage.tsx**
```tsx
import { useState, useRef, useEffect } from 'react';
import { Topbar } from '../../components/layout/Topbar';
import { Send, Download } from 'lucide-react';
interface Message { id: string; role: 'user'|'assistant'; text: string; }
interface Citation { score: number; name: string; clause: string; snippet: string; }
const HISTORY = [
{ id: 'h1', title: 'EU AI Act Article 9 scope', date: '2025-11-18' },
{ id: 'h2', title: 'MIIT training data requirements', date: '2025-11-15' },
{ id: 'h3', title: 'ISO 21434 CSMS audit scope', date: '2025-11-10' },
];
const QUICK = [
'What does EU AI Act Art. 9 require for risk management?',
'Which documents need CSMS certification?',
'Summarize MIIT training data rules',
'What are high-risk AI categories under Annex III?',
];
const MOCK_CITATIONS: Citation[] = [
{ score: 94, name: 'EU AI Act', clause: 'Art. 9(1)', snippet: 'Providers of high-risk AI systems shall establish a risk management system consisting of a continuous iterative process run throughout the entire lifecycle.' },
{ score: 87, name: 'Vehicle AI Safety Manual', clause: '§4.2.1', snippet: 'All AI systems classified as high-risk must maintain a documented risk register with quarterly review cadence.' },
{ score: 72, name: 'ISO/SAE 21434', clause: 'Clause 9.3', snippet: 'The cybersecurity management system shall include AI model update governance procedures and audit log retention policy.' },
];
export function RagChatPage() {
const [messages, setMessages] = useState<Message[]>([
{ id: 'init', role: 'assistant', text: 'Hello! I can answer questions about your indexed regulations and compliance documents. Try asking about EU AI Act requirements, MIIT rules, or ISO/SAE 21434 scope.' }
]);
const [input, setInput] = useState('');
const [streaming, setStreaming] = useState(false);
const [citations, setCitations] = useState<Citation[]>(MOCK_CITATIONS);
const bottomRef = useRef<HTMLDivElement>(null);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
async function send(text?: string) {
const q = (text ?? input).trim();
if (!q || streaming) return;
setInput('');
const userMsg: Message = { id: Date.now().toString(), role: 'user', text: q };
setMessages(m => [...m, userMsg]);
const assistantId = (Date.now() + 1).toString();
setMessages(m => [...m, { id: assistantId, role: 'assistant', text: '' }]);
setStreaming(true);
const ctrl = new AbortController();
abortRef.current = ctrl;
try {
const res = await fetch('/api/v1/rag/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question: q }),
signal: ctrl.signal,
});
if (!res.body) throw new Error('No stream');
const reader = res.body.getReader();
const dec = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += dec.decode(value);
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') break;
try {
const j = JSON.parse(data);
if (j.text) setMessages(m => m.map(msg => msg.id === assistantId ? { ...msg, text: msg.text + j.text } : msg));
if (j.citations) setCitations(j.citations);
} catch { setMessages(m => m.map(msg => msg.id === assistantId ? { ...msg, text: msg.text + data } : msg)); }
}
}
}
} catch (e: unknown) {
if (e instanceof Error && e.name !== 'AbortError') {
setMessages(m => m.map(msg => msg.id === assistantId ? { ...msg, text: 'Could not reach the RAG API. Please check the backend.' } : msg));
}
} finally {
setStreaming(false);
}
}
return (
<div className="chat-page">
<Topbar
title="Regulation Q&A"
actions={<button className="btn sm"><Download size={13} />Export chat</button>}
/>
<div className="chat-body">
<div className="history-pane">
<div className="history-header">Chat history</div>
{HISTORY.map(h => (
<div key={h.id} className="history-item">
<div className="history-title">{h.title}</div>
<div className="history-date">{h.date}</div>
</div>
))}
<div className="quick-header">Quick prompts</div>
{QUICK.map(q => (
<button key={q} className="quick-item" onClick={() => send(q)}>{q}</button>
))}
</div>
<div className="chat-main">
<div className="messages">
{messages.map(msg => (
<div key={msg.id} className={`message msg-${msg.role}`}>
{msg.role === 'assistant' && <div className="msg-avatar">AI</div>}
<div className="msg-bubble">
{msg.text}
{streaming && msg.role === 'assistant' && msg.id === messages[messages.length - 1].id && (
<span className="blink-cursor"></span>
)}
</div>
{msg.role === 'user' && <div className="msg-avatar user-av">You</div>}
</div>
))}
<div ref={bottomRef} />
</div>
<div className="composer">
<div className="quick-chips">
{QUICK.slice(0, 3).map(q => (
<button key={q} className="chip" onClick={() => send(q)}>{q.length > 40 ? q.slice(0, 40) + '…' : q}</button>
))}
</div>
<div className="composer-row">
<textarea
className="composer-input"
placeholder="Ask about your regulations..."
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
rows={2}
/>
<button className="btn primary" onClick={() => send()} disabled={!input.trim() || streaming}>
<Send size={14} />
</button>
</div>
</div>
</div>
<div className="citation-rail">
<div className="citation-header">Sources</div>
{citations.map(c => (
<div key={c.name + c.clause} className="citation-item">
<div className="cit-score">{c.score}%</div>
<div className="cit-info">
<div className="cit-name">{c.name} <span className="cit-clause">{c.clause}</span></div>
<div className="cit-snippet">{c.snippet}</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
```
- [ ] **Step 2: Add Chat page CSS to globals.css**
Append:
```css
/* ── Chat Page ──────────────────────────────────── */
.chat-page { display: flex; flex-direction: column; height: 100%; }
.chat-body { display: grid; grid-template-columns: 280px minmax(0,1fr) 320px; flex: 1; overflow: hidden; min-height: 0; }
.history-pane { border-right: 1px solid var(--border); overflow-y: auto; padding: 14px 0; background: var(--surface); display: flex; flex-direction: column; gap: 0; }
.history-header, .quick-header { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); padding: 10px 16px 6px; }
.history-item { padding: 8px 16px; cursor: pointer; border-radius: 0; transition: background 0.1s; }
.history-item:hover { background: var(--bg); }
.history-title { font-size: 12px; font-weight: 500; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.history-date { font-size: 10px; font-family: var(--font-mono); color: var(--muted); margin-top: 2px; }
.quick-item { background: none; border: none; text-align: left; padding: 8px 16px; font-size: 12px; color: var(--muted); cursor: pointer; line-height: 1.4; transition: color 0.1s, background 0.1s; }
.quick-item:hover { color: var(--accent); background: var(--accent-dim); }
.chat-main { display: flex; flex-direction: column; overflow: hidden; }
.messages { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 14px; }
.message { display: flex; gap: 10px; align-items: flex-end; }
.msg-user { flex-direction: row-reverse; }
.msg-avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; flex-shrink: 0; background: var(--accent-dim); color: var(--accent); }
.msg-avatar.user-av { background: var(--bg); color: var(--muted); border: 1px solid var(--border); }
.msg-bubble { max-width: 72%; padding: 10px 14px; border-radius: 14px; font-size: 13px; line-height: 1.6; }
.msg-assistant .msg-bubble { background: var(--surface); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
.msg-user .msg-bubble { background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
.composer { padding: 12px 16px; border-top: 1px solid var(--border); background: var(--surface); display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
.quick-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.composer-row { display: flex; gap: 8px; align-items: flex-end; }
.composer-input { flex: 1; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); font-size: 13px; font-family: var(--font-body); color: var(--fg); resize: none; outline: none; line-height: 1.5; }
.composer-input:focus { border-color: var(--accent); }
.citation-rail { border-left: 1px solid var(--border); overflow-y: auto; padding: 14px 0; background: var(--surface); }
.citation-header { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); padding: 0 16px 8px; }
.citation-item { display: flex; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--border); }
.cit-score { font-size: 11px; font-weight: 700; font-family: var(--font-mono); color: var(--success); width: 34px; flex-shrink: 0; padding-top: 1px; }
.cit-name { font-size: 12px; font-weight: 600; margin-bottom: 3px; }
.cit-clause { font-size: 10px; font-family: var(--font-mono); color: var(--muted); margin-left: 5px; }
.cit-snippet { font-size: 11px; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.5; }
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/pages/RagChat/RagChatPage.tsx frontend/src/styles/globals.css
git commit -m "feat: implement Regulation Q&A chat page with history pane, streaming, citation rail"
```
---
## Task 12: Final build verification and cleanup
**Files:** No new files.
- [ ] **Step 1: Run full TypeScript build**
```bash
cd frontend && npm run build 2>&1
```
Expected: Zero TypeScript errors, zero import errors.
- [ ] **Step 2: Start dev server and verify all 6 pages render**
```bash
cd frontend && npm run dev
```
Navigate to each route and confirm no console errors:
- `http://localhost:5173/` — Overview
- `http://localhost:5173/status` — System Status
- `http://localhost:5173/signals` — Regulatory Signals
- `http://localhost:5173/documents` — Documents
- `http://localhost:5173/compliance` — Compliance Analysis
- `http://localhost:5173/chat` — Regulation Q&A
- [ ] **Step 3: Verify sidebar nav active states**
Click each nav item. Confirm active left-border highlight and background tint appear correctly.
- [ ] **Step 4: Verify dark mode toggle**
Click the moon/sun icon in the sidebar footer. Confirm `data-theme="dark"` is set on `<html>` and colors invert correctly.
- [ ] **Step 5: Final commit**
```bash
git add -A
git commit -m "feat: complete frontend redesign — all 6 pages implemented per prototype specs"
```