import { useState, useEffect, useRef, useCallback } from "react"; import { Input } from "@pram/ui/components/input"; import { MCQ_FORMATS } from "@/lib/question-formats"; import type { Question } from "@/lib/types"; const TRUE_FALSE_CHOICES = [ { key: "TRUE", label: "True" }, { key: "FALSE", label: "False" }, { key: "NOT_GIVEN", label: "Not Given" }, ]; const AUTHOR_VIEW_CHOICES = [ { key: "YES", label: "Yes" }, { key: "NO", label: "No" }, { key: "NOT_GIVEN", label: "Not Given" }, ]; const radioClass = "flex items-center gap-3 p-3 rounded-[var(--radius-lg)] border-2 border-[var(--oat-border)] bg-[var(--pure-white)] cursor-pointer hover:border-[var(--matcha-300)] transition-all"; const radioSelected = "border-[var(--matcha-600)] bg-[#e8f5ed] ring-2 ring-[var(--matcha-600)]/30 shadow-sm"; const radioDisabled = "opacity-60 cursor-not-allowed"; /** Normalize TF / NG style answers from API or loose model output */ function normalizeTriStateKey( value: string, choices: readonly { key: string }[], ): string { const raw = (value ?? "").trim(); if (!raw) return ""; const u = raw.toUpperCase().replace(/\s+/g, "_"); if (choices.some((c) => c.key === u)) return u; const compact = u.replace(/_/g, ""); const alias: Record = { TRUE: "TRUE", FALSE: "FALSE", NOTGIVEN: "NOT_GIVEN", YES: "YES", NO: "NO", }; return alias[compact] ?? u; } /** Debounced text input to avoid parent re-render on every keystroke */ function DebouncedTextInput({ value, onChange, disabled, placeholder, className, }: { value: string; onChange: (val: string) => void; disabled?: boolean; placeholder?: string; className?: string; }) { const [localValue, setLocalValue] = useState(value); const debounceRef = useRef | undefined>(undefined); // Sync when prop value changes from outside (e.g. restore from server) useEffect(() => { setLocalValue(value); }, [value]); const handleChange = useCallback( (e: React.ChangeEvent) => { const val = e.target.value; setLocalValue(val); clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { onChange(val); }, 400); }, [onChange], ); const handleBlur = useCallback(() => { clearTimeout(debounceRef.current); onChange(localValue); }, [localValue, onChange]); return ( ); } export function QuestionInput({ question, value, onChange, disabled, }: { question: Question; value: string; onChange: (val: string) => void; disabled?: boolean; }) { const format = question.format; const options = question.options as Array<{ key: string; text: string }> | undefined; if (format === "matching_pairs") { if (!options || options.length === 0) { return (
Tidak ada opsi tersedia untuk soal ini.
); } const parseCurrentMapping = (val: string): Map => { const map = new Map(); val.split(",").forEach((pair) => { const [k, v] = pair.split(":").map((s) => s.trim()); if (k && v) map.set(k, v); }); return map; }; const currentMap = value ? parseCurrentMapping(value) : new Map(); const updateMapping = (key: string, val: string) => { currentMap.set(key, val); const serialized = Array.from(currentMap.entries()) .map(([k, v]) => `${k}:${v}`) .join(","); onChange(serialized); }; return (
{options.map((opt) => { const matched = currentMap.get(opt.key) || ""; return (
{opt.key} {opt.text} updateMapping(opt.key, e.target.value)} disabled={disabled} placeholder="Padanan..." aria-label="Padanan jawaban" className="w-24 px-3 py-2 text-sm rounded-[var(--radius-md)] border-2 border-[var(--oat-border)] bg-[var(--pure-white)] focus:outline-none focus:border-[var(--matcha-600)]" />
); })}
); } if ((MCQ_FORMATS as readonly string[]).includes(format ?? "")) { if (!options || options.length === 0) { return (
Tidak ada opsi tersedia untuk soal ini.
); } return (
{options.map((opt) => ( ))}
); } if (format === "true_false_not_given") { const normalizedValue = normalizeTriStateKey(value, TRUE_FALSE_CHOICES); return (
{TRUE_FALSE_CHOICES.map((c) => { const selected = normalizedValue === c.key; return ( ); })}
); } if (format === "author_view") { const normalizedValue = normalizeTriStateKey(value, AUTHOR_VIEW_CHOICES); return (
{AUTHOR_VIEW_CHOICES.map((c) => { const selected = normalizedValue === c.key; return ( ); })}
); } // Fallback: debounced text input for fill_blank and other free-text formats return ( ); }