import { forwardRef, type ReactNode } from "react"; import type { AnalysisResult, Citation, DetectorResult } from "../lib/types"; interface AnalysisReportProps { result: AnalysisResult; completedAt: Date; onNewAnalysis: () => void; } interface ForensicAnalysis { score?: number; synthetic_artifact_probability?: number; manipulation_probability?: number; caption_overlay?: { is_likely?: boolean; confidence?: number; location?: string | null; explanation?: string; }; noise_residual?: Record; frequency_spectrum?: Record; jpeg_blockiness?: Record; error_level_analysis?: Record; duplicate_patch_analysis?: Record; } interface VideoCoverage { mode?: string; exhaustive?: boolean; frame_stride?: number; frames_analyzed?: number; coverage_percent?: number; native_pixels_examined?: number; tile_count?: number; model_input_note?: string; } interface AttachmentFingerprint { sha256?: string; perceptual_hashes?: Record; } interface SourceMatch { status?: string; confidence?: number; matched_citations?: number; explanation?: string; } function objectRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : null; } function formatLabel(value: string): string { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } const EVIDENCE_LABELS: Record = { ai_generation_score: "Dedicated detector AI-class score (not probability)", sampled_frame_ai_generation_score: "Sampled-frame AI likelihood", video_ai_generation_score: "Video AI-generated likelihood", video_manipulation_score: "Video manipulation-class score", metadata_score: "Metadata availability (not authenticity)", visual_consistency_score: "Basic image quality (not authenticity)", compression_score: "Compression consistency (not authenticity)", pixel_forensic_score: "Traditional forensic consistency", ai_artifact_score: "Handcrafted AI-artifact signal", source_score: "Source context", provenance_score: "Verifiable provenance", web_corroboration_score: "Web corroboration", overall_risk_score: "Legacy context risk (not verdict)" }; function evidenceLabel(value: string): string { return EVIDENCE_LABELS[value] ?? formatLabel(value); } function readableStatus(value: string): string { return value.replace(/_/g, " "); } function percent(value?: number | null): string { if (typeof value !== "number" || Number.isNaN(value)) return "Not available"; return `${Math.round(value * 100)}%`; } function score(value?: number | null): string { if (typeof value !== "number" || Number.isNaN(value)) return "Not available"; return `${Math.round(value)}/100`; } function readableCount(value?: number | null): string { if (typeof value !== "number" || Number.isNaN(value)) return "Not available"; return Math.round(value).toLocaleString(); } function shortHash(value?: string): string | null { if (!value) return null; if (value.length <= 22) return value; return `${value.slice(0, 12)}…${value.slice(-8)}`; } function detailInterpretation(record?: Record): string | null { return typeof record?.interpretation === "string" ? record.interpretation : null; } function forensicFrom(result: AnalysisResult): ForensicAnalysis | null { const value = objectRecord(result.technical_details?.forensic_analysis); return value ? (value as ForensicAnalysis) : null; } function videoCoverageFrom(result: AnalysisResult): VideoCoverage | null { const value = objectRecord(result.technical_details?.analysis_coverage); return value ? (value as VideoCoverage) : null; } function fingerprintFrom(result: AnalysisResult): AttachmentFingerprint | null { const webDetails = objectRecord(result.web_research?.details); const value = objectRecord(result.technical_details?.attachment_fingerprint) ?? objectRecord(webDetails?.attachment_fingerprint); return value ? (value as AttachmentFingerprint) : null; } function sourceMatchFrom(result: AnalysisResult): SourceMatch | null { const details = objectRecord(result.web_research?.details); const value = objectRecord(details?.source_match); return value ? (value as SourceMatch) : null; } function detectorSummary(detector: DetectorResult): string { if (typeof detector.manipulation_probability === "number") { return `${percent(detector.manipulation_probability)} manipulation-class score`; } if (typeof detector.synthetic_probability === "number") { return `${percent(detector.synthetic_probability)} AI-class score`; } if (typeof detector.score === "number") return score(detector.score); return readableStatus(detector.status); } function detectorDisplayName(detector: DetectorResult): string { const normalized = detector.name.toLowerCase(); if (normalized.includes("truthshield-image-detector")) return "TruthShield learned image detector"; if (normalized === "local_heuristic_synthetic_likelihood") return "Local heuristic fallback"; return detector.name; } function learnedDetectorAvailable(result: AnalysisResult): boolean { const summary = objectRecord(result.technical_details?.ai_detector_summary); if (typeof summary?.learned_model_available === "boolean") return summary.learned_model_available; return Boolean(result.detectors?.some((detector) => { const provider = detector.details?.model_provider; const name = detector.name.toLowerCase(); return detector.status === "completed" && ( provider === "huggingface_local" || name.includes("truthshield") || name.includes("trained_") ); })); } function aiDetectorScore(result: AnalysisResult): number | null { const evidenceKeys = result.content_type === "video" ? ["video_ai_generation_score", "sampled_frame_ai_generation_score", "ai_generation_score"] : ["ai_generation_score"]; for (const key of evidenceKeys) { const value = result.evidence?.[key]; if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.min(100, value)); } const learned = result.detectors?.find((detector) => detector.status === "completed" && detector.name !== "local_heuristic_synthetic_likelihood" && typeof detector.synthetic_probability === "number" ); return typeof learned?.synthetic_probability === "number" ? Math.max(0, Math.min(100, learned.synthetic_probability * 100)) : null; } function generationVerdict(likelihood: number | null, learnedAvailable: boolean): { headline: string; detail: string } { if (!learnedAvailable) { return { headline: "Trained detector unavailable", detail: "Fallback estimate only — do not treat this as a reliable real-versus-AI verdict" }; } if (likelihood === null) return { headline: "No AI verdict", detail: "The detector returned no valid score" }; if (likelihood >= 90) return { headline: "Likely AI-generated", detail: "Strong video detector signal" }; if (likelihood >= 70) return { headline: "Inconclusive / uncertain", detail: "Detector evidence is not strong enough for an accusation" }; if (likelihood > 30) return { headline: "Mixed AI signals", detail: "The learned model is uncertain" }; if (likelihood <= 15) return { headline: "Likely camera-made", detail: "Low learned-model AI signal" }; return { headline: "Lower AI signal", detail: "The learned model leans away from AI generation" }; } function generationRiskClass(result: AnalysisResult, likelihood: number | null, learnedAvailable: boolean): string { if (result.assessment?.verdict === "likely_authentic") return "risk-trust"; if (result.assessment?.verdict === "likely_ai_generated" || result.assessment?.verdict === "likely_ai_manipulated") return "risk-high"; if (result.assessment?.verdict === "inconclusive") return "risk-medium"; if (!learnedAvailable || likelihood === null) return "risk-low"; if (likelihood >= 70) return "risk-high"; if (likelihood > 30) return "risk-medium"; return "risk-trust"; } function plainConfidence(value?: string): string { if (value === "high") return "High"; if (value === "moderate") return "Medium"; return "Low"; } function feedbackReasons(result: AnalysisResult, side: "generated" | "manipulated" | "authentic"): string[] { const customReasons = side === "generated" ? result.custom_feedback?.reasons_it_might_be_generated ?? result.custom_feedback?.reasons_it_might_be_ai : side === "manipulated" ? result.custom_feedback?.reasons_it_might_be_manipulated : result.custom_feedback?.reasons_it_might_not_be_ai; if (customReasons) return customReasons; if (result.assessment) { if (side === "generated") return result.assessment.evidence_supporting_generation; if (side === "manipulated") return result.assessment.evidence_supporting_manipulation; return result.assessment.evidence_supporting_authenticity; } return side === "authentic" ? result.positive_signals : side === "generated" ? result.warnings : []; } function ReportSection({ index, title, tone, children }: { index: string; title: string; tone?: string; children: ReactNode }) { return (

{title}

{children}
); } function FindingList({ items, emptyMessage }: { items: string[]; emptyMessage: string }) { if (items.length === 0) return

{emptyMessage}

; return (
    {items.map((item, index) =>
  • {item}
  • )}
); } function ExternalArrow() { return ( ); } function CitationList({ citations }: { citations: Citation[] }) { if (citations.length === 0) return

No citations were returned.

; return (
    {citations.slice(0, 5).map((citation, index) => (
  1. {citation.title} {citation.source ? {citation.source} : null} {citation.snippet ?

    {citation.snippet}

    : null}
  2. ))}
); } function TechnicalEvidence({ result }: { result: AnalysisResult }) { const forensic = forensicFrom(result); const coverage = videoCoverageFrom(result); const fingerprint = fingerprintFrom(result); const sourceMatch = sourceMatchFrom(result); const citations = result.citations?.length ? result.citations : (result.web_research?.citations ?? []); const primitiveDetails = Object.entries(result.technical_details ?? {}).filter(([, value]) => ["string", "number", "boolean"].includes(typeof value) ); const evidenceEntries = Object.entries(result.evidence ?? {}).filter( ([key]) => !result.assessment || key !== "overall_risk_score" ); const forensicNotes = forensic ? [ detailInterpretation(forensic.noise_residual), detailInterpretation(forensic.frequency_spectrum), detailInterpretation(forensic.jpeg_blockiness), detailInterpretation(forensic.error_level_analysis), detailInterpretation(forensic.duplicate_patch_analysis) ].filter((note): note is string => Boolean(note)) : []; return (

Analysis overview

Analysis mode
{readableStatus(result.analysis_mode ?? "local heuristic")}
Evidence coverage
{percent(result.confidence)}
{primitiveDetails.map(([key, value]) => (
{formatLabel(key)}
{String(value)}
))}
{evidenceEntries.length ? (

Raw evidence metrics

These are internal model and file-check scores. They are not real-world probabilities.

{evidenceEntries.map(([key, rawValue]) => (
{evidenceLabel(key)}
{Math.round(Number(rawValue) || 0)}
))}
) : null} {result.assessment?.signals?.length ? (

Decision signals

{result.assessment.signals.map((signal) => (
{formatLabel(signal.source)}{readableStatus(signal.signal)}
{readableStatus(signal.status)} {typeof signal.raw_score === "number" ? `raw ${signal.raw_score.toFixed(3)}` : `reliability ${percent(signal.reliability)}`}
))}
) : null} {forensic ? (

Pixel forensics

Forensic score
{score(forensic.score)}
Handcrafted artifact score
{percent(forensic.synthetic_artifact_probability)}
Manipulation probability
{percent(forensic.manipulation_probability)}
{forensic.caption_overlay?.is_likely ? (
Graphic overlay
Likely{typeof forensic.caption_overlay.confidence === "number" ? ` · ${percent(forensic.caption_overlay.confidence)}` : ""}
) : null}
{forensicNotes.length ? : null}
) : null} {coverage || typeof result.frames_analyzed === "number" || result.suspicious_frames?.length ? (

Video and frames

Frames analyzed
{readableCount(coverage?.frames_analyzed ?? result.frames_analyzed)}
{typeof coverage?.coverage_percent === "number" ?
Coverage
{coverage.coverage_percent.toFixed(1)}%
: null} {typeof coverage?.frame_stride === "number" ?
Frame stride
{coverage.frame_stride}
: null} {typeof coverage?.native_pixels_examined === "number" ?
Native pixels
{readableCount(coverage.native_pixels_examined)}
: null} {typeof coverage?.tile_count === "number" ?
Model tiles
{readableCount(coverage.tile_count)}
: null}
{coverage?.model_input_note ?

{coverage.model_input_note}

: null} {result.suspicious_frames?.length ? (
{result.suspicious_frames.map((frame) => (
Frame{frame.frame_index}
Time{typeof frame.timestamp_seconds === "number" ? `${frame.timestamp_seconds.toFixed(2)}s` : "—"}
Truth Score{frame.truth_score}/100
Synthetic signal{percent(frame.synthetic_probability)}
Manipulation signal{percent(frame.manipulation_probability)}

{frame.warnings.join(" · ")}

))}
) : null}
) : null}

Detector outputs

{result.detectors?.length ? (
{result.detectors.map((detector, index) => (
{detectorDisplayName(detector)}{detector.label ? {readableStatus(detector.label)} : null}
{readableStatus(detector.status)} {detectorSummary(detector)}
))}
) :

No detector outputs were returned.

}

Provenance

{result.provenance ? (
{readableStatus(result.provenance.status)}{score(result.provenance.score)}

{result.provenance.summary}

) :

No provenance result was returned.

} {fingerprint ? (
{shortHash(fingerprint.sha256) ?
SHA-256
{shortHash(fingerprint.sha256)}
: null} {Object.entries(fingerprint.perceptual_hashes ?? {}).map(([key, value]) => (
{formatLabel(key)}
{shortHash(value)}
))}
) : null}

Web research

{result.web_research ? ( <>
{readableStatus(result.web_research.status)}{score(result.web_research.score)}

{result.web_research.summary}

{result.web_research.queries.length ?

Queries: {result.web_research.queries.join(" · ")}

: null}
{sourceMatch ? (
Attachment match: {readableStatus(sourceMatch.status ?? "not checked")} {typeof sourceMatch.confidence === "number" ? {percent(sourceMatch.confidence)} confidence : null} {sourceMatch.explanation ?

{sourceMatch.explanation}

: null}
) : null} ) :

No web research was returned.

}
); } const AnalysisReport = forwardRef(function AnalysisReport( { result, completedAt, onNewAnalysis }, ref ) { const generationLikelihood = aiDetectorScore(result); const learnedAvailable = learnedDetectorAvailable(result); const generationResult = generationVerdict(generationLikelihood, learnedAvailable); const assessment = result.assessment ?? null; const headline = result.custom_feedback?.headline ?? assessment?.label ?? generationResult.headline; const plainSummary = result.custom_feedback?.plain_language_summary ?? result.custom_feedback?.explanation ?? assessment?.reason ?? result.summary; const generationReasons = feedbackReasons(result, "generated"); const manipulationReasons = feedbackReasons(result, "manipulated"); const authenticityReasons = feedbackReasons(result, "authentic"); const uncertaintyNote = result.custom_feedback?.uncertainty_note ?? "This result is an estimate, not proof. Editing, compression, screenshots, and unfamiliar AI tools can change the clues the system uses."; const nextSteps = result.custom_feedback?.next_steps?.length ? result.custom_feedback.next_steps : result.recommendations; const showModelScore = learnedAvailable && generationLikelihood !== null; const verdictDetail = assessment ? `${plainConfidence(assessment.confidence)} result strength · based on the available checks` : showModelScore ? "Model estimate only · not a percent chance or proof" : generationResult.detail; const technicalPreview = [ typeof assessment?.generation_score === "number" ? `${Math.round(assessment.generation_score * 100)}% generation-class score` : null, typeof assessment?.manipulation_score === "number" ? `${Math.round(assessment.manipulation_score * 100)}% manipulation-class score` : null, generationLikelihood !== null && typeof assessment?.generation_score !== "number" ? `${Math.round(generationLikelihood)}% ${showModelScore ? "raw AI-class score" : "fallback signal"}` : null, learnedAvailable ? "learned detector active" : "fallback only", typeof result.frames_analyzed === "number" ? `${result.frames_analyzed.toLocaleString()} frames` : null, result.suspicious_frames?.length ? `${result.suspicious_frames.length} suspicious samples` : null, result.detectors?.length ? `${result.detectors.length} detector outputs` : null ].filter((item): item is string => Boolean(item)); const analyzedAt = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }).format(completedAt); return (
{result.content_type === "image" ? "Image" : "Video"} analysis
{assessment ? "Result strength" : (showModelScore ? "AI model signal" : "Model status")}
{assessment ? plainConfidence(assessment.confidence) : (showModelScore ? Math.round(generationLikelihood ?? 0) : "—")} {assessment || !showModelScore ? null : %}
{assessment ? "How strongly the checks support this result" : (showModelScore ? "Raw model score — not probability" : "Fallback checks only")}

{headline}

{verdictDetail}

{assessment ? (

Generation score: {percent(assessment.generation_score)} · Manipulation score: {percent(assessment.manipulation_score)} {` · Policy ${assessment.decision_policy_version}`}

) : null}

Review both sides below before making an important decision.

{plainSummary}

AI detectors compare patterns. They do not know for certain who or what made the file, and a model score is not the percent chance that the result is correct.

{uncertaintyNote}

{nextSteps.length > 0 ? (
    {nextSteps.map((item, index) =>
  1. {item}
  2. )}
) :

Verify important claims with trusted, independent sources.

}

{result.disclaimer}

Technical evidence {technicalPreview.length > 0 ? technicalPreview.join(" · ") : "Forensics · provenance · research"}
); }); export default AnalysisReport;