import { Box, Text, useInput } from "ink"; import { memo, useState } from "react"; import { colors } from "./colors"; import { PasteAwareTextInput } from "./PasteAwareTextInput"; interface QuestionOption { label: string; description: string; } interface Question { question: string; header: string; options: QuestionOption[]; multiSelect: boolean; } type Props = { questions: Question[]; onSubmit: (answers: Record) => void; }; export const QuestionDialog = memo(({ questions, onSubmit }: Props) => { const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [answers, setAnswers] = useState>({}); const [selectedOption, setSelectedOption] = useState(0); const [isOtherMode, setIsOtherMode] = useState(false); const [otherText, setOtherText] = useState(""); const [selectedMulti, setSelectedMulti] = useState>(new Set()); const currentQuestion = questions[currentQuestionIndex]; const optionsWithOther = currentQuestion ? [ ...currentQuestion.options, { label: "Other", description: "Provide a custom response" }, ] : []; const handleSubmitAnswer = (answer: string) => { if (!currentQuestion) return; const newAnswers = { ...answers, [currentQuestion.question]: answer, }; setAnswers(newAnswers); if (currentQuestionIndex < questions.length - 1) { setCurrentQuestionIndex(currentQuestionIndex + 1); setSelectedOption(0); setIsOtherMode(false); setOtherText(""); setSelectedMulti(new Set()); } else { onSubmit(newAnswers); } }; useInput((input, key) => { if (!currentQuestion) return; if (isOtherMode) { if (key.escape) { setIsOtherMode(false); setOtherText(""); } return; } if (key.upArrow) { setSelectedOption((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedOption((prev) => Math.min(optionsWithOther.length - 1, prev + 1), ); } else if (key.return) { if (currentQuestion.multiSelect) { if (selectedOption === optionsWithOther.length - 1) { setIsOtherMode(true); } else if (selectedMulti.size > 0) { const selectedLabels = Array.from(selectedMulti) .map((i) => optionsWithOther[i]?.label) .filter(Boolean) .join(", "); handleSubmitAnswer(selectedLabels); } } else { if (selectedOption === optionsWithOther.length - 1) { setIsOtherMode(true); } else { handleSubmitAnswer(optionsWithOther[selectedOption]?.label || ""); } } } else if (input === " " && currentQuestion.multiSelect) { if (selectedOption < optionsWithOther.length - 1) { setSelectedMulti((prev) => { const newSet = new Set(prev); if (newSet.has(selectedOption)) { newSet.delete(selectedOption); } else { newSet.add(selectedOption); } return newSet; }); } } else if (input >= "1" && input <= "9") { const optionIndex = Number.parseInt(input, 10) - 1; if (optionIndex < optionsWithOther.length) { if (currentQuestion.multiSelect) { if (optionIndex < optionsWithOther.length - 1) { setSelectedMulti((prev) => { const newSet = new Set(prev); if (newSet.has(optionIndex)) { newSet.delete(optionIndex); } else { newSet.add(optionIndex); } return newSet; }); } } else { if (optionIndex === optionsWithOther.length - 1) { setIsOtherMode(true); } else { handleSubmitAnswer(optionsWithOther[optionIndex]?.label || ""); } } } } }); const handleOtherSubmit = (text: string) => { handleSubmitAnswer(text); }; if (!currentQuestion) return null; return ( [{currentQuestion.header}]{" "} {currentQuestion.question} {questions.length > 1 && ( Question {currentQuestionIndex + 1} of {questions.length} )} {isOtherMode ? ( Type your response (Esc to cancel): > ) : ( {optionsWithOther.map((option, index) => { const isSelected = index === selectedOption; const isChecked = selectedMulti.has(index); const color = isSelected ? colors.approval.header : undefined; return ( {isSelected ? ">" : " "} {currentQuestion.multiSelect && index < optionsWithOther.length - 1 && ( [{isChecked ? "x" : " "}] )} {index + 1}. {option.label} {option.description && ( {option.description} )} ); })} {currentQuestion.multiSelect ? "Space to toggle, Enter to confirm selection" : `Enter to select, or type 1-${optionsWithOther.length}`} )} ); }); QuestionDialog.displayName = "QuestionDialog";