Files
AIRegulation-DocAnalysis/Prototype/regulation-hub-export.html

1060 lines
59 KiB
HTML
Raw Permalink Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>T-Systems Regulation Hub</title>
<style>
/* ═══════════════════════════════════════════════════════════════════════
DESIGN TOKENS
Note for developers: swap these six rail + six canvas tokens to re-skin.
Accent is always #e20074 (T-Systems Magenta).
═══════════════════════════════════════════════════════════════════════ */
:root {
/* Sidebar rail */
--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);
/* Content canvas */
--bg: #f2f4f7;
--surface: #ffffff;
--fg: #111827;
--muted: #6b7280;
--border: #e5e7eb;
/* Brand */
--accent: #e20074;
--accent-dim: rgba(226,0,116,.10);
--accent-hover: #c8006a;
--success: #16a34a;
--warn: #d97706;
--danger: #dc2626;
/* Typography */
--font-display: "TeleNeoWeb-Bold","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
--font-body: "TeleNeoWeb-Regular","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
--font-mono: ui-monospace,"JetBrains Mono",Menlo,monospace;
/* Geometry */
--radius-sm: 6px;
--radius-md: 10px;
--radius-pill: 9999px;
--sidebar-w: 232px;
--shadow-card: 0 1px 4px rgba(0,0,0,.06), 0 0 0 1px rgba(0,0,0,.04);
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
}
/* ─── Dark mode ────────────────────────────────────────────────────── */
[data-theme="dark"] {
--rail-bg: #1a1c22;
--rail-surface: #22242c;
--rail-fg: #f0f2f5;
--rail-muted: #7a8390;
--rail-border: #2d3038;
--rail-hover: rgba(255,255,255,.05);
--rail-active: rgba(226,0,116,.12);
--bg: #111318;
--surface: #1a1c22;
--fg: #f0f2f5;
--muted: #7a8390;
--border: #2d3038;
}
[data-theme="dark"] body { color-scheme: dark; }
[data-theme="dark"] .topbar { background: rgba(17,19,24,.9); }
[data-theme="dark"] .filter-bar,
[data-theme="dark"] .stats-bar-flush { background: var(--surface); }
[data-theme="dark"] .stat-cell { background: var(--surface); }
[data-theme="dark"] .ev-card { background: var(--surface); }
[data-theme="dark"] .detail-card,
[data-theme="dark"] .output-card,
[data-theme="dark"] .docs-card { background: var(--surface); }
[data-theme="dark"] .kpi { background: #1e2028; }
[data-theme="dark"] .task-row,
[data-theme="dark"] .program-row,
[data-theme="dark"] .event-row { background: #1e2028; }
/* ─── Reset ─────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; }
html, body { height: 100%; }
body {
background: var(--bg); color: var(--fg);
font-family: var(--font-body); font-size: 14px; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
h1,h2,h3,h4 { font-family: var(--font-display); line-height: 1.2; letter-spacing: -0.015em; text-wrap: balance; }
a { color: inherit; text-decoration: none; }
button, input, select { font: inherit; cursor: pointer; }
/* ═══════════════════════════════════════════════════════════════════════
APP SHELL
═══════════════════════════════════════════════════════════════════════ */
.app-shell { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
/* ═══════════════════════════════════════════════════════════════════════
SIDEBAR
═══════════════════════════════════════════════════════════════════════ */
.sidebar {
position: sticky; top: 0; height: 100vh;
overflow-y: auto; overflow-x: hidden;
display: flex; flex-direction: column;
background: var(--rail-bg);
border-right: 1px solid var(--rail-border);
z-index: 20;
}
.sidebar-brand {
display: flex; align-items: center; gap: 10px;
height: 54px; padding: 0 16px;
border-bottom: 1px solid var(--rail-border); flex-shrink: 0;
}
.brand-logo {
width: 28px; height: 28px; background: var(--accent);
border-radius: 7px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.brand-logo svg { color: #fff; }
.brand-name { font-family: var(--font-display); font-size: 13px; color: var(--rail-fg); font-weight: 700; line-height: 1.2; }
.brand-sub { font-size: 10px; color: var(--rail-muted); font-family: var(--font-mono); letter-spacing: .04em; margin-top: 2px; }
.sidebar-nav { flex: 1; padding: 10px 0; }
.nav-group { padding: 0 8px 4px; }
.nav-group + .nav-group { margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--rail-border); }
.nav-group-label {
display: block; font-family: var(--font-mono); font-size: 10px;
text-transform: uppercase; letter-spacing: .12em;
color: var(--rail-muted); padding: 0 8px 6px;
}
.nav-item {
display: flex; align-items: center; gap: 9px;
height: 34px; padding: 0 8px;
border-radius: var(--radius-sm); color: #4b5563;
font-size: 13px; position: relative;
transition: background 120ms, color 120ms;
}
.nav-item:hover { background: var(--rail-hover); color: var(--rail-fg); text-decoration: none; }
.nav-item.active { background: var(--rail-active); color: var(--accent); font-weight: 600; }
.nav-item.active::before {
content: ""; position: absolute; left: 0; top: 6px; bottom: 6px;
width: 3px; border-radius: 0 3px 3px 0; background: var(--accent);
}
.nav-icon { width: 15px; height: 15px; flex-shrink: 0; opacity: .55; }
.nav-item:hover .nav-icon, .nav-item.active .nav-icon { opacity: 1; }
.nav-badge {
margin-left: auto; min-width: 18px; height: 17px; padding: 0 5px;
border-radius: var(--radius-pill);
background: var(--accent-dim); color: var(--accent);
font-size: 10px; font-family: var(--font-mono); font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.sidebar-footer { border-top: 1px solid var(--rail-border); padding: 10px 8px; flex-shrink: 0; }
.sidebar-user {
display: flex; align-items: center; gap: 9px;
padding: 8px; border-radius: var(--radius-sm); cursor: pointer;
transition: background 120ms;
}
.sidebar-user:hover { background: var(--rail-hover); }
.avatar {
width: 28px; height: 28px; border-radius: 50%;
background: var(--accent); color: #fff;
font-size: 11px; font-weight: 700;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.user-name { font-size: 12px; font-weight: 600; color: var(--rail-fg); }
.user-role { font-size: 10px; color: var(--rail-muted); font-family: var(--font-mono); margin-top: 1px; }
.sidebar-action {
display: flex; align-items: center; gap: 9px;
height: 32px; padding: 0 8px; border-radius: var(--radius-sm);
color: var(--rail-muted); font-size: 12px;
border: none; background: transparent; width: 100%; text-align: left;
transition: background 120ms, color 120ms;
}
.sidebar-action:hover { background: var(--rail-hover); color: var(--rail-fg); }
/* ═══════════════════════════════════════════════════════════════════════
CONTENT AREA SHARED
═══════════════════════════════════════════════════════════════════════ */
.content-area { display: flex; flex-direction: column; min-width: 0; min-height: 100vh; }
/* Page views — JS toggles .view-active */
.page-view { display: none; flex-direction: column; flex: 1; }
.page-view.view-active { display: flex; }
.topbar {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; gap: 10px;
height: 54px; padding: 0 24px;
border-bottom: 1px solid var(--border);
background: rgba(242,244,247,.9);
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
flex-shrink: 0;
}
[data-theme="dark"] .topbar { background: rgba(17,19,24,.9); }
.topbar-title { font-weight: 600; font-size: 14px; flex: 1; color: var(--fg); }
.topbar-sub { color: var(--muted); font-family: var(--font-mono); font-size: 10px; margin-left: 4px; font-weight: 400; }
.search {
display: flex; align-items: center; gap: 8px;
border: 1px solid var(--border); border-radius: var(--radius-sm);
background: var(--surface); padding: 0 10px; height: 32px; width: 240px;
transition: border-color 140ms;
}
.search:focus-within { border-color: color-mix(in oklab, var(--accent), transparent 60%); }
.search input { border: 0; outline: none; background: transparent; width: 100%; color: var(--fg); font-size: 13px; }
.search input::placeholder { color: var(--muted); }
.btn {
height: 32px; border-radius: var(--radius-sm); border: 1px solid var(--border);
padding: 0 12px; background: var(--surface); color: var(--fg);
font-size: 13px; font-weight: 500;
transition: background 120ms, border-color 120ms;
white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;
}
.btn:hover { background: var(--bg); border-color: #b0b7c0; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-primary:disabled { opacity: .45; cursor: not-allowed; }
.btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
.footer {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
min-height: 34px; padding: 0 20px; border-top: 1px solid var(--border);
color: var(--muted); font-size: 10px; font-family: var(--font-mono);
letter-spacing: .1em; text-transform: uppercase; flex-shrink: 0;
}
.footer-live { display: inline-flex; align-items: center; gap: 7px; }
.footer-dot {
width: 6px; height: 6px; border-radius: 50%;
background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2);
}
/* Shared utility */
.status {
display: inline-flex; align-items: center; gap: 5px;
padding: 2px 8px; border-radius: var(--radius-pill);
font-size: 11px; font-family: var(--font-mono); font-weight: 600;
width: fit-content; white-space: nowrap;
}
.status::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.status.ok { color: var(--success); background: color-mix(in oklab,var(--success),transparent 90%); }
.status.warn { color: var(--warn); background: color-mix(in oklab,var(--warn),transparent 90%); }
.status.risk { color: var(--danger); background: color-mix(in oklab,var(--danger),transparent 90%); }
.status.info { color: #3b82f6; background: color-mix(in oklab,#3b82f6,transparent 90%); }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; }
.note { font-size: 12px; color: var(--muted); line-height: 1.5; }
.ghost-link { color: var(--muted); font-size: 12px; padding: 2px 0; border-radius: 4px; transition: color 120ms; }
.ghost-link:hover { color: var(--fg); text-decoration: none; }
.pill {
display: inline-flex; align-items: center; height: 20px; padding: 0 8px;
border-radius: var(--radius-pill); border: 1px solid var(--border);
color: var(--muted); font-size: 10px; font-family: var(--font-mono); font-weight: 500;
}
.eyebrow { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--accent); margin-bottom: 6px; }
/* ═══════════════════════════════════════════════════════════════════════
VIEW 1 — SYSTEM STATUS (dashboard-sidebar)
═══════════════════════════════════════════════════════════════════════ */
.page { padding: 20px; display: grid; gap: 18px; flex: 1; }
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.page-head h1 { font-size: clamp(20px, 2.4vw, 28px); }
.page-head-desc { margin-top: 4px; font-size: 13px; color: var(--muted); max-width: 520px; line-height: 1.55; }
.card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 16px 18px; }
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; }
.stat-card { border-top: 2px solid var(--border); transition: border-color 200ms; }
.stat-card:hover { border-top-color: var(--accent); }
.stat-card .s-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
.stat-card .s-value { margin-top: 10px; font-size: 32px; line-height: 1; font-family: var(--font-display); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; color: var(--fg); }
.stat-card .s-sub { margin-top: 8px; font-size: 11px; color: var(--muted); line-height: 1.5; }
.panel-grid { display: grid; grid-template-columns: 1.4fr 0.9fr; gap: 18px; }
.stack { display: grid; gap: 18px; }
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.section-head h2 { font-size: 16px; }
.task-list, .program-list, .event-list { display: grid; gap: 8px; }
.task-row, .program-row, .event-row {
display: grid; gap: 10px; border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 10px 12px;
background: var(--surface); transition: border-color 120ms, box-shadow 120ms;
}
.task-row:hover, .program-row:hover { border-color: #c8cdd8; box-shadow: var(--shadow-sm); }
.task-row { grid-template-columns: 1.6fr 0.8fr 0.8fr 0.6fr; align-items: center; }
.program-row { grid-template-columns: 1fr auto; align-items: start; }
.event-row { grid-template-columns: 76px 1fr; align-items: start; }
.kpi-strip { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; margin-top: 12px; }
.kpi { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; background: #f8f9fb; }
.kpi strong { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 17px; color: var(--fg); }
.kpi-label { font-size: 11px; color: var(--muted); margin-bottom: 4px; }
.meter { height: 3px; border-radius: 999px; background: #e5e7eb; overflow: hidden; margin-top: 8px; }
.meter > span { display: block; height: 100%; background: var(--accent); border-radius: inherit; }
.task-cta {
display: inline-flex; align-items: center; gap: 4px;
height: 26px; padding: 0 10px; border-radius: var(--radius-sm);
border: 1px solid var(--border); background: transparent; color: var(--muted);
font-size: 11px; white-space: nowrap; cursor: pointer;
transition: border-color 120ms, color 120ms, background 120ms;
}
.task-cta:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); text-decoration: none; }
/* ═══════════════════════════════════════════════════════════════════════
VIEW 2 — REGULATORY SIGNALS (perception)
═══════════════════════════════════════════════════════════════════════ */
.stats-bar-flush {
display: grid; grid-template-columns: repeat(4,1fr);
gap: 1px; background: var(--border);
border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.stat-cell { background: var(--surface); padding: 14px 20px; }
.stat-cell .s-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
.stat-cell .s-value { margin-top: 6px; font-size: 28px; line-height: 1; font-family: var(--font-display); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.stat-cell .s-sub { margin-top: 4px; font-size: 11px; color: var(--muted); }
.filter-bar {
display: flex; align-items: center; gap: 6px;
padding: 10px 20px; border-bottom: 1px solid var(--border);
background: var(--surface); flex-shrink: 0; flex-wrap: wrap;
}
.filter-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.chip {
padding: 3px 10px; border-radius: var(--radius-pill); border: 1px solid var(--border);
background: transparent; color: var(--muted);
font-size: 11px; font-family: var(--font-mono); transition: all 100ms;
}
.chip:hover { border-color: var(--fg); color: var(--fg); }
.chip.on { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); font-weight: 600; }
.sep { width: 1px; height: 16px; background: var(--border); flex-shrink: 0; margin: 0 2px; }
.perception-split { flex: 1; display: grid; grid-template-columns: 360px 1fr; min-height: 0; }
.feed-pane { display: flex; flex-direction: column; border-right: 1px solid var(--border); min-height: 0; background: var(--bg); }
.feed-pane-head {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px 10px; border-bottom: 1px solid var(--border);
background: var(--surface); flex-shrink: 0;
}
.feed-pane-head h2 { font-size: 13px; font-weight: 700; }
.feed-count { font-family: var(--font-mono); font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
.feed-scroll { flex: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 6px; }
.feed-scroll::-webkit-scrollbar { width: 4px; }
.feed-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.ev-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-md); padding: 11px 13px;
cursor: pointer; transition: border-color 100ms, box-shadow 100ms;
}
.ev-card:hover { border-color: #c4c9d4; }
.ev-card.selected { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
.ev-head { display: flex; align-items: center; gap: 6px; margin-bottom: 7px; }
.src-tag { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }
.std-code { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
.ev-title { font-weight: 600; font-size: 12px; line-height: 1.4; margin-bottom: 4px; }
.ev-summary { font-size: 11px; color: var(--muted); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ev-foot { display: flex; align-items: center; gap: 6px; margin-top: 7px; }
.ev-date { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
.ev-tag { font-size: 10px; font-family: var(--font-mono); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border); color: var(--muted); }
.imp-dot { margin-left: auto; font-size: 10px; font-family: var(--font-mono); }
.loading-msg { font-family: var(--font-mono); font-size: 12px; color: var(--muted); text-align: center; padding: 40px 0; }
.analysis-pane { display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: 16px 20px; gap: 12px; }
.analysis-pane::-webkit-scrollbar { width: 4px; }
.analysis-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.analysis-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; text-align: center; color: var(--muted); min-height: 300px; }
.analysis-empty-ring { width: 40px; height: 40px; border-radius: 50%; border: 1.5px solid var(--border); display: flex; align-items: center; justify-content: center; }
.analysis-empty-label { font-size: 13px; }
.analysis-empty-hint { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
.detail-card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 14px 16px; }
.detail-head { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
.detail-title { font-weight: 700; font-size: 14px; line-height: 1.3; margin-bottom: 5px; }
.detail-summary { font-size: 13px; color: var(--muted); line-height: 1.6; }
.detail-meta { display: flex; gap: 14px; margin-top: 10px; flex-wrap: wrap; }
.meta-item { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
.meta-item strong { color: var(--fg); }
.action-row { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
.output-card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 14px 16px; display: none; }
.output-head { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin-bottom: 10px; display: flex; align-items: center; gap: 7px; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
.blink { animation: blink 1s step-end infinite; }
.md-h2 { font-size: 13px; font-weight: 700; color: var(--accent); margin: 14px 0 5px; font-family: var(--font-display); }
.md-h3 { font-size: 12px; font-weight: 700; margin: 10px 0 3px; }
.md-li { display: flex; gap: 7px; margin-bottom: 3px; padding-left: 3px; font-size: 12px; line-height: 1.6; }
.md-li-dot { color: var(--accent); flex-shrink: 0; }
.md-p { font-size: 12px; line-height: 1.7; margin-bottom: 3px; }
.md-empty { height: 5px; }
.docs-card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 12px 16px; display: none; }
.docs-head { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin-bottom: 8px; }
.doc-row { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0; border-top: 1px solid var(--border); }
.doc-row:first-of-type { border-top: none; }
.doc-score { font-family: var(--font-mono); font-size: 11px; color: var(--accent); font-weight: 700; flex-shrink: 0; width: 32px; }
.doc-name { font-size: 12px; font-weight: 600; line-height: 1.4; }
.doc-clause { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
.doc-snippet { font-size: 11px; color: var(--muted); line-height: 1.5; margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* ═══════════════════════════════════════════════════════════════════════
RESPONSIVE
═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 1200px) {
.stats-grid { grid-template-columns: repeat(2,1fr); }
.panel-grid { grid-template-columns: 1fr; }
.kpi-strip { grid-template-columns: 1fr 1fr; }
.task-row { grid-template-columns: 1fr auto; }
}
@media (max-width: 1100px) {
.stats-bar-flush { grid-template-columns: 1fr 1fr; }
.perception-split { grid-template-columns: 300px 1fr; }
}
@media (max-width: 800px) {
.app-shell { grid-template-columns: 1fr; }
.sidebar { display: none; }
.stats-grid, .kpi-strip { grid-template-columns: 1fr 1fr; }
.stats-bar-flush { grid-template-columns: 1fr 1fr; }
.perception-split { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="app-shell">
<!-- ═══════════════════════════════════════════════════════════════════════
SIDEBAR
═══════════════════════════════════════════════════════════════════════ -->
<aside class="sidebar" aria-label="Primary navigation">
<div class="sidebar-brand">
<div class="brand-logo">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M2 3.5h12v1.5H2zm5.25 1.5h1.5v8h-1.5zm-3 2h2.25v1.5H4.25zm5.25 0h2.25v1.5H9.5zm-3 3h2.5v1.5H6.5z" fill="currentColor"/>
</svg>
</div>
<div>
<div class="brand-name">T-Systems</div>
<div class="brand-sub">Regulation Hub</div>
</div>
</div>
<nav class="sidebar-nav" aria-label="Primary">
<div class="nav-group">
<span class="nav-group-label">Main</span>
<a class="nav-item" href="#" onclick="navigate('overview');return false;">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M2 2h5v5H2zm7 0h5v5H9zM2 9h5v5H2zm7 0h5v5H9z" fill="currentColor" opacity=".6"/></svg>
Overview
</a>
<a class="nav-item" id="nav-signals" href="#" onclick="navigate('signals');return false;">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="2.5" fill="currentColor"/>
<path d="M8 2.5C4.91 2.5 2.5 5.42 2.5 8S4.91 13.5 8 13.5 13.5 10.58 13.5 8 11.09 2.5 8 2.5zm0 9.5C5.52 12 3.5 10.24 3.5 8S5.52 4 8 4s4.5 1.76 4.5 4-2.02 4-4.5 4z" fill="currentColor" opacity=".45"/>
</svg>
Regulatory Signals
<span class="nav-badge" id="badge-high"></span>
</a>
<a class="nav-item active" id="nav-status" href="#" onclick="navigate('status');return false;">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M1.5 2.5h13v1H1.5zm0 3h13v1H1.5zm0 3h8v1h-8zm0 3h6v1h-6z" fill="currentColor"/></svg>
System Status
<span class="nav-badge">3</span>
</a>
</div>
<div class="nav-group">
<span class="nav-group-label">Workbench</span>
<a class="nav-item" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-management.html">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M3 1h7l3 3v11H3V1zm1 1v12h8V5h-3V2H4zm5 .5V4h1.5L9 1.5zM6 7h4v1H6zm0 2h4v1H6zm0 2h3v1H6z" fill="currentColor"/></svg>
Documents
</a>
<a class="nav-item" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/compliance-analysis.html">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M8 1l7 3-1 6a7 7 0 01-6 5A7 7 0 011 10L0 4l8-3zm0 1.2L1.3 4.8l.8 5.1A6 6 0 008 14.8a6 6 0 005.9-4.9l.8-5.1L8 2.2zM7.5 5h1v4.5h-1V5zm0 5.5h1v1h-1v-1z" fill="currentColor"/></svg>
Compliance Analysis
</a>
</div>
<div class="nav-group">
<span class="nav-group-label">Chat</span>
<a class="nav-item" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/regulation-chat.html">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M2 2h12a1 1 0 011 1v8a1 1 0 01-1 1H5l-3 2.5V3a1 1 0 011-1zm0 1v9.5L4.5 11H14V3H2zm2 2h8v1H4zm0 2h6v1H4z" fill="currentColor"/></svg>
Regulation Q&amp;A
</a>
</div>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="avatar">TS</div>
<div>
<div class="user-name">T-Systems User</div>
<div class="user-role">Compliance Analyst</div>
</div>
</div>
<button class="sidebar-action" type="button" onclick="toggleTheme()">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3a5 5 0 100 10A5 5 0 008 3zM2 8a6 6 0 1112 0A6 6 0 012 8z" fill="currentColor"/></svg>
<span id="theme-label">Dark mode</span>
</button>
</div>
</aside>
<!-- ═══════════════════════════════════════════════════════════════════════
CONTENT AREA
═══════════════════════════════════════════════════════════════════════ -->
<div class="content-area">
<!-- ─── VIEW: SYSTEM STATUS ───────────────────────────────────────── -->
<div class="page-view view-active" id="view-status">
<header class="topbar">
<span class="topbar-title">System Status</span>
<div class="search">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6.5 1a5.5 5.5 0 014.23 9.02l3.62 3.62-.7.71-3.63-3.63A5.5 5.5 0 116.5 1zm0 1a4.5 4.5 0 100 9 4.5 4.5 0 000-9z" fill="currentColor" opacity=".4"/></svg>
<input type="search" placeholder="Search regulations, documents…" aria-label="Search" />
</div>
<button class="btn">Export status</button>
<a class="btn btn-primary" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/upload-modal.html">New upload</a>
</header>
<main class="page">
<section class="page-head">
<div>
<div class="eyebrow">System Status</div>
<h1>System Status</h1>
<p class="page-head-desc">Ingestion pipeline, active compliance programs, and regulatory watch — all in one place.</p>
</div>
<span class="pill">v1.0.0</span>
</section>
<!-- Stats -->
<section class="stats-grid">
<article class="card stat-card">
<div class="s-label">Documents total</div>
<div class="s-value mono" id="ds-docs"></div>
<div class="s-sub">Ingested into the knowledge base</div>
</article>
<article class="card stat-card">
<div class="s-label">Vector chunks</div>
<div class="s-value mono" id="ds-chunks"></div>
<div class="s-sub">regulations_dense_1024_v2 serving retrieval</div>
</article>
<article class="card stat-card">
<div class="s-label">High-impact signals</div>
<div class="s-value mono" id="ds-high"></div>
<div class="s-sub">Regulatory signals requiring immediate review</div>
</article>
<article class="card stat-card">
<div class="s-label">Last 90 days</div>
<div class="s-value mono" id="ds-90d"></div>
<div class="s-sub">Recent regulatory publications</div>
</article>
</section>
<!-- Two-column -->
<section class="panel-grid">
<div class="stack">
<article class="card">
<div class="section-head">
<h2>Workflow queue</h2>
<a class="ghost-link" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-management.html">Open documents &rarr;</a>
</div>
<div class="task-list">
<div class="task-row">
<div>
<strong>GB/T 31484-2015 battery density revision</strong>
<div class="note">Uploaded by EV Safety Team · version 2026-04 addendum</div>
</div>
<span class="status warn">Embedding</span>
<span class="mono">chunk build active</span>
<a class="task-cta" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-detail.html">Inspect &rarr;</a>
</div>
<div class="task-row">
<div>
<strong>UNECE R155 annex interpretation note</strong>
<div class="note">Parser artifacts ready · waiting for analyst assignment</div>
</div>
<span class="status ok">Ready</span>
<span class="mono">19 clauses linked</span>
<a class="task-cta" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/compliance-analysis.html">Analyze &rarr;</a>
</div>
<div class="task-row">
<div>
<strong>GB 26112-2010 roof strength scan</strong>
<div class="note">OCR confidence dropped below threshold on 6 pages</div>
</div>
<span class="status risk">Failed</span>
<span class="mono">Retry #2</span>
<a class="task-cta" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-management.html">Resolve &rarr;</a>
</div>
</div>
</article>
<article class="card">
<div class="section-head">
<h2>Active compliance programs</h2>
<a class="ghost-link" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/compliance-analysis.html">Review findings &rarr;</a>
</div>
<div class="program-list">
<div class="program-row">
<div>
<strong>Intelligent cockpit homologation</strong>
<p class="note">42 related standards across driver monitoring, EMC, and child safety. Four findings still open for MY27 platform.</p>
</div>
<span class="status risk">High risk</span>
</div>
<div class="program-row">
<div>
<strong>Battery swap certification dossier</strong>
<p class="note">Clause mapping complete. Thermal event test evidence package awaiting supplier document refresh.</p>
</div>
<span class="status warn">Pending</span>
</div>
<div class="program-row">
<div>
<strong>Connected fleet cybersecurity</strong>
<p class="note">RAG checks aligned with UNECE R155. Chat follow-up requested on remote key rotation obligations.</p>
</div>
<span class="status ok">On track</span>
</div>
</div>
<div class="kpi-strip">
<div class="kpi">
<div class="kpi-label">Retrieval hit rate</div>
<strong>87%</strong>
<div class="meter"><span style="width:87%"></span></div>
</div>
<div class="kpi">
<div class="kpi-label">Evidence coverage</div>
<strong>72%</strong>
<div class="meter"><span style="width:72%"></span></div>
</div>
<div class="kpi">
<div class="kpi-label">Reviewer SLA</div>
<strong>18h</strong>
<div class="meter"><span style="width:64%"></span></div>
</div>
</div>
</article>
</div>
<div class="stack">
<article class="card">
<div class="section-head"><h2>System health</h2><a class="ghost-link" href="#">Refresh</a></div>
<div class="task-list">
<div class="task-row" style="grid-template-columns:1fr auto">
<div><strong>Aliyun parser backend</strong><div class="note">Poll interval 5 s · timeout 900 s</div></div>
<span class="status warn">Queue depth 7</span>
</div>
<div class="task-row" style="grid-template-columns:1fr auto">
<div><strong>Embedding model</strong><div class="note">text-embedding-v3 · dimension 1024</div></div>
<span class="status ok">Healthy</span>
</div>
<div class="task-row" style="grid-template-columns:1fr auto">
<div><strong>Vector store</strong><div class="note">Milvus regulations_dense_1024_v2</div></div>
<span class="status ok">Serving</span>
</div>
</div>
</article>
<article class="card">
<div class="section-head">
<h2>Regulatory watch</h2>
<a class="ghost-link" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/regulation-chat.html">Ask chat &rarr;</a>
</div>
<div class="event-list">
<div class="event-row">
<span class="mono note">2d ago</span>
<div>
<strong>GB 38031 thermal propagation draft updated</strong>
<p class="note" style="-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden">Potential impact on current battery enclosure narrative. Evidence gap flagged in two supplier submissions.</p>
</div>
</div>
<div class="event-row">
<span class="mono note">5d ago</span>
<div>
<strong>UNECE R155 Q&amp;A added note on incident response logs</strong>
<p class="note" style="-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden">Connected fleet program must confirm retention windows and ownership controls.</p>
</div>
</div>
<div class="event-row">
<span class="mono note">12d ago</span>
<div>
<strong>GB/T 18487 charging interface interpretation circulated</strong>
<p class="note" style="-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden">No blocker yet, but three documents should be re-run against the new clause wording.</p>
</div>
</div>
</div>
</article>
</div>
</section>
</main>
<footer class="footer">
<span>T-Systems Regulation Hub</span>
<div class="footer-live"><span class="footer-dot"></span><span>Online</span></div>
</footer>
</div><!-- /view-status -->
<!-- ─── VIEW: REGULATORY SIGNALS ─────────────────────────────────── -->
<div class="page-view" id="view-signals">
<div class="topbar">
<span class="topbar-title">
Regulatory Signals
<span class="topbar-sub">Real-time monitoring &middot; Knowledge-base impact analysis</span>
</span>
<div class="search" style="width:220px">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6.5 1a5.5 5.5 0 014.23 9.02l3.62 3.62-.7.71-3.63-3.63A5.5 5.5 0 116.5 1zm0 1a4.5 4.5 0 100 9 4.5 4.5 0 000-9z" fill="currentColor" opacity=".4"/></svg>
<input type="search" placeholder="Search signals…" aria-label="Search signals" />
</div>
<button class="btn btn-sm" onclick="loadFeed()">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M13.5 8a5.5 5.5 0 11-1.1-3.3" stroke="currentColor" stroke-width="1.4"/><path d="M10 4.5l2.5.2.3-2.7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Refresh
</button>
</div>
<div class="stats-bar-flush">
<div class="stat-cell"><div class="s-label">Total signals</div><div class="s-value" id="stat-total">&mdash;</div><div class="s-sub">All regulatory events in feed</div></div>
<div class="stat-cell"><div class="s-label">High impact</div><div class="s-value" id="stat-high" style="color:var(--danger)">&mdash;</div><div class="s-sub">Requires immediate review</div></div>
<div class="stat-cell"><div class="s-label">Medium impact</div><div class="s-value" id="stat-med" style="color:var(--warn)">&mdash;</div><div class="s-sub">Scheduled for assessment</div></div>
<div class="stat-cell"><div class="s-label">Last 90 days</div><div class="s-value" id="stat-90d" style="color:var(--accent)">&mdash;</div><div class="s-sub">Recent publications</div></div>
</div>
<div class="filter-bar">
<span class="filter-label">Source</span>
<button class="chip on" data-src="" onclick="setSource(this,'')">All</button>
<button class="chip" data-src="MIIT" onclick="setSource(this,'MIIT')">MIIT</button>
<button class="chip" data-src="UN-ECE" onclick="setSource(this,'UN-ECE')">UN-ECE</button>
<button class="chip" data-src="ISO" onclick="setSource(this,'ISO')">ISO</button>
<button class="chip" data-src="国标委" onclick="setSource(this,'国标委')">GB Comm.</button>
<button class="chip" data-src="EUR-Lex" onclick="setSource(this,'EUR-Lex')">EUR-Lex</button>
<button class="chip" data-src="IATF" onclick="setSource(this,'IATF')">IATF</button>
<div class="sep"></div>
<span class="filter-label">Impact</span>
<button class="chip on" data-imp="" onclick="setImpact(this,'')">All</button>
<button class="chip" data-imp="high" onclick="setImpact(this,'high')">High</button>
<button class="chip" data-imp="medium" onclick="setImpact(this,'medium')">Medium</button>
<button class="chip" data-imp="low" onclick="setImpact(this,'low')">Low</button>
</div>
<div class="perception-split" style="flex:1;min-height:0">
<div class="feed-pane">
<div class="feed-pane-head">
<h2>Signal feed</h2>
<span class="feed-count" id="feed-count"></span>
</div>
<div class="feed-scroll" id="feed-scroll">
<div class="loading-msg">Loading…</div>
</div>
</div>
<div class="analysis-pane" id="analysis-pane">
<div class="analysis-empty" id="analysis-empty">
<div class="analysis-empty-ring">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2a6 6 0 100 12A6 6 0 008 2zm0 2v4l3 1.5-.5 1-3.5-1.75V4H8z" fill="currentColor" opacity=".3"/></svg>
</div>
<div class="analysis-empty-label">Select a signal to view impact analysis</div>
<div class="analysis-empty-hint">← Choose from the signal feed</div>
</div>
</div>
</div>
<div class="footer">
<div class="footer-live"><span class="footer-dot"></span><span>Live feed</span></div>
<span>T-Systems Regulation Hub</span>
</div>
</div><!-- /view-signals -->
</div><!-- /content-area -->
</div><!-- /app-shell -->
<script>
/* ══════════════════════════════════════════════════════════════════════
NAVIGATION — JS view switching (no page reload)
══════════════════════════════════════════════════════════════════════ */
const VIEWS = {
status: { view: 'view-status', nav: 'nav-status' },
signals: { view: 'view-signals', nav: 'nav-signals' },
};
let currentView = 'status';
function navigate(id) {
if (!VIEWS[id]) return;
// Hide all views
document.querySelectorAll('.page-view').forEach(v => v.classList.remove('view-active'));
// Remove all active nav states (only within sidebar nav-items that have IDs)
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
// Show target view
document.getElementById(VIEWS[id].view).classList.add('view-active');
document.getElementById(VIEWS[id].nav).classList.add('active');
currentView = id;
// Bootstrap data for signals view on first open
if (id === 'signals' && document.getElementById('feed-scroll').querySelector('.loading-msg')) {
loadStats(); loadFeed();
}
}
/* ══════════════════════════════════════════════════════════════════════
THEME TOGGLE
══════════════════════════════════════════════════════════════════════ */
function toggleTheme() {
const html = document.documentElement;
const next = html.dataset.theme === 'dark' ? 'light' : 'dark';
html.dataset.theme = next;
localStorage.setItem('theme', next);
document.getElementById('theme-label').textContent = next === 'dark' ? 'Light mode' : 'Dark mode';
}
(function () {
const saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.dataset.theme = saved;
document.getElementById('theme-label').textContent = saved === 'dark' ? 'Light mode' : 'Dark mode';
}
})();
/* ══════════════════════════════════════════════════════════════════════
API — SYSTEM STATUS
══════════════════════════════════════════════════════════════════════ */
const API_BASE = 'http://6.86.80.9:5173/api/v1';
async function loadDashboardStats() {
try {
const r = await fetch(`${API_BASE}/perception/stats`);
if (!r.ok) return;
const s = await r.json();
const set = (id, val) => { const el = document.getElementById(id); if (el && val != null) el.textContent = val; };
set('ds-high', s.high_impact);
set('ds-90d', s.recent_90d);
set('ds-docs', s.total);
} catch (e) { /* silent */ }
}
loadDashboardStats();
/* ══════════════════════════════════════════════════════════════════════
API — REGULATORY SIGNALS
══════════════════════════════════════════════════════════════════════ */
const SRC_COLOR = {
MIIT: '#e20074', 'UN-ECE': '#3b82f6', ISO: '#7c3aed',
'国标委': '#059669', 'EUR-Lex': '#d97706', IATF: '#7c3aed'
};
const IMP_COLOR = { high: 'var(--danger)', medium: 'var(--warn)', low: 'var(--success)' };
const IMP_LABEL = { high: 'High', medium: 'Medium', low: 'Low' };
const STA_LABEL = { enacted: 'Enacted', draft: 'Draft', consultation: 'Consultation' };
const STA_CLASS = { enacted: 'ok', draft: 'warn', consultation: 'info' };
let currentSource = '', currentImpact = '', selectedId = null, abortCtrl = null;
let allEvents = [];
async function loadStats() {
try {
const r = await fetch(`${API_BASE}/perception/stats`);
if (!r.ok) return;
const s = await r.json();
document.getElementById('stat-total').textContent = s.total ?? '—';
document.getElementById('stat-high').textContent = s.high_impact ?? '—';
document.getElementById('stat-med').textContent = s.medium_impact ?? '—';
document.getElementById('stat-90d').textContent = s.recent_90d ?? '—';
const badge = document.getElementById('badge-high');
if (badge) badge.textContent = s.high_impact ?? '—';
// Also update dashboard card
const h = document.getElementById('ds-high');
if (h && s.high_impact != null) h.textContent = s.high_impact;
} catch (e) { console.warn('stats:', e); }
}
async function loadFeed() {
const scroll = document.getElementById('feed-scroll');
scroll.innerHTML = '<div class="loading-msg">Loading…</div>';
const params = new URLSearchParams();
if (currentSource) params.set('source', currentSource);
if (currentImpact) params.set('impact_level', currentImpact);
try {
const r = await fetch(`${API_BASE}/perception/events?${params}`);
if (!r.ok) throw new Error(r.status);
const data = await r.json();
allEvents = data.events || [];
const countEl = document.getElementById('feed-count');
if (countEl) countEl.textContent = `${allEvents.length} / ${data.total}`;
renderFeed(allEvents);
} catch (e) {
scroll.innerHTML = `<div class="loading-msg" style="color:var(--danger)">Failed to load: ${e.message}</div>`;
}
}
function renderFeed(events) {
const scroll = document.getElementById('feed-scroll');
if (!events.length) { scroll.innerHTML = '<div class="loading-msg">No signals match the current filters.</div>'; return; }
scroll.innerHTML = events.map(ev => {
const sc = SRC_COLOR[ev.source] || '#888';
const stc = STA_CLASS[ev.status] || 'info';
const sel = ev.id === selectedId;
return `<div class="ev-card${sel ? ' selected' : ''}" onclick="selectEvent('${ev.id}')" id="card-${ev.id}">
<div class="ev-head">
<span class="src-tag" style="color:${sc};background:${sc}18">${ev.source}</span>
<span class="std-code">${ev.standard_code}</span>
<span class="status ${stc}" style="margin-left:auto">${STA_LABEL[ev.status] || ev.status}</span>
</div>
<div class="ev-title">${ev.title}</div>
<div class="ev-summary">${ev.summary}</div>
<div class="ev-foot">
<span class="ev-date">${ev.published_at}</span>
${(ev.tags||[]).slice(0,2).map(t=>`<span class="ev-tag">${t}</span>`).join('')}
<span class="imp-dot" style="color:${IMP_COLOR[ev.impact_level]||'var(--muted)'}">&#9679; ${IMP_LABEL[ev.impact_level]||ev.impact_level}</span>
</div>
</div>`;
}).join('');
}
function selectEvent(id) {
if (id === selectedId) return;
selectedId = id;
document.querySelectorAll('.ev-card').forEach(c => c.classList.remove('selected'));
const card = document.getElementById(`card-${id}`);
if (card) card.classList.add('selected');
const ev = allEvents.find(e => e.id === id);
if (ev) renderDetail(ev);
}
function renderDetail(ev) {
const sc = SRC_COLOR[ev.source] || '#888';
const stc = STA_CLASS[ev.status] || 'info';
const pane = document.getElementById('analysis-pane');
pane.innerHTML = `
<div class="detail-card">
<div class="detail-head">
<span class="src-tag" style="color:${sc};background:${sc}18;font-size:10px;padding:2px 7px;border-radius:4px">${ev.source}</span>
<span style="font-family:var(--font-mono);font-size:10px;color:var(--muted)">${ev.standard_code}</span>
<span class="status ${stc}" style="margin-left:auto">${STA_LABEL[ev.status]||ev.status}</span>
</div>
<div class="detail-title">${ev.title}</div>
<div class="detail-summary">${ev.summary}</div>
<div class="detail-meta">
<span class="meta-item">Published <strong>${ev.published_at}</strong></span>
${ev.effective_at ? `<span class="meta-item">Effective <strong>${ev.effective_at}</strong></span>` : ''}
<span class="meta-item">Category <strong>${ev.category}</strong></span>
<span class="meta-item">Impact <strong style="color:${IMP_COLOR[ev.impact_level]||'var(--muted)'}">${IMP_LABEL[ev.impact_level]||ev.impact_level}</strong></span>
</div>
</div>
<div class="action-row">
<button class="btn btn-primary" id="btn-analyze" onclick="startAnalysis('${ev.id}')">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg>
Run impact analysis
</button>
<button class="btn" id="btn-abort" style="display:none" onclick="stopAnalysis()">Stop</button>
${ev.source_url ? `<a href="${ev.source_url}" target="_blank" class="btn"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M9 2h5v5M9.5 6.5L14 2M3 4h4M3 8h8M3 12h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Source</a>` : ''}
</div>
<div class="docs-card" id="docs-card"><div class="docs-head">Affected documents</div><div id="docs-list"></div></div>
<div class="output-card" id="output-card">
<div class="output-head" id="output-head">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg>
AI impact analysis
</div>
<div id="output-body"></div>
</div>`;
}
async function startAnalysis(eventId) {
if (abortCtrl) abortCtrl.abort();
abortCtrl = new AbortController();
const btnA = document.getElementById('btn-analyze');
const btnX = document.getElementById('btn-abort');
btnA.disabled = true; btnX.style.display = '';
const outputCard = document.getElementById('output-card');
const outputHead = document.getElementById('output-head');
const outputBody = document.getElementById('output-body');
const docsCard = document.getElementById('docs-card');
outputCard.style.display = '';
outputBody.innerHTML = '';
outputHead.innerHTML = `<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg> AI impact analysis <span class="blink" style="color:var(--accent)">&#9611;</span>`;
try {
const res = await fetch(`${API_BASE}/perception/events/${eventId}/analyze`, {
method: 'POST', headers: { Accept: 'text/event-stream' }, signal: abortCtrl.signal
});
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = '', rawText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const parts = buf.split('\n\n');
buf = parts.pop() ?? '';
for (const block of parts) {
if (!block.trim()) continue;
let evtName = 'message';
const dataLines = [];
for (const line of block.split('\n')) {
if (line.startsWith('event:')) evtName = line.slice(6).trim();
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
}
const payload = dataLines.join('\n');
if (!payload) continue;
if (evtName === 'sources') { try { renderDocs(JSON.parse(payload), docsCard); } catch {} }
else if (evtName === 'content') { rawText += payload; outputBody.innerHTML = renderMarkdown(rawText); }
}
}
} catch (e) {
if (e.name !== 'AbortError') {
const ob = document.getElementById('output-body');
if (ob) ob.innerHTML += `<div style="color:var(--danger);font-size:12px;margin-top:8px">Analysis error: ${e.message}</div>`;
}
} finally {
const blink = document.querySelector('#output-head .blink');
if (blink) blink.remove();
const ab = document.getElementById('btn-abort'); if (ab) ab.style.display = 'none';
const ba = document.getElementById('btn-analyze');
if (ba) { ba.disabled = false; ba.innerHTML = `<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg> Re-analyse`; }
}
}
function stopAnalysis() {
if (abortCtrl) { abortCtrl.abort(); abortCtrl = null; }
const ab = document.getElementById('btn-abort'); if (ab) ab.style.display = 'none';
const ba = document.getElementById('btn-analyze'); if (ba) ba.disabled = false;
const h = document.getElementById('output-head'); if (h) { const b = h.querySelector('.blink'); if (b) b.remove(); }
}
function renderDocs(docs, card) {
if (!docs || !docs.length) return;
card.style.display = '';
document.getElementById('docs-list').innerHTML = docs.map(d => `
<div class="doc-row">
<div class="doc-score">${Math.round(d.score * 100)}%</div>
<div>
<div class="doc-name">${d.doc_name}</div>
<div class="doc-clause">${d.clause || ''}</div>
<div class="doc-snippet">${d.snippet || ''}</div>
</div>
</div>`).join('');
}
function renderMarkdown(text) {
return text.split('\n').map(line => {
if (line.startsWith('## ')) return `<div class="md-h2">${line.slice(3)}</div>`;
if (line.startsWith('### ')) return `<div class="md-h3">${line.slice(4)}</div>`;
if (line.startsWith('- ') || line.startsWith('* ')) {
const c = line.slice(2).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
return `<div class="md-li"><span class="md-li-dot">·</span><span>${c}</span></div>`;
}
if (/^\d+\./.test(line)) return `<div class="md-p" style="padding-left:8px">${line.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')}</div>`;
if (!line.trim()) return '<div class="md-empty"></div>';
return `<div class="md-p">${line.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')}</div>`;
}).join('');
}
function setSource(btn, src) {
currentSource = src;
document.querySelectorAll('[data-src]').forEach(c => c.classList.toggle('on', c.dataset.src === src));
loadFeed();
}
function setImpact(btn, imp) {
currentImpact = imp;
document.querySelectorAll('[data-imp]').forEach(c => c.classList.toggle('on', c.dataset.imp === imp));
loadFeed();
}
</script>
</body>
</html>