fakeshield-api / fakeshield /src /pages /AudioLab /AudioReportPanel.tsx
Akash4911's picture
Production Deploy: Improved robustness and logging
66b6851
Raw
History Blame Contribute Delete
10.6 kB
import React, { useMemo } from 'react';
import type { AudioResult, AudioReason } from '../../services/audioService';
import { Shield, AlertTriangle, CheckCircle, Info, Download, Trash2 } from 'lucide-react';
interface AudioReportPanelProps {
result: AudioResult;
onReset: () => void;
}
function generateCaseId(result: AudioResult): string {
// Deterministic case ID derived from result data — stable across re-renders
const seed = `${result.ai_probability}-${result.verdict}-${result.audio_metadata.duration_sec}`;
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = ((hash << 5) - hash + seed.charCodeAt(i)) & 0xffffffff;
}
return Math.abs(hash).toString(36).toUpperCase().padStart(9, '0').slice(0, 9);
}
const AudioReportPanel: React.FC<AudioReportPanelProps> = ({ result, onReset }) => {
// Stable case ID — won't change on re-renders
const caseId = useMemo(() => generateCaseId(result), [result]);
const getSeverityColor = (severity: string) => {
switch (severity.toLowerCase()) {
case 'critical': return 'var(--accent-red)';
case 'high': return 'var(--accent-orange)';
case 'medium': return 'var(--accent-yellow)';
default: return '#00E5CC';
}
};
const handleDownload = () => {
const report = {
case_id: caseId,
generated_at: new Date().toISOString(),
verdict: result.verdict,
ai_probability: result.ai_probability,
threat_level: result.threat_level,
confidence: result.confidence,
agreement: result.agreement,
fusion_rule: result.fusion_rule,
forensic_summary: result.forensic_summary,
recommended_action: result.recommended_action,
audio_metadata: result.audio_metadata,
signal_scores: result.signal_scores,
stability_score: result.stability_score,
primary_reasons: result.primary_reasons,
supporting_reasons: result.supporting_reasons,
exonerating_factors: result.exonerating_factors,
};
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `fakeshield-audio-report-${caseId}.json`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="flex flex-col h-full animate-fade-in">
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="text-2xl font-display font-bold text-[var(--text-heading)]">Forensic Report</h2>
<p className="text-sm font-medium text-[var(--text-secondary)] mt-1">
Case ID: {caseId}
</p>
</div>
<div className="flex gap-2">
<button
onClick={handleDownload}
title="Download JSON report"
className="p-2 rounded-lg bg-[var(--btn-secondary-bg)] border border-[var(--panel-border)] text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:border-[rgba(0,229,204,0.2)] transition-all"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={onReset}
title="Reset — start new scan"
className="p-2 rounded-lg bg-[var(--btn-secondary-bg)] border border-[var(--panel-border)] text-[var(--text-muted)] hover:text-[var(--accent-red)] hover:border-[var(--accent-red-border)] transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="space-y-6 overflow-y-auto pr-2 custom-scrollbar flex-1">
{/* Summary Card */}
<div className="p-5 rounded-xl border border-[var(--panel-border)] bg-[var(--bg-secondary)] relative overflow-hidden">
<div
className="absolute top-0 left-0 w-1 h-full"
style={{ background: getSeverityColor(result.threat_level) }}
/>
<div className="flex items-start gap-4">
<div
className="p-3 rounded-lg flex-shrink-0"
style={{ background: `${getSeverityColor(result.threat_level)}15` }}
>
{result.threat_level === 'CRITICAL' || result.threat_level === 'HIGH' ? (
<AlertTriangle className="w-6 h-6" style={{ color: getSeverityColor(result.threat_level) }} />
) : (
<CheckCircle className="w-6 h-6" style={{ color: getSeverityColor(result.threat_level) }} />
)}
</div>
<div>
<h4
className="font-bold text-lg leading-tight"
style={{ color: getSeverityColor(result.threat_level) }}
>
{result.verdict.replace(/_/g, ' ')}
</h4>
<p className="text-sm text-[var(--text-secondary)] mt-1 italic leading-relaxed">
"{result.forensic_summary}"
</p>
</div>
</div>
</div>
{/* Fusion rule badge */}
{result.fusion_rule && (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[var(--panel-border)] bg-[var(--bg-primary)]">
<span className="text-xs font-medium text-[var(--text-secondary)]">
Decision Rule:
</span>
<span className="text-xs font-semibold text-[var(--accent-blue)] capitalize">
{result.fusion_rule.replace(/_/g, ' ').toLowerCase()}
</span>
</div>
)}
{/* Action Recommendation */}
<div className="p-4 rounded-xl border border-[var(--accent-green-border)] bg-[var(--accent-green-transparent)]">
<h5 className="text-xs font-semibold text-[var(--accent-green)] mb-2 flex items-center">
<Shield className="w-3 h-3 mr-2" />
Counter-Measures
</h5>
<p className="text-sm text-[var(--text-primary)] font-medium leading-relaxed">
{result.recommended_action}
</p>
</div>
{/* Audio metadata strip */}
<div className="grid grid-cols-2 gap-2">
{[
{ label: 'Duration', value: `${result.audio_metadata.duration_sec.toFixed(1)}s` },
{ label: 'Segments', value: `${result.audio_metadata.num_chunks}` },
{ label: 'Format', value: result.audio_metadata.format_hint.toUpperCase() },
{ label: 'Stability', value: `${((result.stability_score ?? 1) * 100).toFixed(0)}%` },
].map(({ label, value }) => (
<div
key={label}
className="p-3 rounded-lg border border-[var(--panel-border)] bg-[var(--bg-primary)] flex flex-col"
>
<span className="text-xs font-medium text-[var(--text-secondary)] mb-1">{label}</span>
<span className="text-sm font-semibold text-[var(--accent-blue)]">{value}</span>
</div>
))}
</div>
{/* Primary Reasons */}
{result.primary_reasons.length > 0 && (
<div>
<h5 className="text-sm font-semibold text-[var(--text-primary)] mb-4 flex items-center">
<Shield className="w-4 h-4 mr-2 text-[var(--accent-blue)]" />
Primary Evidence Indicators
</h5>
<div className="space-y-3">
{result.primary_reasons.map((reason: AudioReason, i: number) => (
<div
key={i}
className="p-4 rounded-lg bg-[var(--bg-primary)] border border-[var(--panel-border)] hover:border-[var(--accent-blue-border)] transition-all"
>
<div className="flex justify-between items-start mb-2">
<span
className="text-xs font-bold uppercase tracking-tight"
style={{ color: getSeverityColor(reason.severity) }}
>
{reason.signal} Analysis
</span>
<span className="text-xs font-medium px-2 py-1 rounded bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--panel-border)]">
{Math.round(reason.score * 100)}% match
</span>
</div>
<p className="text-sm font-semibold text-[var(--text-primary)] mb-1">{reason.message}</p>
<p className="text-xs text-[var(--text-muted)] leading-relaxed">{reason.evidence}</p>
</div>
))}
</div>
</div>
)}
{result.primary_reasons.length === 0 && (
<p className="text-xs text-[var(--text-muted)] italic px-2">
No primary synthesis indicators detected.
</p>
)}
{/* Supporting Reasons */}
{result.supporting_reasons.length > 0 && (
<div>
<h5 className="text-sm font-semibold text-[var(--text-primary)] mb-4 flex items-center">
<Info className="w-4 h-4 mr-2 text-[var(--accent-green)]" />
Supporting Forensic Signals
</h5>
<div className="space-y-2">
{result.supporting_reasons.map((reason: AudioReason, i: number) => (
<div
key={i}
className="p-3 rounded-lg bg-[var(--bg-primary)] border border-[var(--panel-border)]/50 opacity-80 hover:opacity-100 transition-all"
>
<p className="text-xs font-semibold text-[var(--text-secondary)]">{reason.message}</p>
<p className="text-xs text-[var(--text-muted)] mt-1 italic">{reason.evidence}</p>
</div>
))}
</div>
</div>
)}
{/* Exonerating Factors */}
{result.exonerating_factors.length > 0 && (
<div className="pb-6">
<h5 className="text-sm font-semibold text-[var(--text-primary)] mb-4 flex items-center">
<CheckCircle className="w-4 h-4 mr-2 text-green-500" />
Human Authenticity Markers
</h5>
<ul className="space-y-1">
{result.exonerating_factors.map((factor: string, i: number) => (
<li key={i} className="text-xs text-[var(--text-muted)] flex items-start gap-2">
<span className="text-green-500 mt-1 flex-shrink-0"></span>
{factor}
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
};
export default AudioReportPanel;