{claim.text}
{claim.risks.length ? claim.risks.map(risk =>No taxonomy match.
}From f57d072065727526eb3c004e156eb95abf445286 Mon Sep 17 00:00:00 2001 From: Spbd1 <148923621+Spbd1@users.noreply.github.com> Date: Mon, 18 May 2026 05:57:42 +0000 Subject: [PATCH] Build frontend dashboard workflows --- frontend/index.html | 14 +- frontend/scripts/build_frontend.mjs | 9 +- frontend/scripts/dev_server.mjs | 7 +- frontend/src/App.tsx | 25 ++- frontend/src/api/client.ts | 187 ++++++++++++------ frontend/src/api/types.ts | 105 +++++++++- .../src/components/analyze/AnalysisReport.tsx | 16 +- .../src/components/analyze/AnalyzePage.tsx | 35 +++- frontend/src/components/analyze/ClaimCard.tsx | 17 +- .../components/analyze/EvidenceHighlight.tsx | 5 +- .../src/components/analyze/ExportButtons.tsx | 37 +++- frontend/src/components/analyze/RiskCard.tsx | 15 +- .../src/components/analyze/TextInputPanel.tsx | 37 +++- .../evaluation/ErrorAnalysisTable.tsx | 15 +- .../components/evaluation/EvaluationPage.tsx | 17 +- .../components/evaluation/MetricsCards.tsx | 10 +- frontend/src/components/layout/AppShell.tsx | 9 +- frontend/src/components/layout/Header.tsx | 25 ++- frontend/src/components/layout/Sidebar.tsx | 23 ++- .../src/components/reports/ReportPreview.tsx | 10 +- .../src/components/reports/ReportsPage.tsx | 18 +- .../components/review/FeedbackControls.tsx | 15 +- frontend/src/components/review/ReviewItem.tsx | 30 ++- frontend/src/components/review/ReviewPage.tsx | 12 +- .../src/components/review/ReviewQueue.tsx | 10 +- frontend/src/components/shared/Badge.tsx | 3 +- frontend/src/components/shared/Button.tsx | 7 +- frontend/src/components/shared/Card.tsx | 3 +- frontend/src/components/shared/EmptyState.tsx | 2 +- frontend/src/components/shared/ErrorState.tsx | 2 +- .../src/components/shared/LoadingState.tsx | 2 +- frontend/src/runtime-dashboard.js | 24 +++ frontend/src/styles/global.css | 111 +++++++---- 33 files changed, 700 insertions(+), 157 deletions(-) create mode 100644 frontend/src/runtime-dashboard.js diff --git a/frontend/index.html b/frontend/index.html index 6f14d63..5572dd9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1 +1,13 @@ -
The dashboard is a Chrome-friendly operator console for the Argument-Risk-Engine backend. It supports text analysis, taxonomy inspection, Excel taxonomy operations, model-provider configuration, human review, evaluation, and report export.
+Use npm install and npm run dev from frontend/, or the repository one-command setup, then keep the backend running on the configured API base.
Analysis ID: {result.analysis_id}
Risks: {String(result.summary.risk_count ?? 0)}
{result.claims.map((claim, idx) =>Analysis ID: {result.text_id} · {result.llm_used ? 'LLM assisted' : 'Deterministic'}{result.deterministic_fallback_used ? ' · fallback used' : ''}
Choose the active backend provider, tune retrieval breadth, and produce a reviewable risk report.
Run an analysis to see scores, claim cards, risk cards, highlighted evidence, healthy patterns, warnings, and export controls.
}No taxonomy match.
}{claim.claim_type}
{firstRisk ?
{String(pattern.label ?? pattern.pattern ?? 'Healthy signal')}: {String(pattern.explanation ?? pattern.evidence_span ?? '')}
)}${markdown.replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>' }[char] ?? char))}{risk.explanation}
Evidence: “{risk.evidence.quote}”{risk.risk_id} · {risk.category}
{risk.explanation}
+ {risk.evidence_span ?Evidence: “{risk.evidence_span}”
: null} + {risk.false_positive_warning ?{risk.false_positive_warning}
: null} + {risk.needs_human_review ?MVP placeholder wired for local file-backed workflows.
{JSON.stringify(item, null, 2)} |
None reported.
}MVP placeholder wired for local file-backed workflows.
Run the backend mini evaluation set and inspect metrics, false positives, false negatives, and evidence span misses.
{JSON.stringify(result, null, 2)}No evaluation run yet.
}MVP placeholder wired for local file-backed workflows.
Taxonomy-grounded review signals for human analysts.
Argument-Risk-Engine Dashboard
+Taxonomy-grounded argument risk signals for human analysts.
+MVP placeholder wired for local file-backed workflows.
Select a generated report to preview Markdown, HTML, or JSON.
+ const content = format === 'json' ? report.json : format === 'html' ? report.html : report.markdown + if (!content) returnThis report does not include {format} content.
+ return format === 'html' ? :{content}
+}
diff --git a/frontend/src/components/reports/ReportsPage.tsx b/frontend/src/components/reports/ReportsPage.tsx
index 7ef6158..e0d5b49 100644
--- a/frontend/src/components/reports/ReportsPage.tsx
+++ b/frontend/src/components/reports/ReportsPage.tsx
@@ -1,2 +1,18 @@
+import { useEffect, useState } from 'react'
+import { downloadReport, fetchDemoReport, listReports, saveGeneratedReport } from '../../api/client'
+import type { GeneratedReport, ReportFormat } from '../../api/types'
+import { Button } from '../shared/Button'
import { Card } from '../shared/Card'
-export function ReportsPage() { return MVP placeholder wired for local file-backed workflows.
List generated reports, preview content, and download JSON, Markdown, or HTML when available.
MVP placeholder wired for local file-backed workflows.
MVP placeholder wired for local file-backed workflows.
{new Date(record.created_at).toLocaleString()}
{record.source_text}
+Detected labels: {riskIds.length ? riskIds.join(', ') : 'none'}
+MVP placeholder wired for local file-backed workflows.
Review prior analysis items, mark correctness, add corrected labels, and persist notes to the backend feedback endpoint.
MVP placeholder wired for local file-backed workflows.
{message}
} +export function EmptyState({ title = 'No data yet', message }: { title?: string; message: string }) { return{message}
{message}
} +export function ErrorState({ message, title = 'Something went wrong' }: { message: string; title?: string }) { return{message}
Loading...
} +export function LoadingState({ label = 'Loading…' }: { label?: string }) { return${md.replace(/[&<>]/g,s=>({'&':'&','<':'<','>':'>'}[s]))}`});save('are.generated.reports',state.reports)}},'Save to Reports')), ...(result.warnings||[]).map(w=>el('p',{class:'warning'},w)), ...(result.claims||[]).map(c=>el('article',{class:'claim-card'},el('h3',{},c.claim_id),el('p',{class:'claim-text'},c.text),...(c.detected_risks||[]).map(r=>el('article',{class:'risk-card'},el('h4',{},r.label),el('p',{},r.explanation),el('p',{class:'evidence'},`Evidence: “${r.evidence_span}”`),r.false_positive_warning?el('p',{class:'warning'},r.false_positive_warning):'')),(c.healthy_patterns||[]).length?el('div',{class:'healthy-panel'},'Healthy patterns: ',c.healthy_patterns.length):''))) } catch(e){ resultBox.replaceChildren(errorBox(e.message)) } } return el('div',{class:'page-grid two-column'},card(el('h2',{},'Analyze text'),el('div',{class:'button-row'},['Everyone who supports that policy caused the problem, so their entire proposal should be rejected.','The study has a small sample and wide confidence intervals, so the conclusion should be treated cautiously.'].map((x,i)=>el('button',{onclick:()=>text.value=x},`Load example ${i+1}`))),el('label',{class:'field-label'},'Model provider',provider),text,el('div',{class:'control-grid'},el('label',{class:'field-label'},'top_k',topK),el('label',{class:'check'},healthy,' Include healthy patterns'),el('label',{class:'check'},fallback,' Allow deterministic fallback')),el('button',{onclick:run},'Analyze text')),card(resultBox)) }
+async function loadTaxonomy(){ if(!state.taxonomy.length) state.taxonomy=(await api('/taxonomy')).entries||[] }
+function taxonomyPage(){ const box=card(el('div',{class:'state state-loading'},'Loading taxonomy…')); loadTaxonomy().then(()=>render()).catch(e=>box.replaceChildren(errorBox(e.message))); if(!state.taxonomy.length) return box; const q=el('input',{class:'search-box',placeholder:'Search taxonomy'}); const table=el('div',{class:'table-wrap'}); const drawer=el('aside',{class:'drawer'},el('p',{class:'muted'},'Select an entry for full details.')); function fill(){ const rows=state.taxonomy.filter(x=>JSON.stringify(x).toLowerCase().includes(q.value.toLowerCase())); table.replaceChildren(el('table',{class:'taxonomy-table'},el('thead',{},el('tr',{},['ID','Name','Category','Pack','Status','Classification'].map(h=>el('th',{},h)))),el('tbody',{},rows.map(x=>el('tr',{onclick:()=>drawer.replaceChildren(el('h3',{},x.name),el('code',{},x.id),el('p',{},x.short_definition||''),el('h4',{},'Signals'),el('ul',{},...(x.signals||[]).map(s=>el('li',{},s))),el('h4',{},'False-positive warnings'),el('ul',{},...(x.common_false_positives||[]).map(s=>el('li',{},s))))},el('td',{},x.id),el('td',{},x.name),el('td',{},x.canonical_category),el('td',{},x.pack),el('td',{},x.activation_status),el('td',{},x.enabled_for_classification?'Enabled':'Disabled')))))) } q.addEventListener('input',fill); fill(); return card(el('div',{class:'section-header'},el('div',{},el('h2',{},'Taxonomy Browser'),el('p',{class:'muted'},'Read-only search/filter table with detail drawer.')),el('strong',{},state.taxonomy.length)),q,el('div',{class:'taxonomy-layout'},table,drawer)) }
+function workbenchPage(){ const out=el('div',{class:'stack'}); async function refresh(){ out.replaceChildren(el('div',{class:'state state-loading'},'Loading workbench…')); try{ const [coverage,quality,packs]=await Promise.all([api('/taxonomy-workbench/coverage'),api('/taxonomy-workbench/quality-report'),api('/taxonomy-workbench/packs')]); out.replaceChildren(el('div',{class:'chips'},...(packs.packs||[]).map(p=>el('span',{class:'badge'},`${p.pack}: ${p.entry_count}`))),el('div',{class:'metric-grid'},['entry_count','active_count','review_required_count','missing_false_positive_warnings_count'].map(k=>el('div',{},el('strong',{},coverage[k]??0),el('span',{},k.replaceAll('_',' '))))),el('h3',{},'Quality audit'),el('p',{class:quality.ok?'muted':'warning'},`${quality.error_count} errors · ${quality.warning_count} warnings`))}catch(e){out.replaceChildren(errorBox(e.message))}} refresh(); const file=el('input',{type:'file',accept:'.xlsx'}); return card(el('h2',{},'Taxonomy Workbench'),el('p',{class:'muted'},'Import/export Excel, validate taxonomy, inspect coverage/quality, and activate/deactivate entries.'),el('div',{class:'button-row'},file,el('button',{onclick:async()=>{const fd=new FormData();fd.append('file',file.files[0]); await api('/taxonomy-workbench/import-excel',{method:'POST',body:fd}); refresh()}},'Import Excel'),el('button',{onclick:()=>location.href=`${API_BASE}/taxonomy-workbench/export-excel`},'Export Excel'),el('button',{onclick:async()=>alert(JSON.stringify(await api('/taxonomy-workbench/validate',{method:'POST'}),null,2))},'Validate taxonomy')),out,activationControls()) }
+function activationControls(){ const wrap=el('div',{class:'stack'},el('h3',{},'Activation controls')); loadTaxonomy().then(()=>{ const select=el('select',{},...state.taxonomy.map(x=>el('option',{value:x.id},`${x.name} (${x.activation_status})`))); const update=async(status)=>{await api(`/taxonomy-workbench/entries/${encodeURIComponent(select.value)}/activation`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({activation_status:status,enabled_for_classification:status==='active'})}); state.taxonomy=[]; alert('Activation updated')}; wrap.append(select,el('div',{class:'button-row'},el('button',{onclick:()=>update('active')},'Activate'),el('button',{onclick:()=>update('review_required')},'Deactivate / review'),el('button',{onclick:()=>update('deprecated')},'Deprecate')))}); return wrap }
+function settingsPage(){ const box=card(el('div',{class:'state state-loading'},'Loading providers…')); loadProviders().then(()=>render()); if(!state.providers.length) return box; return el('div',{class:'provider-grid'},...state.providers.map(p=>card(el('div',{class:'section-header'},el('div',{},el('h3',{},p.label),el('p',{class:'muted'},`${p.provider_id} · ${p.provider_type}`)),el('span',{class:'badge'},p.provider_id===state.activeProvider?'Active':p.enabled?'Available':'Disabled')),p.provider_type==='deterministic'?el('p',{},'Works offline with no API key.'):el('div',{class:'settings-form'},['base_url','model_name','api_key_env_var'].map(k=>{const i=el('input',{value:p[k]||''});i.oninput=()=>p[k]=i.value;return el('label',{class:'field-label'},k.replaceAll('_',' '),i)})),el('div',{class:'button-row'},el('button',{onclick:async()=>{await api(`/settings/active-model-provider`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider_id:p.provider_id})});state.activeProvider=p.provider_id;render()}},'Select active provider'),el('button',{onclick:async()=>api(`/settings/model-providers/${p.provider_id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)})},'Save profile'),el('button',{onclick:async()=>alert(JSON.stringify(await api(`/settings/model-providers/${p.provider_id}/test`,{method:'POST'}),null,2))},'Test connection'))))) }
+function reviewPage(){ return card(el('h2',{},'Review outputs'),state.reviews.length?'':el('p',{class:'muted'},'Run an analysis first.'),...state.reviews.map(r=>{ const notes=el('textarea',{rows:3}); const decision=el('select',{},...['correct','incorrect','partial','insufficient_evidence'].map(d=>el('option',{value:d},d))); return el('article',{class:'review-item'},el('h3',{},r.analysis.text_id),el('p',{class:'quote-block'},r.source_text),decision,el('input',{placeholder:'corrected labels'}),notes,el('button',{onclick:async()=>api('/review/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({analysis_id:r.analysis.text_id,decision:decision.value,notes:notes.value})})},'Save notes'))})) }
+function evaluationPage(){ const out=el('div',{class:'stack'},el('p',{class:'muted'},'No evaluation run yet.')); return card(el('div',{class:'section-header'},el('div',{},el('h2',{},'Evaluation'),el('p',{class:'muted'},'Run evaluation and inspect error analysis.')),el('button',{onclick:async()=>{out.replaceChildren('Running…');try{const r=await api('/evaluation/run');out.replaceChildren(el('div',{class:'metric-grid'},el('div',{},el('strong',{},r.items||0),el('span',{},'Items')),el('div',{},el('strong',{},r.analyses?.length||0),el('span',{},'Analyses'))),el('h3',{},'False positives / negatives / evidence span misses'),el('pre',{},JSON.stringify(r,null,2)))}catch(e){out.replaceChildren(errorBox(e.message))}}},'Run evaluation')),out) }
+function reportsPage(){ return el('div',{class:'page-grid two-column'},card(el('h2',{},'Reports'),state.reports.length?'':el('p',{class:'muted'},'Save a report from Analyze.'),...state.reports.map(r=>el('button',{class:'list-button',onclick:()=>{document.querySelector('#preview').textContent=r.markdown||r.json||r.html||''}},r.title))),card(el('div',{class:'button-row'},...state.reports[0]?.formats?.map(f=>el('button',{onclick:()=>download(state.reports[0][f==='markdown'?'markdown':f],`${state.reports[0].id}.${f==='markdown'?'md':f}`)},`Download ${f}`))||[]),el('pre',{id:'preview',class:'report-preview'},state.reports[0]?.markdown||''))) }
+function render(){ const map={Analyze:analyzePage,'Taxonomy Browser':taxonomyPage,'Taxonomy Workbench':workbenchPage,'Model Settings':settingsPage,Review:reviewPage,Evaluation:evaluationPage,Reports:reportsPage,About:()=>card(el('h2',{},'About'),el('p',{},'Chrome-friendly dashboard for argument risk analysis, taxonomy operations, review, evaluation, and reports.'))}; shell(map[state.page]()) }
+loadProviders().finally(render)
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 70d8b40..86a9d5a 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -1,44 +1,83 @@
-:root { color: #172033; background: #f6f8fb; font-family: Inter, system-ui, sans-serif; }
+:root { color: #172033; background: #f6f8fb; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; }
+* { box-sizing: border-box; }
body { margin: 0; }
+button, input, textarea, select { font: inherit; }
.shell { display: flex; min-height: 100vh; }
-.sidebar { background: #101827; color: white; padding: 1.5rem; width: 220px; display: flex; flex-direction: column; gap: .75rem; }
-.sidebar a { color: #dce7ff; text-decoration: none; }
-main { flex: 1; padding: 2rem; }
-.header { margin-bottom: 1.5rem; }
-.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; }
-.card { background: white; border: 1px solid #dfe5ef; border-radius: 14px; padding: 1rem; box-shadow: 0 8px 24px rgba(16, 24, 39, .06); }
-.stack { display: grid; gap: .75rem; }
-textarea, input { border: 1px solid #c8d2e1; border-radius: 10px; padding: .75rem; font: inherit; }
-.button, button { border: 0; border-radius: 10px; padding: .7rem 1rem; background: #2454d6; color: white; cursor: pointer; }
-.badge { background: #ecf1ff; color: #24449a; padding: .2rem .5rem; border-radius: 999px; font-size: .8rem; }
-.claim, .risk-card { border-top: 1px solid #edf1f7; padding-top: .75rem; margin-top: .75rem; }
+.sidebar { background: #101827; color: white; padding: 1.25rem; width: 250px; display: flex; flex-direction: column; gap: 1.25rem; position: sticky; top: 0; height: 100vh; }
+.brand { display: grid; gap: .15rem; border-bottom: 1px solid rgba(255,255,255,.15); padding-bottom: 1rem; }
+.brand span { font-size: 1.5rem; font-weight: 800; letter-spacing: .08em; }
+.brand small { color: #b8c6df; }
+.sidebar nav { display: grid; gap: .35rem; }
+.nav-item { width: 100%; text-align: left; border: 0; border-radius: 10px; padding: .75rem .85rem; background: transparent; color: #dce7ff; cursor: pointer; }
+.nav-item:hover, .nav-item.active { background: #1d2a44; color: white; }
+.main { flex: 1; padding: 1.5rem; min-width: 0; }
+.header { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; margin-bottom: 1.5rem; }
+.header h1 { margin: .1rem 0; font-size: clamp(1.6rem, 3vw, 2.35rem); }
+.header p { margin: 0; color: #617089; }
+.eyebrow { color: #2454d6 !important; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; font-size: .78rem; }
+.api-pill { border: 1px solid #dfe5ef; background: white; color: #475569; border-radius: 999px; padding: .5rem .75rem; white-space: nowrap; font-size: .85rem; }
+.status-dot { display: inline-block; width: .55rem; height: .55rem; border-radius: 50%; background: #22a06b; margin-right: .4rem; }
+.page-grid { display: grid; gap: 1rem; }
+.two-column { grid-template-columns: minmax(320px, .85fr) minmax(360px, 1.15fr); align-items: start; }
+.card { background: white; border: 1px solid #dfe5ef; border-radius: 16px; padding: 1rem; box-shadow: 0 8px 24px rgba(16, 24, 39, .06); }
+.stack { display: grid; gap: .9rem; }
+textarea, input, select { border: 1px solid #c8d2e1; border-radius: 10px; padding: .7rem .75rem; background: white; color: #172033; width: 100%; }
+textarea { resize: vertical; }
+button, .button { border: 0; border-radius: 10px; padding: .7rem 1rem; background: #2454d6; color: white; cursor: pointer; font-weight: 650; }
+button:disabled, .button:disabled { opacity: .55; cursor: not-allowed; }
+.button-secondary, .button.button-secondary, button.secondary { background: #edf2ff; color: #24449a; border: 1px solid #cbd8ff; }
+.button-danger { background: #b42318; }
+.badge { display: inline-flex; align-items: center; width: fit-content; background: #ecf1ff; color: #24449a; padding: .22rem .58rem; border-radius: 999px; font-size: .8rem; font-weight: 700; }
+.badge-success { background: #e8f7ef; color: #12724a; }
+.badge-warning { background: #fff6db; color: #8a4b00; }
+.badge-danger { background: #ffe8e4; color: #a12222; }
+.badge-info { background: #e8f2ff; color: #175cd3; }
.muted { color: #617089; }
.error { color: #a12222; }
-table { width: 100%; border-collapse: collapse; }
-td { border-bottom: 1px solid #edf1f7; padding: .45rem; }
+.warning { color: #794400; background: #fff7e6; border: 1px solid #ffd58a; border-radius: 10px; padding: .65rem; }
+.state { border-radius: 12px; padding: .85rem; border: 1px solid #dfe5ef; background: #fbfdff; }
+.state-error { border-color: #ffc9c3; background: #fff3f0; color: #8a1f11; }
+.state-loading { color: #475569; }
+.state-empty { background: #f8fafc; color: #475569; }
+.spinner { display: inline-block; width: .8rem; height: .8rem; margin-right: .5rem; border-radius: 50%; border: 2px solid #b7c2d7; border-top-color: #2454d6; }
.section-header, .drawer-header { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
-.search-box { width: 100%; box-sizing: border-box; margin: .75rem 0; }
-.filter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .75rem; margin-bottom: 1rem; }
-.field-label { display: grid; gap: .25rem; color: #334155; font-size: .85rem; text-transform: capitalize; }
-select { border: 1px solid #c8d2e1; border-radius: 10px; padding: .65rem; font: inherit; background: white; }
-.taxonomy-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); gap: 1rem; align-items: start; }
+.section-header.compact { align-items: center; }
+.button-row, .segmented { display: flex; flex-wrap: wrap; gap: .65rem; align-items: center; }
+.control-grid, .filter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .75rem; }
+.field-label { display: grid; gap: .3rem; color: #334155; font-size: .9rem; font-weight: 650; }
+.field-label span, .check { font-weight: 500; color: #617089; }
+.check { display: flex; gap: .45rem; align-items: center; }
+.check input { width: auto; }
+.search-box { margin: .75rem 0; }
+.metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: .75rem; }
+.metric-grid div { border: 1px solid #edf1f7; border-radius: 12px; padding: .75rem; display: grid; gap: .15rem; }
+.metric-grid strong { font-size: 1.5rem; color: #17368f; }
+.metric-grid span { color: #617089; text-transform: capitalize; }
+.claim-card, .review-item, .risk-card { border-top: 1px solid #edf1f7; padding-top: .85rem; margin-top: .5rem; }
+.risk-card { border: 1px solid #e4eaf4; border-radius: 12px; padding: .85rem; background: #fbfdff; }
+.claim-text, .quote-block { border-left: 4px solid #9fb6ff; background: #f7f9ff; padding: .75rem; border-radius: 8px; }
+.evidence { color: #334155; }
+mark { background: #fff1a8; padding: .05rem .15rem; border-radius: 4px; }
+.healthy-panel { border: 1px solid #c7ebd7; background: #f0fbf5; border-radius: 12px; padding: .75rem; }
+.inline-stats { display: flex; flex-wrap: wrap; gap: .75rem; margin: .5rem 0; }
+.inline-stats div { border: 1px solid #edf1f7; border-radius: 10px; padding: .35rem .55rem; }
+.inline-stats dt { color: #617089; font-size: .75rem; }
+.inline-stats dd { margin: 0; font-weight: 700; }
+.taxonomy-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); gap: 1rem; align-items: start; }
.table-wrap { overflow: auto; max-height: 620px; border: 1px solid #edf1f7; border-radius: 12px; }
-.taxonomy-table th { position: sticky; top: 0; background: #f8fafc; text-align: left; border-bottom: 1px solid #dfe5ef; padding: .55rem; font-size: .82rem; }
-.taxonomy-table tr { cursor: pointer; }
-.taxonomy-table tr:hover, .selected-row { background: #f4f7ff; }
+table { width: 100%; border-collapse: collapse; }
+th { position: sticky; top: 0; background: #f8fafc; text-align: left; border-bottom: 1px solid #dfe5ef; padding: .55rem; font-size: .82rem; z-index: 1; }
+td { border-bottom: 1px solid #edf1f7; padding: .55rem; vertical-align: top; }
+tr:hover, .selected-row { background: #f4f7ff; }
.drawer { border: 1px solid #dfe5ef; border-radius: 12px; padding: 1rem; background: #fbfdff; max-height: 620px; overflow: auto; }
-.drawer h3, .drawer h4 { margin-bottom: .25rem; }
-.button-row { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; margin: .75rem 0; }
-.metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: .75rem; margin: .75rem 0; }
-.metric-grid div { border: 1px solid #edf1f7; border-radius: 12px; padding: .75rem; display: grid; gap: .25rem; }
-.metric-grid strong { font-size: 1.5rem; color: #17368f; }
-.metric-grid span { color: #617089; }
.chips { display: flex; flex-wrap: wrap; gap: .5rem; }
-.issue-list { display: grid; gap: .45rem; padding-left: 1.1rem; }
-.workbench-grid { display: grid; gap: 1rem; }
-@media (max-width: 980px) { .taxonomy-layout { grid-template-columns: 1fr; } .drawer { max-height: none; } }
-.warning { color: #8a4b00; background: #fff7e6; border: 1px solid #ffd58a; border-radius: 10px; padding: .65rem; }
-.settings-page { grid-column: 1 / -1; }
-.provider-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 1rem; }
-.settings-form { display: grid; gap: .75rem; margin-top: .75rem; }
-.provider-test-panel { border-top: 1px solid #edf1f7; margin-top: .75rem; padding-top: .75rem; }
+.issue-list { display: grid; gap: .35rem; padding-left: 1.1rem; }
+.provider-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); gap: 1rem; }
+.settings-form { display: grid; gap: .75rem; }
+.report-list, .review-list { display: grid; gap: .65rem; }
+.list-button { background: #f8fafc; color: #172033; border: 1px solid #dfe5ef; text-align: left; display: grid; gap: .15rem; }
+.list-button.active, .list-button:hover { background: #edf2ff; border-color: #9fb6ff; }
+.report-preview, pre { white-space: pre-wrap; overflow: auto; background: #0f172a; color: #e2e8f0; border-radius: 12px; padding: 1rem; max-height: 560px; }
+.report-frame { width: 100%; min-height: 520px; border: 1px solid #dfe5ef; border-radius: 12px; background: white; }
+code { background: #eef2f7; border-radius: 6px; padding: .1rem .3rem; }
+@media (max-width: 980px) { .shell { display: block; } .sidebar { position: static; width: auto; height: auto; } .sidebar nav { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } .two-column, .taxonomy-layout { grid-template-columns: 1fr; } .header { display: grid; } .api-pill { white-space: normal; } }