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[] } type AskUserInput = { questions?: Question[] question?: string options?: QuestionOption[] } type Props = { 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, options: obj.options }] } return [] } export function AskUserQuestion({ toolUseId, input, result }: Props) { const { respondToPermission } = useChatStore() const activeTabId = useTabStore((s) => s.activeTabId) const pendingPermission = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.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() || Object.values(selections).join(', ') }, [freeText, questions, resultAnswers, selections]) const submitted = Object.keys(resultAnswers).length > 0 || hasSubmitted const handleSelect = (qIndex: number, label: string) => { if (submitted) return setSelections((prev) => { // Toggle: deselect if already selected if (prev[qIndex] === 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 = selections[i] if (selected) parts.push(selected) } const response = freeText.trim() || parts.join('; ') || '' if (!response) return if (!activeTabId || !pendingRequest) return const answers = questions.reduce>((acc, question, index) => { if (freeText.trim()) { acc[question.question] = freeText.trim() } else if (selections[index]) { acc[question.question] = selections[index]! } return acc }, {}) setHasSubmitted(true) respondToPermission(activeTabId, 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] !== undefined) 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] !== undefined 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[activeTab] === opt.label 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 && (
)}
) }