Files
edbridge-scholars/src/components/lessons/ContextEliminationWidget.tsx
2026-03-12 02:39:34 +06:00

218 lines
7.2 KiB
TypeScript

import { useState } from "react";
import { CheckCircle2, RotateCcw, ChevronRight } from "lucide-react";
export interface VocabOption {
id: string;
definition: string;
isCorrect: boolean;
elimReason: string; // why wrong (for eliminated options) or why right (for correct option)
}
export interface VocabExercise {
sentence: string;
word: string; // the target word — will be highlighted
question: string;
options: VocabOption[];
}
interface ContextEliminationWidgetProps {
exercises: VocabExercise[];
accentColor?: string;
}
export default function ContextEliminationWidget({
exercises,
accentColor = "rose",
}: ContextEliminationWidgetProps) {
const [activeEx, setActiveEx] = useState(0);
const [eliminated, setEliminated] = useState<Set<string>>(new Set());
const [revealed, setRevealed] = useState(false);
const [triedCorrect, setTriedCorrect] = useState(false);
const exercise = exercises[activeEx];
const wrongIds = exercise.options
.filter((o) => !o.isCorrect)
.map((o) => o.id);
const eliminate = (id: string) => {
const opt = exercise.options.find((o) => o.id === id)!;
if (opt.isCorrect) {
setTriedCorrect(true);
setTimeout(() => setTriedCorrect(false), 1500);
} else {
const newElim = new Set([...eliminated, id]);
setEliminated(newElim);
if (wrongIds.every((wid) => newElim.has(wid))) {
setRevealed(true);
}
}
};
const reset = () => {
setEliminated(new Set());
setRevealed(false);
setTriedCorrect(false);
};
const switchEx = (i: number) => {
setActiveEx(i);
setEliminated(new Set());
setRevealed(false);
setTriedCorrect(false);
};
// Highlight the target word in the sentence
const renderSentence = () => {
const idx = exercise.sentence
.toLowerCase()
.indexOf(exercise.word.toLowerCase());
if (idx === -1) return <>{exercise.sentence}</>;
return (
<>
{exercise.sentence.slice(0, idx)}
<mark
className={`bg-${accentColor}-200 text-${accentColor}-900 font-bold px-0.5 rounded not-italic`}
>
{exercise.sentence.slice(idx, idx + exercise.word.length)}
</mark>
{exercise.sentence.slice(idx + exercise.word.length)}
</>
);
};
return (
<div className="rounded-2xl border border-gray-200 bg-white overflow-hidden shadow-sm">
{/* Tab strip */}
{exercises.length > 1 && (
<div className="flex border-b border-gray-200 bg-gray-50 overflow-x-auto">
{exercises.map((_, i) => (
<button
key={i}
onClick={() => switchEx(i)}
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors ${
i === activeEx
? `bg-white border-b-2 border-${accentColor}-600 text-${accentColor}-700`
: "text-gray-500 hover:text-gray-700"
}`}
>
Word {i + 1}
</button>
))}
</div>
)}
{/* Sentence in context */}
<div
className={`px-5 py-4 border-b border-gray-100 bg-${accentColor}-50`}
>
<p
className={`text-xs font-semibold uppercase tracking-wider text-${accentColor}-500 mb-2`}
>
Sentence in Context
</p>
<p className="text-gray-700 italic leading-relaxed text-sm">
{renderSentence()}
</p>
</div>
{/* Question + instruction */}
<div className="px-5 pt-4 pb-2">
<p className="font-medium text-gray-800 text-sm mb-1">
{exercise.question}
</p>
<p className="text-xs text-gray-400 italic">
{revealed
? "You found it! The correct definition is highlighted."
: 'Click "Eliminate" on definitions that don\'t fit the context. Work by elimination.'}
</p>
</div>
{/* Tried to eliminate correct option flash */}
{triedCorrect && (
<div className="mx-5 mb-2 px-3 py-2 bg-amber-50 border border-amber-200 rounded-xl text-xs text-amber-700 font-medium">
Can't eliminate that one it fits the context too well!
</div>
)}
{/* Options */}
<div className="px-5 py-3 space-y-2">
{exercise.options.map((opt) => {
const isElim = eliminated.has(opt.id);
const isAnswer = opt.isCorrect && revealed;
let wrapCls = "border-gray-200 bg-white";
if (isAnswer) wrapCls = "border-green-400 bg-green-50";
else if (isElim) wrapCls = "border-gray-100 bg-gray-50";
return (
<div
key={opt.id}
className={`rounded-xl border px-4 py-3 transition-all ${wrapCls} ${isElim ? "opacity-50" : ""}`}
>
<div className="flex items-start gap-3">
<span
className={`text-xs font-bold mt-0.5 shrink-0 ${isElim ? "text-gray-400" : isAnswer ? "text-green-700" : "text-gray-500"}`}
>
{opt.id}.
</span>
<div className="flex-1 min-w-0">
<p
className={`text-sm leading-snug ${
isElim
? "text-gray-400 line-through"
: isAnswer
? "text-green-800 font-semibold"
: "text-gray-700"
}`}
>
{opt.definition}
</p>
{isElim && (
<p className="text-xs text-gray-400 mt-0.5 italic">
{opt.elimReason}
</p>
)}
{isAnswer && (
<p className="text-xs text-green-700 mt-1">
{opt.elimReason}
</p>
)}
</div>
<div className="shrink-0">
{isAnswer && (
<CheckCircle2 className="w-5 h-5 text-green-500" />
)}
{!isElim && !isAnswer && !revealed && (
<button
onClick={() => eliminate(opt.id)}
className="text-xs font-semibold text-red-500 hover:text-red-700 hover:bg-red-50 px-2.5 py-1 rounded-lg transition-colors border border-red-200 hover:border-red-300"
>
Eliminate
</button>
)}
</div>
</div>
</div>
);
})}
</div>
<div className="px-5 pb-5 flex items-center gap-3">
<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" /> Reset
</button>
{revealed && activeEx < exercises.length - 1 && (
<button
onClick={() => switchEx(activeEx + 1)}
className={`ml-auto flex items-center gap-1.5 text-sm font-semibold text-${accentColor}-700 hover:text-${accentColor}-900 transition-colors`}
>
Next word <ChevronRight className="w-4 h-4" />
</button>
)}
</div>
</div>
);
}