diff --git a/admin/inertia/pages/easy-setup/index.tsx b/admin/inertia/pages/easy-setup/index.tsx index f310db8..c1fa42e 100644 --- a/admin/inertia/pages/easy-setup/index.tsx +++ b/admin/inertia/pages/easy-setup/index.tsx @@ -106,7 +106,7 @@ const ADDITIONAL_TOOLS: Capability[] = [ }, ] -type WizardStep = 1 | 2 | 3 | 4 +type WizardStep = 1 | 2 | 3 | 4 | 5 const CURATED_MAP_COLLECTIONS_KEY = 'curated-map-collections' const CURATED_CATEGORIES_KEY = 'curated-categories' @@ -199,6 +199,19 @@ export default function EasySetupWizard(props: { // Services that are already installed const installedServices = props.system.services.filter((service) => service.installed) + // Canonical "is AI part of this user's setup?" predicate (RFC #883 / issue #905). + // Single source consumed by step-indicator render, navigation skip logic, the + // review summary, and handleFinish. The AI step renders if and only if this + // is true; if false, the wizard collapses to 4 steps and the AI step is + // skipped on both forward and back nav. + const isAiInSetup = useMemo( + () => + selectedServices.includes(SERVICE_NAMES.OLLAMA) || + installedServices.some((s) => s.service_name === SERVICE_NAMES.OLLAMA) || + remoteOllamaEnabled, + [selectedServices, installedServices, remoteOllamaEnabled] + ) + const toggleMapCollection = (slug: string) => { setSelectedMapCollections((prev) => prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug] @@ -313,24 +326,29 @@ export default function EasySetupWizard(props: { // Get primary disk/filesystem info for storage projection const storageInfo = getPrimaryDiskInfo(systemInfo?.disk, systemInfo?.fsSize) + // Final step number (4 when AI is off, 5 when AI is on). Centralizing this + // here so canProceedToNextStep / handleNext / handleBack / the bottom-bar + // Next-vs-Finish switch all read the same value. + const finalStep: WizardStep = isAiInSetup ? 5 : 4 + const canProceedToNextStep = () => { if (!isOnline) return false // Must be online to proceed - if (currentStep === 1) return true // Can skip app installation - if (currentStep === 2) return true // Can skip map downloads - if (currentStep === 3) return true // Can skip ZIM downloads - return false + // Every step before the review is skippable; the review step shows Finish, not Next. + return currentStep < finalStep } const handleNext = () => { - if (currentStep < 4) { - setCurrentStep((prev) => (prev + 1) as WizardStep) - } + if (currentStep >= finalStep) return + // Skip the AI step (4) on forward nav when isAiInSetup is false. + const next = currentStep === 3 && !isAiInSetup ? 5 : currentStep + 1 + setCurrentStep(next as WizardStep) } const handleBack = () => { - if (currentStep > 1) { - setCurrentStep((prev) => (prev - 1) as WizardStep) - } + if (currentStep <= 1) return + // Skip the AI step (4) on back nav when isAiInSetup is false. + const prev = currentStep === 5 && !isAiInSetup ? 3 : currentStep - 1 + setCurrentStep(prev as WizardStep) } const handleFinish = async () => { @@ -347,13 +365,11 @@ export default function EasySetupWizard(props: { try { // Persist the auto-index policy choice before kicking off downloads so // any content that finishes during this same wizard run sees the right - // policy. Skipped when the user did not select the AI capability — the - // KV stays null and the first-chat JIT prompt (#899) handles the - // decision later if/when the user enables AI. - const aiSelected = - selectedServices.includes(SERVICE_NAMES.OLLAMA) || - installedServices.some((s) => s.service_name === SERVICE_NAMES.OLLAMA) - if (aiSelected) { + // policy. Skipped when AI is not in the user's setup; the KV stays null + // and the first-chat JIT prompt (#899) handles the decision later if/when + // the user enables AI. Uses the canonical isAiInSetup predicate so step + // 3 / step 4 / step 5 / handleFinish never disagree (issue #905). + if (isAiInSetup) { try { await api.updateSetting('rag.defaultIngestPolicy', ingestPolicy) } catch (err) { @@ -454,12 +470,25 @@ export default function EasySetupWizard(props: { }, []) const renderStepIndicator = () => { - const steps = [ - { number: 1, label: 'Apps' }, - { number: 2, label: 'Maps' }, - { number: 3, label: 'Content' }, - { number: 4, label: 'Review' }, - ] + // `step` is the stable WizardStep value (1=Apps, 2=Maps, 3=Content, + // 4=AI, 5=Review). `displayNumber` is the sequential position shown in + // the dot (always 1..N) so users see "1 2 3 4" when AI is off and + // "1 2 3 4 5" when AI is on, with no gap. + const baseSteps: Array<{ step: WizardStep; label: string }> = isAiInSetup + ? [ + { step: 1, label: 'Apps' }, + { step: 2, label: 'Maps' }, + { step: 3, label: 'Content' }, + { step: 4, label: 'AI' }, + { step: 5, label: 'Review' }, + ] + : [ + { step: 1, label: 'Apps' }, + { step: 2, label: 'Maps' }, + { step: 3, label: 'Content' }, + { step: 5, label: 'Review' }, + ] + const steps = baseSteps.map((s, idx) => ({ ...s, displayNumber: idx + 1 })) return (