mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-24 20:28:16 +02:00
feat(onboarding): allow adding multiple provider keys in BYOK
This commit is contained in:
parent
36da053b8d
commit
2fb0e7402a
2 changed files with 53 additions and 24 deletions
|
|
@ -36,7 +36,8 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) {
|
||||||
llmProvider, setLlmProvider, modelsLoading, modelsError,
|
llmProvider, setLlmProvider, modelsLoading, modelsError,
|
||||||
activeConfig, testState, setTestState, showApiKey,
|
activeConfig, testState, setTestState, showApiKey,
|
||||||
showBaseURL, canTest, showMoreProviders, setShowMoreProviders,
|
showBaseURL, canTest, showMoreProviders, setShowMoreProviders,
|
||||||
updateProviderConfig, handleTestAndSaveLlmConfig, handleBack,
|
updateProviderConfig, handleTestAndSaveLlmConfig, handleTestAndAddAnother,
|
||||||
|
connectedFlavors, handleNext, handleBack,
|
||||||
upsellDismissed, setUpsellDismissed, handleSwitchToRowboat,
|
upsellDismissed, setUpsellDismissed, handleSwitchToRowboat,
|
||||||
} = state
|
} = state
|
||||||
|
|
||||||
|
|
@ -77,6 +78,9 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) {
|
||||||
<div className="text-sm font-semibold">{provider.name}</div>
|
<div className="text-sm font-semibold">{provider.name}</div>
|
||||||
<div className="text-xs text-muted-foreground">{provider.description}</div>
|
<div className="text-xs text-muted-foreground">{provider.description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{connectedFlavors.has(provider.id) && (
|
||||||
|
<CheckCircle2 className="size-4 text-green-600 dark:text-green-400 ml-auto shrink-0" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)
|
)
|
||||||
|
|
@ -232,14 +236,23 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleTestAndSaveLlmConfig}
|
variant="outline"
|
||||||
|
onClick={handleTestAndAddAnother}
|
||||||
disabled={!canTest || testState.status === "testing"}
|
disabled={!canTest || testState.status === "testing"}
|
||||||
|
>
|
||||||
|
Save & add another
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={canTest ? handleTestAndSaveLlmConfig : handleNext}
|
||||||
|
disabled={testState.status === "testing" || (!canTest && connectedFlavors.size === 0)}
|
||||||
className="min-w-[140px]"
|
className="min-w-[140px]"
|
||||||
>
|
>
|
||||||
{testState.status === "testing" ? (
|
{testState.status === "testing" ? (
|
||||||
<><Loader2 className="size-4 animate-spin mr-2" />Testing...</>
|
<><Loader2 className="size-4 animate-spin mr-2" />Testing...</>
|
||||||
) : (
|
) : (canTest || connectedFlavors.size === 0) ? (
|
||||||
"Test & Continue"
|
"Test & Continue"
|
||||||
|
) : (
|
||||||
|
"Continue"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
||||||
status: "idle",
|
status: "idle",
|
||||||
})
|
})
|
||||||
|
const [connectedFlavors, setConnectedFlavors] = useState<Set<LlmProviderFlavor>>(new Set())
|
||||||
const [showMoreProviders, setShowMoreProviders] = useState(false)
|
const [showMoreProviders, setShowMoreProviders] = useState(false)
|
||||||
|
|
||||||
// OAuth provider states
|
// OAuth provider states
|
||||||
|
|
@ -409,17 +410,15 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
onComplete()
|
onComplete()
|
||||||
}, [onComplete])
|
}, [onComplete])
|
||||||
|
|
||||||
const handleTestAndSaveLlmConfig = useCallback(async () => {
|
// Test the active provider's credentials and persist its config. Returns
|
||||||
if (!canTest) return
|
// whether it succeeded so callers can decide whether to advance or stay.
|
||||||
|
const testAndSaveActiveProvider = useCallback(async (): Promise<boolean> => {
|
||||||
|
if (!canTest) return false
|
||||||
setTestState({ status: "testing" })
|
setTestState({ status: "testing" })
|
||||||
try {
|
try {
|
||||||
const apiKey = activeConfig.apiKey.trim() || undefined
|
const apiKey = activeConfig.apiKey.trim() || undefined
|
||||||
const baseURL = activeConfig.baseURL.trim() || undefined
|
const baseURL = activeConfig.baseURL.trim() || undefined
|
||||||
const provider = {
|
const provider = { flavor: llmProvider, apiKey, baseURL }
|
||||||
flavor: llmProvider,
|
|
||||||
apiKey,
|
|
||||||
baseURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the provider's models from the key — this both validates the
|
// Fetch the provider's models from the key — this both validates the
|
||||||
// credentials and gives us the list to populate the chat picker.
|
// 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) {
|
if (!result.success) {
|
||||||
setTestState({ status: "error", error: result.error })
|
setTestState({ status: "error", error: result.error })
|
||||||
toast.error(result.error || "Connection test failed")
|
toast.error(result.error || "Connection test failed")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const models: string[] = result.models ?? []
|
const models: string[] = result.models ?? []
|
||||||
const preferred = preferredDefaults[llmProvider]
|
const preferred = preferredDefaults[llmProvider]
|
||||||
const model =
|
const model =
|
||||||
(preferred && models.includes(preferred) && preferred) ||
|
(preferred && models.includes(preferred) && preferred) ||
|
||||||
models[0] ||
|
models[0] || activeConfig.model.trim() || ""
|
||||||
activeConfig.model.trim() ||
|
|
||||||
""
|
|
||||||
|
|
||||||
const providerConfig = {
|
await window.ipc.invoke("models:saveConfig", { provider, model, models })
|
||||||
provider,
|
|
||||||
model,
|
|
||||||
models,
|
|
||||||
}
|
|
||||||
|
|
||||||
setTestState({ status: "success" })
|
|
||||||
await window.ipc.invoke("models:saveConfig", providerConfig)
|
|
||||||
window.dispatchEvent(new Event('models-config-changed'))
|
window.dispatchEvent(new Event('models-config-changed'))
|
||||||
handleNext()
|
setTestState({ status: "success" })
|
||||||
|
setConnectedFlavors(prev => new Set(prev).add(llmProvider))
|
||||||
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Connection test failed:", error)
|
console.error("Connection test failed:", error)
|
||||||
setTestState({ status: "error", error: "Connection test failed" })
|
setTestState({ status: "error", error: "Connection test failed" })
|
||||||
toast.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
|
// Check connection status for all providers
|
||||||
const refreshAllStatuses = useCallback(async () => {
|
const refreshAllStatuses = useCallback(async () => {
|
||||||
|
|
@ -639,10 +653,12 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
showBaseURL,
|
showBaseURL,
|
||||||
isLocalProvider,
|
isLocalProvider,
|
||||||
canTest,
|
canTest,
|
||||||
|
connectedFlavors,
|
||||||
showMoreProviders,
|
showMoreProviders,
|
||||||
setShowMoreProviders,
|
setShowMoreProviders,
|
||||||
updateProviderConfig,
|
updateProviderConfig,
|
||||||
handleTestAndSaveLlmConfig,
|
handleTestAndSaveLlmConfig,
|
||||||
|
handleTestAndAddAnother,
|
||||||
|
|
||||||
// OAuth state
|
// OAuth state
|
||||||
providers,
|
providers,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue