(null)
+ const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
- async function run() { setError(''); try { setResult(await analyzeText(text)) } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } }
- return Analyze {error && }{result && }
+
+ useEffect(() => { Promise.all([fetchModelProviders(), fetchActiveModelProvider()]).then(([items, active]) => { setProviders(items); setProviderId(active.provider_id) }).catch(() => undefined) }, [])
+
+ async function run() {
+ setLoading(true); setError('')
+ try {
+ const analysis = await analyzeText({ text, model_provider_id: providerId, top_k: topK, include_healthy_patterns: includeHealthy, allow_deterministic_fallback: allowFallback })
+ setResult(analysis)
+ saveReviewRecord({ id: analysis.text_id, created_at: new Date().toISOString(), analysis, source_text: text })
+ } catch (err) { setError(err instanceof Error ? err.message : 'Unknown analysis error') } finally { setLoading(false) }
+ }
+
+ return
+
Analyze text Choose the active backend provider, tune retrieval breadth, and produce a reviewable risk report.
+ Model provider setProviderId(event.target.value)}>{providers.length ? providers.map(provider => {provider.label} ) : {providerId} }
+
+ {error ? : null}
+
+
{loading ? : result ? : Run an analysis to see scores, claim cards, risk cards, highlighted evidence, healthy patterns, warnings, and export controls.
}
+
}
diff --git a/frontend/src/components/analyze/ClaimCard.tsx b/frontend/src/components/analyze/ClaimCard.tsx
index 2cbe055..60970c1 100644
--- a/frontend/src/components/analyze/ClaimCard.tsx
+++ b/frontend/src/components/analyze/ClaimCard.tsx
@@ -1,3 +1,16 @@
-import type { Claim } from '../../api/types'
+import type { AnalyzedClaim } from '../../api/types'
+import { Badge } from '../shared/Badge'
+import { EmptyState } from '../shared/EmptyState'
+import { EvidenceHighlight } from './EvidenceHighlight'
import { RiskCard } from './RiskCard'
-export function ClaimCard({ claim }: { claim: Claim }) { return {claim.text} {claim.risks.length ? claim.risks.map(risk => ) : No taxonomy match.
} }
+
+export function ClaimCard({ claim }: { claim: AnalyzedClaim }) {
+ const firstRisk = claim.detected_risks[0]
+ return
+ Claim {claim.claim_id} {claim.claim_type}
{claim.detected_risks.length} risks
+ {firstRisk ? : claim.text}
+ {claim.warnings.length ? {claim.warnings.map(warning => {warning} )} : null}
+ {claim.detected_risks.length ? {claim.detected_risks.map(risk => )}
: }
+ {claim.healthy_patterns.length ? Healthy patterns {claim.healthy_patterns.map((pattern, index) =>
{String(pattern.label ?? pattern.pattern ?? 'Healthy signal')}: {String(pattern.explanation ?? pattern.evidence_span ?? '')}
)}
: null}
+
+}
diff --git a/frontend/src/components/analyze/EvidenceHighlight.tsx b/frontend/src/components/analyze/EvidenceHighlight.tsx
index 5793726..ec362cd 100644
--- a/frontend/src/components/analyze/EvidenceHighlight.tsx
+++ b/frontend/src/components/analyze/EvidenceHighlight.tsx
@@ -1 +1,4 @@
-export function EvidenceHighlight({ quote }: { quote: string }) { return {quote} }
+export function EvidenceHighlight({ text, start, end }: { text: string; start: number; end: number }) {
+ if (start < 0 || end <= start || end > text.length) return {text}
+ return {text.slice(0, start)}{text.slice(start, end)} {text.slice(end)}
+}
diff --git a/frontend/src/components/analyze/ExportButtons.tsx b/frontend/src/components/analyze/ExportButtons.tsx
index 06283c0..eb3e53b 100644
--- a/frontend/src/components/analyze/ExportButtons.tsx
+++ b/frontend/src/components/analyze/ExportButtons.tsx
@@ -1 +1,36 @@
-export function ExportButtons() { return Export JSON Export Markdown
}
+import type { AnalysisResponse, GeneratedReport } from '../../api/types'
+import { downloadText, saveGeneratedReport } from '../../api/client'
+import { Button } from '../shared/Button'
+
+export function analysisToMarkdown(result: AnalysisResponse, sourceText: string): string {
+ const lines = [`# Argument Risk Analysis`, '', `Analysis ID: ${result.text_id}`, `Provider: ${result.model_provider_id} (${result.model_name})`, `Overall risk score: ${result.overall_risk_score.toFixed(2)}`, `Risk level: ${result.risk_level}`, `Needs human review: ${result.needs_human_review ? 'yes' : 'no'}`, '', '## Source text', '', sourceText, '', '## Claims']
+ result.claims.forEach(claim => {
+ lines.push('', `### ${claim.claim_id}`, claim.text)
+ if (!claim.detected_risks.length) lines.push('', 'No detected taxonomy risks.')
+ claim.detected_risks.forEach(risk => lines.push('', `- **${risk.label}** (${risk.risk_level}, score ${risk.risk_score.toFixed(2)}): ${risk.explanation}`, ` - Evidence: “${risk.evidence_span}”`))
+ if (claim.healthy_patterns.length) lines.push('', `Healthy patterns: ${claim.healthy_patterns.map(item => String(item.label ?? item.pattern ?? 'signal')).join(', ')}`)
+ })
+ if (result.warnings.length) lines.push('', '## Warnings', ...result.warnings.map(warning => `- ${warning}`))
+ return lines.join('\n')
+}
+
+export function ExportButtons({ result, sourceText }: { result: AnalysisResponse; sourceText: string }) {
+ const markdown = analysisToMarkdown(result, sourceText)
+ const json = JSON.stringify(result, null, 2)
+ const saveReport = () => saveGeneratedReport({
+ id: result.text_id,
+ title: `Analysis ${result.text_id}`,
+ created_at: new Date().toISOString(),
+ analysis_id: result.text_id,
+ formats: ['json', 'markdown', 'html'],
+ json,
+ markdown,
+ html: `${markdown.replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>' }[char] ?? char))} `,
+ } satisfies GeneratedReport)
+ return
+ downloadText(json, `${result.text_id}.json`, 'application/json')}>Export JSON
+ downloadText(markdown, `${result.text_id}.md`, 'text/markdown')}>Export Markdown
+ navigator.clipboard.writeText(markdown)}>Copy report
+ Save to Reports
+
+}
diff --git a/frontend/src/components/analyze/RiskCard.tsx b/frontend/src/components/analyze/RiskCard.tsx
index 076af4f..9d1e78f 100644
--- a/frontend/src/components/analyze/RiskCard.tsx
+++ b/frontend/src/components/analyze/RiskCard.tsx
@@ -1,3 +1,14 @@
-import type { Risk } from '../../api/types'
+import type { DetectedRisk } from '../../api/types'
import { Badge } from '../shared/Badge'
-export function RiskCard({ risk }: { risk: Risk }) { return {risk.severity} {risk.name} {risk.explanation}
Evidence: “{risk.evidence.quote}” }
+
+function tone(level: string) { return level === 'high' || level === 'critical' ? 'danger' : level === 'medium' ? 'warning' : 'info' }
+export function RiskCard({ risk }: { risk: DetectedRisk }) {
+ return
+ {risk.label} {risk.risk_id} · {risk.category}
{risk.risk_level}
+
Score {risk.risk_score.toFixed(2)}
Confidence {Math.round(risk.confidence * 100)}%
Severity {risk.severity}
+ {risk.explanation}
+ {risk.evidence_span ? Evidence: “{risk.evidence_span}”
: null}
+ {risk.false_positive_warning ? {risk.false_positive_warning}
: null}
+ {risk.needs_human_review ? Human review recommended : null}
+
+}
diff --git a/frontend/src/components/analyze/TextInputPanel.tsx b/frontend/src/components/analyze/TextInputPanel.tsx
index 76e85e1..271b60f 100644
--- a/frontend/src/components/analyze/TextInputPanel.tsx
+++ b/frontend/src/components/analyze/TextInputPanel.tsx
@@ -1,4 +1,37 @@
+import type { ProviderProfile } from '../../api/types'
import { Button } from '../shared/Button'
-export function TextInputPanel({ text, setText, onAnalyze }: { text: string; setText: (value: string) => void; onAnalyze: () => void }) {
- return
+import { Badge } from '../shared/Badge'
+
+const examples = [
+ '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.',
+ 'If this reform passes, society will collapse within months and no family will be safe.',
+]
+
+export function TextInputPanel({ text, setText, providerId, providers, topK, setTopK, includeHealthy, setIncludeHealthy, allowFallback, setAllowFallback, loading, onAnalyze }: {
+ text: string
+ setText: (value: string) => void
+ providerId: string
+ providers: ProviderProfile[]
+ topK: number
+ setTopK: (value: number) => void
+ includeHealthy: boolean
+ setIncludeHealthy: (value: boolean) => void
+ allowFallback: boolean
+ setAllowFallback: (value: boolean) => void
+ loading: boolean
+ onAnalyze: () => void
+}) {
+ const provider = providers.find(item => item.provider_id === providerId)
+ return
+
{examples.map((example, index) => setText(example)}>Load example {index + 1} )}
+
}
diff --git a/frontend/src/components/evaluation/ErrorAnalysisTable.tsx b/frontend/src/components/evaluation/ErrorAnalysisTable.tsx
index 676114d..ee8d485 100644
--- a/frontend/src/components/evaluation/ErrorAnalysisTable.tsx
+++ b/frontend/src/components/evaluation/ErrorAnalysisTable.tsx
@@ -1,2 +1,13 @@
-import { Card } from '../shared/Card'
-export function ErrorAnalysisTable() { return ErrorAnalysisTable MVP placeholder wired for local file-backed workflows.
}
+import type { EvaluationResult } from '../../api/types'
+import { EmptyState } from '../shared/EmptyState'
+
+function rows(result: EvaluationResult, key: 'false_positives' | 'false_negatives' | 'evidence_span_misses') { return (result[key] ?? []) as Array> }
+export function ErrorAnalysisTable({ result }: { result: EvaluationResult }) {
+ const sections = [
+ ['False positives', rows(result, 'false_positives')],
+ ['False negatives', rows(result, 'false_negatives')],
+ ['Evidence span misses', rows(result, 'evidence_span_misses')],
+ ] as const
+ if (sections.every(([, items]) => !items.length)) return
+ return {sections.map(([title, items]) =>
{title} {items.length ? {items.map((item, index) => {JSON.stringify(item, null, 2)} )}
: None reported.
})}
+}
diff --git a/frontend/src/components/evaluation/EvaluationPage.tsx b/frontend/src/components/evaluation/EvaluationPage.tsx
index 841ca65..009dae8 100644
--- a/frontend/src/components/evaluation/EvaluationPage.tsx
+++ b/frontend/src/components/evaluation/EvaluationPage.tsx
@@ -1,2 +1,17 @@
+import { useState } from 'react'
+import { downloadText, runEvaluation } from '../../api/client'
+import type { EvaluationResult } from '../../api/types'
+import { Button } from '../shared/Button'
import { Card } from '../shared/Card'
-export function EvaluationPage() { return Evaluation MVP placeholder wired for local file-backed workflows.
}
+import { ErrorState } from '../shared/ErrorState'
+import { LoadingState } from '../shared/LoadingState'
+import { MetricsCards } from './MetricsCards'
+import { ErrorAnalysisTable } from './ErrorAnalysisTable'
+
+export function EvaluationPage() {
+ const [result, setResult] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+ async function run() { setLoading(true); setError(''); try { setResult(await runEvaluation()) } catch (err) { setError(err instanceof Error ? err.message : 'Evaluation failed') } finally { setLoading(false) } }
+ return Evaluation Run the backend mini evaluation set and inspect metrics, false positives, false negatives, and evidence span misses.
{loading ? 'Running…' : 'Run evaluation'} {loading ? : null}{error ? : null}{result ? <>Raw evaluation JSON {JSON.stringify(result, null, 2)} downloadText(JSON.stringify(result, null, 2), 'evaluation.json', 'application/json')}>Export evaluation JSON > : No evaluation run yet.
}
+}
diff --git a/frontend/src/components/evaluation/MetricsCards.tsx b/frontend/src/components/evaluation/MetricsCards.tsx
index be9f9bd..b29b90b 100644
--- a/frontend/src/components/evaluation/MetricsCards.tsx
+++ b/frontend/src/components/evaluation/MetricsCards.tsx
@@ -1,2 +1,8 @@
-import { Card } from '../shared/Card'
-export function MetricsCards() { return MetricsCards MVP placeholder wired for local file-backed workflows.
}
+import type { EvaluationResult } from '../../api/types'
+
+export function MetricsCards({ result }: { result: EvaluationResult }) {
+ const metrics = result.metrics ?? {}
+ const fallback = { Items: Number(result.items ?? 0), Analyses: result.analyses?.length ?? 0 }
+ const entries = Object.keys(metrics).length ? Object.entries(metrics) : Object.entries(fallback)
+ return {entries.map(([name, value]) =>
{typeof value === 'number' ? value.toFixed(Number.isInteger(value) ? 0 : 2) : String(value)} {name.replaceAll('_', ' ')}
)}
+}
diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx
index 48f95af..707a831 100644
--- a/frontend/src/components/layout/AppShell.tsx
+++ b/frontend/src/components/layout/AppShell.tsx
@@ -1 +1,8 @@
-import { Header } from './Header'; import { Sidebar } from './Sidebar'; export function AppShell({ children }: { children: React.ReactNode }) { return {children}
}
+import type { ReactNode } from 'react'
+import type { PageId } from '../../App'
+import { Header } from './Header'
+import { Sidebar } from './Sidebar'
+
+export function AppShell({ activePage, onNavigate, children }: { activePage: PageId; onNavigate: (page: PageId) => void; children: ReactNode }) {
+ return {children}
+}
diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx
index 3f792dd..1f29d2e 100644
--- a/frontend/src/components/layout/Header.tsx
+++ b/frontend/src/components/layout/Header.tsx
@@ -1 +1,24 @@
-export function Header() { return }
+import { API_BASE } from '../../api/client'
+import type { PageId } from '../../App'
+
+const titles: Record = {
+ analyze: 'Analyze Text',
+ taxonomy: 'Taxonomy Browser',
+ workbench: 'Taxonomy Workbench',
+ settings: 'Model Settings',
+ review: 'Review Outputs',
+ evaluation: 'Evaluation',
+ reports: 'Reports',
+ about: 'About',
+}
+
+export function Header({ activePage }: { activePage: PageId }) {
+ return
+}
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 6c69f75..fd4406e 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -1,2 +1,21 @@
-const items = ['Analyze', 'Taxonomy', 'Workbench', 'Model Settings', 'Review', 'Evaluation', 'Reports']
-export function Sidebar() { return ARE {items.map(item => {item} )} }
+import type { PageId } from '../../App'
+
+const items: Array<{ id: PageId; label: string }> = [
+ { id: 'analyze', label: 'Analyze' },
+ { id: 'taxonomy', label: 'Taxonomy Browser' },
+ { id: 'workbench', label: 'Taxonomy Workbench' },
+ { id: 'settings', label: 'Model Settings' },
+ { id: 'review', label: 'Review' },
+ { id: 'evaluation', label: 'Evaluation' },
+ { id: 'reports', label: 'Reports' },
+ { id: 'about', label: 'About' },
+]
+
+export function Sidebar({ activePage, onNavigate }: { activePage: PageId; onNavigate: (page: PageId) => void }) {
+ return
+ ARE Argument Risk Engine
+
+ {items.map(item => onNavigate(item.id)}>{item.label} )}
+
+
+}
diff --git a/frontend/src/components/reports/ReportPreview.tsx b/frontend/src/components/reports/ReportPreview.tsx
index 475f98c..615eac0 100644
--- a/frontend/src/components/reports/ReportPreview.tsx
+++ b/frontend/src/components/reports/ReportPreview.tsx
@@ -1,2 +1,8 @@
-import { Card } from '../shared/Card'
-export function ReportPreview() { return ReportPreview MVP placeholder wired for local file-backed workflows.
}
+import type { GeneratedReport, ReportFormat } from '../../api/types'
+
+export function ReportPreview({ report, format }: { report?: GeneratedReport; format: ReportFormat }) {
+ if (!report) return Select a generated report to preview Markdown, HTML, or JSON.
+ const content = format === 'json' ? report.json : format === 'html' ? report.html : report.markdown
+ if (!content) return This 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 Reports MVP placeholder wired for local file-backed workflows.
}
+import { EmptyState } from '../shared/EmptyState'
+import { ReportPreview } from './ReportPreview'
+
+export function ReportsPage() {
+ const [reports, setReports] = useState([])
+ const [selectedId, setSelectedId] = useState('')
+ const [format, setFormat] = useState('markdown')
+ const selected = reports.find(report => report.id === selectedId) ?? reports[0]
+ const reload = () => { const items = listReports(); setReports(items); setSelectedId(current => current || items[0]?.id || '') }
+ useEffect(reload, [])
+ async function loadDemo() { const markdown = await fetchDemoReport(); saveGeneratedReport({ id: `demo-${Date.now()}`, title: 'Demo backend report', created_at: new Date().toISOString(), formats: ['markdown'], markdown }); reload() }
+ return Reports List generated reports, preview content, and download JSON, Markdown, or HTML when available.
Load demo report {reports.length ? {reports.map(report => setSelectedId(report.id)}>{report.title} {new Date(report.created_at).toLocaleString()} )}
: } setFormat(event.target.value as ReportFormat)}>Markdown HTML JSON {selected ? selected.formats.map(item => downloadReport(selected, item)}>Download {item} ) : null}
+}
diff --git a/frontend/src/components/review/FeedbackControls.tsx b/frontend/src/components/review/FeedbackControls.tsx
index ac08f31..d387d47 100644
--- a/frontend/src/components/review/FeedbackControls.tsx
+++ b/frontend/src/components/review/FeedbackControls.tsx
@@ -1,2 +1,13 @@
-import { Card } from '../shared/Card'
-export function FeedbackControls() { return FeedbackControls MVP placeholder wired for local file-backed workflows.
}
+import type { ReviewDecision } from '../../api/types'
+import { Button } from '../shared/Button'
+
+const decisions: Array<{ value: ReviewDecision; label: string }> = [
+ { value: 'correct', label: 'Correct' },
+ { value: 'incorrect', label: 'Incorrect' },
+ { value: 'partial', label: 'Partial' },
+ { value: 'insufficient_evidence', label: 'Insufficient evidence' },
+]
+
+export function FeedbackControls({ decision, onDecision }: { decision: ReviewDecision; onDecision: (decision: ReviewDecision) => void }) {
+ return {decisions.map(item => onDecision(item.value)}>{item.label} )}
+}
diff --git a/frontend/src/components/review/ReviewItem.tsx b/frontend/src/components/review/ReviewItem.tsx
index 991ab81..5f7b3d0 100644
--- a/frontend/src/components/review/ReviewItem.tsx
+++ b/frontend/src/components/review/ReviewItem.tsx
@@ -1,2 +1,28 @@
-import { Card } from '../shared/Card'
-export function ReviewItem() { return ReviewItem MVP placeholder wired for local file-backed workflows.
}
+import { useState } from 'react'
+import { submitReviewFeedback, updateReviewRecord } from '../../api/client'
+import type { ReviewDecision, ReviewRecord } from '../../api/types'
+import { Button } from '../shared/Button'
+import { Badge } from '../shared/Badge'
+import { FeedbackControls } from './FeedbackControls'
+
+export function ReviewItem({ record, onSaved }: { record: ReviewRecord; onSaved: () => void }) {
+ const [decision, setDecision] = useState(record.feedback?.decision ?? 'correct')
+ const [labels, setLabels] = useState(record.feedback?.corrected_labels?.join(', ') ?? '')
+ const [notes, setNotes] = useState(record.feedback?.notes ?? '')
+ const [saving, setSaving] = useState(false)
+ const riskIds = Array.from(new Set(record.analysis.claims.flatMap(claim => claim.detected_risks.map(risk => risk.risk_id))))
+ async function save() {
+ setSaving(true)
+ const feedback = { analysis_id: record.analysis.text_id, taxonomy_id: riskIds[0] ?? null, decision, notes, corrected_labels: labels.split(',').map(label => label.trim()).filter(Boolean) }
+ try { await submitReviewFeedback(feedback); updateReviewRecord({ ...record, feedback }); onSaved() } finally { setSaving(false) }
+ }
+ return
+ {record.analysis.text_id} {new Date(record.created_at).toLocaleString()}
{record.feedback ? record.feedback.decision : 'unreviewed'}
+ {record.source_text}
+ Detected labels: {riskIds.length ? riskIds.join(', ') : 'none'}
+
+ Corrected labels setLabels(event.target.value)} placeholder="comma-separated taxonomy ids" />
+ Notes
+ {saving ? 'Saving…' : 'Save review notes'}
+
+}
diff --git a/frontend/src/components/review/ReviewPage.tsx b/frontend/src/components/review/ReviewPage.tsx
index fd864fd..6156be2 100644
--- a/frontend/src/components/review/ReviewPage.tsx
+++ b/frontend/src/components/review/ReviewPage.tsx
@@ -1,2 +1,12 @@
+import { useEffect, useState } from 'react'
+import { listReviewRecords } from '../../api/client'
+import type { ReviewRecord } from '../../api/types'
import { Card } from '../shared/Card'
-export function ReviewPage() { return Review MVP placeholder wired for local file-backed workflows.
}
+import { ReviewQueue } from './ReviewQueue'
+
+export function ReviewPage() {
+ const [records, setRecords] = useState([])
+ const reload = () => setRecords(listReviewRecords())
+ useEffect(reload, [])
+ return Review outputs Review prior analysis items, mark correctness, add corrected labels, and persist notes to the backend feedback endpoint.
{records.length} items
+}
diff --git a/frontend/src/components/review/ReviewQueue.tsx b/frontend/src/components/review/ReviewQueue.tsx
index 55d8c8e..3bce85a 100644
--- a/frontend/src/components/review/ReviewQueue.tsx
+++ b/frontend/src/components/review/ReviewQueue.tsx
@@ -1,2 +1,8 @@
-import { Card } from '../shared/Card'
-export function ReviewQueue() { return ReviewQueue MVP placeholder wired for local file-backed workflows.
}
+import type { ReviewRecord } from '../../api/types'
+import { EmptyState } from '../shared/EmptyState'
+import { ReviewItem } from './ReviewItem'
+
+export function ReviewQueue({ records, onSaved }: { records: ReviewRecord[]; onSaved: () => void }) {
+ if (!records.length) return
+ return {records.map(record => )}
+}
diff --git a/frontend/src/components/shared/Badge.tsx b/frontend/src/components/shared/Badge.tsx
index 3b46b68..28cf312 100644
--- a/frontend/src/components/shared/Badge.tsx
+++ b/frontend/src/components/shared/Badge.tsx
@@ -1 +1,2 @@
-export function Badge({ children }: { children: React.ReactNode }) { return {children} }
+import type { ReactNode } from 'react'
+export function Badge({ children, tone = 'neutral' }: { children: ReactNode; tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'info' }) { return {children} }
diff --git a/frontend/src/components/shared/Button.tsx b/frontend/src/components/shared/Button.tsx
index 2d3f347..2789f2e 100644
--- a/frontend/src/components/shared/Button.tsx
+++ b/frontend/src/components/shared/Button.tsx
@@ -1 +1,6 @@
-export function Button(props: React.ButtonHTMLAttributes) { return }
+import type { ButtonHTMLAttributes, ReactNode } from 'react'
+
+type ButtonProps = ButtonHTMLAttributes & { variant?: 'primary' | 'secondary' | 'danger'; children: ReactNode }
+export function Button({ variant = 'primary', className = '', children, ...props }: ButtonProps) {
+ return {children}
+}
diff --git a/frontend/src/components/shared/Card.tsx b/frontend/src/components/shared/Card.tsx
index 626e0ee..507b581 100644
--- a/frontend/src/components/shared/Card.tsx
+++ b/frontend/src/components/shared/Card.tsx
@@ -1 +1,2 @@
-export function Card({ children }: { children: React.ReactNode }) { return }
+import type { ReactNode } from 'react'
+export function Card({ children, className = '' }: { children: ReactNode; className?: string }) { return }
diff --git a/frontend/src/components/shared/EmptyState.tsx b/frontend/src/components/shared/EmptyState.tsx
index bead755..4647e4a 100644
--- a/frontend/src/components/shared/EmptyState.tsx
+++ b/frontend/src/components/shared/EmptyState.tsx
@@ -1 +1 @@
-export function EmptyState({ message }: { message: string }) { return {message}
}
+export function EmptyState({ title = 'No data yet', message }: { title?: string; message: string }) { return }
diff --git a/frontend/src/components/shared/ErrorState.tsx b/frontend/src/components/shared/ErrorState.tsx
index d19cb60..bbe8e4f 100644
--- a/frontend/src/components/shared/ErrorState.tsx
+++ b/frontend/src/components/shared/ErrorState.tsx
@@ -1 +1 @@
-export function ErrorState({ message }: { message: string }) { return {message}
}
+export function ErrorState({ message, title = 'Something went wrong' }: { message: string; title?: string }) { return }
diff --git a/frontend/src/components/shared/LoadingState.tsx b/frontend/src/components/shared/LoadingState.tsx
index fa8aaf3..c026ec5 100644
--- a/frontend/src/components/shared/LoadingState.tsx
+++ b/frontend/src/components/shared/LoadingState.tsx
@@ -1 +1 @@
-export function LoadingState() { return Loading...
}
+export function LoadingState({ label = 'Loading…' }: { label?: string }) { return {label}
}
diff --git a/frontend/src/runtime-dashboard.js b/frontend/src/runtime-dashboard.js
new file mode 100644
index 0000000..414f702
--- /dev/null
+++ b/frontend/src/runtime-dashboard.js
@@ -0,0 +1,24 @@
+const API_BASE = (localStorage.getItem('are.apiBase') || 'http://localhost:8000/api').replace(/\/$/, '')
+const state = { page: 'Analyze', providers: [], activeProvider: 'deterministic_baseline', taxonomy: [], reports: JSON.parse(localStorage.getItem('are.generated.reports') || '[]'), reviews: JSON.parse(localStorage.getItem('are.review.records') || '[]') }
+const pages = ['Analyze','Taxonomy Browser','Taxonomy Workbench','Model Settings','Review','Evaluation','Reports','About']
+const root = document.getElementById('root')
+const el = (tag, attrs = {}, ...children) => { const node = document.createElement(tag); Object.entries(attrs).forEach(([k,v]) => { if (k === 'class') node.className = v; else if (k.startsWith('on')) node.addEventListener(k.slice(2).toLowerCase(), v); else if (v !== undefined && v !== null) node.setAttribute(k, v) }); children.flat().forEach(c => node.append(c?.nodeType ? c : document.createTextNode(String(c ?? '')))); return node }
+async function api(path, options) { const r = await fetch(`${API_BASE}${path}`, options); const ct = r.headers.get('content-type') || ''; const body = ct.includes('json') ? await r.json() : await r.text(); if (!r.ok || body?.detail) throw new Error(body?.detail || r.statusText); return body }
+function download(text, name, type='text/plain') { const a = el('a'); a.href = URL.createObjectURL(new Blob([text], { type })); a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 500) }
+function save(key, value) { localStorage.setItem(key, JSON.stringify(value)) }
+function shell(content) { root.replaceChildren(el('div',{class:'shell'}, el('aside',{class:'sidebar'}, el('div',{class:'brand'}, el('span',{},'ARE'), el('small',{},'Argument Risk Engine')), el('nav',{}, pages.map(p => el('button',{class:`nav-item ${state.page===p?'active':''}`, onclick:()=>{state.page=p; render()}},p)))), el('main',{class:'main'}, el('header',{class:'header'}, el('div',{}, el('p',{class:'eyebrow'},'Argument-Risk-Engine Dashboard'), el('h1',{},state.page), el('p',{},'Taxonomy-grounded argument risk signals for human analysts.')), el('div',{class:'api-pill'},'Backend: ',API_BASE)), content))) }
+function card(...children) { return el('section',{class:'card stack'},...children) }
+function errorBox(message) { return el('div',{class:'state state-error'}, el('strong',{},'Error'), el('p',{},message)) }
+async function loadProviders() { try { state.providers = (await api('/settings/model-providers')).providers || []; state.activeProvider = (await api('/settings/active-model-provider')).provider_id || state.activeProvider } catch {} }
+function markdown(result, text) { return `# Argument Risk Analysis\n\nAnalysis ID: ${result.text_id}\nProvider: ${result.model_provider_id}\nOverall score: ${result.overall_risk_score}\nRisk level: ${result.risk_level}\n\n## Source\n${text}\n\n## Claims\n` + (result.claims||[]).map(c => `### ${c.claim_id}\n${c.text}\n` + (c.detected_risks||[]).map(r => `- **${r.label}** (${r.risk_level}): ${r.explanation}\n - Evidence: “${r.evidence_span}”`).join('\n')).join('\n\n') }
+function analyzePage() { let resultBox = el('div',{class:'stack'}, el('p',{class:'muted'},'Run an analysis to see scores, claim cards, risk cards, highlighted evidence, warnings, and export controls.')); const text = el('textarea',{rows:10},'Everyone always caused this problem because of that policy.'); const provider = el('select',{}, ...(state.providers.length?state.providers:[{provider_id:state.activeProvider,label:state.activeProvider}]).map(p=>el('option',{value:p.provider_id},p.label))); provider.value = state.activeProvider; const topK = el('input',{type:'number',min:'1',value:'8'}); const healthy = el('input',{type:'checkbox'}); healthy.checked = true; const fallback = el('input',{type:'checkbox'}); fallback.checked = true; async function run(){ resultBox.replaceChildren(el('div',{class:'state state-loading'},'Analyzing…')); try { const result = await api('/analysis/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:text.value,model_provider_id:provider.value,top_k:Number(topK.value),include_healthy_patterns:healthy.checked,allow_deterministic_fallback:fallback.checked})}); state.reviews.unshift({id:result.text_id,created_at:new Date().toISOString(),analysis:result,source_text:text.value}); save('are.review.records',state.reviews.slice(0,50)); const md = markdown(result,text.value); resultBox.replaceChildren(el('div',{class:'section-header'},el('div',{},el('h2',{},'Analysis report'),el('p',{class:'muted'},`Analysis ID: ${result.text_id} · ${result.model_name}`)),el('span',{class:'badge'},result.risk_level)),el('div',{class:'metric-grid'},el('div',{},el('strong',{},Number(result.overall_risk_score||0).toFixed(2)),el('span',{},'Overall score')),el('div',{},el('strong',{},(result.claims||[]).length),el('span',{},'Claims'))),el('div',{class:'button-row'},el('button',{onclick:()=>download(JSON.stringify(result,null,2),`${result.text_id}.json`,'application/json')},'Export JSON'),el('button',{onclick:()=>download(md,`${result.text_id}.md`,'text/markdown')},'Export Markdown'),el('button',{onclick:()=>navigator.clipboard.writeText(md)},'Copy report'),el('button',{onclick:()=>{state.reports.unshift({id:result.text_id,title:`Analysis ${result.text_id}`,created_at:new Date().toISOString(),formats:['json','markdown','html'],json:JSON.stringify(result,null,2),markdown:md,html:`${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; } }