Spaces:
Running
Running
| import React, { useState, useMemo, useRef } from 'react'; | |
| import { SlideData } from '../data/slides'; | |
| import { styles } from '../styles/appStyles'; | |
| // Number of decimals to show when revealing correct answers | |
| const PRECISION = 4; // means usually need fewer decimals than proportions | |
| const TOLERANCE = 1e-6; | |
| /** | |
| * Interactive worksheet for the *difference of two means* (large‑sample Z / small‑sample t). | |
| * | |
| * • Press ⏎ inside any input to jump to the next one (added July 2025). | |
| */ | |
| const InteractiveWorksheet = ({ | |
| defaults, | |
| }: { | |
| /* Expected keys in defaults: | |
| x1, s1, n1, x2, s2, n2 — all strings that parse to numbers. */ | |
| defaults: NonNullable<SlideData['calculatorDefaults']>; | |
| }) => { | |
| /* --------------------- pull parameters from the slide object --------------------- */ | |
| const x1 = parseFloat((defaults as any).x1); | |
| const s1 = parseFloat((defaults as any).s1); | |
| const n1 = parseInt((defaults as any).n1, 10); | |
| const x2 = parseFloat((defaults as any).x2); | |
| const s2 = parseFloat((defaults as any).s2); | |
| const n2 = parseInt((defaults as any).n2, 10); | |
| /* --------------------------- worksheet state setup --------------------------- */ | |
| const initialAnswers = { | |
| diff: '', | |
| var1: '', | |
| var2: '', | |
| varDiff: '', | |
| se: '', | |
| moe: '', | |
| ciLower: '', | |
| ciUpper: '', | |
| } as const; | |
| const [userAnswers, setUserAnswers] = useState<typeof initialAnswers>( | |
| initialAnswers | |
| ); | |
| const [isSubmitted, setIsSubmitted] = useState(false); | |
| /** Refs to each <input> so we can move focus */ | |
| const inputRefs = useRef<Array<HTMLInputElement | null>>([]); | |
| /** Handle typing in any input cell */ | |
| const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const { name, value } = e.target; | |
| setUserAnswers((prev) => ({ ...prev, [name]: value })); | |
| }; | |
| /** Handle Enter key: jump to next input */ | |
| const handleKeyDown = (index: number) => ( | |
| e: React.KeyboardEvent<HTMLInputElement> | |
| ) => { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| let nextIdx = index + 1; | |
| while (nextIdx < inputRefs.current.length) { | |
| const nextEl = inputRefs.current[nextIdx]; | |
| if (nextEl && !nextEl.disabled) { | |
| nextEl.focus(); | |
| break; | |
| } | |
| nextIdx += 1; | |
| } | |
| } | |
| }; | |
| /* ---------------------------- correct solutions ----------------------------- */ | |
| const correctAnswers = useMemo(() => { | |
| const diff = x1 - x2; | |
| const var1 = (s1 ** 2) / n1; | |
| const var2 = (s2 ** 2) / n2; | |
| const varDiff = var1 + var2; | |
| const se = Math.sqrt(varDiff); | |
| const moe = 1.96 * se; // 95 % Z‑critical, good for n > 30 each side | |
| const ciLower = diff - moe; | |
| const ciUpper = diff + moe; | |
| return { diff, var1, var2, varDiff, se, moe, ciLower, ciUpper } as const; | |
| }, [x1, s1, n1, x2, s2, n2]); | |
| /* ------------------------------- utilities ---------------------------------- */ | |
| const checkSingleAnswer = ( | |
| userVal: string, | |
| correctVal: number | |
| ): 'unchecked' | 'correct' | 'incorrect' => { | |
| if (!isSubmitted) return 'unchecked'; | |
| const num = parseFloat(userVal.replace(',', '.')); | |
| if (Number.isNaN(num)) return 'incorrect'; | |
| return Math.abs(num - correctVal) < TOLERANCE ? 'correct' : 'incorrect'; | |
| }; | |
| /* --------------------------- worksheet blueprint --------------------------- */ | |
| const calculationSteps: { | |
| key: keyof typeof initialAnswers; | |
| desc: string; | |
| formula: string; | |
| }[] = [ | |
| { key: 'diff', desc: 'Difference', formula: 'x̄₁ − x̄₂' }, | |
| { | |
| key: 'var1', | |
| desc: 'Variance of Group 1', | |
| formula: 's₁² / n₁', | |
| }, | |
| { | |
| key: 'var2', | |
| desc: 'Variance of Group 2', | |
| formula: 's₂² / n₂', | |
| }, | |
| { | |
| key: 'varDiff', | |
| desc: 'Variance of Difference', | |
| formula: 'Var(x̄₁) + Var(x̄₂)', | |
| }, | |
| { key: 'se', desc: 'Standard Error (SE)', formula: '√Var(Difference)' }, | |
| { | |
| key: 'moe', | |
| desc: 'Margin of Error (95 % CI)', | |
| formula: '1.96 × SE', | |
| }, | |
| { key: 'ciLower', desc: 'CI Lower', formula: 'Difference − MoE' }, | |
| { key: 'ciUpper', desc: 'CI Upper', formula: 'Difference + MoE' }, | |
| ]; | |
| /* -------------------------------------------------------------------------- */ | |
| /* render */ | |
| /* -------------------------------------------------------------------------- */ | |
| return ( | |
| <div style={styles.worksheetContainer}> | |
| {/* Problem header ---------------------------------------------------- */} | |
| <div style={styles.worksheetHeader}> | |
| <div style={styles.worksheetDataItem}> | |
| <strong>Group 1:</strong> x̄₁ = {x1}, s₁ = {s1}, n₁ = {n1} | |
| </div> | |
| <div style={styles.worksheetDataItem}> | |
| <strong>Group 2:</strong> x̄₂ = {x2}, s₂ = {s2}, n₂ = {n2} | |
| </div> | |
| </div> | |
| {/* Column headings ---------------------------------------------------- */} | |
| <div | |
| style={{ | |
| ...styles.worksheetRow, | |
| borderBottom: '2px solid #003366', | |
| paddingBottom: '0.5rem', | |
| marginBottom: '0.5rem', | |
| }} | |
| > | |
| <div style={styles.worksheetRowHeader}>Calculation Step</div> | |
| <div style={{ ...styles.worksheetRowHeader, textAlign: 'center' }}> | |
| Formula | |
| </div> | |
| <div style={{ ...styles.worksheetRowHeader, textAlign: 'center' }}> | |
| Your Answer | |
| </div> | |
| <div style={styles.worksheetRowHeader}>Check</div> | |
| </div> | |
| {/* Dynamic rows ------------------------------------------------------- */} | |
| {calculationSteps.map((step, idx) => { | |
| const userAnswer = userAnswers[step.key]; | |
| const hasInput = userAnswer.trim() !== ''; | |
| const status = checkSingleAnswer( | |
| userAnswer, | |
| correctAnswers[step.key] | |
| ); | |
| const showFeedback = isSubmitted && hasInput; | |
| const isIncorrect = showFeedback && status === 'incorrect'; | |
| return ( | |
| <div key={step.key} style={styles.worksheetRow}> | |
| <div style={styles.worksheetDescription}>{step.desc}</div> | |
| <div style={styles.worksheetFormula}>{step.formula}</div> | |
| {/* Input and feedback */} | |
| <div style={{ display: 'flex', flexDirection: 'column' }}> | |
| <input | |
| type="number" | |
| name={step.key} | |
| value={userAnswer} | |
| onChange={handleInputChange} | |
| onKeyDown={handleKeyDown(idx)} | |
| ref={(el) => { | |
| inputRefs.current[idx] = el; | |
| }} | |
| style={styles.worksheetInput} | |
| aria-label={step.desc} | |
| disabled={isSubmitted} | |
| /> | |
| {isIncorrect && ( | |
| <span style={styles.correctAnswerText}> | |
| Correct: | |
| {correctAnswers[step.key].toFixed(PRECISION)} | |
| </span> | |
| )} | |
| </div> | |
| <div | |
| style={{ | |
| ...styles.feedbackIcon, | |
| ...(status === 'correct' | |
| ? styles.correctFeedback | |
| : styles.incorrectFeedback), | |
| }} | |
| > | |
| {showFeedback && (status === 'correct' ? '✓' : '✗')} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {/* Action buttons ----------------------------------------------------- */} | |
| <div | |
| style={{ | |
| marginTop: '1.5rem', | |
| textAlign: 'center', | |
| display: 'flex', | |
| justifyContent: 'center', | |
| gap: '1rem', | |
| }} | |
| > | |
| <button | |
| style={styles.button} | |
| onClick={() => setIsSubmitted(true)} | |
| disabled={isSubmitted} | |
| > | |
| Check My Work | |
| </button> | |
| <button | |
| style={{ ...styles.button, backgroundColor: '#6c757d' }} | |
| onClick={() => { | |
| setUserAnswers(initialAnswers); | |
| setIsSubmitted(false); | |
| if (inputRefs.current[0]) inputRefs.current[0].focus(); | |
| }} | |
| > | |
| Reset | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default InteractiveWorksheet; | |