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 @@ -Argument-Risk-Engine
+ + + + + + Argument-Risk-Engine + + + +
+ + + diff --git a/frontend/scripts/build_frontend.mjs b/frontend/scripts/build_frontend.mjs index 3870943..1a13f59 100644 --- a/frontend/scripts/build_frontend.mjs +++ b/frontend/scripts/build_frontend.mjs @@ -1,9 +1,10 @@ -import { mkdirSync, copyFileSync, writeFileSync } from 'node:fs' +import { mkdirSync, copyFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const root = dirname(dirname(fileURLToPath(import.meta.url))) const dist = join(root, 'dist') -mkdirSync(dist, { recursive: true }) +mkdirSync(join(dist, 'src/styles'), { recursive: true }) copyFileSync(join(root, 'index.html'), join(dist, 'index.html')) -writeFileSync(join(dist, 'app.js'), 'console.log("Argument-Risk-Engine dashboard build");\n') -console.log('Built frontend MVP into dist/') +copyFileSync(join(root, 'src/styles/global.css'), join(dist, 'src/styles/global.css')) +copyFileSync(join(root, 'src/runtime-dashboard.js'), join(dist, 'app.js')) +console.log('Built frontend dashboard into dist/') diff --git a/frontend/scripts/dev_server.mjs b/frontend/scripts/dev_server.mjs index 2cac789..0a1b08c 100644 --- a/frontend/scripts/dev_server.mjs +++ b/frontend/scripts/dev_server.mjs @@ -3,10 +3,11 @@ import { readFileSync, existsSync } from 'node:fs' import { extname, join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const root = dirname(dirname(fileURLToPath(import.meta.url))) -const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.tsx': 'application/javascript', '.ts': 'application/javascript' } +const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json' } const server = http.createServer((req, res) => { - let path = req.url === '/' ? '/index.html' : req.url.split('?')[0] - let file = join(root, path) + const urlPath = req.url === '/' ? '/index.html' : req.url.split('?')[0] + const mappedPath = urlPath === '/app.js' ? '/src/runtime-dashboard.js' : urlPath + let file = join(root, mappedPath) if (!existsSync(file)) file = join(root, 'index.html') res.writeHead(200, { 'Content-Type': types[extname(file)] || 'text/plain' }) res.end(readFileSync(file)) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 82f2e56..5e6b2fd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { AnalyzePage } from './components/analyze/AnalyzePage' import { AppShell } from './components/layout/AppShell' import { EvaluationPage } from './components/evaluation/EvaluationPage' @@ -6,7 +7,29 @@ import { ReviewPage } from './components/review/ReviewPage' import { ModelSettingsPage } from './components/settings/ModelSettingsPage' import { TaxonomyPage } from './components/taxonomy/TaxonomyPage' import { TaxonomyWorkbenchPage } from './components/taxonomy_workbench/TaxonomyWorkbenchPage' +import { Card } from './components/shared/Card' + +export type PageId = 'analyze' | 'taxonomy' | 'workbench' | 'settings' | 'review' | 'evaluation' | 'reports' | 'about' + +function AboutPage() { + return +

About this dashboard

+

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.

+
+} export default function App() { - return
+ const [activePage, setActivePage] = useState('analyze') + const page = { + analyze: , + taxonomy: , + workbench: , + settings: , + review: , + evaluation: , + reports: , + about: , + }[activePage] + return {page} } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 886123b..aa13e82 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,125 +1,186 @@ -import type { ActiveProviderResponse, AnalysisResponse, ProviderListResponse, ProviderProfile, ProviderTestResponse, TaxonomyCoverage, TaxonomyEntry, TaxonomyImportResult, TaxonomyPackSummary, TaxonomyQualityReport, TaxonomyValidationResult } from './types' +import type { + ActiveProviderResponse, + AnalysisRequest, + AnalysisResponse, + EvaluationResult, + GeneratedReport, + ProviderListResponse, + ProviderProfile, + ProviderTestResponse, + ReportFormat, + ReviewFeedback, + ReviewRecord, + TaxonomyCoverage, + TaxonomyEntry, + TaxonomyImportResult, + TaxonomyPackSummary, + TaxonomyQualityReport, + TaxonomyValidationResult, +} from './types' + +export const API_BASE = (import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api').replace(/\/$/, '') +const REVIEW_KEY = 'are.review.records' +const REPORT_KEY = 'are.generated.reports' + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(`${API_BASE}${path}`, init) + const contentType = response.headers.get('content-type') ?? '' + const payload = contentType.includes('application/json') ? await response.json() : await response.text() + if (!response.ok || (typeof payload === 'object' && payload !== null && 'detail' in payload)) { + const detail = typeof payload === 'object' && payload !== null && 'detail' in payload ? String((payload as { detail: unknown }).detail) : response.statusText + throw new Error(detail || `Request failed: ${response.status}`) + } + return payload as T +} -const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api' +function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + link.remove() + URL.revokeObjectURL(url) +} -export async function analyzeText(text: string): Promise { - const response = await fetch(`${API_BASE}/analysis/analyze`, { +export function downloadText(content: string, filename: string, type = 'text/plain'): void { + downloadBlob(new Blob([content], { type }), filename) +} + +export async function analyzeText(payload: AnalysisRequest): Promise { + return request('/analysis/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text }), + body: JSON.stringify(payload), }) - if (!response.ok) throw new Error('Analysis request failed') - return response.json() } export async function fetchTaxonomy(params: URLSearchParams = new URLSearchParams()): Promise { const query = params.toString() - const response = await fetch(`${API_BASE}/taxonomy${query ? `?${query}` : ''}`) - if (!response.ok) throw new Error('Taxonomy request failed') - const payload = await response.json() - return payload.entries -} - -export async function searchTaxonomy(q: string): Promise { - const response = await fetch(`${API_BASE}/taxonomy/search?q=${encodeURIComponent(q)}`) - if (!response.ok) throw new Error('Taxonomy search failed') - const payload = await response.json() + const payload = await request<{ entries: TaxonomyEntry[] }>(`/taxonomy${query ? `?${query}` : ''}`) return payload.entries } export async function fetchTaxonomyPacks(): Promise { - const response = await fetch(`${API_BASE}/taxonomy-workbench/packs`) - if (!response.ok) throw new Error('Taxonomy packs request failed') - return (await response.json()).packs + return (await request<{ packs: TaxonomyPackSummary[] }>('/taxonomy-workbench/packs')).packs } export async function fetchTaxonomyCoverage(): Promise { - const response = await fetch(`${API_BASE}/taxonomy-workbench/coverage`) - if (!response.ok) throw new Error('Taxonomy coverage request failed') - return response.json() + return request('/taxonomy-workbench/coverage') } export async function fetchTaxonomyQuality(): Promise { - const response = await fetch(`${API_BASE}/taxonomy-workbench/quality-report`) - if (!response.ok) throw new Error('Taxonomy quality request failed') - return response.json() + return request('/taxonomy-workbench/quality-report') } export async function validateTaxonomy(): Promise { - const response = await fetch(`${API_BASE}/taxonomy-workbench/validate`, { method: 'POST' }) - if (!response.ok) throw new Error('Taxonomy validation failed') - return response.json() + return request('/taxonomy-workbench/validate', { method: 'POST' }) } export async function importTaxonomyWorkbook(file: File): Promise { const formData = new FormData() formData.append('file', file) - const response = await fetch(`${API_BASE}/taxonomy-workbench/import-excel`, { - method: 'POST', - body: formData, - }) - if (!response.ok) throw new Error('Taxonomy workbook import failed') - return response.json() + return request('/taxonomy-workbench/import-excel', { method: 'POST', body: formData }) } -export function exportTaxonomyWorkbook(): void { - window.location.href = `${API_BASE}/taxonomy-workbench/export-excel` +export async function exportTaxonomyWorkbook(): Promise { + const response = await fetch(`${API_BASE}/taxonomy-workbench/export-excel`) + if (!response.ok) throw new Error('Taxonomy workbook export failed') + const disposition = response.headers.get('content-disposition') ?? '' + const filename = disposition.match(/filename="?([^";]+)"?/)?.[1] ?? 'taxonomy.xlsx' + downloadBlob(await response.blob(), filename) } export async function updateTaxonomyActivation(riskId: string, activationStatus: string, enabledForClassification?: boolean): Promise { - const response = await fetch(`${API_BASE}/taxonomy-workbench/entries/${encodeURIComponent(riskId)}/activation`, { + const payload = await request<{ entry: TaxonomyEntry }>(`/taxonomy-workbench/entries/${encodeURIComponent(riskId)}/activation`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ activation_status: activationStatus, enabled_for_classification: enabledForClassification }), }) - if (!response.ok) throw new Error('Activation update failed') - const payload = await response.json() - if (payload.detail) throw new Error(payload.detail) return payload.entry } - export async function fetchModelProviders(): Promise { - const response = await fetch(`${API_BASE}/settings/model-providers`) - if (!response.ok) throw new Error('Model providers request failed') - const payload: ProviderListResponse = await response.json() + const payload = await request('/settings/model-providers') return payload.providers } export async function saveModelProvider(profile: ProviderProfile): Promise { - const response = await fetch(`${API_BASE}/settings/model-providers/${encodeURIComponent(profile.provider_id)}`, { + return request(`/settings/model-providers/${encodeURIComponent(profile.provider_id)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(profile), }) - if (!response.ok) throw new Error('Model provider update failed') - const payload = await response.json() - if (payload.detail) throw new Error(payload.detail) - return payload } export async function fetchActiveModelProvider(): Promise { - const response = await fetch(`${API_BASE}/settings/active-model-provider`) - if (!response.ok) throw new Error('Active provider request failed') - return response.json() + return request('/settings/active-model-provider') } export async function setActiveModelProvider(providerId: string): Promise { - const response = await fetch(`${API_BASE}/settings/active-model-provider`, { + return request('/settings/active-model-provider', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider_id: providerId }), }) - if (!response.ok) throw new Error('Active provider update failed') - const payload = await response.json() - if (payload.detail) throw new Error(payload.detail) - return payload } export async function testModelProvider(providerId: string): Promise { - const response = await fetch(`${API_BASE}/settings/model-providers/${encodeURIComponent(providerId)}/test`, { method: 'POST' }) - if (!response.ok) throw new Error('Provider test failed') - const payload = await response.json() - if (payload.detail) throw new Error(payload.detail) - return payload + return request(`/settings/model-providers/${encodeURIComponent(providerId)}/test`, { method: 'POST' }) +} + +export async function runEvaluation(): Promise { + return request('/evaluation/run') +} + +export async function submitReviewFeedback(feedback: ReviewFeedback): Promise { + await request<{ status: string }>('/review/feedback', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(feedback), + }) +} + +function readStored(key: string, fallback: T): T { + try { return JSON.parse(localStorage.getItem(key) || '') as T } catch { return fallback } +} + +function writeStored(key: string, value: T): void { + localStorage.setItem(key, JSON.stringify(value)) +} + +export function listReviewRecords(): ReviewRecord[] { + return readStored(REVIEW_KEY, []) +} + +export function saveReviewRecord(record: ReviewRecord): void { + const records = listReviewRecords().filter(item => item.id !== record.id) + writeStored(REVIEW_KEY, [record, ...records].slice(0, 50)) +} + +export function updateReviewRecord(record: ReviewRecord): void { + writeStored(REVIEW_KEY, listReviewRecords().map(item => item.id === record.id ? record : item)) +} + +export function listReports(): GeneratedReport[] { + return readStored(REPORT_KEY, []) +} + +export function saveGeneratedReport(report: GeneratedReport): void { + writeStored(REPORT_KEY, [report, ...listReports().filter(item => item.id !== report.id)].slice(0, 50)) +} + +export function downloadReport(report: GeneratedReport, format: ReportFormat): void { + const content = format === 'json' ? report.json : format === 'html' ? report.html : report.markdown + if (!content) throw new Error(`No ${format} preview is available for this report`) + const extension = format === 'markdown' ? 'md' : format + const type = format === 'json' ? 'application/json' : format === 'html' ? 'text/html' : 'text/markdown' + downloadText(content, `${report.id}.${extension}`, type) +} + +export async function fetchDemoReport(): Promise { + const response = await fetch(`${API_BASE}/reports/demo.md`) + if (!response.ok) throw new Error('Could not fetch demo report') + return response.text() } diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index ff42580..796d836 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -1,7 +1,64 @@ -export type AnalysisRequest = { text: string } -export type Risk = { taxonomy_id: string; name: string; severity: string; confidence: number; score: number; explanation: string; evidence: { quote: string; start: number; end: number }; mitigation: string } -export type Claim = { text: string; risks: Risk[] } -export type AnalysisResponse = { analysis_id: string; summary: Record; claims: Claim[]; risks: Risk[] } +export type RiskLevel = 'low' | 'medium' | 'high' | 'critical' | string + +export type AnalysisRequest = { + text: string + mode?: string + model_provider_id?: string + top_k?: number + include_healthy_patterns?: boolean + allow_deterministic_fallback?: boolean + include_retrieval_diagnostics?: boolean +} + +export type DetectedRisk = { + risk_id: string + category: string + label: string + severity: string + confidence: number + risk_score: number + risk_level: RiskLevel + evidence_span: string + evidence_start_char: number + evidence_end_char: number + explanation: string + false_positive_warning: string + needs_human_review: boolean +} + +export type HealthyPattern = { + label?: string + pattern?: string + explanation?: string + evidence_span?: string + [key: string]: unknown +} + +export type AnalyzedClaim = { + claim_id: string + text: string + claim_type: string + start_char: number + end_char: number + detected_risks: DetectedRisk[] + healthy_patterns: HealthyPattern[] + warnings: string[] + retrieval_diagnostics: Record +} + +export type AnalysisResponse = { + text_id: string + mode: string + model_provider_id: string + model_name: string + llm_used: boolean + deterministic_fallback_used: boolean + claims: AnalyzedClaim[] + overall_risk_score: number + risk_level: RiskLevel + needs_human_review: boolean + warnings: string[] +} export type TaxonomyEntry = { id: string @@ -34,10 +91,6 @@ export type TaxonomyEntry = { healthy_suppressor: boolean model_assisted_allowed: boolean notes: string - description?: string - keywords?: string[] - severity?: string - active?: boolean } export type TaxonomyFilters = { @@ -51,6 +104,7 @@ export type TaxonomyFilters = { false_positive_sensitivity: string } +export type TaxonomyIssue = { code: string; message: string; severity: string; entry_id?: string; row_number?: number } export type TaxonomyImportResult = { entry_count: number; errors: string[]; warnings: string[]; backup_paths?: string[] } export type TaxonomyCoverage = { entry_count: number @@ -66,12 +120,11 @@ export type TaxonomyCoverage = { missing_examples_count: number missing_false_positive_warnings_count: number } -export type TaxonomyIssue = { code: string; message: string; severity: string; entry_id?: string; row_number?: number } export type TaxonomyQualityReport = { ok: boolean; entry_count: number; active_classification_count: number; error_count: number; warning_count: number; errors: TaxonomyIssue[]; warnings: TaxonomyIssue[] } export type TaxonomyPackSummary = { pack: string; entry_count: number; active_count: number; enabled_for_classification_count: number } export type TaxonomyValidationResult = { ok: boolean; entry_count: number; active_classification_count: number; errors: TaxonomyIssue[]; warnings: TaxonomyIssue[] } -export type ProviderType = 'deterministic' | 'openai_compatible' +export type ProviderType = 'deterministic' | 'openai_compatible' | string export type ProviderProfile = { provider_id: string label: string @@ -89,3 +142,35 @@ export type ProviderProfile = { export type ProviderListResponse = { providers: ProviderProfile[] } export type ActiveProviderResponse = { provider_id: string; provider: ProviderProfile | null } export type ProviderTestResponse = { provider_id: string; status: string; latency_ms: number; warnings: string[]; models: string[]; detail: string } + +export type ReviewDecision = 'correct' | 'incorrect' | 'partial' | 'insufficient_evidence' +export type ReviewFeedback = { analysis_id: string; taxonomy_id?: string | null; decision: ReviewDecision; notes: string } +export type ReviewRecord = { + id: string + created_at: string + analysis: AnalysisResponse + source_text: string + feedback?: ReviewFeedback & { corrected_labels?: string[] } +} + +export type EvaluationResult = { + items?: number + analyses?: AnalysisResponse[] + metrics?: Record + false_positives?: Array> + false_negatives?: Array> + evidence_span_misses?: Array> + [key: string]: unknown +} + +export type ReportFormat = 'json' | 'markdown' | 'html' +export type GeneratedReport = { + id: string + title: string + created_at: string + analysis_id?: string + formats: ReportFormat[] + json?: string + markdown?: string + html?: string +} diff --git a/frontend/src/components/analyze/AnalysisReport.tsx b/frontend/src/components/analyze/AnalysisReport.tsx index 3f8c86d..9aae0a4 100644 --- a/frontend/src/components/analyze/AnalysisReport.tsx +++ b/frontend/src/components/analyze/AnalysisReport.tsx @@ -1,3 +1,17 @@ import type { AnalysisResponse } from '../../api/types' +import { Badge } from '../shared/Badge' import { ClaimCard } from './ClaimCard' -export function AnalysisReport({ result }: { result: AnalysisResponse }) { return

Report

Analysis ID: {result.analysis_id}

Risks: {String(result.summary.risk_count ?? 0)}

{result.claims.map((claim, idx) => )}
} +import { ExportButtons } from './ExportButtons' + +function tone(level: string) { return level === 'high' || level === 'critical' ? 'danger' : level === 'medium' ? 'warning' : 'success' } +export function AnalysisReport({ result, sourceText }: { result: AnalysisResponse; sourceText: string }) { + const riskCount = result.claims.reduce((count, claim) => count + claim.detected_risks.length, 0) + const healthyCount = result.claims.reduce((count, claim) => count + claim.healthy_patterns.length, 0) + return
+

Analysis report

Analysis ID: {result.text_id} · {result.llm_used ? 'LLM assisted' : 'Deterministic'}{result.deterministic_fallback_used ? ' · fallback used' : ''}

{result.risk_level}
+
{result.overall_risk_score.toFixed(2)}Overall score
{riskCount}Risk labels
{healthyCount}Healthy patterns
{result.claims.length}Claims
+ {result.warnings.length ?
Warnings
    {result.warnings.map(warning =>
  • {warning}
  • )}
: null} + + {result.claims.map(claim => )} +
+} diff --git a/frontend/src/components/analyze/AnalyzePage.tsx b/frontend/src/components/analyze/AnalyzePage.tsx index 2af9a8e..ca2927b 100644 --- a/frontend/src/components/analyze/AnalyzePage.tsx +++ b/frontend/src/components/analyze/AnalyzePage.tsx @@ -1,15 +1,40 @@ -import { useState } from 'react' -import { analyzeText } from '../../api/client' -import type { AnalysisResponse } from '../../api/types' +import { useEffect, useState } from 'react' +import { analyzeText, fetchActiveModelProvider, fetchModelProviders, saveReviewRecord } from '../../api/client' +import type { AnalysisResponse, ProviderProfile } from '../../api/types' import { Card } from '../shared/Card' import { ErrorState } from '../shared/ErrorState' +import { LoadingState } from '../shared/LoadingState' import { AnalysisReport } from './AnalysisReport' import { TextInputPanel } from './TextInputPanel' export function AnalyzePage() { const [text, setText] = useState('Everyone always caused this problem because of that policy.') + const [providerId, setProviderId] = useState('deterministic_baseline') + const [providers, setProviders] = useState([]) + const [topK, setTopK] = useState(8) + const [includeHealthy, setIncludeHealthy] = useState(true) + const [allowFallback, setAllowFallback] = useState(true) const [result, setResult] = useState(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.

+ + + {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
} +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
+ + + + +
+} 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