import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { UUID } from 'uuidjs'; import { toast } from 'sonner'; import { ArrowLeft, ArrowRight, Check, Sparkles, PartyPopper, Loader2, X, ExternalLink, Rocket, Wrench, } from 'lucide-react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { userInfo, systemInfo, bootstrapWorkspaceSession, initializeSystemInfo, } from '@/app/infra/http'; import { Adapter, Bot, Pipeline, WizardProgress, } from '@/app/infra/entities/api'; import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import { PipelineConfigTab, PipelineConfigStage, } from '@/app/infra/entities/pipeline'; import { DynamicFormItemConfig, getDefaultValues, parseDynamicFormItemType, } from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import ProviderForm from '@/app/home/components/models-dialog/component/provider-form/ProviderForm'; import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { groupByCategory, getCategoryLabel, } from '@/app/infra/entities/adapter-categories'; import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs'; import i18n from 'i18next'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; import { cn } from '@/lib/utils'; import { LanguageSelector } from '@/components/ui/language-selector'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- const TOTAL_STEPS = 4; // --------------------------------------------------------------------------- // Main Wizard Page (full-screen, no sidebar) // --------------------------------------------------------------------------- export default function WizardPage() { const { t } = useTranslation(); const navigate = useNavigate(); // ---- Wizard state ---- const [currentStep, setCurrentStep] = useState(0); const [selectedAdapter, setSelectedAdapter] = useState(null); const [selectedRunner, setSelectedRunner] = useState(null); const [botName, setBotName] = useState(''); const [botDescription, _setBotDescription] = useState(''); const [adapterConfig, setAdapterConfig] = useState>( {}, ); const [runnerConfig, setRunnerConfig] = useState>({}); const [aiEngineMode, setAiEngineMode] = useState< 'orchestrated' | 'llm' | null >(null); const [modelSource, setModelSource] = useState<'space' | 'custom' | null>( null, ); const [providerCreated, setProviderCreated] = useState(false); const [createdProviderUuid, setCreatedProviderUuid] = useState( null, ); const [modelsAdded, setModelsAdded] = useState(false); const [createdBotUuid, setCreatedBotUuid] = useState(null); const [webhookUrl, setWebhookUrl] = useState(''); const [extraWebhookUrl, setExtraWebhookUrl] = useState(''); // ---- Remote data ---- const [adapters, setAdapters] = useState([]); const [aiConfigTab, setAiConfigTab] = useState( null, ); const [isLoading, setIsLoading] = useState(true); const [isCreatingBot, setIsCreatingBot] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isSavingBot, setIsSavingBot] = useState(false); const [botSaved, setBotSaved] = useState(false); // ---- Helper: persist wizard progress to backend (fire-and-forget) ---- const saveProgress = useCallback( (overrides: Partial = {}) => { const progress: WizardProgress = { step: overrides.step ?? currentStep, selected_adapter: overrides.selected_adapter ?? selectedAdapter, created_bot_uuid: overrides.created_bot_uuid ?? createdBotUuid, bot_saved: overrides.bot_saved ?? botSaved, selected_runner: overrides.selected_runner ?? selectedRunner, }; httpClient.saveWizardProgress(progress).catch((err) => { console.error('Failed to save wizard progress', err); }); }, [currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner], ); // ---- Fetch remote data & restore progress ---- useEffect(() => { let cancelled = false; (async () => { try { // Resolve the Account's Workspace before loading scoped wizard data. const workspaceResult = await bootstrapWorkspaceSession(); if (workspaceResult.status === 'selection-required') { navigate('/workspaces/select?returnTo=%2Fwizard', { replace: true }); return; } if (workspaceResult.status === 'unavailable') { throw new Error('No Workspace is available for this Account'); } await initializeSystemInfo({ throwOnError: true }); const [adaptersResp, metadataResp] = await Promise.all([ httpClient.getAdapters(), httpClient.getGeneralPipelineMetadata(), ]); if (cancelled) return; setAdapters(adaptersResp.adapters); const aiTab = metadataResp.configs.find((c) => c.name === 'ai'); if (aiTab) setAiConfigTab(aiTab); // Restore wizard progress if available const progress = systemInfo.wizard_progress; if (progress && progress.created_bot_uuid) { // Verify the bot still exists before restoring try { const botData = await httpClient.getBot(progress.created_bot_uuid); if (cancelled) return; setSelectedAdapter(progress.selected_adapter); setCreatedBotUuid(progress.created_bot_uuid); setBotSaved(progress.bot_saved ?? false); setSelectedRunner(progress.selected_runner); // Restore bot name from fetched bot data setBotName(botData.bot.name); // Restore webhook URLs const runtimeValues = botData.bot.adapter_runtime_values as | Record | undefined; setWebhookUrl((runtimeValues?.webhook_full_url as string) || ''); setExtraWebhookUrl( (runtimeValues?.extra_webhook_full_url as string) || '', ); // Restore step (cap at step 2 — step 3 means done) setCurrentStep(Math.min(progress.step, 2)); } catch { // Bot no longer exists — clear stale progress and start fresh httpClient .saveWizardProgress({ step: 0, selected_adapter: null, created_bot_uuid: null, bot_saved: false, selected_runner: null, }) .catch(() => {}); } } } catch (err) { console.error('Failed to load wizard data', err); toast.error(t('wizard.loadError')); } finally { if (!cancelled) setIsLoading(false); } })(); return () => { cancelled = true; }; }, [navigate, t]); // ---- Derived data ---- const runnerStage: PipelineConfigStage | undefined = useMemo( () => aiConfigTab?.stages.find((s) => s.name === 'runner'), [aiConfigTab], ); const runnerOptions = useMemo(() => { if (!runnerStage) return []; const runnerField = runnerStage.config.find((c) => c.name === 'runner'); return runnerField?.options ?? []; }, [runnerStage]); const selectedRunnerConfigStage: PipelineConfigStage | undefined = useMemo(() => { if (!selectedRunner || !aiConfigTab) return undefined; return aiConfigTab.stages.find((s) => s.name === selectedRunner); }, [selectedRunner, aiConfigTab]); // Adapter spec config for the selected adapter const selectedAdapterConfig: IDynamicFormItemSchema[] = useMemo(() => { const adapter = adapters.find((a) => a.name === selectedAdapter); if (!adapter) return []; return adapter.spec.config.map( (item) => new DynamicFormItemConfig({ default: item.default, id: UUID.generate(), label: item.label, description: item.description, name: item.name, required: item.required, type: parseDynamicFormItemType(item.type), options: item.options, show_if: item.show_if, login_platform: item.login_platform, url: item.url, download_filename: item.download_filename, help_links: item.help_links, help_label: item.help_label, }), ); }, [adapters, selectedAdapter]); // Runner config items const selectedRunnerConfigItems: IDynamicFormItemSchema[] = useMemo(() => { if (!selectedRunnerConfigStage) return []; return selectedRunnerConfigStage.config.map( (item) => new DynamicFormItemConfig({ default: item.default, id: UUID.generate(), label: item.label, description: item.description, name: item.name, required: item.required, type: parseDynamicFormItemType(item.type), options: item.options, show_if: item.show_if, login_platform: item.login_platform, url: item.url, download_filename: item.download_filename, help_links: item.help_links, help_label: item.help_label, }), ); }, [selectedRunnerConfigStage]); // ---- Runner selection with progress saving ---- const handleSelectRunner = useCallback( (runner: string) => { setSelectedRunner(runner); saveProgress({ step: 2, selected_runner: runner }); }, [saveProgress], ); const handleAiEngineModeSelect = useCallback( (mode: 'orchestrated' | 'llm') => { setAiEngineMode(mode); setSelectedRunner(null); setModelSource(null); setProviderCreated(false); }, [], ); // ---- Navigation helpers ---- const canProceed = useCallback((): boolean => { switch (currentStep) { case 0: return selectedAdapter !== null; case 1: return createdBotUuid !== null && botSaved; case 2: if (aiEngineMode === 'orchestrated') { return selectedRunner !== null; } if (aiEngineMode === 'llm') { return ( modelSource === 'space' || (modelSource === 'custom' && modelsAdded) ); } return false; default: return false; } }, [ currentStep, selectedAdapter, createdBotUuid, botSaved, aiEngineMode, selectedRunner, modelSource, providerCreated, ]); const goNext = useCallback(() => { if (currentStep < TOTAL_STEPS - 1 && canProceed()) { const nextStep = currentStep + 1; setCurrentStep(nextStep); saveProgress({ step: nextStep }); } }, [currentStep, canProceed, saveProgress]); const goPrev = useCallback(() => { if (currentStep === 2) { // Intra-step back navigation for AI Engine step if (aiEngineMode === 'orchestrated' && selectedRunner) { setSelectedRunner(null); return; } // LLM + custom: config form → model scan if (aiEngineMode === 'llm' && modelSource === 'custom' && modelsAdded) { setModelsAdded(false); setSelectedRunner(null); return; } // LLM + custom: model scan → provider form if ( aiEngineMode === 'llm' && modelSource === 'custom' && providerCreated && !modelsAdded ) { setProviderCreated(false); setCreatedProviderUuid(null); return; } // LLM + custom: provider form → model source if ( aiEngineMode === 'llm' && modelSource === 'custom' && !providerCreated ) { setModelSource(null); return; } // LLM: model source → main choice if (modelSource) { setModelSource(null); return; } // Main choice → previous step if (aiEngineMode) { setAiEngineMode(null); return; } } if (currentStep > 0) { const prevStep = currentStep - 1; setCurrentStep(prevStep); saveProgress({ step: prevStep }); } }, [currentStep, saveProgress, aiEngineMode, selectedRunner, modelSource, providerCreated, modelsAdded]); // ---- Create Bot (Step 0) ---- // Creates a disabled bot using the adapter label as name. const handleCreateBot = useCallback(async () => { if (!selectedAdapter) return; setIsCreatingBot(true); try { // Use adapter label as default bot name const adapter = adapters.find((a) => a.name === selectedAdapter); const defaultName = adapter ? extractI18nObject(adapter.label) : selectedAdapter; setBotName(defaultName); const defaultConfig = adapter ? getDefaultValues(adapter.spec.config) : {}; const bot: Bot = { name: defaultName, description: '', adapter: selectedAdapter, adapter_config: defaultConfig, enable: false, }; const resp = await httpClient.createBot(bot); setCreatedBotUuid(resp.uuid); // Fetch runtime info to get webhook URL(s) try { const botData = await httpClient.getBot(resp.uuid); const runtimeValues = botData.bot.adapter_runtime_values as | Record | undefined; setWebhookUrl((runtimeValues?.webhook_full_url as string) || ''); setExtraWebhookUrl( (runtimeValues?.extra_webhook_full_url as string) || '', ); } catch { // Non-critical — webhook URL display is optional } // Advance to Step 1 setCurrentStep(1); // Persist progress saveProgress({ step: 1, selected_adapter: selectedAdapter, created_bot_uuid: resp.uuid, bot_saved: false, selected_runner: null, }); } catch (err) { const apiErr = err as { msg?: string }; toast.error( t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''), ); } finally { setIsCreatingBot(false); } }, [selectedAdapter, adapters, t, saveProgress]); // ---- Save Bot Config & Enable (Step 1) ---- // Updates the bot's adapter config and enables it. const handleSaveBot = useCallback(async () => { if (!createdBotUuid || !selectedAdapter) return; setIsSavingBot(true); try { await httpClient.updateBot(createdBotUuid, { name: botName, description: botDescription || '', adapter: selectedAdapter, adapter_config: adapterConfig, enable: true, }); setBotSaved(true); // Re-fetch runtime info to get updated webhook URL(s) try { const botData = await httpClient.getBot(createdBotUuid); const runtimeValues = botData.bot.adapter_runtime_values as | Record | undefined; setWebhookUrl((runtimeValues?.webhook_full_url as string) || ''); setExtraWebhookUrl( (runtimeValues?.extra_webhook_full_url as string) || '', ); } catch { // Non-critical } // Persist progress saveProgress({ step: 1, bot_saved: true }); } catch (err) { const apiErr = err as { msg?: string }; toast.error( t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''), ); } finally { setIsSavingBot(false); } }, [ createdBotUuid, selectedAdapter, botName, botDescription, adapterConfig, t, saveProgress, ]); // ---- Create Pipeline & Link (Step 2 finish) ---- const handleFinish = useCallback(async () => { if (!selectedRunner || !createdBotUuid) return; setIsSubmitting(true); try { // 1. Create pipeline (backend fills config from default template) const pipeline: Pipeline = { name: `${botName} Pipeline`, description: botDescription || '', config: {}, }; const pipelineResp = await httpClient.createPipeline(pipeline); // 2. Fetch the created pipeline to get the full default config // (includes trigger, safety, ai, output sections). // Then merge only the AI section with the wizard's runner config. const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid); const fullConfig = createdPipeline.pipeline.config; const mergedConfig = { ...fullConfig, ai: { ...fullConfig.ai, runner: { runner: selectedRunner }, [selectedRunner]: runnerConfig, }, }; await httpClient.updatePipeline(pipelineResp.uuid, { name: `${botName} Pipeline`, description: botDescription || '', config: mergedConfig, }); // 3. Link pipeline to the bot created in Step 1 const botData = await httpClient.getBot(createdBotUuid); const existingBot = botData.bot; await httpClient.updateBot(createdBotUuid, { name: existingBot.name, description: existingBot.description, adapter: existingBot.adapter, adapter_config: existingBot.adapter_config, enable: existingBot.enable, use_pipeline_uuid: pipelineResp.uuid, }); setCurrentStep(3); } catch (err) { const apiErr = err as { msg?: string }; toast.error( t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''), ); } finally { setIsSubmitting(false); } }, [ selectedRunner, createdBotUuid, botName, botDescription, runnerConfig, t, ]); // ---- Space auth redirect ---- const handleSpaceAuth = useCallback(async () => { try { const callbackUrl = `${window.location.origin}/auth/space/callback`; const resp = await httpClient.getSpaceAuthorizeUrl(callbackUrl); window.location.href = resp.authorize_url; } catch (err) { console.error('Failed to get space authorize URL', err); toast.error(t('wizard.spaceAuthError')); } }, [t]); const handleModelSourceSelect = useCallback( (source: 'space' | 'custom') => { setModelSource(source); if (source === 'space') { handleSpaceAuth(); } }, [handleSpaceAuth], ); // ---- Check if local account ---- // Re-evaluated after remote data fetch (when userInfo is populated) const isLocalAccount = !isLoading && (!userInfo || userInfo.account_type === 'local'); // ---- Skip handler ---- const [showSkipConfirm, setShowSkipConfirm] = useState(false); const [isSkipping, setIsSkipping] = useState(false); const handleSkipConfirm = useCallback(async () => { setIsSkipping(true); try { if (systemInfo.wizard_status === 'none') { await httpClient.updateWizardStatus('skipped'); systemInfo.wizard_status = 'skipped'; } // Always clear persisted progress so re-entering starts fresh await httpClient.saveWizardProgress({ step: 0, selected_adapter: null, created_bot_uuid: null, bot_saved: false, selected_runner: null, }); systemInfo.wizard_progress = null; } catch { toast.error(t('wizard.skipSaveError')); setIsSkipping(false); return; } setIsSkipping(false); setShowSkipConfirm(false); navigate('/home'); }, [navigate, t]); // ---- Render ---- if (isLoading) { return (
); } const stepLabels = [ t('wizard.step.platform'), t('wizard.step.botConfig'), t('wizard.step.aiEngine'), t('wizard.step.done'), ]; return (
{/* Top bar: Skip button */}
{t('sidebar.quickStart')}
{currentStep < 3 && ( )}
{/* Stepper header */}
{stepLabels.map((label, idx) => (
{idx < currentStep ? ( ) : ( idx + 1 )}
{idx < TOTAL_STEPS - 1 && (
)}
))}
{/* Step content */}
{currentStep === 0 && ( )} {currentStep === 1 && ( )} {currentStep === 2 && ( { setProviderCreated(true); setCreatedProviderUuid(uuid ?? null); }} onModelsAdded={() => { setModelsAdded(true); handleSelectRunner('local-agent'); }} onResetModelSource={() => setModelSource(null)} runnerConfigItems={selectedRunnerConfigItems} runnerConfigValues={runnerConfig} onRunnerConfigChange={setRunnerConfig} /> )} {currentStep === 3 && }
{/* Footer navigation */} {currentStep < 3 && (
{currentStep === 0 ? ( ) : currentStep === 1 ? ( ) : ( )}
)} {/* Skip confirmation dialog */} {t('wizard.skip')} {t('wizard.skipConfirmMessage')}
); } // --------------------------------------------------------------------------- // Step 0: Select Platform // --------------------------------------------------------------------------- function StepPlatform({ adapters, selected, onSelect, }: { adapters: Adapter[]; selected: string | null; onSelect: (name: string) => void; }) { const { t } = useTranslation(); const groupedAdapters = useMemo(() => { const withCategories = adapters.map((a) => ({ ...a, categories: a.spec.categories, })); return groupByCategory(withCategories); }, [adapters]); return (

{t('wizard.platform.title')}

{t('wizard.platform.description')}

{groupedAdapters.map((group) => (
{group.categoryId && (

{getCategoryLabel(t, group.categoryId)}

)}
{group.items.map((adapter) => ( onSelect(adapter.name)} >
{extractI18nObject(adapter.label)}
{selected === adapter.name && (
)}

{extractI18nObject(adapter.description)}

{(() => { const docUrl = getAdapterDocUrl( adapter.spec.help_links, i18n.language, ); return docUrl ? ( e.stopPropagation()} > {t('bots.viewAdapterDocs')} ) : null; })()}
))}
))}
); } // --------------------------------------------------------------------------- // Step 1: Bot Configuration + Logs // --------------------------------------------------------------------------- function StepBotConfig({ adapterConfigItems, adapterConfigValues, onAdapterConfigChange, selectedAdapterName, adapters, createdBotUuid, isSavingBot, botSaved, onSaveBot, webhookUrl, extraWebhookUrl, }: { adapterConfigItems: IDynamicFormItemSchema[]; adapterConfigValues: Record; onAdapterConfigChange: (v: Record) => void; selectedAdapterName: string | null; adapters: Adapter[]; createdBotUuid: string | null; isSavingBot: boolean; botSaved: boolean; onSaveBot: () => void; webhookUrl: string; extraWebhookUrl: string; }) { const { t } = useTranslation(); const adapterLabel = useMemo(() => { const a = adapters.find((ad) => ad.name === selectedAdapterName); return a ? extractI18nObject(a.label) : (selectedAdapterName ?? ''); }, [adapters, selectedAdapterName]); // Stable callback ref const onAdapterConfigRef = useRef(onAdapterConfigChange); onAdapterConfigRef.current = onAdapterConfigChange; const stableAdapterConfigCb = useCallback( (val: object) => onAdapterConfigRef.current(val as Record), [], ); return (

{t('wizard.botConfig.title')}

{t('wizard.botConfig.description')}

{/* Left column: Adapter config form */}
{adapterConfigItems.length > 0 && (
{t('wizard.config.platformConfig', { platform: adapterLabel, })} {selectedAdapterName && (() => { const selectedAdapter = adapters.find( (a) => a.name === selectedAdapterName, ); const docUrl = getAdapterDocUrl( selectedAdapter?.spec.help_links, i18n.language, ); return docUrl ? ( {t('bots.viewAdapterDocs')} ) : null; })()}
} onSubmit={stableAdapterConfigCb} systemContext={{ is_wizard: true, webhook_url: webhookUrl, extra_webhook_url: extraWebhookUrl, outbound_ips: systemInfo.outbound_ips, }} />
)} {/* Bot saved indicator */} {botSaved && (
{t('wizard.botConfig.botSaved')}
)}
{/* Right column: Bot logs */} {createdBotUuid && ( {t('wizard.botConfig.logsTitle')} {t('wizard.botConfig.logsDescription')} )}
); } // --------------------------------------------------------------------------- // Model scan sub-component (used within Step 2) // --------------------------------------------------------------------------- function WizardModelScan({ providerUuid, onModelsAdded, }: { providerUuid: string | null; onModelsAdded: () => void; }) { const { t } = useTranslation(); const [scanning, setScanning] = useState(false); const [scannedModels, setScannedModels] = useState< { name: string; context_length?: number | null }[] >([]); const [selectedModels, setSelectedModels] = useState>(new Set()); const [adding, setAdding] = useState(false); const [scanDone, setScanDone] = useState(false); // Auto-scan on mount useEffect(() => { if (!providerUuid) return; let cancelled = false; (async () => { setScanning(true); try { const resp = await httpClient.scanProviderModels( providerUuid, 'llm', ); if (!cancelled) { setScannedModels(resp.models ?? []); setScanDone(true); } } catch { if (!cancelled) setScanDone(true); } finally { if (!cancelled) setScanning(false); } })(); return () => { cancelled = true; }; }, [providerUuid]); const toggleModel = (name: string) => { setSelectedModels((prev) => { const next = new Set(prev); if (next.has(name)) next.delete(name); else next.add(name); return next; }); }; const handleAddSelected = async () => { if (!providerUuid || selectedModels.size === 0) return; setAdding(true); try { for (const name of selectedModels) { const model = scannedModels.find((m) => m.name === name); await httpClient.createProviderLLMModel({ name, provider_uuid: providerUuid, abilities: (model as { abilities?: string[] })?.abilities || ['llm'], reasoning_config: { level: 'provider_default' }, context_length: model?.context_length ?? null, extra_args: {}, } as never); } toast.success( t('wizard.provider.modelsAdded', { count: selectedModels.size }), ); onModelsAdded(); } catch { toast.error(t('wizard.provider.modelsAddError')); } finally { setAdding(false); } }; return (

{t('wizard.provider.scanTitle')}

{t('wizard.provider.scanDescription')}

{scanning && (
{t('wizard.provider.scanning')}
)} {!scanning && scanDone && scannedModels.length === 0 && (

{t('wizard.provider.noModelsFound')}

)} {!scanning && scannedModels.length > 0 && ( <>
{scannedModels.map((model) => ( ))}
)}
); } // --------------------------------------------------------------------------- // Step 2: Select & Configure AI Engine // --------------------------------------------------------------------------- function StepAIEngine({ runnerOptions, aiEngineMode, onAiEngineModeSelect, selectedRunner, onSelectRunner, isLocalAccount, onSpaceAuth, modelSource, onModelSourceSelect, providerCreated, createdProviderUuid, modelsAdded, onProviderCreated, onModelsAdded, onResetModelSource, runnerConfigItems, runnerConfigValues, onRunnerConfigChange, }: { runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[]; aiEngineMode: 'orchestrated' | 'llm' | null; onAiEngineModeSelect: (mode: 'orchestrated' | 'llm') => void; selectedRunner: string | null; onSelectRunner: (name: string) => void; isLocalAccount: boolean; onSpaceAuth: () => void; modelSource: 'space' | 'custom' | null; onModelSourceSelect: (source: 'space' | 'custom') => void; providerCreated: boolean; createdProviderUuid: string | null; modelsAdded: boolean; onProviderCreated: (uuid?: string) => void; onModelsAdded: () => void; onResetModelSource: () => void; runnerConfigItems: IDynamicFormItemSchema[]; runnerConfigValues: Record; onRunnerConfigChange: (v: Record) => void; }) { const { t } = useTranslation(); // Stable callback ref const onRunnerConfigRef = useRef(onRunnerConfigChange); onRunnerConfigRef.current = onRunnerConfigChange; const stableRunnerConfigCb = useCallback( (val: object) => onRunnerConfigRef.current(val as Record), [], ); const runnerLabel = useMemo(() => { const r = runnerOptions.find((o) => o.name === selectedRunner); return r ? extractI18nObject(r.label) : (selectedRunner ?? ''); }, [runnerOptions, selectedRunner]); // Orchestrated runners: everything except local-agent const orchestratedRunners = runnerOptions.filter( (o) => o.name !== 'local-agent', ); // ---- Layer 0: Main choice ---- if (!aiEngineMode) { return (

{t('wizard.aiEngine.title')}

{t('wizard.aiEngine.description')}

{/* Orchestrated Agent Apps */}
onAiEngineModeSelect('orchestrated')} >

{t('wizard.aiEngine.orchestrated.title')}

{t('wizard.aiEngine.orchestrated.description')}

{/* Direct LLM */}
onAiEngineModeSelect('llm')} >

{t('wizard.aiEngine.llm.title')}

{t('wizard.aiEngine.llm.description')}

); } // ---- Layer 1a: Orchestrated runners ---- if (aiEngineMode === 'orchestrated' && !selectedRunner) { return (

{t('wizard.aiEngine.orchestrated.selectTitle')}

{t('wizard.aiEngine.orchestrated.selectDescription')}

{orchestratedRunners.map((opt) => ( onSelectRunner(opt.name)} >
{extractI18nObject(opt.label)} {opt.name}
))}
); } // ---- Layer 1b: LLM model source selection ---- if (aiEngineMode === 'llm' && !modelSource) { return (

{t('wizard.modelSource.title')}

{t('wizard.modelSource.description')}

{/* LangBot Service */}
onModelSourceSelect('space')} >

{t('wizard.modelSource.space.title')}

{t('wizard.modelSource.space.description')}

{/* Custom provider */}
onModelSourceSelect('custom')} >

{t('wizard.modelSource.custom.title')}

{t('wizard.modelSource.custom.description')}

); } // ---- Layer 2: Custom provider creation form ---- if ( aiEngineMode === 'llm' && modelSource === 'custom' && !providerCreated ) { return (

{t('wizard.provider.title')}

{t('wizard.provider.description')}

); } // ---- Layer 2.5: Model scan & add after provider creation ---- if ( aiEngineMode === 'llm' && modelSource === 'custom' && providerCreated && !modelsAdded ) { return ( ); } // ---- Layer 3: Runner configuration form ---- return (

{t('wizard.aiEngine.title')}

{t('wizard.aiEngine.description')}

{/* Left: runner list (only for orchestrated mode) */} {aiEngineMode === 'orchestrated' && (
{orchestratedRunners.map((opt) => { const isSelected = selectedRunner === opt.name; return ( onSelectRunner(opt.name)} >
{extractI18nObject(opt.label)} {opt.name}
{isSelected && (
)}
); })}
)} {/* Right: runner configuration */}
{runnerConfigItems.length > 0 && ( {t('wizard.config.aiConfig', { engine: runnerLabel })} } onSubmit={stableRunnerConfigCb} systemContext={{ is_wizard: true }} /> )}
); } // --------------------------------------------------------------------------- // Step 3: Done // --------------------------------------------------------------------------- function StepDone() { const { t } = useTranslation(); const navigate = useNavigate(); const [particles] = useState(() => Array.from({ length: 30 }, (_, i) => ({ id: i, left: Math.random() * 100, delay: Math.random() * 2, duration: 2 + Math.random() * 2, size: 4 + Math.random() * 6, color: [ 'bg-purple-400', 'bg-pink-400', 'bg-orange-400', 'bg-blue-400', 'bg-green-400', 'bg-yellow-400', ][Math.floor(Math.random() * 6)], })), ); const [isCompleting, setIsCompleting] = useState(false); const handleBack = useCallback(async () => { setIsCompleting(true); try { if (systemInfo.wizard_status === 'none') { await httpClient.updateWizardStatus('completed'); systemInfo.wizard_status = 'completed'; } // Always clear persisted progress so re-entering starts fresh await httpClient.saveWizardProgress({ step: 0, selected_adapter: null, created_bot_uuid: null, bot_saved: false, selected_runner: null, }); systemInfo.wizard_progress = null; } catch { toast.error(t('wizard.completeSaveError')); setIsCompleting(false); return; } setIsCompleting(false); navigate('/home/bots'); }, [navigate, t]); return (
{/* Confetti particles */}
{particles.map((p) => (
))}

{t('wizard.done.title')}

{t('wizard.done.description')}

); }