/**
* FactCheckBadge Component
*
* Displays a small badge for fact-checking statements.
* Shows loading state during verification, then displays verdict badge.
* Clicking opens the FactCheckModal with full details.
*/
'use client';
import { useState, useCallback } from 'react';
import { Loader2, Search } from 'lucide-react';
import { useLocale } from 'next-intl';
import type { FactCheck, FactCheckVerdict } from '@/lib/factcheck/types';
import { VERDICT_DISPLAY, getVerdictClasses } from '@/lib/factcheck/types';
import { FactCheckModal } from './FactCheckModal';
interface FactCheckBadgeProps {
statementId: string;
statementText: string;
/** Pre-loaded fact-check result (if available) */
existingFactCheck?: FactCheck | null;
}
export function FactCheckBadge({
statementId,
statementText,
existingFactCheck,
}: FactCheckBadgeProps) {
const locale = useLocale();
const [isLoading, setIsLoading] = useState(false);
const [factCheck, setFactCheck] = useState<FactCheck | null>(existingFactCheck || null);
const [error, setError] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isCached, setIsCached] = useState(false);
// Truncate statement text for API call (max 2000 chars)
const truncatedText = statementText.slice(0, 2000);
const handleFactCheck = useCallback(async () => {
if (isLoading || factCheck) return;
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/factcheck/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
claim_text: truncatedText,
mode: 'fast',
statement_id: statementId,
}),
});
const data = await response.json();
if (data.success && data.factCheck) {
setFactCheck(data.factCheck);
setIsCached(data.cached || false);
setIsModalOpen(true);
} else {
setError(data.error || 'Failed to verify');
}
} catch (err) {
setError('Network error');
console.error('[FactCheckBadge] Error:', err);
} finally {
setIsLoading(false);
}
}, [isLoading, factCheck, truncatedText, statementId]);
const handleBadgeClick = useCallback(() => {
if (factCheck) {
setIsModalOpen(true);
} else {
handleFactCheck();
}
}, [factCheck, handleFactCheck]);
// Loading state
if (isLoading) {
return (
<span
className="inline-flex items-center justify-center w-5 h-5 rounded-full text-gray-400"
title={locale === 'fr' ? 'Vérification...' : 'Checking...'}
>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
</span>
);
}
// Error state
if (error) {
return (
<button
onClick={handleFactCheck}
className="text-xs text-red-500 hover:text-red-600 underline cursor-pointer"
>
{locale === 'fr' ? 'Erreur - Réessayer' : 'Error - Retry'}
</button>
);
}
// Verdict badge (after fact-check)
if (factCheck) {
const display = VERDICT_DISPLAY[factCheck.verdict as FactCheckVerdict];
return (
<>
<button
onClick={handleBadgeClick}
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer transition-opacity hover:opacity-80 ${getVerdictClasses(factCheck.verdict as FactCheckVerdict)}`}
title={factCheck.rationale_short || display.label}
>
<span>{display.icon}</span>
<span>{locale === 'fr' ? translateVerdict(factCheck.verdict as FactCheckVerdict) : display.label}</span>
</button>
<FactCheckModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
factCheck={factCheck}
isCached={isCached}
/>
</>
);
}
// Initial state - small icon button
return (
<button
onClick={handleFactCheck}
className="inline-flex items-center justify-center w-5 h-5 rounded-full text-gray-400 hover:bg-accent-red/10 hover:text-accent-red dark:hover:bg-accent-red/20 cursor-pointer transition-colors"
title={locale === 'fr' ? 'Vérifier ce fait' : 'Fact check this'}
>
<Search className="w-3.5 h-3.5" />
</button>
);
}
/**
* Translate verdict to French
*/
function translateVerdict(verdict: FactCheckVerdict): string {
switch (verdict) {
case 'TRUE':
return 'Vrai';
case 'FALSE':
return 'Faux';
case 'MISLEADING':
return 'Trompeur';
case 'NEEDS_CONTEXT':
return 'Contexte requis';
case 'UNVERIFIABLE':
return 'Non vérifiable';
default:
return verdict;
}
}