diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index 2a7fa327..81f03995 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -541,11 +541,9 @@ export const ModelControls: React.FC = ({ const inputModalityIcons = getModalityIcons(currentMetadata, 'input'); const outputModalityIcons = getModalityIcons(currentMetadata, 'output'); - // Providers/models can reload (directory switch/config sync) without changing - // currentProviderId/currentModelId; include providers to avoid stale variants. - const availableVariants = React.useMemo(() => { - return getCurrentModelVariants(); - }, [getCurrentModelVariants]); + // Compute from current model each render to avoid stale variants + // in draft/session transitions. + const availableVariants = getCurrentModelVariants(); const hasVariants = availableVariants.length > 0; const costRows = [ diff --git a/packages/ui/src/components/chat/QuestionCard.tsx b/packages/ui/src/components/chat/QuestionCard.tsx index a645e04b..fa6a670d 100644 --- a/packages/ui/src/components/chat/QuestionCard.tsx +++ b/packages/ui/src/components/chat/QuestionCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { RiCheckLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react'; +import { RiArrowRightSLine, RiCheckLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react'; import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; @@ -76,25 +76,43 @@ export const QuestionCard: React.FC = ({ question }) => { const selectedForActive = selectedOptions[activeIndex] ?? []; const isCustomActive = Boolean(customMode[activeIndex]); - const requiredSatisfied = React.useMemo(() => { - if (questions.length === 0) return false; - + const unansweredIndexes = React.useMemo(() => { + const pending: number[] = []; for (let index = 0; index < questions.length; index += 1) { const isCustom = Boolean(customMode[index]); if (isCustom) { const value = (customText[index] ?? '').trim(); - if (!value) return false; + if (!value) pending.push(index); continue; } const answers = selectedOptions[index] ?? []; if (answers.length === 0) { - return false; + pending.push(index); + } + } + return pending; + }, [customMode, customText, questions.length, selectedOptions]); + + const requiredSatisfied = React.useMemo(() => { + if (questions.length === 0) return false; + return unansweredIndexes.length === 0; + }, [questions.length, unansweredIndexes.length]); + + const handleNextUnanswered = React.useCallback(() => { + if (questions.length === 0 || unansweredIndexes.length === 0) return; + + const start = isSummaryTab ? -1 : activeIndex; + for (let offset = 1; offset <= questions.length; offset += 1) { + const candidate = (start + offset + questions.length) % questions.length; + if (unansweredIndexes.includes(candidate)) { + setActiveTab(String(candidate)); + return; } } - return true; - }, [customMode, customText, questions.length, selectedOptions]); + setActiveTab(String(unansweredIndexes[0])); + }, [activeIndex, isSummaryTab, questions.length, unansweredIndexes]); const buildAnswersPayload = React.useCallback((): string[][] => { const answers: string[][] = []; @@ -197,6 +215,8 @@ export const QuestionCard: React.FC = ({ question }) => { {tabs.map((tab) => { const isActive = activeTab === tab.value; const isSummary = tab.value === SUMMARY_TAB; + const tabIndex = isSummary ? -1 : Number(tab.value); + const isAnswered = !isSummary && Number.isFinite(tabIndex) && !unansweredIndexes.includes(tabIndex); return (