import { useState } from "react"; import { CheckCircle2, XCircle, RotateCcw } from "lucide-react"; // ── Types ────────────────────────────────────────────────────────────────── export type Verdict = "supported" | "contradicted" | "neither"; export interface ChartSeries { name: string; data: { label: string; value: number }[]; } export interface ChartData { type: "bar" | "line"; title: string; yLabel?: string; xLabel?: string; source?: string; unit?: string; // e.g. '%', '°C', 'min' series: ChartSeries[]; } export interface DataClaim { text: string; verdict: Verdict; explanation: string; } export interface DataExercise { title: string; chart: ChartData; claims: DataClaim[]; } // ── Chart palette ────────────────────────────────────────────────────────── const PALETTE = [ "#3b82f6", "#8b5cf6", "#f97316", "#10b981", "#ef4444", "#ec4899", ]; // ── BarChart ─────────────────────────────────────────────────────────────── function BarChart({ chart }: { chart: ChartData }) { const [hovered, setHovered] = useState<{ si: number; pi: number } | null>( null, ); const labels = chart.series[0].data.map((d) => d.label); const allValues = chart.series.flatMap((s) => s.data.map((d) => d.value)); const maxVal = Math.max(...allValues); // Round up max to nearest 10 for cleaner y-axis const yMax = Math.ceil(maxVal / 10) * 10; const yTicks = [0, yMax * 0.25, yMax * 0.5, yMax * 0.75, yMax]; const chartH = 180; // px height of bar area return (

{chart.title}

{/* Y-axis */}
{yTicks.map((t) => ( {t} {chart.unit ?? ""} ))}
{/* Bar groups */}
{labels.map((_, pi) => (
{/* Bar group */}
{chart.series.map((s, si) => { const val = s.data[pi].value; const heightPct = (val / yMax) * 100; const isHov = hovered?.si === si && hovered?.pi === pi; return (
setHovered({ si, pi })} onMouseLeave={() => setHovered(null)} > {/* Value label on hover */} {isHov && (
{val} {chart.unit ?? ""}
)}
); })}
))}
{/* X-axis labels */}
{labels.map((label, i) => (
{label}
))}
{chart.xLabel && (

{chart.xLabel}

)} {/* Legend */} {chart.series.length > 1 && (
{chart.series.map((s, si) => (
{s.name}
))}
)} {/* Hover info bar */} {hovered && (
{chart.series[hovered.si].name} {" — "} {chart.series[0].data[hovered.pi].label}:{" "} {chart.series[hovered.si].data[hovered.pi].value} {chart.unit ?? ""}
)} {chart.source && (

Source: {chart.source}

)}
); } // ── LineChart ────────────────────────────────────────────────────────────── function LineChart({ chart }: { chart: ChartData }) { const [hovered, setHovered] = useState<{ si: number; pi: number } | null>( null, ); const W = 480, H = 200; const PAD = { top: 20, right: 20, bottom: 36, left: 48 }; const cW = W - PAD.left - PAD.right; const cH = H - PAD.top - PAD.bottom; const allValues = chart.series.flatMap((s) => s.data.map((d) => d.value)); const minVal = Math.min(...allValues); const maxVal = Math.max(...allValues); const spread = maxVal - minVal || 1; // Add 10% padding on y-axis const yPad = spread * 0.15; const yMin = minVal - yPad; const yMax = maxVal + yPad; const yRange = yMax - yMin; const labels = chart.series[0].data.map((d) => d.label); const xStep = cW / (labels.length - 1); const xPos = (i: number) => PAD.left + i * xStep; const yPos = (v: number) => PAD.top + cH - ((v - yMin) / yRange) * cH; // Y-axis ticks: 5 evenly spaced const yTicks = Array.from( { length: 5 }, (_, i) => minVal + ((maxVal - minVal) / 4) * i, ); return (

{chart.title}

{/* Grid lines */} {yTicks.map((t, i) => { const y = yPos(t); return ( {t % 1 === 0 ? t : t.toFixed(2)} {chart.unit ?? ""} ); })} {/* Lines + dots */} {chart.series.map((s, si) => { const color = PALETTE[si % PALETTE.length]; const pts = s.data .map((d, i) => `${xPos(i)},${yPos(d.value)}`) .join(" "); return ( {s.data.map((d, pi) => { const isHov = hovered?.si === si && hovered?.pi === pi; const cx = xPos(pi); const cy = yPos(d.value); return ( setHovered({ si, pi })} onMouseLeave={() => setHovered(null)} /> {isHov && ( <> {d.value} {chart.unit ?? ""} )} ); })} ); })} {/* X-axis labels */} {labels.map((label, i) => ( {label} ))} {/* Axes */} {/* Y-axis label */} {chart.yLabel && ( {chart.yLabel} )}
{/* Legend */} {chart.series.length > 1 && (
{chart.series.map((s, si) => (
{s.name}
))}
)} {/* Hover tooltip */} {hovered && (
{chart.series[hovered.si].name} {" · "} {chart.series[0].data[hovered.pi].label}:{" "} {chart.series[hovered.si].data[hovered.pi].value} {chart.unit ?? ""}
)} {chart.source && (

Source: {chart.source}

)}
); } // ── Main widget ──────────────────────────────────────────────────────────── const VERDICT_LABELS: Record = { supported: "Supported by data", contradicted: "Contradicted by data", neither: "Neither proven nor disproven", }; interface DataClaimWidgetProps { exercises: DataExercise[]; accentColor?: string; } // Pre-resolved accent classes to avoid Tailwind purge issues const ACCENT_CLASSES: Record< string, { tab: string; header: string; label: string; btn: string } > = { amber: { tab: "border-b-2 border-amber-600 text-amber-700", header: "bg-amber-50", label: "text-amber-600", btn: "bg-amber-600 hover:bg-amber-700", }, teal: { tab: "border-b-2 border-teal-600 text-teal-700", header: "bg-teal-50", label: "text-teal-600", btn: "bg-teal-600 hover:bg-teal-700", }, purple: { tab: "border-b-2 border-purple-600 text-purple-700", header: "bg-purple-50", label: "text-purple-600", btn: "bg-purple-600 hover:bg-purple-700", }, fuchsia: { tab: "border-b-2 border-fuchsia-600 text-fuchsia-700", header: "bg-fuchsia-50", label: "text-fuchsia-600", btn: "bg-fuchsia-600 hover:bg-fuchsia-700", }, }; export default function DataClaimWidget({ exercises, accentColor = "amber", }: DataClaimWidgetProps) { const [activeEx, setActiveEx] = useState(0); const [answers, setAnswers] = useState>({}); const [submitted, setSubmitted] = useState(false); const exercise = exercises[activeEx]; const allAnswered = exercise.claims.every((_, i) => answers[i] !== undefined); const score = submitted ? exercise.claims.filter((c, i) => answers[i] === c.verdict).length : 0; const c = ACCENT_CLASSES[accentColor] ?? ACCENT_CLASSES.amber; const reset = () => { setAnswers({}); setSubmitted(false); }; const switchEx = (i: number) => { setActiveEx(i); setAnswers({}); setSubmitted(false); }; return (
{/* Tabs */} {exercises.length > 1 && (
{exercises.map((ex, i) => ( ))}
)} {/* Chart */}

Data Source

{exercise.chart.type === "bar" ? ( ) : ( )}
{/* Claims */}

For each claim, decide if the data{" "} supports,{" "} contradicts, or{" "} neither proves nor disproves {" "} it:

{exercise.claims.map((claim, i) => { const userAnswer = answers[i]; const isCorrect = submitted && userAnswer === claim.verdict; const isWrong = submitted && userAnswer !== undefined && userAnswer !== claim.verdict; return (

Claim {i + 1}: {claim.text}

{(["supported", "contradicted", "neither"] as Verdict[]).map( (v) => { const isSelected = userAnswer === v; const isCorrectOpt = submitted && v === claim.verdict; let cls = "border-gray-200 text-gray-600 hover:border-gray-400 hover:bg-gray-50"; if (isSelected && !submitted) cls = `border-amber-500 bg-amber-50 text-amber-800 font-semibold`; if (submitted) { if (isCorrectOpt) cls = "border-green-400 bg-green-100 text-green-800 font-semibold"; else if (isSelected) cls = "border-red-300 bg-red-100 text-red-700"; else cls = "border-gray-100 text-gray-400"; } return ( ); }, )}
{submitted && (
{isCorrect ? ( ) : ( )}

{!isCorrect && ( Answer: {VERDICT_LABELS[claim.verdict]}.{" "} )} {claim.explanation}

)}
); })}
{/* Footer */}
{!submitted ? ( ) : (

{score}/{exercise.claims.length} correct

)}
); }