UAIDE / src /components /VerdictCard.jsx
ATS-27's picture
Upload folder using huggingface_hub
af980d7 verified
Raw
History Blame Contribute Delete
6.49 kB
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ShieldCheck, ShieldAlert, AlertTriangle, ChevronDown, Info, Zap, BarChart2 } from 'lucide-react';
import styles from './VerdictCard.module.css';
const VERDICT_CONFIG = {
authentic: {
label: 'Authentic',
sublabel: 'Human-generated content',
Icon: ShieldCheck,
colorClass: 'authentic',
gradient: 'linear-gradient(135deg, #00c67a 0%, #00a064 100%)',
ringColor: 'var(--authentic-glow)',
},
ai_generated: {
label: 'AI-Generated',
sublabel: 'Synthetic content detected',
Icon: ShieldAlert,
colorClass: 'aiGenerated',
gradient: 'linear-gradient(135deg, #ff4757 0%, #d63031 100%)',
ringColor: 'var(--ai-generated-glow)',
},
suspect: {
label: 'Suspect',
sublabel: 'Inconclusive — manual review advised',
Icon: AlertTriangle,
colorClass: 'suspect',
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
ringColor: 'var(--suspect-glow)',
},
};
function ScoreArc({ score, verdict }) {
const radius = 54;
const cx = 70;
const cy = 70;
const circumference = 2 * Math.PI * radius;
const dashOffset = circumference - (score / 100) * circumference;
const color = verdict === 'authentic' ? 'var(--authentic)'
: verdict === 'ai_generated' ? 'var(--ai-generated)'
: 'var(--suspect)';
return (
<svg width="140" height="140" className={styles.scoreArc}>
<circle
cx={cx} cy={cy} r={radius}
fill="none" stroke="var(--bg-surface-2)"
strokeWidth="10"
/>
<motion.circle
cx={cx} cy={cy} r={radius}
fill="none" stroke={color}
strokeWidth="10"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={circumference}
animate={{ strokeDashoffset: dashOffset }}
transition={{ duration: 1.4, ease: 'easeOut', delay: 0.3 }}
transform={`rotate(-90 ${cx} ${cy})`}
/>
<text x={cx} y={cy - 8} textAnchor="middle" className={styles.scoreText}
fill="var(--text-primary)" fontSize="26" fontWeight="800" fontFamily="Inter, sans-serif">
{score.toFixed(1)}
</text>
<text x={cx} y={cy + 14} textAnchor="middle"
fill="var(--text-muted)" fontSize="11" fontWeight="600" fontFamily="Inter, sans-serif">
% confidence
</text>
</svg>
);
}
export default function VerdictCard({ result }) {
const config = VERDICT_CONFIG[result.verdict];
const { Icon } = config;
const [showBreakdown, setShowBreakdown] = useState(false);
return (
<motion.div
className={`${styles.card} ${styles[config.colorClass]}`}
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
>
{/* Verdict header */}
<div className={styles.verdictHeader}>
<div className={styles.verdictIconWrap} style={{ background: config.gradient }}>
<Icon size={22} color="white" strokeWidth={2.5} />
</div>
<div>
<p className={styles.verdictLabel}>{config.label}</p>
<p className={styles.verdictSub}>{config.sublabel}</p>
</div>
</div>
{/* Score arc */}
<div className={styles.scoreWrap}>
<ScoreArc score={result.confidenceScore} verdict={result.verdict} />
<p className={styles.scoreCaption}>AI Involvement Score</p>
</div>
{/* Model breakdown */}
<button
className={styles.breakdownToggle}
onClick={() => setShowBreakdown(!showBreakdown)}
aria-expanded={showBreakdown}
>
<div className={styles.breakdownToggleLeft}>
<BarChart2 size={14} />
<span>Model Breakdown</span>
</div>
<ChevronDown
size={15}
style={{ transform: showBreakdown ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s ease' }}
/>
</button>
<AnimatePresence>
{showBreakdown && (
<motion.div
className={styles.breakdown}
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25 }}
>
<div className={styles.breakdownInner}>
{result.modelBreakdown.map((m, i) => (
<div key={m.model} className={styles.modelRow}>
<div className={styles.modelRowHeader}>
<span className={styles.modelName}>{m.model}</span>
<span className={`${styles.modelScore} font-mono`}>{m.score.toFixed(1)}%</span>
</div>
<div className={styles.modelBar}>
<motion.div
className={styles.modelBarFill}
initial={{ width: 0 }}
animate={{ width: `${m.score}%` }}
transition={{ duration: 0.8, delay: i * 0.1, ease: 'easeOut' }}
/>
</div>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Quick stats */}
<div className={styles.quickStats}>
<div className={styles.statItem}>
<Zap size={13} />
<span className={styles.statLabel}>Processing time</span>
<span className={`${styles.statValue} font-mono`}>{result.processingTime}</span>
</div>
<div className={styles.statItem}>
<Info size={13} />
<span className={styles.statLabel}>Analysis ID</span>
<span className={`${styles.statValue} font-mono`}>{result.analysisId}</span>
</div>
</div>
{/* Artifacts summary */}
<div className={styles.artifactsList}>
<p className={styles.artifactsTitle}>Detected Artifacts</p>
{result.artifacts.map((artifact) => (
<div key={artifact.id} className={styles.artifactRow}>
<div className={`${styles.severityDot} ${styles['sev_' + artifact.severity]}`} />
<div className={styles.artifactContent}>
<span className={styles.artifactType}>{artifact.type}</span>
<span className={`${styles.severityBadge} ${styles['sevBadge_' + artifact.severity]}`}>
{artifact.severity}
</span>
</div>
</div>
))}
</div>
</motion.div>
);
}