feat: Anti-Fake-Evidence System (Phase 1-4b)
Implement full evidence integrity pipeline to prevent compliance theater: - Confidence levels (E0-E4), truth status tracking, assertion engine - Four-Eyes approval workflow, audit trail, reject endpoint - Evidence distribution dashboard, LLM audit routes - Traceability matrix (backend endpoint + Compliance Hub UI tab) - Anti-fake badges, control status machine, normative patterns - 2 migrations, 4 test suites, MkDocs documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { ConfidenceLevelBadge } from '../evidence/components/anti-fake-badges'
|
||||
|
||||
// Types
|
||||
interface DashboardData {
|
||||
@@ -25,6 +26,15 @@ interface DashboardData {
|
||||
evidence_by_status: Record<string, number>
|
||||
total_risks: number
|
||||
risks_by_level: Record<string, number>
|
||||
multi_score?: {
|
||||
requirement_coverage: number
|
||||
evidence_strength: number
|
||||
validation_quality: number
|
||||
evidence_freshness: number
|
||||
control_effectiveness: number
|
||||
overall_readiness: number
|
||||
hard_blocks: string[]
|
||||
} | null
|
||||
}
|
||||
|
||||
interface Regulation {
|
||||
@@ -106,7 +116,46 @@ interface ScoreSnapshot {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type TabKey = 'overview' | 'roadmap' | 'modules' | 'trend'
|
||||
interface TraceabilityAssertion {
|
||||
id: string
|
||||
sentence_text: string
|
||||
assertion_type: string
|
||||
confidence: number
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
interface TraceabilityEvidence {
|
||||
id: string
|
||||
title: string
|
||||
evidence_type: string
|
||||
confidence_level: string
|
||||
status: string
|
||||
assertions: TraceabilityAssertion[]
|
||||
}
|
||||
|
||||
interface TraceabilityCoverage {
|
||||
has_evidence: boolean
|
||||
has_assertions: boolean
|
||||
all_assertions_verified: boolean
|
||||
min_confidence_level: string | null
|
||||
}
|
||||
|
||||
interface TraceabilityControl {
|
||||
id: string
|
||||
control_id: string
|
||||
title: string
|
||||
status: string
|
||||
domain: string
|
||||
evidence: TraceabilityEvidence[]
|
||||
coverage: TraceabilityCoverage
|
||||
}
|
||||
|
||||
interface TraceabilityMatrixData {
|
||||
controls: TraceabilityControl[]
|
||||
summary: Record<string, number>
|
||||
}
|
||||
|
||||
type TabKey = 'overview' | 'roadmap' | 'modules' | 'trend' | 'traceability'
|
||||
|
||||
const DOMAIN_LABELS: Record<string, string> = {
|
||||
gov: 'Governance',
|
||||
@@ -148,6 +197,17 @@ export default function ComplianceHubPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [seeding, setSeeding] = useState(false)
|
||||
const [savingSnapshot, setSavingSnapshot] = useState(false)
|
||||
const [evidenceDistribution, setEvidenceDistribution] = useState<{
|
||||
by_confidence: Record<string, number>
|
||||
four_eyes_pending: number
|
||||
total: number
|
||||
} | null>(null)
|
||||
const [traceabilityMatrix, setTraceabilityMatrix] = useState<TraceabilityMatrixData | null>(null)
|
||||
const [traceabilityLoading, setTraceabilityLoading] = useState(false)
|
||||
const [traceabilityFilter, setTraceabilityFilter] = useState<'all' | 'covered' | 'uncovered' | 'fully_verified'>('all')
|
||||
const [traceabilityDomainFilter, setTraceabilityDomainFilter] = useState<string>('all')
|
||||
const [expandedControls, setExpandedControls] = useState<Set<string>>(new Set())
|
||||
const [expandedEvidence, setExpandedEvidence] = useState<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
@@ -157,6 +217,7 @@ export default function ComplianceHubPage() {
|
||||
if (activeTab === 'roadmap' && !roadmap) loadRoadmap()
|
||||
if (activeTab === 'modules' && !moduleStatus) loadModuleStatus()
|
||||
if (activeTab === 'trend' && scoreHistory.length === 0) loadScoreHistory()
|
||||
if (activeTab === 'traceability' && !traceabilityMatrix) loadTraceabilityMatrix()
|
||||
}, [activeTab]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const loadData = async () => {
|
||||
@@ -182,6 +243,12 @@ export default function ComplianceHubPage() {
|
||||
const data = await actionsRes.json()
|
||||
setNextActions(data.actions || [])
|
||||
}
|
||||
|
||||
// Evidence distribution (Anti-Fake-Evidence Phase 3)
|
||||
try {
|
||||
const evidenceDistRes = await fetch('/api/sdk/v1/compliance/dashboard/evidence-distribution')
|
||||
if (evidenceDistRes.ok) setEvidenceDistribution(await evidenceDistRes.json())
|
||||
} catch { /* silent */ }
|
||||
} catch (err) {
|
||||
console.error('Failed to load compliance data:', err)
|
||||
setError('Verbindung zum Backend fehlgeschlagen')
|
||||
@@ -214,6 +281,31 @@ export default function ComplianceHubPage() {
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
const loadTraceabilityMatrix = async () => {
|
||||
setTraceabilityLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/compliance/dashboard/traceability-matrix')
|
||||
if (res.ok) setTraceabilityMatrix(await res.json())
|
||||
} catch { /* silent */ }
|
||||
finally { setTraceabilityLoading(false) }
|
||||
}
|
||||
|
||||
const toggleControlExpanded = (id: string) => {
|
||||
setExpandedControls(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id); else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const toggleEvidenceExpanded = (id: string) => {
|
||||
setExpandedEvidence(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id); else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const saveSnapshot = async () => {
|
||||
setSavingSnapshot(true)
|
||||
try {
|
||||
@@ -259,6 +351,7 @@ export default function ComplianceHubPage() {
|
||||
{ key: 'roadmap', label: 'Roadmap' },
|
||||
{ key: 'modules', label: 'Module' },
|
||||
{ key: 'trend', label: 'Trend' },
|
||||
{ key: 'traceability', label: 'Traceability' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -411,6 +504,115 @@ export default function ComplianceHubPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Anti-Fake-Evidence Section (Phase 3) */}
|
||||
{dashboard && (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Anti-Fake-Evidence Status</h3>
|
||||
|
||||
{/* Confidence Distribution Bar */}
|
||||
{evidenceDistribution && evidenceDistribution.total > 0 && (
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-slate-500 mb-2">Confidence-Verteilung ({evidenceDistribution.total} Nachweise)</p>
|
||||
<div className="flex h-6 rounded-full overflow-hidden">
|
||||
{(['E0', 'E1', 'E2', 'E3', 'E4'] as const).map(level => {
|
||||
const count = evidenceDistribution.by_confidence[level] || 0
|
||||
const pct = (count / evidenceDistribution.total) * 100
|
||||
if (pct === 0) return null
|
||||
const colors: Record<string, string> = {
|
||||
E0: 'bg-red-400', E1: 'bg-yellow-400', E2: 'bg-blue-400', E3: 'bg-green-400', E4: 'bg-emerald-400'
|
||||
}
|
||||
return (
|
||||
<div key={level} className={`${colors[level]} flex items-center justify-center text-xs text-white font-medium`}
|
||||
style={{ width: `${pct}%` }} title={`${level}: ${count}`}>
|
||||
{pct >= 10 ? `${level} (${count})` : ''}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-slate-500">
|
||||
{(['E0', 'E1', 'E2', 'E3', 'E4'] as const).map(level => {
|
||||
const count = evidenceDistribution.by_confidence[level] || 0
|
||||
const dotColors: Record<string, string> = {
|
||||
E0: 'bg-red-400', E1: 'bg-yellow-400', E2: 'bg-blue-400', E3: 'bg-green-400', E4: 'bg-emerald-400'
|
||||
}
|
||||
return (
|
||||
<span key={level} className="flex items-center gap-1">
|
||||
<span className={`w-2 h-2 rounded-full ${dotColors[level]}`} />
|
||||
{level}: {count}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Multi-Score Dimensions */}
|
||||
{dashboard.multi_score && (
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-slate-500 mb-2">Multi-dimensionaler Score</p>
|
||||
<div className="space-y-2">
|
||||
{([
|
||||
{ key: 'requirement_coverage', label: 'Anforderungsabdeckung', color: 'bg-blue-500' },
|
||||
{ key: 'evidence_strength', label: 'Evidence-Staerke', color: 'bg-green-500' },
|
||||
{ key: 'validation_quality', label: 'Validierungsqualitaet', color: 'bg-purple-500' },
|
||||
{ key: 'evidence_freshness', label: 'Aktualitaet', color: 'bg-yellow-500' },
|
||||
{ key: 'control_effectiveness', label: 'Control-Wirksamkeit', color: 'bg-indigo-500' },
|
||||
] as const).map(dim => {
|
||||
const value = (dashboard.multi_score as Record<string, number>)[dim.key] || 0
|
||||
return (
|
||||
<div key={dim.key} className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-600 w-44 truncate">{dim.label}</span>
|
||||
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full ${dim.color} rounded-full transition-all`} style={{ width: `${value}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-600 w-12 text-right">{typeof value === 'number' ? value.toFixed(0) : value}%</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="flex items-center gap-3 pt-2 border-t border-slate-100">
|
||||
<span className="text-xs font-semibold text-slate-700 w-44">Audit-Readiness</span>
|
||||
<div className="flex-1 h-3 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full transition-all ${
|
||||
(dashboard.multi_score.overall_readiness || 0) >= 80 ? 'bg-green-500' :
|
||||
(dashboard.multi_score.overall_readiness || 0) >= 60 ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`} style={{ width: `${dashboard.multi_score.overall_readiness || 0}%` }} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-700 w-12 text-right">
|
||||
{typeof dashboard.multi_score.overall_readiness === 'number' ? dashboard.multi_score.overall_readiness.toFixed(0) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom row: Four-Eyes + Hard Blocks */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="text-center p-3 rounded-lg bg-yellow-50">
|
||||
<div className="text-2xl font-bold text-yellow-700">{evidenceDistribution?.four_eyes_pending || 0}</div>
|
||||
<div className="text-xs text-yellow-600 mt-1">Four-Eyes Reviews ausstehend</div>
|
||||
</div>
|
||||
{dashboard.multi_score?.hard_blocks && dashboard.multi_score.hard_blocks.length > 0 ? (
|
||||
<div className="p-3 rounded-lg bg-red-50">
|
||||
<div className="text-xs font-medium text-red-700 mb-1">Hard Blocks ({dashboard.multi_score.hard_blocks.length})</div>
|
||||
<ul className="space-y-1">
|
||||
{dashboard.multi_score.hard_blocks.slice(0, 3).map((block: string, i: number) => (
|
||||
<li key={i} className="text-xs text-red-600 flex items-start gap-1">
|
||||
<span className="text-red-400 mt-0.5">•</span>
|
||||
<span>{block}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-3 rounded-lg bg-green-50">
|
||||
<div className="text-2xl font-bold text-green-700">0</div>
|
||||
<div className="text-xs text-green-600 mt-1">Keine Hard Blocks</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Actions + Findings */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Next Actions */}
|
||||
@@ -805,6 +1007,232 @@ export default function ComplianceHubPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Traceability Tab */}
|
||||
{activeTab === 'traceability' && (
|
||||
<div className="p-6 space-y-6">
|
||||
{traceabilityLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
<span className="ml-3 text-slate-500">Traceability Matrix wird geladen...</span>
|
||||
</div>
|
||||
) : !traceabilityMatrix ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
Keine Daten verfuegbar. Stellen Sie sicher, dass Controls und Evidence vorhanden sind.
|
||||
</div>
|
||||
) : (() => {
|
||||
const summary = traceabilityMatrix.summary
|
||||
const totalControls = summary.total_controls || 0
|
||||
const covered = summary.covered || 0
|
||||
const fullyVerified = summary.fully_verified || 0
|
||||
const uncovered = summary.uncovered || 0
|
||||
|
||||
const filteredControls = (traceabilityMatrix.controls || []).filter(ctrl => {
|
||||
if (traceabilityFilter === 'covered' && !ctrl.coverage.has_evidence) return false
|
||||
if (traceabilityFilter === 'uncovered' && ctrl.coverage.has_evidence) return false
|
||||
if (traceabilityFilter === 'fully_verified' && !ctrl.coverage.all_assertions_verified) return false
|
||||
if (traceabilityDomainFilter !== 'all' && ctrl.domain !== traceabilityDomainFilter) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const domains = [...new Set(traceabilityMatrix.controls.map(c => c.domain))].sort()
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-purple-700">{totalControls}</div>
|
||||
<div className="text-sm text-purple-600">Total Controls</div>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-blue-700">{covered}</div>
|
||||
<div className="text-sm text-blue-600">Abgedeckt</div>
|
||||
</div>
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-green-700">{fullyVerified}</div>
|
||||
<div className="text-sm text-green-600">Vollst. verifiziert</div>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-red-700">{uncovered}</div>
|
||||
<div className="text-sm text-red-600">Unabgedeckt</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<div className="flex gap-1">
|
||||
{([
|
||||
{ key: 'all', label: 'Alle' },
|
||||
{ key: 'covered', label: 'Abgedeckt' },
|
||||
{ key: 'uncovered', label: 'Nicht abgedeckt' },
|
||||
{ key: 'fully_verified', label: 'Vollst. verifiziert' },
|
||||
] as const).map(f => (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => setTraceabilityFilter(f.key)}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
traceabilityFilter === f.key
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-4 w-px bg-slate-300" />
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<button
|
||||
onClick={() => setTraceabilityDomainFilter('all')}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
traceabilityDomainFilter === 'all'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Alle Domains
|
||||
</button>
|
||||
{domains.map(d => (
|
||||
<button
|
||||
key={d}
|
||||
onClick={() => setTraceabilityDomainFilter(d)}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
traceabilityDomainFilter === d
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{DOMAIN_LABELS[d] || d}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls List */}
|
||||
<div className="space-y-2">
|
||||
{filteredControls.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-400">
|
||||
Keine Controls fuer diesen Filter gefunden.
|
||||
</div>
|
||||
) : filteredControls.map(ctrl => {
|
||||
const isExpanded = expandedControls.has(ctrl.id)
|
||||
const coverageIcon = ctrl.coverage.all_assertions_verified
|
||||
? { symbol: '\u2713', color: 'text-green-600 bg-green-50' }
|
||||
: ctrl.coverage.has_evidence
|
||||
? { symbol: '\u25D0', color: 'text-yellow-600 bg-yellow-50' }
|
||||
: { symbol: '\u2717', color: 'text-red-600 bg-red-50' }
|
||||
|
||||
return (
|
||||
<div key={ctrl.id} className="border rounded-lg overflow-hidden">
|
||||
{/* Control Row */}
|
||||
<button
|
||||
onClick={() => toggleControlExpanded(ctrl.id)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<span className="text-slate-400 text-xs">{isExpanded ? '\u25BC' : '\u25B6'}</span>
|
||||
<span className={`w-7 h-7 flex items-center justify-center rounded-full text-sm font-medium ${coverageIcon.color}`}>
|
||||
{coverageIcon.symbol}
|
||||
</span>
|
||||
<code className="text-xs bg-slate-100 px-2 py-0.5 rounded text-slate-600 font-mono">{ctrl.control_id}</code>
|
||||
<span className="text-sm text-slate-800 flex-1 truncate">{ctrl.title}</span>
|
||||
<span className="text-xs bg-slate-100 text-slate-500 px-2 py-0.5 rounded">{DOMAIN_LABELS[ctrl.domain] || ctrl.domain}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
ctrl.status === 'implemented' ? 'bg-green-100 text-green-700'
|
||||
: ctrl.status === 'in_progress' ? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{ctrl.status}
|
||||
</span>
|
||||
<ConfidenceLevelBadge level={ctrl.coverage.min_confidence_level} />
|
||||
<span className="text-xs text-slate-400 min-w-[3rem] text-right">
|
||||
{ctrl.evidence.length} Ev.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Expanded: Evidence list */}
|
||||
{isExpanded && (
|
||||
<div className="border-t bg-slate-50">
|
||||
{ctrl.evidence.length === 0 ? (
|
||||
<div className="px-8 py-3 text-xs text-slate-400 italic">
|
||||
Kein Evidence verknuepft.
|
||||
</div>
|
||||
) : ctrl.evidence.map(ev => {
|
||||
const evExpanded = expandedEvidence.has(ev.id)
|
||||
return (
|
||||
<div key={ev.id} className="border-b last:border-b-0">
|
||||
<button
|
||||
onClick={() => toggleEvidenceExpanded(ev.id)}
|
||||
className="w-full flex items-center gap-3 px-8 py-2 text-left hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<span className="text-slate-400 text-xs">{evExpanded ? '\u25BC' : '\u25B6'}</span>
|
||||
<span className="text-sm text-slate-700 flex-1 truncate">{ev.title}</span>
|
||||
<span className="text-xs bg-white border px-2 py-0.5 rounded text-slate-500">{ev.evidence_type}</span>
|
||||
<ConfidenceLevelBadge level={ev.confidence_level} />
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
ev.status === 'valid' ? 'bg-green-100 text-green-700'
|
||||
: ev.status === 'expired' ? 'bg-red-100 text-red-700'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{ev.status}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 min-w-[3rem] text-right">
|
||||
{ev.assertions.length} Ass.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Expanded: Assertions list */}
|
||||
{evExpanded && ev.assertions.length > 0 && (
|
||||
<div className="bg-white border-t">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-12 py-1.5 text-left text-slate-500 font-medium">Aussage</th>
|
||||
<th className="px-3 py-1.5 text-center text-slate-500 font-medium w-20">Typ</th>
|
||||
<th className="px-3 py-1.5 text-center text-slate-500 font-medium w-24">Konfidenz</th>
|
||||
<th className="px-3 py-1.5 text-center text-slate-500 font-medium w-16">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{ev.assertions.map(a => (
|
||||
<tr key={a.id} className="hover:bg-slate-50">
|
||||
<td className="px-12 py-1.5 text-slate-700">{a.sentence_text}</td>
|
||||
<td className="px-3 py-1.5 text-center text-slate-500">{a.assertion_type}</td>
|
||||
<td className="px-3 py-1.5 text-center">
|
||||
<span className={`font-medium ${
|
||||
a.confidence >= 0.8 ? 'text-green-600'
|
||||
: a.confidence >= 0.5 ? 'text-yellow-600'
|
||||
: 'text-red-600'
|
||||
}`}>
|
||||
{(a.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-center">
|
||||
{a.verified
|
||||
? <span className="text-green-600 font-medium">{'\u2713'}</span>
|
||||
: <span className="text-slate-400">{'\u2717'}</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user