import { type ApprovalRequestId } from "@t3tools/contracts"; import { memo, useEffect, useEffectEvent, useRef, useState } from "react"; import { type PendingUserInput } from "../../session-logic"; import { derivePendingUserInputProgress, type PendingUserInputDraftAnswer, } from "../../pendingUserInput"; import { CheckIcon } from "lucide-react"; import { cn } from "~/lib/utils"; interface PendingUserInputPanelProps { pendingUserInputs: PendingUserInput[]; respondingRequestIds: ApprovalRequestId[]; answers: Record; questionIndex: number; onToggleOption: (questionId: string, optionLabel: string) => void; onAdvance: () => void; } export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserInputPanel({ pendingUserInputs, respondingRequestIds, answers, questionIndex, onToggleOption, onAdvance, }: PendingUserInputPanelProps) { if (pendingUserInputs.length === 0) return null; const activePrompt = pendingUserInputs[0]; if (!activePrompt) return null; return ( ); }); const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard({ prompt, isResponding, answers, questionIndex, onToggleOption, onAdvance, }: { prompt: PendingUserInput; isResponding: boolean; answers: Record; questionIndex: number; onToggleOption: (questionId: string, optionLabel: string) => void; onAdvance: () => void; }) { const progress = derivePendingUserInputProgress(prompt.questions, answers, questionIndex); const activeQuestion = progress.activeQuestion; const autoAdvanceTimerRef = useRef(null); const onAdvanceRef = useRef(onAdvance); const [optimisticSingleSelect, setOptimisticSingleSelect] = useState<{ questionId: string; optionLabel: string; } | null>(null); useEffect(() => { onAdvanceRef.current = onAdvance; }, [onAdvance]); useEffect(() => { if (!activeQuestion || activeQuestion.multiSelect || !optimisticSingleSelect) { return; } if (optimisticSingleSelect.questionId !== activeQuestion.id) { setOptimisticSingleSelect(null); return; } if ( progress.customAnswer.trim().length === 0 && progress.selectedOptionLabels.includes(optimisticSingleSelect.optionLabel) ) { setOptimisticSingleSelect(null); } }, [ activeQuestion, optimisticSingleSelect, progress.customAnswer, progress.selectedOptionLabels, ]); // Clear auto-advance timer on unmount useEffect(() => { return () => { if (autoAdvanceTimerRef.current !== null) { window.clearTimeout(autoAdvanceTimerRef.current); } }; }, []); const handleOptionSelection = useEffectEvent((questionId: string, optionLabel: string) => { if (activeQuestion?.multiSelect) { onToggleOption(questionId, optionLabel); return; } setOptimisticSingleSelect({ questionId, optionLabel }); onToggleOption(questionId, optionLabel); if (autoAdvanceTimerRef.current !== null) { window.clearTimeout(autoAdvanceTimerRef.current); } autoAdvanceTimerRef.current = window.setTimeout(() => { autoAdvanceTimerRef.current = null; onAdvanceRef.current(); }, 200); }); // Keyboard shortcut: number keys 1-9 select corresponding options when focus is // outside editable fields. Multi-select prompts toggle options in place; single- // select prompts keep the existing auto-advance behavior. useEffect(() => { if (!activeQuestion || isResponding) return; const handler = (event: globalThis.KeyboardEvent) => { if (event.metaKey || event.ctrlKey || event.altKey) return; const target = event.target; if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { return; } if ( target instanceof HTMLElement && target.closest('[contenteditable]:not([contenteditable="false"])') ) { return; } const digit = Number.parseInt(event.key, 10); if (Number.isNaN(digit) || digit < 1 || digit > 9) return; const optionIndex = digit - 1; if (optionIndex >= activeQuestion.options.length) return; const option = activeQuestion.options[optionIndex]; if (!option) return; event.preventDefault(); handleOptionSelection(activeQuestion.id, option.label); }; document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, [activeQuestion, isResponding]); if (!activeQuestion) { return null; } const customAnswerActive = progress.customAnswer.trim().length > 0; return (
{activeQuestion.header} {prompt.questions.length > 1 ? ( {questionIndex + 1}/{prompt.questions.length} ) : null}

{activeQuestion.question}

{activeQuestion.multiSelect ? (

Select one or more options.

) : null}
{activeQuestion.options.map((option, index) => { const isOptimisticallySelected = optimisticSingleSelect?.questionId === activeQuestion.id && optimisticSingleSelect.optionLabel === option.label; const isSelected = isOptimisticallySelected || (!customAnswerActive && progress.selectedOptionLabels.includes(option.label)); const shortcutKey = index < 9 ? index + 1 : null; const className = cn( "group flex w-full items-center gap-3 rounded-lg border px-3 py-2 text-left outline-none transition-all duration-150 focus-visible:border-primary/40 focus-visible:ring-1 focus-visible:ring-primary/25", isSelected ? "border-primary/30 bg-primary/8 text-foreground" : "border-transparent bg-muted/22 text-foreground/85 hover:border-border/45 hover:bg-muted/34", isResponding && "opacity-50 cursor-not-allowed", !isResponding && "cursor-pointer", ); const content = ( <>
{option.label} {option.description && option.description !== option.label ? ( {option.description} ) : null}
{isSelected ? ( ) : shortcutKey !== null ? ( {shortcutKey} ) : null} ); return (
{ if (isResponding) return; handleOptionSelection(activeQuestion.id, option.label); }} className={className} > {content}
); })}
); });