'use client'; import { useState } from 'react'; import { Sparkles, Loader2 } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { StatusBadge } from '@/components/ui/status-badge'; import { toast } from 'sonner'; import { useSession } from '@/lib/auth-client'; import { hasPermission } from '@/lib/permissions'; export interface ClassificationCardData { id: string; verdict: 'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS'; confidence: string | null; summary: string | null; reasons: string[]; recommendedActions: string[]; requiresApproval: boolean; createdAt: string; } interface ClassificationCardProps { campaignId: string; classification: ClassificationCardData | null; onReclassified: () => void; } const VERDICT_VARIANT_CLASS: Record = { SPAM: 'bg-slate-500/15 text-slate-600', UNWANTED: 'bg-amber-500/15 text-amber-600', THREAT: 'bg-destructive/15 text-destructive', USER_AWARENESS: 'bg-emerald-500/15 text-emerald-600', }; const ACTION_LABEL: Record = { block_sender: 'Block sender', purge_message: 'Purge message', warn_user: 'Warn user', no_action: 'No action', reset_password: 'Reset password', isolate_endpoint: 'Isolate endpoint', disable_forwarding_rule: 'Disable forwarding rule', acknowledge_user: 'Acknowledge user', }; function humanizeAction(actionType: string): string { return ( ACTION_LABEL[actionType] ?? actionType .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' ') ); } export function ClassificationCard({ campaignId, classification, onReclassified, }: ClassificationCardProps) { const { data: session } = useSession(); const [isReclassifying, setIsReclassifying] = useState(false); const role = (session?.user as { role?: string } | undefined)?.role ?? 'user'; const canReclassify = hasPermission(role, 'phishing', 'analyze'); // Guard: this card renders nothing for a null classification. The review // page (not this card) chooses the replacement UI for both null-classification // paths — the default "grouped but not yet classified" empty state (plan 06) // and the D-08 ungrouped-report Alert (plan 06) — so we never dereference // classification.verdict/.reasons/etc. here. if (classification == null) return null; async function handleReclassify() { setIsReclassifying(true); try { const res = await fetch(`/api/phishing/campaigns/${campaignId}/classify`, { method: 'POST', }); const data = await res.json(); if (!res.ok) throw new Error(data.message ?? data.error ?? 'Reclassify failed'); toast.success('Ticket reclassified'); onReclassified(); } catch (err) { toast.error(`Reclassify failed: ${err instanceof Error ? err.message : 'Unknown error'}`); } finally { setIsReclassifying(false); } } return (
Classification {canReclassify && ( )}
{classification.verdict} {classification.confidence != null && Number.isFinite(Number(classification.confidence)) && ( {Math.round(Number(classification.confidence) * 100)}% confidence )}
{classification.summary && (

{classification.summary}

)} {classification.reasons.length > 0 && (
    {classification.reasons.map((reason, idx) => (
  • {reason}
  • ))}
)} {classification.recommendedActions.length > 0 && (
{classification.recommendedActions.map((actionType) => ( {humanizeAction(actionType)} ))}
)} {classification.requiresApproval && ( This classification recommends a destructive action and requires explicit approval before remediation can proceed. )}
); }