diff --git a/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx b/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx index 3c27ff31..4ece02c9 100644 --- a/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx +++ b/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx @@ -36,7 +36,8 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) { llmProvider, setLlmProvider, modelsLoading, modelsError, activeConfig, testState, setTestState, showApiKey, showBaseURL, canTest, showMoreProviders, setShowMoreProviders, - updateProviderConfig, handleTestAndSaveLlmConfig, handleBack, + updateProviderConfig, handleTestAndSaveLlmConfig, handleTestAndAddAnother, + connectedFlavors, handleNext, handleBack, upsellDismissed, setUpsellDismissed, handleSwitchToRowboat, } = state @@ -77,6 +78,9 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) {
{provider.name}
{provider.description}
+ {connectedFlavors.has(provider.id) && ( + + )} ) @@ -232,14 +236,23 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) { )} + diff --git a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts index 681f3957..12b880e4 100644 --- a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts +++ b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts @@ -41,6 +41,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({ status: "idle", }) + const [connectedFlavors, setConnectedFlavors] = useState>(new Set()) const [showMoreProviders, setShowMoreProviders] = useState(false) // OAuth provider states @@ -409,17 +410,15 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { onComplete() }, [onComplete]) - const handleTestAndSaveLlmConfig = useCallback(async () => { - if (!canTest) return + // Test the active provider's credentials and persist its config. Returns + // whether it succeeded so callers can decide whether to advance or stay. + const testAndSaveActiveProvider = useCallback(async (): Promise => { + if (!canTest) return false setTestState({ status: "testing" }) try { const apiKey = activeConfig.apiKey.trim() || undefined const baseURL = activeConfig.baseURL.trim() || undefined - const provider = { - flavor: llmProvider, - apiKey, - baseURL, - } + const provider = { flavor: llmProvider, apiKey, baseURL } // Fetch the provider's models from the key — this both validates the // credentials and gives us the list to populate the chat picker. @@ -427,33 +426,48 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { if (!result.success) { setTestState({ status: "error", error: result.error }) toast.error(result.error || "Connection test failed") - return + return false } const models: string[] = result.models ?? [] const preferred = preferredDefaults[llmProvider] const model = (preferred && models.includes(preferred) && preferred) || - models[0] || - activeConfig.model.trim() || - "" + models[0] || activeConfig.model.trim() || "" - const providerConfig = { - provider, - model, - models, - } - - setTestState({ status: "success" }) - await window.ipc.invoke("models:saveConfig", providerConfig) + await window.ipc.invoke("models:saveConfig", { provider, model, models }) window.dispatchEvent(new Event('models-config-changed')) - handleNext() + setTestState({ status: "success" }) + setConnectedFlavors(prev => new Set(prev).add(llmProvider)) + return true } catch (error) { console.error("Connection test failed:", error) setTestState({ status: "error", error: "Connection test failed" }) toast.error("Connection test failed") + return false } - }, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, canTest, llmProvider, handleNext]) + }, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, canTest, llmProvider]) + + // Save the active provider and advance to the next step. + const handleTestAndSaveLlmConfig = useCallback(async () => { + const ok = await testAndSaveActiveProvider() + if (ok) handleNext() + }, [testAndSaveActiveProvider, handleNext]) + + // Save the active provider but stay on the step. Switch to the next provider the + // user hasn't connected yet so the form is fresh and the buttons re-enable once + // they enter that key. (Clearing the current field instead left the buttons + // disabled on an empty form with no clear next step.) + const handleTestAndAddAnother = useCallback(async () => { + const ok = await testAndSaveActiveProvider() + if (!ok) return + // setConnectedFlavors is async, so include the just-saved provider here. + const connectedNow = new Set(connectedFlavors).add(llmProvider) + const order: LlmProviderFlavor[] = ["openai", "anthropic", "google", "openrouter", "aigateway", "ollama", "openai-compatible"] + const next = order.find(p => !connectedNow.has(p)) + if (next) setLlmProvider(next) + setTestState({ status: "idle" }) + }, [testAndSaveActiveProvider, connectedFlavors, llmProvider]) // Check connection status for all providers const refreshAllStatuses = useCallback(async () => { @@ -639,10 +653,12 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { showBaseURL, isLocalProvider, canTest, + connectedFlavors, showMoreProviders, setShowMoreProviders, updateProviderConfig, handleTestAndSaveLlmConfig, + handleTestAndAddAnother, // OAuth state providers,