import { useEffect, useMemo, useState } from 'react' import type { ApiClient } from '@/api/client' import type { ChatToolCall } from '@/chat/types' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { MarkdownRenderer } from '@/components/MarkdownRenderer' import { isAskUserQuestionToolName, parseAskUserQuestionInput, type AskUserQuestionQuestion } from '@/components/ToolCard/askUserQuestion' import { cn } from '@/lib/utils' import { usePlatform } from '@/hooks/usePlatform' import { Spinner } from '@/components/Spinner' import { useTranslation } from '@/lib/use-translation' function SelectionMark(props: { checked: boolean; mode: 'single' | 'multi' }) { const mark = props.mode === 'multi' ? (props.checked ? '☑' : '☐') : (props.checked ? '●' : '○') return ( {mark} ) } function OptionRow(props: { checked: boolean mode: 'single' | 'multi' disabled: boolean title: string description?: string | null onClick: () => void }) { return ( ) } function computeAnswersForQuestion( question: AskUserQuestionQuestion, selectedOptionIndices: number[], otherSelected: boolean, otherText: string ): string[] { const answers: string[] = [] for (const idx of selectedOptionIndices) { const opt = question.options[idx] if (!opt) continue const label = opt.label.trim() if (label.length > 0) answers.push(label) } const other = otherText.trim() if (otherSelected && other.length > 0) { answers.push(other) } return answers } export function AskUserQuestionFooter(props: { api: ApiClient sessionId: string tool: ChatToolCall disabled: boolean onDone: () => void }) { const { t } = useTranslation() const { haptic } = usePlatform() const permission = props.tool.permission const parsed = useMemo(() => parseAskUserQuestionInput(props.tool.input), [props.tool.input]) const questions = parsed.questions const [step, setStep] = useState(0) const [selectedByQuestion, setSelectedByQuestion] = useState([]) const [otherSelectedByQuestion, setOtherSelectedByQuestion] = useState([]) const [otherTextByQuestion, setOtherTextByQuestion] = useState([]) const [fallbackText, setFallbackText] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) useEffect(() => { setStep(0) setSelectedByQuestion(questions.map(() => [])) setOtherSelectedByQuestion(questions.map(() => false)) setOtherTextByQuestion(questions.map(() => '')) setFallbackText('') setLoading(false) setError(null) }, [props.tool.id]) if (!permission || permission.status !== 'pending') return null if (!isAskUserQuestionToolName(props.tool.name)) return null const run = async (action: () => Promise, hapticType: 'success' | 'error') => { if (props.disabled) return setError(null) try { await action() haptic.notification(hapticType) props.onDone() } catch (e) { haptic.notification('error') setError(e instanceof Error ? e.message : t('dialog.error.default')) } } const total = Math.max(1, questions.length) const clampedStep = Math.min(Math.max(step, 0), total - 1) const mode: 'single' | 'multi' = questions[clampedStep]?.multiSelect ? 'multi' : 'single' const validateQuestion = (idx: number): string[] | null => { if (questions.length === 0) { const text = fallbackText.trim() return text.length > 0 ? [text] : null } const question = questions[idx] if (!question) return null const answers = computeAnswersForQuestion( question, selectedByQuestion[idx] ?? [], otherSelectedByQuestion[idx] ?? false, otherTextByQuestion[idx] ?? '' ) return answers.length > 0 ? answers : null } const submit = async () => { if (loading) return const answers: Record = {} if (questions.length === 0) { const a0 = validateQuestion(0) if (!a0) { setError(t('tool.selectOption')) return } answers['0'] = a0 } else { for (let i = 0; i < questions.length; i += 1) { const a = validateQuestion(i) if (!a) { setError(t('tool.selectOption')) setStep(i) return } answers[String(i)] = a } } setLoading(true) await run(() => props.api.approvePermission(props.sessionId, permission.id, { answers }), 'success') setLoading(false) } const next = () => { if (questions.length === 0) return const a = validateQuestion(clampedStep) if (!a) { setError(t('tool.selectOption')) return } setError(null) setStep((s) => Math.min(s + 1, questions.length - 1)) } const prev = () => { setError(null) setStep((s) => Math.max(s - 1, 0)) } const toggleOption = (qIdx: number, optIdx: number) => { const q = questions[qIdx] if (!q) return haptic.selection() setSelectedByQuestion((prevSelected) => { const nextSelected = prevSelected.slice() const cur = new Set(nextSelected[qIdx] ?? []) if (q.multiSelect) { if (cur.has(optIdx)) cur.delete(optIdx) else cur.add(optIdx) nextSelected[qIdx] = Array.from(cur).sort((a, b) => a - b) return nextSelected } nextSelected[qIdx] = [optIdx] return nextSelected }) if (!q.multiSelect) { setOtherSelectedByQuestion((prevOther) => { const nextOther = prevOther.slice() nextOther[qIdx] = false return nextOther }) } } const toggleOther = (qIdx: number) => { const q = questions[qIdx] if (!q) return haptic.selection() if (!q.multiSelect) { setSelectedByQuestion((prevSelected) => { const nextSelected = prevSelected.slice() nextSelected[qIdx] = [] return nextSelected }) setOtherSelectedByQuestion((prevOther) => { const nextOther = prevOther.slice() nextOther[qIdx] = true return nextOther }) return } setOtherSelectedByQuestion((prevOther) => { const nextOther = prevOther.slice() nextOther[qIdx] = !nextOther[qIdx] return nextOther }) } const updateOtherText = (qIdx: number, value: string) => { setOtherTextByQuestion((prevText) => { const nextText = prevText.slice() nextText[qIdx] = value return nextText }) if (value.trim().length > 0) { setOtherSelectedByQuestion((prevOther) => { const nextOther = prevOther.slice() nextOther[qIdx] = true return nextOther }) } } return (
{t('tool.question')} [{clampedStep + 1}/{total}]
{error ? (
{error}
) : null} {questions.length === 0 ? (
{t('tool.askUserQuestion.fallback')}