916 lines
40 KiB
HTML
916 lines
40 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8" />
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
|
|
<title>Regulatory Signals — T-Systems Regulation Hub</title>
|
||
|
|
<style>
|
||
|
|
/* ─── Design tokens (identical to dashboard-sidebar.html) ────────── */
|
||
|
|
: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;
|
||
|
|
|
||
|
|
--accent: #e20074;
|
||
|
|
--accent-dim: rgba(226,0,116,.10);
|
||
|
|
--accent-hover: #c8006a;
|
||
|
|
--success: #16a34a;
|
||
|
|
--warn: #d97706;
|
||
|
|
--danger: #dc2626;
|
||
|
|
|
||
|
|
--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;
|
||
|
|
|
||
|
|
--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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── 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; }
|
||
|
|
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 (light rail — identical to dashboard) ───────────────── */
|
||
|
|
.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); line-height: 1.2; font-weight: 700; }
|
||
|
|
.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;
|
||
|
|
transition: background 120ms, color 120ms;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
.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; }
|
||
|
|
|
||
|
|
/* ─── Content area ───────────────────────────────────────────────── */
|
||
|
|
.content-area { display: flex; flex-direction: column; min-width: 0; min-height: 100vh; }
|
||
|
|
|
||
|
|
.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;
|
||
|
|
}
|
||
|
|
.topbar-title { font-weight: 600; font-size: 14px; }
|
||
|
|
.topbar-sub { color: var(--muted); font-family: var(--font-mono); font-size: 10px; margin-left: 4px; font-weight: 400; }
|
||
|
|
|
||
|
|
.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; }
|
||
|
|
|
||
|
|
.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: 220px;
|
||
|
|
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); }
|
||
|
|
|
||
|
|
/* ─── Stats bar ──────────────────────────────────────────────────── */
|
||
|
|
.stats-bar {
|
||
|
|
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); }
|
||
|
|
|
||
|
|
/* ─── Work area (filter bar + split) ────────────────────────────── */
|
||
|
|
.work-area { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
||
|
|
|
||
|
|
.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; }
|
||
|
|
|
||
|
|
/* ─── Two-pane split ─────────────────────────────────────────────── */
|
||
|
|
.perception-split {
|
||
|
|
flex: 1; display: grid;
|
||
|
|
grid-template-columns: 360px 1fr;
|
||
|
|
min-height: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Feed pane ─────────────────────────────────────────────────── */
|
||
|
|
.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 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; }
|
||
|
|
|
||
|
|
/* Event card */
|
||
|
|
.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 ─────────────────────────────────────────────── */
|
||
|
|
.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 */
|
||
|
|
.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 */
|
||
|
|
.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 */
|
||
|
|
.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; }
|
||
|
|
|
||
|
|
/* Status pills */
|
||
|
|
.status {
|
||
|
|
display: inline-flex; align-items: center; gap: 5px;
|
||
|
|
padding: 2px 8px; border-radius: var(--radius-pill);
|
||
|
|
font-size: 10px; font-family: var(--font-mono); font-weight: 600;
|
||
|
|
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%); }
|
||
|
|
|
||
|
|
/* Footer */
|
||
|
|
.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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 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"] .stats-bar { background: var(--border); }
|
||
|
|
[data-theme="dark"] .stat-cell { background: var(--surface); }
|
||
|
|
[data-theme="dark"] .filter-bar { 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); }
|
||
|
|
|
||
|
|
/* Sidebar action */
|
||
|
|
.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; cursor: pointer;
|
||
|
|
transition: background 120ms, color 120ms;
|
||
|
|
}
|
||
|
|
.sidebar-action:hover { background: var(--rail-hover); color: var(--rail-fg); }
|
||
|
|
|
||
|
|
/* Responsive */
|
||
|
|
@media (max-width: 1100px) {
|
||
|
|
.stats-bar { 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-bar { grid-template-columns: 1fr 1fr; }
|
||
|
|
.perception-split { grid-template-columns: 1fr; }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="app-shell">
|
||
|
|
|
||
|
|
<!-- ─── Sidebar (dark rail) ─────────────────────────────────────────── -->
|
||
|
|
<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="cc29bcb0-df2d-4d50-9428-7caa406ecb29/index.html">
|
||
|
|
<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 active" href="perception.html">
|
||
|
|
<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" href="dashboard-sidebar.html">
|
||
|
|
<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
|
||
|
|
</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&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>Dark mode</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<!-- ─── Content ───────────────────────────────────────────────────────── -->
|
||
|
|
<div class="content-area">
|
||
|
|
<!-- Topbar -->
|
||
|
|
<div class="topbar">
|
||
|
|
<span class="topbar-title">
|
||
|
|
Regulatory Signals
|
||
|
|
<span class="topbar-sub">Real-time monitoring · Knowledge-base impact analysis</span>
|
||
|
|
</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 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>
|
||
|
|
|
||
|
|
<!-- Stats bar — inlined (no card borders, flush panel style) -->
|
||
|
|
<div class="stats-bar">
|
||
|
|
<div class="stat-cell">
|
||
|
|
<div class="s-label">Total signals</div>
|
||
|
|
<div class="s-value" id="stat-total">—</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)">—</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)">—</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)">—</div>
|
||
|
|
<div class="s-sub">Recent publications</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Filter bar -->
|
||
|
|
<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>
|
||
|
|
|
||
|
|
<!-- Two-pane split -->
|
||
|
|
<div class="perception-split work-area">
|
||
|
|
<!-- Feed pane -->
|
||
|
|
<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>
|
||
|
|
|
||
|
|
<!-- Analysis pane -->
|
||
|
|
<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>
|
||
|
|
|
||
|
|
<!-- Footer -->
|
||
|
|
<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>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// ─── Theme toggle ────────────────────────────────────────────────────
|
||
|
|
function toggleTheme() {
|
||
|
|
const html = document.documentElement;
|
||
|
|
const next = html.dataset.theme === 'dark' ? 'light' : 'dark';
|
||
|
|
html.dataset.theme = next;
|
||
|
|
localStorage.setItem('theme', next);
|
||
|
|
const spans = document.querySelectorAll('.sidebar-action span');
|
||
|
|
spans.forEach(s => s.textContent = next === 'dark' ? 'Light mode' : 'Dark mode');
|
||
|
|
}
|
||
|
|
(function() {
|
||
|
|
const saved = localStorage.getItem('theme');
|
||
|
|
if (saved) document.documentElement.dataset.theme = saved;
|
||
|
|
})();
|
||
|
|
|
||
|
|
const API = 'http://6.86.80.9:5173/api/v1';
|
||
|
|
|
||
|
|
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}/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 ?? '—';
|
||
|
|
} 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}/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)'}">● ${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)">▋</span>`;
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/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)) {
|
||
|
|
const c = line.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
|
||
|
|
return `<div class="md-p" style="padding-left:8px">${c}</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();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Boot
|
||
|
|
loadStats();
|
||
|
|
loadFeed();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|