diff --git a/ui/src/components/NexusOnboardingWizard.tsx b/ui/src/components/NexusOnboardingWizard.tsx index ce6cabdd..509ac164 100644 --- a/ui/src/components/NexusOnboardingWizard.tsx +++ b/ui/src/components/NexusOnboardingWizard.tsx @@ -1,4 +1,4 @@ -// [nexus] Replacement onboarding wizard — 3-step flow: hardware detection, mode selection, root directory +// [nexus] Replacement onboarding wizard — 4-step flow: hardware detection, mode selection, provider selection, root directory // Exports `OnboardingWizard` to match the named import in App.tsx. // Wired via Vite alias: all imports of ./components/OnboardingWizard are // redirected here at build time; the original file is preserved for upstream rebase. @@ -11,6 +11,7 @@ import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { companiesApi } from "../api/companies"; import { agentsApi } from "../api/agents"; +import { puterProxyApi } from "../api/puter-proxy"; import { queryKeys } from "../lib/queryKeys"; import { resolveRouteOnboardingOptions } from "../lib/onboarding-route"; import { Dialog, DialogPortal } from "@/components/ui/dialog"; @@ -19,10 +20,11 @@ import { Input } from "@/components/ui/input"; import { cn } from "../lib/utils"; import { ModeSelector } from "./onboarding/ModeSelector"; import { HardwareSummaryStep } from "./onboarding/HardwareSummaryStep"; +import { ProviderSelectionStep } from "./onboarding/ProviderSelectionStep"; import { useHardwareInfo } from "../hooks/useHardwareInfo"; import { updateNexusSettings, type NexusMode } from "../api/hardware"; -// [nexus] 3-step onboarding wizard: hardware detection → mode selection → root directory +// [nexus] 4-step onboarding wizard: hardware detection → mode selection → provider selection → root directory export function OnboardingWizard() { const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog(); const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany(); @@ -49,12 +51,17 @@ export function OnboardingWizard() { setRouteDismissed(false); }, [location.pathname]); - // Step state: 1 = hardware detection, 2 = mode selection, 3 = root directory + // Step state: 1 = hardware detection, 2 = mode selection, 3 = provider selection, 4 = root directory const [step, setStep] = useState(1); // Mode state: "both" pre-selected per UI-SPEC const [selectedMode, setSelectedMode] = useState("both"); + // [nexus] Provider credentials — captured in React state for post-company-creation submission + const [puterToken, setPuterToken] = useState(null); + const [googleOAuthStateId, setGoogleOAuthStateId] = useState(null); + const [apiKeyData, setApiKeyData] = useState<{ provider: string; apiKey: string } | null>(null); + // Form state const [rootDir, setRootDir] = useState(""); const [loading, setLoading] = useState(false); @@ -63,9 +70,10 @@ export function OnboardingWizard() { // [nexus] Hardware detection — only fetch when wizard is open const { data: hardwareInfo, isLoading: hwLoading, isError: hwError } = useHardwareInfo(effectiveOnboardingOpen); - // [nexus] Adapter detection state — probe for Hermes when wizard opens + // [nexus] Adapter detection state — probe for adapters when wizard opens const [defaultAdapter, setDefaultAdapter] = useState<"claude_local" | "hermes_local">("claude_local"); const [probing, setProbing] = useState(false); + const [detectedAdapters, setDetectedAdapters] = useState>({}); // Reset form when wizard closes useEffect(() => { @@ -75,19 +83,32 @@ export function OnboardingWizard() { setLoading(false); setStep(1); setSelectedMode("both"); + setPuterToken(null); + setGoogleOAuthStateId(null); + setApiKeyData(null); } }, [effectiveOnboardingOpen]); - // [nexus] Probe for Hermes availability when wizard opens + // [nexus] Probe for adapter availability when wizard opens (parallel, fire-and-forget) useEffect(() => { if (!effectiveOnboardingOpen) return; setProbing(true); - agentsApi.probeAdapter("hermes_local") - .then((data) => { - if (data.available) setDefaultAdapter("hermes_local"); - }) - .catch(() => {}) // graceful — keep claude_local - .finally(() => setProbing(false)); + + const probes = Promise.all([ + agentsApi.probeAdapter("hermes_local").catch(() => ({ available: false, status: "error" })), + agentsApi.probeAdapter("claude_local").catch(() => ({ available: false, status: "error" })), + agentsApi.probeAdapter("openclaw_gateway").catch(() => ({ available: false, status: "error" })), + ]); + + probes.then(([hermes, claude, openclaw]) => { + const detected: Record = { + hermes_local: hermes.available, + claude_local: claude.available, + openclaw_gateway: openclaw.available, + }; + setDetectedAdapters(detected); + if (hermes.available) setDefaultAdapter("hermes_local"); + }).catch(() => {}).finally(() => setProbing(false)); }, [effectiveOnboardingOpen]); function handleClose() { @@ -153,8 +174,6 @@ export function OnboardingWizard() { }; // Step 2: Create PM agent with role "ceo" for elevated permissions - // (display label is "Project Manager" via AGENT_ROLE_LABELS; ceo/ bundle - // has been rewritten with PM content in 04-01) await agentsApi.create(company.id, { name: "Project Manager", role: "ceo", @@ -183,6 +202,18 @@ export function OnboardingWizard() { // Non-blocking — mode defaults to "both" if save fails } + // [nexus] Store collected provider credentials after company creation — all non-blocking + if (puterToken) { + await puterProxyApi.storeToken(company.id, puterToken).catch(() => {}); + } + if (googleOAuthStateId) { + // Claim Google OAuth tokens for this company + await puterProxyApi.claimGoogleTokens(googleOAuthStateId, company.id).catch(() => {}); + } + if (apiKeyData) { + await puterProxyApi.storeApiKey(company.id, apiKeyData.provider, apiKeyData.apiKey).catch(() => {}); + } + // Navigate to dashboard — not an issue detail page closeOnboarding(); navigate(`/${company.issuePrefix}/dashboard`); @@ -211,7 +242,7 @@ export function OnboardingWizard() { )} > {/* Step indicator */} -

Step {step} of 3

+

Step {step} of 4

{/* Step 1 — Hardware Detection */} {step === 1 && ( @@ -269,8 +300,40 @@ export function OnboardingWizard() { )} - {/* Step 3 — Root Directory */} + {/* Step 3 — Provider Selection (NEW) */} {step === 3 && ( + <> +
+

+ Choose a provider +

+

+ No API keys needed for the zero-config path. +

+
+ + setApiKeyData({ provider, apiKey })} + onSkip={() => setStep(4)} + onContinue={() => setStep(4)} + detectedAdapters={detectedAdapters} + /> + + + + )} + + {/* Step 4 — Root Directory (was step 3) */} + {step === 4 && ( <> {/* Header */}
@@ -349,7 +412,7 @@ export function OnboardingWizard() { + + {/* Puter auth component — shown when Puter is selected */} + {selectedProvider === "puter" && ( + { + onPuterToken(token); + setProviderReady(true); + }} + onError={setError} + /> + )} + + {/* Google card */} + + + {/* Google OAuth component — shown when Google is selected */} + {selectedProvider === "google" && ( + { + onGoogleOAuthState(stateId); + setProviderReady(true); + }} + onError={setError} + /> + )} + + {/* API key card */} + + + {/* API key form — shown when API key is selected */} + {selectedProvider === "apikey" && ( + { + onApiKey(prov, key); + setProviderReady(true); + }} + onError={setError} + /> + )} +
+ + {/* Error display */} + {error && ( +

+ {error} +

+ )} + + {/* Continue button — shown when provider auth is complete */} + {providerReady && ( + + )} + + {/* Skip button — always visible */} + + + ); +}