chore(build): refactor codebase for production
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CheckCircle2, XCircle, RotateCcw } from 'lucide-react';
|
||||
import { useState } from "react";
|
||||
import { CheckCircle2, XCircle, RotateCcw } from "lucide-react";
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export type Verdict = 'supported' | 'contradicted' | 'neither';
|
||||
export type Verdict = "supported" | "contradicted" | "neither";
|
||||
|
||||
export interface ChartSeries {
|
||||
name: string;
|
||||
@ -11,12 +11,12 @@ export interface ChartSeries {
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
type: 'bar' | 'line';
|
||||
type: "bar" | "line";
|
||||
title: string;
|
||||
yLabel?: string;
|
||||
xLabel?: string;
|
||||
source?: string;
|
||||
unit?: string; // e.g. '%', '°C', 'min'
|
||||
unit?: string; // e.g. '%', '°C', 'min'
|
||||
series: ChartSeries[];
|
||||
}
|
||||
|
||||
@ -34,15 +34,24 @@ export interface DataExercise {
|
||||
|
||||
// ── Chart palette ──────────────────────────────────────────────────────────
|
||||
|
||||
const PALETTE = ['#3b82f6', '#8b5cf6', '#f97316', '#10b981', '#ef4444', '#ec4899'];
|
||||
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 [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 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;
|
||||
@ -52,22 +61,36 @@ function BarChart({ chart }: { chart: ChartData }) {
|
||||
|
||||
return (
|
||||
<div className="px-2">
|
||||
<p className="text-xs font-semibold text-gray-600 text-center mb-4">{chart.title}</p>
|
||||
<p className="text-xs font-semibold text-gray-600 text-center mb-4">
|
||||
{chart.title}
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{/* Y-axis */}
|
||||
<div className="flex flex-col-reverse justify-between items-end pr-1" style={{ height: chartH, minWidth: 32 }}>
|
||||
{yTicks.map(t => (
|
||||
<span key={t} className="text-[10px] text-gray-400 leading-none">{t}{chart.unit ?? ''}</span>
|
||||
<div
|
||||
className="flex flex-col-reverse justify-between items-end pr-1"
|
||||
style={{ height: chartH, minWidth: 32 }}
|
||||
>
|
||||
{yTicks.map((t) => (
|
||||
<span key={t} className="text-[10px] text-gray-400 leading-none">
|
||||
{t}
|
||||
{chart.unit ?? ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bar groups */}
|
||||
<div className="flex-1 flex items-end gap-2 border-b border-l border-gray-300" style={{ height: chartH }}>
|
||||
{labels.map((label, pi) => (
|
||||
<div
|
||||
className="flex-1 flex items-end gap-2 border-b border-l border-gray-300"
|
||||
style={{ height: chartH }}
|
||||
>
|
||||
{labels.map((_, pi) => (
|
||||
<div key={pi} className="flex-1 flex flex-col items-center gap-0">
|
||||
{/* Bar group */}
|
||||
<div className="w-full flex items-end gap-0.5" style={{ height: chartH - 2 }}>
|
||||
<div
|
||||
className="w-full flex items-end gap-0.5"
|
||||
style={{ height: chartH - 2 }}
|
||||
>
|
||||
{chart.series.map((s, si) => {
|
||||
const val = s.data[pi].value;
|
||||
const heightPct = (val / yMax) * 100;
|
||||
@ -79,9 +102,11 @@ function BarChart({ chart }: { chart: ChartData }) {
|
||||
style={{
|
||||
height: `${heightPct}%`,
|
||||
backgroundColor: isHov
|
||||
? PALETTE[si % PALETTE.length] + 'dd'
|
||||
: PALETTE[si % PALETTE.length] + 'cc',
|
||||
outline: isHov ? `2px solid ${PALETTE[si % PALETTE.length]}` : 'none',
|
||||
? PALETTE[si % PALETTE.length] + "dd"
|
||||
: PALETTE[si % PALETTE.length] + "cc",
|
||||
outline: isHov
|
||||
? `2px solid ${PALETTE[si % PALETTE.length]}`
|
||||
: "none",
|
||||
}}
|
||||
onMouseEnter={() => setHovered({ si, pi })}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
@ -90,9 +115,12 @@ function BarChart({ chart }: { chart: ChartData }) {
|
||||
{isHov && (
|
||||
<div
|
||||
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-white whitespace-nowrap z-10 pointer-events-none"
|
||||
style={{ backgroundColor: PALETTE[si % PALETTE.length] }}
|
||||
style={{
|
||||
backgroundColor: PALETTE[si % PALETTE.length],
|
||||
}}
|
||||
>
|
||||
{val}{chart.unit ?? ''}
|
||||
{val}
|
||||
{chart.unit ?? ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -107,17 +135,32 @@ function BarChart({ chart }: { chart: ChartData }) {
|
||||
{/* X-axis labels */}
|
||||
<div className="flex gap-2 ml-10 mt-1">
|
||||
{labels.map((label, i) => (
|
||||
<div key={i} className="flex-1 text-center text-[10px] text-gray-500 leading-tight">{label}</div>
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 text-center text-[10px] text-gray-500 leading-tight"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{chart.xLabel && <p className="text-[10px] text-gray-400 text-center mt-1">{chart.xLabel}</p>}
|
||||
{chart.xLabel && (
|
||||
<p className="text-[10px] text-gray-400 text-center mt-1">
|
||||
{chart.xLabel}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
{chart.series.length > 1 && (
|
||||
<div className="flex flex-wrap gap-3 mt-3 justify-center">
|
||||
{chart.series.map((s, si) => (
|
||||
<div key={si} className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: PALETTE[si % PALETTE.length] }} />
|
||||
<div
|
||||
key={si}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-600"
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-sm"
|
||||
style={{ backgroundColor: PALETTE[si % PALETTE.length] }}
|
||||
/>
|
||||
{s.name}
|
||||
</div>
|
||||
))}
|
||||
@ -127,17 +170,26 @@ function BarChart({ chart }: { chart: ChartData }) {
|
||||
{/* Hover info bar */}
|
||||
{hovered && (
|
||||
<div className="mt-3 text-xs text-center text-gray-600 bg-gray-50 rounded-lg py-1.5 px-3">
|
||||
<span className="font-semibold" style={{ color: PALETTE[hovered.si % PALETTE.length] }}>
|
||||
<span
|
||||
className="font-semibold"
|
||||
style={{ color: PALETTE[hovered.si % PALETTE.length] }}
|
||||
>
|
||||
{chart.series[hovered.si].name}
|
||||
</span>
|
||||
{' — '}
|
||||
{chart.series[0].data[hovered.pi].label}: <span className="font-semibold">
|
||||
{chart.series[hovered.si].data[hovered.pi].value}{chart.unit ?? ''}
|
||||
{" — "}
|
||||
{chart.series[0].data[hovered.pi].label}:{" "}
|
||||
<span className="font-semibold">
|
||||
{chart.series[hovered.si].data[hovered.pi].value}
|
||||
{chart.unit ?? ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chart.source && <p className="text-[10px] text-gray-400 text-center mt-2">Source: {chart.source}</p>}
|
||||
{chart.source && (
|
||||
<p className="text-[10px] text-gray-400 text-center mt-2">
|
||||
Source: {chart.source}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -145,14 +197,17 @@ function BarChart({ chart }: { chart: ChartData }) {
|
||||
// ── LineChart ──────────────────────────────────────────────────────────────
|
||||
|
||||
function LineChart({ chart }: { chart: ChartData }) {
|
||||
const [hovered, setHovered] = useState<{ si: number; pi: number } | null>(null);
|
||||
const [hovered, setHovered] = useState<{ si: number; pi: number } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const W = 480, H = 200;
|
||||
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 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;
|
||||
@ -163,28 +218,51 @@ function LineChart({ chart }: { chart: ChartData }) {
|
||||
const yMax = maxVal + yPad;
|
||||
const yRange = yMax - yMin;
|
||||
|
||||
const labels = chart.series[0].data.map(d => d.label);
|
||||
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);
|
||||
const yTicks = Array.from(
|
||||
{ length: 5 },
|
||||
(_, i) => minVal + ((maxVal - minVal) / 4) * i,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-gray-600 text-center mb-2">{chart.title}</p>
|
||||
<p className="text-xs font-semibold text-gray-600 text-center mb-2">
|
||||
{chart.title}
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<svg viewBox={`0 0 ${W} ${H}`} className="w-full" style={{ maxHeight: 220 }}>
|
||||
<svg
|
||||
viewBox={`0 0 ${W} ${H}`}
|
||||
className="w-full"
|
||||
style={{ maxHeight: 220 }}
|
||||
>
|
||||
{/* Grid lines */}
|
||||
{yTicks.map((t, i) => {
|
||||
const y = yPos(t);
|
||||
return (
|
||||
<g key={i}>
|
||||
<line x1={PAD.left} x2={W - PAD.right} y1={y} y2={y} stroke="#e5e7eb" strokeWidth="1" />
|
||||
<text x={PAD.left - 4} y={y + 3.5} textAnchor="end" fontSize="9" fill="#9ca3af">
|
||||
{t % 1 === 0 ? t : t.toFixed(2)}{chart.unit ?? ''}
|
||||
<line
|
||||
x1={PAD.left}
|
||||
x2={W - PAD.right}
|
||||
y1={y}
|
||||
y2={y}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<text
|
||||
x={PAD.left - 4}
|
||||
y={y + 3.5}
|
||||
textAnchor="end"
|
||||
fontSize="9"
|
||||
fill="#9ca3af"
|
||||
>
|
||||
{t % 1 === 0 ? t : t.toFixed(2)}
|
||||
{chart.unit ?? ""}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
@ -193,10 +271,18 @@ function LineChart({ chart }: { chart: ChartData }) {
|
||||
{/* 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(' ');
|
||||
const pts = s.data
|
||||
.map((d, i) => `${xPos(i)},${yPos(d.value)}`)
|
||||
.join(" ");
|
||||
return (
|
||||
<g key={si}>
|
||||
<polyline points={pts} fill="none" stroke={color} strokeWidth="2.5" strokeLinejoin="round" />
|
||||
<polyline
|
||||
points={pts}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
{s.data.map((d, pi) => {
|
||||
const isHov = hovered?.si === si && hovered?.pi === pi;
|
||||
const cx = xPos(pi);
|
||||
@ -204,20 +290,36 @@ function LineChart({ chart }: { chart: ChartData }) {
|
||||
return (
|
||||
<g key={pi}>
|
||||
<circle
|
||||
cx={cx} cy={cy} r={isHov ? 7 : 5}
|
||||
fill={color} stroke="white" strokeWidth="2"
|
||||
style={{ cursor: 'pointer', transition: 'r 0.1s' }}
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={isHov ? 7 : 5}
|
||||
fill={color}
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
style={{ cursor: "pointer", transition: "r 0.1s" }}
|
||||
onMouseEnter={() => setHovered({ si, pi })}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
/>
|
||||
{isHov && (
|
||||
<>
|
||||
<rect
|
||||
x={cx - 28} y={cy - 26} width="56" height="18"
|
||||
rx="4" fill="#1f2937"
|
||||
x={cx - 28}
|
||||
y={cy - 26}
|
||||
width="56"
|
||||
height="18"
|
||||
rx="4"
|
||||
fill="#1f2937"
|
||||
/>
|
||||
<text x={cx} y={cy - 13} textAnchor="middle" fontSize="10" fill="white" fontWeight="bold">
|
||||
{d.value}{chart.unit ?? ''}
|
||||
<text
|
||||
x={cx}
|
||||
y={cy - 13}
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
fill="white"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{d.value}
|
||||
{chart.unit ?? ""}
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
@ -230,21 +332,45 @@ function LineChart({ chart }: { chart: ChartData }) {
|
||||
|
||||
{/* X-axis labels */}
|
||||
{labels.map((label, i) => (
|
||||
<text key={i} x={xPos(i)} y={H - PAD.bottom + 14} textAnchor="middle" fontSize="9.5" fill="#6b7280">
|
||||
<text
|
||||
key={i}
|
||||
x={xPos(i)}
|
||||
y={H - PAD.bottom + 14}
|
||||
textAnchor="middle"
|
||||
fontSize="9.5"
|
||||
fill="#6b7280"
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
))}
|
||||
|
||||
{/* Axes */}
|
||||
<line x1={PAD.left} x2={PAD.left} y1={PAD.top} y2={H - PAD.bottom} stroke="#d1d5db" strokeWidth="1.5" />
|
||||
<line x1={PAD.left} x2={W - PAD.right} y1={H - PAD.bottom} y2={H - PAD.bottom} stroke="#d1d5db" strokeWidth="1.5" />
|
||||
<line
|
||||
x1={PAD.left}
|
||||
x2={PAD.left}
|
||||
y1={PAD.top}
|
||||
y2={H - PAD.bottom}
|
||||
stroke="#d1d5db"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<line
|
||||
x1={PAD.left}
|
||||
x2={W - PAD.right}
|
||||
y1={H - PAD.bottom}
|
||||
y2={H - PAD.bottom}
|
||||
stroke="#d1d5db"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
|
||||
{/* Y-axis label */}
|
||||
{chart.yLabel && (
|
||||
<text
|
||||
x={12} y={H / 2}
|
||||
x={12}
|
||||
y={H / 2}
|
||||
transform={`rotate(-90, 12, ${H / 2})`}
|
||||
textAnchor="middle" fontSize="9" fill="#9ca3af"
|
||||
textAnchor="middle"
|
||||
fontSize="9"
|
||||
fill="#9ca3af"
|
||||
>
|
||||
{chart.yLabel}
|
||||
</text>
|
||||
@ -256,8 +382,14 @@ function LineChart({ chart }: { chart: ChartData }) {
|
||||
{chart.series.length > 1 && (
|
||||
<div className="flex flex-wrap gap-3 mt-1 justify-center">
|
||||
{chart.series.map((s, si) => (
|
||||
<div key={si} className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||
<div className="w-5 h-0.5" style={{ backgroundColor: PALETTE[si % PALETTE.length] }} />
|
||||
<div
|
||||
key={si}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-600"
|
||||
>
|
||||
<div
|
||||
className="w-5 h-0.5"
|
||||
style={{ backgroundColor: PALETTE[si % PALETTE.length] }}
|
||||
/>
|
||||
{s.name}
|
||||
</div>
|
||||
))}
|
||||
@ -267,17 +399,26 @@ function LineChart({ chart }: { chart: ChartData }) {
|
||||
{/* Hover tooltip */}
|
||||
{hovered && (
|
||||
<div className="mt-2 text-xs text-center text-gray-600 bg-gray-50 rounded-lg py-1.5 px-3">
|
||||
<span className="font-semibold" style={{ color: PALETTE[hovered.si % PALETTE.length] }}>
|
||||
<span
|
||||
className="font-semibold"
|
||||
style={{ color: PALETTE[hovered.si % PALETTE.length] }}
|
||||
>
|
||||
{chart.series[hovered.si].name}
|
||||
</span>
|
||||
{' · '}
|
||||
{chart.series[0].data[hovered.pi].label}: <span className="font-semibold">
|
||||
{chart.series[hovered.si].data[hovered.pi].value}{chart.unit ?? ''}
|
||||
{" · "}
|
||||
{chart.series[0].data[hovered.pi].label}:{" "}
|
||||
<span className="font-semibold">
|
||||
{chart.series[hovered.si].data[hovered.pi].value}
|
||||
{chart.unit ?? ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chart.source && <p className="text-[10px] text-gray-400 text-center mt-2">Source: {chart.source}</p>}
|
||||
{chart.source && (
|
||||
<p className="text-[10px] text-gray-400 text-center mt-2">
|
||||
Source: {chart.source}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -285,9 +426,9 @@ function LineChart({ chart }: { chart: ChartData }) {
|
||||
// ── Main widget ────────────────────────────────────────────────────────────
|
||||
|
||||
const VERDICT_LABELS: Record<Verdict, string> = {
|
||||
supported: 'Supported by data',
|
||||
contradicted: 'Contradicted by data',
|
||||
neither: 'Neither proven nor disproven',
|
||||
supported: "Supported by data",
|
||||
contradicted: "Contradicted by data",
|
||||
neither: "Neither proven nor disproven",
|
||||
};
|
||||
|
||||
interface DataClaimWidgetProps {
|
||||
@ -296,25 +437,60 @@ interface DataClaimWidgetProps {
|
||||
}
|
||||
|
||||
// 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' },
|
||||
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) {
|
||||
export default function DataClaimWidget({
|
||||
exercises,
|
||||
accentColor = "amber",
|
||||
}: DataClaimWidgetProps) {
|
||||
const [activeEx, setActiveEx] = useState(0);
|
||||
const [answers, setAnswers] = useState<Record<number, Verdict>>({});
|
||||
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 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); };
|
||||
const reset = () => {
|
||||
setAnswers({});
|
||||
setSubmitted(false);
|
||||
};
|
||||
const switchEx = (i: number) => {
|
||||
setActiveEx(i);
|
||||
setAnswers({});
|
||||
setSubmitted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white overflow-hidden shadow-sm">
|
||||
@ -326,7 +502,7 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
|
||||
key={i}
|
||||
onClick={() => switchEx(i)}
|
||||
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors bg-white ${
|
||||
i === activeEx ? c.tab : 'text-gray-500 hover:text-gray-700'
|
||||
i === activeEx ? c.tab : "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{ex.title}
|
||||
@ -337,73 +513,100 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
|
||||
|
||||
{/* Chart */}
|
||||
<div className={`px-5 pt-5 pb-4 border-b border-gray-200 ${c.header}`}>
|
||||
<p className={`text-xs font-bold uppercase tracking-wider mb-4 ${c.label}`}>Data Source</p>
|
||||
{exercise.chart.type === 'bar'
|
||||
? <BarChart chart={exercise.chart} />
|
||||
: <LineChart chart={exercise.chart} />
|
||||
}
|
||||
<p
|
||||
className={`text-xs font-bold uppercase tracking-wider mb-4 ${c.label}`}
|
||||
>
|
||||
Data Source
|
||||
</p>
|
||||
{exercise.chart.type === "bar" ? (
|
||||
<BarChart chart={exercise.chart} />
|
||||
) : (
|
||||
<LineChart chart={exercise.chart} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Claims */}
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
For each claim, decide if the data{' '}
|
||||
<strong className="text-green-700">supports</strong>,{' '}
|
||||
<strong className="text-red-600">contradicts</strong>, or{' '}
|
||||
<strong className="text-gray-600">neither proves nor disproves</strong> it:
|
||||
For each claim, decide if the data{" "}
|
||||
<strong className="text-green-700">supports</strong>,{" "}
|
||||
<strong className="text-red-600">contradicts</strong>, or{" "}
|
||||
<strong className="text-gray-600">
|
||||
neither proves nor disproves
|
||||
</strong>{" "}
|
||||
it:
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{exercise.claims.map((claim, i) => {
|
||||
const userAnswer = answers[i];
|
||||
const isCorrect = submitted && userAnswer === claim.verdict;
|
||||
const isWrong = submitted && userAnswer !== undefined && userAnswer !== claim.verdict;
|
||||
const isWrong =
|
||||
submitted &&
|
||||
userAnswer !== undefined &&
|
||||
userAnswer !== claim.verdict;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`rounded-xl border p-4 transition-all ${
|
||||
submitted
|
||||
? isCorrect ? 'border-green-300 bg-green-50'
|
||||
: isWrong ? 'border-red-200 bg-red-50'
|
||||
: 'border-gray-200'
|
||||
: 'border-gray-200'
|
||||
? isCorrect
|
||||
? "border-green-300 bg-green-50"
|
||||
: isWrong
|
||||
? "border-red-200 bg-red-50"
|
||||
: "border-gray-200"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm text-gray-800 mb-3">
|
||||
<span className="font-bold text-gray-400 mr-2">Claim {i + 1}:</span>
|
||||
<span className="font-bold text-gray-400 mr-2">
|
||||
Claim {i + 1}:
|
||||
</span>
|
||||
{claim.text}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['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 (
|
||||
<button
|
||||
key={v}
|
||||
disabled={submitted}
|
||||
onClick={() => setAnswers(prev => ({ ...prev, [i]: v }))}
|
||||
className={`px-3 py-1.5 rounded-full border text-xs transition-all ${cls}`}
|
||||
>
|
||||
{VERDICT_LABELS[v]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{(["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 (
|
||||
<button
|
||||
key={v}
|
||||
disabled={submitted}
|
||||
onClick={() =>
|
||||
setAnswers((prev) => ({ ...prev, [i]: v }))
|
||||
}
|
||||
className={`px-3 py-1.5 rounded-full border text-xs transition-all ${cls}`}
|
||||
>
|
||||
{VERDICT_LABELS[v]}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
{submitted && (
|
||||
<div className="mt-3 pt-2 border-t border-gray-100 flex gap-2">
|
||||
{isCorrect
|
||||
? <CheckCircle2 className="w-4 h-4 text-green-600 shrink-0 mt-0.5" />
|
||||
: <XCircle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
|
||||
}
|
||||
{isCorrect ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-600 shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
|
||||
)}
|
||||
<p className="text-xs text-gray-600 leading-relaxed">
|
||||
{!isCorrect && (
|
||||
<span className="font-semibold text-red-700">Answer: {VERDICT_LABELS[claim.verdict]}. </span>
|
||||
<span className="font-semibold text-red-700">
|
||||
Answer: {VERDICT_LABELS[claim.verdict]}.{" "}
|
||||
</span>
|
||||
)}
|
||||
{claim.explanation}
|
||||
</p>
|
||||
@ -422,7 +625,9 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
|
||||
disabled={!allAnswered}
|
||||
onClick={() => setSubmitted(true)}
|
||||
className={`px-5 py-2.5 rounded-full text-sm font-bold text-white transition-colors ${
|
||||
allAnswered ? c.btn : 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
allAnswered
|
||||
? c.btn
|
||||
: "bg-gray-200 text-gray-400 cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
Check all answers
|
||||
@ -432,7 +637,10 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
|
||||
<p className="text-sm font-semibold text-gray-700">
|
||||
{score}/{exercise.claims.length} correct
|
||||
</p>
|
||||
<button onClick={reset} className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 transition-colors">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" /> Try again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user