"use client" /** * Design OS — Exam lock template showcase. * One question at a time, header tools, settings popover, demo overlays. */ import * as React from "react" import { ExamLockAnswerProgress, ExamLockHeaderToolbar, ExamLockInterruptionPanel, ExamLockQuestionBody, ExamLockQuestionNav, ExamLockQuestionRenderer, ExamLockResumeAuthDialog, ExamLockTimerDisplay, EXAM_LOCK_DEMO_CLINICAL_FIGURE, isExamLockQuestionAnswered, useExamLockColorMode, useExamLockSessionController, type ExamLockAnswerValue, type ExamLockDeliveryQuestion, } from "@/components/exam-lock" import { ExamLockTemplate } from "@/components/templates/exam-lock-template" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Shortcut } from "@/components/ui/dropdown-menu" import { Separator } from "@/components/ui/separator" import { NAV_USER } from "@/lib/mock/navigation" import { cn } from "@/lib/utils" import { useNavigate } from "react-router" const EXAM_TITLE = "Clinical reasoning sample exam" const INITIAL_SECONDS = 45 * 60 const MOCK_QUESTION_DEFS = [ { type: "mcq_single", questionId: "q1", stem: "Review the monitor strip, then select the priority assessment before pharmacologic intervention for acute shortness of breath with bilateral crackles.", stemMedia: EXAM_LOCK_DEMO_CLINICAL_FIGURE, options: [ "Obtain a 12-lead ECG", "Auscultate posterior lung fields", "Measure oxygen saturation and apply supplemental O₂ if indicated", "Order a chest X-ray", ], }, { type: "true_false", questionId: "q2", stem: "Jugular venous distension with peripheral edema supports prioritizing fluid restriction in heart failure exacerbation.", }, { type: "fill_blank", questionId: "q3", stem: "Before administering a new medication, the nurse performs the {{blank}} check to reduce the risk of {{blank}}.", formatHint: "Use clinical terms. One or two words per blank.", }, { type: "essay", questionId: "q4", stem: "Describe your nursing priorities in the first 10 minutes of care for a patient with acute shortness of breath and bilateral crackles.", maxCharacters: 2000, formatHint: "Write in complete sentences. Address assessment, safety, and communication.", }, ] as const function mockQuestionAt(index: number): ExamLockDeliveryQuestion { const def = MOCK_QUESTION_DEFS[index]! return { ...def, questionNumber: index + 1 } as ExamLockDeliveryQuestion } const KEYBOARD_ROWS = [ ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], ["A", "S", "D", "F", "G", "H", "J", "K", "L"], ["Z", "X", "C", "V", "B", "N", "M"], ] as const const CALCULATOR_TOP_ROW = ["C", "÷", "×", "⌫"] as const const CALCULATOR_ROWS = [ ["7", "8", "9", "−"], ["4", "5", "6", "+"], ["1", "2", "3", "="], ] as const function ExamCalculator({ className }: { className?: string }) { const [display, setDisplay] = React.useState("0") const [pending, setPending] = React.useState(null) const [operator, setOperator] = React.useState(null) const appendDigit = (digit: string) => { setDisplay(prev => (prev === "0" ? digit : `${prev}${digit}`)) } const clearAll = () => { setDisplay("0") setPending(null) setOperator(null) } const applyOperator = (op: string) => { setPending(display) setOperator(op) setDisplay("0") } const equals = () => { if (pending == null || operator == null) return const a = Number.parseFloat(pending) const b = Number.parseFloat(display) let result = 0 switch (operator) { case "+": result = a + b break case "−": result = a - b break case "×": result = a * b break case "÷": result = b === 0 ? 0 : a / b break default: return } setDisplay(String(Number.isInteger(result) ? result : result.toFixed(2))) setPending(null) setOperator(null) } const onKey = (key: string) => { if (key === "C") { clearAll() return } if (key === "⌫") { setDisplay(prev => (prev.length <= 1 ? "0" : prev.slice(0, -1))) return } if (key === "=") { equals() return } if (["+", "−", "×", "÷"].includes(key)) { applyOperator(key) return } if (key === ".") { setDisplay(prev => (prev.includes(".") ? prev : `${prev}.`)) return } appendDigit(key) } const topRow = CALCULATOR_TOP_ROW const rows = CALCULATOR_ROWS return (
{display}
{topRow.map(key => ( ))} {rows.flat().map(key => ( ))}
) } export function ExamLockShowcaseClient() { const navigate = useNavigate() const { colorMode, setColorMode } = useExamLockColorMode() const [simulateOffline, setSimulateOffline] = React.useState(false) const [simulateBlur, setSimulateBlur] = React.useState(false) const [handRaised, setHandRaised] = React.useState(false) const [connectOpen, setConnectOpen] = React.useState(false) const [resumeAuthOpen, setResumeAuthOpen] = React.useState(false) const [exitConfirmOpen, setExitConfirmOpen] = React.useState(false) const [answers, setAnswers] = React.useState>({}) const [questionIndex, setQuestionIndex] = React.useState(0) // The showcase never supplies a Submit action in `headerActions`, so the // post-submit screen stays out of reach here — the flag only exists to keep // the template's submitted branch wired. const [submitted] = React.useState(false) const [keyboardOpen, setKeyboardOpen] = React.useState(false) const [calculatorOpen, setCalculatorOpen] = React.useState(false) const [keyboardBuffer, setKeyboardBuffer] = React.useState("") const [eliminatedByQuestion, setEliminatedByQuestion] = React.useState< Record >({}) const questionCount = MOCK_QUESTION_DEFS.length const deliveryQuestion = React.useMemo( () => mockQuestionAt(questionIndex), [questionIndex], ) const answeredCount = MOCK_QUESTION_DEFS.filter((item, index) => isExamLockQuestionAnswered(mockQuestionAt(index), answers[item.questionId]), ).length const eliminatedOptions = eliminatedByQuestion[deliveryQuestion.questionId] ?? [] const sessionActive = !submitted const session = useExamLockSessionController({ initialSeconds: INITIAL_SECONDS, sessionActive, submitted, forceOffline: simulateOffline, forceVisibilityHidden: simulateBlur, handRaised, onRaiseHand: () => setHandRaised(true), onLowerHand: () => setHandRaised(false), onConnectWithPerson: () => setConnectOpen(true), }) const resumeSession = session.resume const choiceQuestion = deliveryQuestion.type === "mcq_single" || deliveryQuestion.type === "true_false" const toggleEliminated = React.useCallback( (option: string) => { const qid = deliveryQuestion.questionId setEliminatedByQuestion(prev => { const current = prev[qid] ?? [] const next = current.includes(option) ? current.filter(item => item !== option) : [...current, option] return { ...prev, [qid]: next } }) const currentAnswer = answers[qid] if (typeof currentAnswer === "string" && currentAnswer === option) { setAnswers(prev => { const next = { ...prev } delete next[qid] return next }) } }, [answers, deliveryQuestion.questionId], ) React.useEffect(() => { if (session.sessionPaused) { setKeyboardOpen(false) setCalculatorOpen(false) } else { setHandRaised(false) setConnectOpen(false) setResumeAuthOpen(false) } }, [session.sessionPaused]) const goPrevious = React.useCallback(() => { setQuestionIndex(i => Math.max(0, i - 1)) }, []) const goNext = React.useCallback(() => { setQuestionIndex(i => Math.min(questionCount - 1, i + 1)) }, [questionCount]) const appendKeyboardChar = React.useCallback((char: string) => { setKeyboardBuffer(prev => `${prev}${char}`) }, []) const requestResumeAuth = React.useCallback(() => { setResumeAuthOpen(true) }, []) const authorizeResume = React.useCallback(() => { setSimulateOffline(false) setSimulateBlur(false) resumeSession({ manual: true }) }, [resumeSession]) const exitExamLock = React.useCallback(() => { setExitConfirmOpen(false) navigate("/design-os/dashboard") }, [navigate]) const interruptionProps = React.useMemo(() => { if (!session.interruptionProps) return null const props = session.interruptionProps if (!props.supportActions?.retry) return props return { ...props, supportActions: { ...props.supportActions, retry: { ...props.supportActions.retry, onClick: requestResumeAuth, }, }, } }, [requestResumeAuth, session.interruptionProps]) return ( <> = questionCount - 1} onInvoke={goNext} /> } sessionPaused={session.sessionPaused} interruption={ interruptionProps ? : null } progress={ submitted || session.sessionPaused ? null : ( ) } learner={{ name: NAV_USER.name, avatar: NAV_USER.avatar }} headerToolbar={ submitted || session.sessionPaused ? null : ( setKeyboardOpen(true)} onCalculatorOpen={() => setCalculatorOpen(true)} settingsContent={