import { useMemo, useRef, useState } from 'react' import { useChatStore } from '../../stores/chatStore' import { useTabStore } from '../../stores/tabStore' import { useTranslation } from '../../i18n' import { Button } from '../shared/Button' type QuestionOption = { label: string description?: string } type Question = { question: string header?: string options?: QuestionOption[] multiSelect?: boolean } type AskUserInput = { questions?: Question[] question?: string header?: string options?: QuestionOption[] multiSelect?: boolean } type Props = { sessionId?: string | null toolUseId: string input: unknown result?: unknown } /** * Parse the AskUserQuestion input which may come in different shapes. */ function parseInput(input: unknown): Question[] { if (!input || typeof input !== 'object') return [] const obj = input as AskUserInput // Shape 1: { questions: [...] } if (Array.isArray(obj.questions)) { return obj.questions } // Shape 2: { question: "...", options: [...] } if (typeof obj.question === 'string') { return [{ question: obj.question, header: obj.header, options: obj.options, multiSelect: obj.multiSelect, }] } return [] } type QuestionSelections = Record function getSelectedAnswer(question: Question, selected: string[] | undefined) { if (!selected || selected.length === 0) return '' return question.multiSelect ? selected.join(', ') : selected[0] ?? '' } export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props) { const { respondToPermission } = useChatStore() const activeTabId = useTabStore((s) => s.activeTabId) const targetSessionId = sessionId ?? activeTabId const pendingPermission = useChatStore((s) => targetSessionId ? s.sessions[targetSessionId]?.pendingPermission : undefined) const t = useTranslation() const questions = parseInput(input) const inputObject = (input && typeof input === 'object') ? input as Record : {} const [activeTab, setActiveTab] = useState(0) const [selections, setSelections] = useState({}) const [freeText, setFreeText] = useState('') const [hasSubmitted, setHasSubmitted] = useState(false) const composingRef = useRef(false) if (questions.length === 0) return null const resultAnswers = useMemo(() => { if (!result || typeof result !== 'object') return {} const answers = (result as { answers?: unknown }).answers return answers && typeof answers === 'object' ? answers as Record : {} }, [result]) const pendingRequest = pendingPermission?.toolUseId === toolUseId ? pendingPermission : null const answeredText = useMemo(() => { if (Object.keys(resultAnswers).length > 0) { return questions .map((question) => resultAnswers[question.question]) .filter((answer): answer is string => typeof answer === 'string' && answer.trim().length > 0) .join(', ') } return freeText.trim() || questions .map((question, index) => getSelectedAnswer(question, selections[index])) .filter(Boolean) .join('; ') }, [freeText, questions, resultAnswers, selections]) const submitted = Object.keys(resultAnswers).length > 0 || hasSubmitted const handleSelect = (qIndex: number, label: string) => { if (submitted) return setSelections((prev) => { const question = questions[qIndex] const selected = prev[qIndex] ?? [] if (question?.multiSelect) { const nextSelected = selected.includes(label) ? selected.filter((value) => value !== label) : [...selected, label] const next = { ...prev } if (nextSelected.length > 0) { next[qIndex] = nextSelected } else { delete next[qIndex] } return next } if (selected[0] === label) { const next = { ...prev } delete next[qIndex] return next } return { ...prev, [qIndex]: [label] } }) setFreeText('') } const handleSubmit = () => { if (submitted) return const parts: string[] = [] for (let i = 0; i < questions.length; i++) { const selected = getSelectedAnswer(questions[i]!, selections[i]) if (selected) parts.push(selected) } const response = freeText.trim() || parts.join('; ') || '' if (!response) return if (!targetSessionId || !pendingRequest) return const answers = questions.reduce>((acc, question, index) => { if (freeText.trim()) { acc[question.question] = freeText.trim() } else { const selected = getSelectedAnswer(question, selections[index]) if (selected) acc[question.question] = selected } return acc }, {}) setHasSubmitted(true) respondToPermission(targetSessionId, pendingRequest.requestId, true, { updatedInput: { ...inputObject, answers, }, }) } // All questions must be answered (via selection or free text) to enable submit const allAnswered = freeText.trim().length > 0 || questions.every((_, i) => (selections[i]?.length ?? 0) > 0) const safeActiveTab = Math.min(activeTab, questions.length - 1) const activeQuestion = questions[safeActiveTab] if (!activeQuestion) return null return (
{/* Header */}
help
{t('question.needsInput')} {submitted && ( {t('question.answered')} )}
{/* Question tabs — horizontal tab bar (only show when multiple questions) */} {questions.length > 1 && (
{questions.map((q, i) => { const isActive = safeActiveTab === i const isAnswered = (selections[i]?.length ?? 0) > 0 const tabLabel = q.header || `Q${i + 1}` return ( ) })}
)} {/* Active question content */}

{activeQuestion.question}

{/* Option cards */} {activeQuestion.options && activeQuestion.options.length > 0 && (
{activeQuestion.options.map((opt, optIndex) => { const isSelected = selections[safeActiveTab]?.includes(opt.label) ?? false const isMultiSelect = activeQuestion.multiSelect === true return ( ) })}
)} {/* Free text input */} {!submitted && (
{ setFreeText(e.target.value) if (e.target.value.trim()) setSelections({}) }} onCompositionStart={() => { composingRef.current = true }} onCompositionEnd={() => { composingRef.current = false }} onKeyDown={(e) => { if (composingRef.current || e.nativeEvent.isComposing || e.keyCode === 229) return if (e.key === 'Enter' && allAnswered) handleSubmit() }} placeholder={t('question.typePlaceholder')} className="w-full px-3 py-2 text-sm bg-[var(--color-surface)] border border-[var(--color-outline-variant)]/40 rounded-[var(--radius-md)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:border-[var(--color-secondary)] focus:ring-1 focus:ring-[var(--color-secondary)]/30" />
)} {/* Submitted answer display */} {submitted && (
check_circle {t('question.answeredPrefix')}{answeredText}
)}
{/* Submit button */} {!submitted && (
)}
) }