import { useCallback, type Dispatch, type SetStateAction } from "react"; import type { TFunction } from "i18next"; import type { ApplySettingsPayload, MaybeRestartHostEngine, PendingRestartSections, } from "@/components/settings/contracts"; import { agentDraftFromPayload } from "@/components/settings/models/ModelsSettings"; import { CUSTOM_PROVIDER_CREATION_KEY, providerFormFromRow, type CustomProviderDraft, } from "@/components/settings/models/ProviderSettings"; import type { ModelSettingsState } from "@/components/settings/models/useModelSettingsState"; import { normalizeContextWindowTokens } from "@/components/settings/shared/ModelControls"; import { ApiError, completeProviderOAuth, createModelConfiguration, createProviderSettings, deleteModelConfiguration, loginProviderOAuth, logoutProviderOAuth, migrateModelConfigurations, updateModelCallOrder, updateModelConfiguration, updateProviderSettings, } from "@/lib/api"; import type { NanobotClient } from "@/lib/nanobot-client"; import type { ProviderOAuthAuthorizationRequired, ProviderOAuthCompletionResult, ProviderOAuthLoginResult, ProviderOAuthPending, ProviderSettingsUpdate, SettingsPayload, } from "@/lib/types"; function isProviderOAuthAuthorizationRequired( payload: ProviderOAuthLoginResult, ): payload is ProviderOAuthAuthorizationRequired { return (payload as ProviderOAuthAuthorizationRequired).status === "authorization_required"; } function isProviderOAuthPending( payload: ProviderOAuthCompletionResult, ): payload is ProviderOAuthPending { return (payload as ProviderOAuthPending).status === "pending"; } interface ModelSettingsActionsOptions { state: ModelSettingsState; settings: SettingsPayload | null; client: NanobotClient; t: TFunction; applyPayload: ApplySettingsPayload; maybeRestartHostEngine: MaybeRestartHostEngine; setPendingRestartSections: Dispatch>; setError: Dispatch>; onModelNameChange: (modelName: string | null) => void; remoteBrowserAccess: boolean; closeProviderOAuthFlow: () => void; installCapabilities: (names: string[]) => Promise; modelDirty: boolean; configuredModelProviderOptions: Array<{ name: string; label: string }>; } export function useModelSettingsActions({ state, settings, client, t, applyPayload, maybeRestartHostEngine, setPendingRestartSections, setError, onModelNameChange, remoteBrowserAccess, closeProviderOAuthFlow, installCapabilities, modelDirty, configuredModelProviderOptions, }: ModelSettingsActionsOptions) { const { expandedProvider, form, modelCallOrder, modelCallOrderSaving, modelConfigurationSaving, modelMigrationSaving, modelPresetBeforeCreateRef, modelPresetCreating, modelPresetEditingName, modelPresetPendingDelete, providerForms, providerOAuthCompleting, providerOAuthFlowRef, providerOAuthResponse, providerSaving, saving, setEditingProviderKeys, setExpandedProvider, setForm, setModelCallOrder, setModelCallOrderSaving, setModelConfigurationSaving, setModelMigrationSaving, setModelPresetCreating, setModelPresetEditingName, setModelPresetNameError, setModelPresetPendingDelete, setProviderForms, setProviderOAuthCompleting, setProviderOAuthDialogError, setProviderOAuthFlow, setProviderOAuthResponse, setProviderSaving, setSaving, setVisibleProviderKeys, visibleProviderKeys, } = state; const presetNameConflict = (name: string, currentName?: string) => { const normalized = name.toLowerCase(); return settings?.model_presets.some( (preset) => !preset.is_default && preset.name !== currentName && preset.name.toLowerCase() === normalized, ) ?? false; }; const showPresetNameConflict = () => { setModelPresetNameError( t("settings.models.presetNameDuplicate", { defaultValue: "A preset with this name already exists.", }), ); setError(null); }; const handlePresetSaveError = (reason: unknown) => { if (reason instanceof ApiError && reason.status === 409) { showPresetNameConflict(); return; } setError((reason as Error).message); }; const saveModelSettings = async () => { if ( !settings || saving || modelCallOrderSaving || modelConfigurationSaving ) { return; } if (modelPresetCreating) { const name = form.modelPreset.trim(); const provider = form.provider.trim(); const model = form.model.trim(); if ( !name || !provider || !model || form.maxTokens <= 0 || form.contextWindowTokens <= 0 || form.temperature < 0 || form.temperature > 2 ) { return; } if (presetNameConflict(name)) { showPresetNameConflict(); return; } setModelPresetNameError(null); setModelConfigurationSaving(true); try { const payload = await createModelConfiguration(client, { name, provider, model, maxTokens: form.maxTokens, contextWindowTokens: form.contextWindowTokens, temperature: form.temperature, reasoningEffort: form.reasoningEffort || null, }); const createdPreset = payload.created_model_preset; const nextOrder = createdPreset ? [...modelCallOrder, createdPreset] : null; applyPayload(payload); if (createdPreset) { setForm(agentDraftFromPayload(payload, createdPreset)); setModelPresetEditingName(createdPreset); } let finalPayload = payload; if (nextOrder) { const orderedPayload = await updateModelCallOrder(client, nextOrder); applyPayload(orderedPayload); finalPayload = orderedPayload; } if (createdPreset) { setForm(agentDraftFromPayload(finalPayload, createdPreset)); setModelPresetEditingName(createdPreset); } modelPresetBeforeCreateRef.current = null; onModelNameChange(finalPayload.agent.model || null); setModelPresetNameError(null); setError(null); } catch (err) { handlePresetSaveError(err); } finally { setModelConfigurationSaving(false); } return; } if (!modelDirty) return; const selectedPreset = settings.model_presets.find( (preset) => !preset.is_default && preset.name === modelPresetEditingName, ); if (!selectedPreset) return; const nextName = form.modelPreset.trim(); const nameChanged = form.modelPreset !== selectedPreset.name; if (nameChanged && presetNameConflict(nextName, selectedPreset.name)) { showPresetNameConflict(); return; } setModelPresetNameError(null); const reasoningEffort = form.reasoningEffort || null; setSaving(true); try { const payload = await updateModelConfiguration(client, { name: selectedPreset.name, newName: nameChanged ? nextName : undefined, model: form.model !== selectedPreset.model ? form.model : undefined, provider: form.provider !== selectedPreset.provider ? form.provider : undefined, maxTokens: form.maxTokens !== selectedPreset.max_tokens ? form.maxTokens : undefined, contextWindowTokens: form.contextWindowTokens !== normalizeContextWindowTokens(selectedPreset.context_window_tokens) ? form.contextWindowTokens : undefined, temperature: form.temperature !== selectedPreset.temperature ? form.temperature : undefined, reasoningEffort: reasoningEffort !== selectedPreset.reasoning_effort ? reasoningEffort : undefined, }); applyPayload(payload); setForm(agentDraftFromPayload(payload, nextName)); setModelPresetEditingName(nextName); onModelNameChange(payload.agent.model || null); setModelPresetNameError(null); setError(null); } catch (err) { handlePresetSaveError(err); } finally { setSaving(false); } }; const beginModelPresetCreation = () => { if (!settings || saving || modelCallOrderSaving || modelConfigurationSaving) return; const primaryPreset = settings.model_presets.find( (preset) => !preset.is_default && preset.name === settings.model_call_order?.[0], ); const currentProvider = primaryPreset?.provider === "auto" ? primaryPreset.resolved_provider ?? settings.agent.resolved_provider : primaryPreset?.provider ?? settings.agent.provider; const provider = configuredModelProviderOptions.find((option) => option.name === currentProvider)?.name ?? configuredModelProviderOptions[0]?.name ?? ""; modelPresetBeforeCreateRef.current = modelPresetEditingName; setModelPresetNameError(null); setForm((prev) => ({ ...prev, modelPreset: "", provider, model: "", maxTokens: primaryPreset?.max_tokens ?? settings.agent.max_tokens, contextWindowTokens: normalizeContextWindowTokens( primaryPreset?.context_window_tokens ?? settings.agent.context_window_tokens, ), temperature: primaryPreset?.temperature ?? settings.agent.temperature, reasoningEffort: primaryPreset?.reasoning_effort ?? settings.agent.reasoning_effort ?? "", })); setModelPresetCreating(true); }; const cancelModelPresetCreation = () => { if (!settings || modelConfigurationSaving) return; const previousPreset = modelPresetBeforeCreateRef.current; setModelPresetCreating(false); setModelPresetNameError(null); setForm(agentDraftFromPayload(settings, previousPreset ?? undefined)); setModelPresetEditingName(previousPreset ?? agentDraftFromPayload(settings).modelPreset); modelPresetBeforeCreateRef.current = null; }; const changeModelCallOrder = async (nextOrder: string[]) => { const unchanged = nextOrder.length === modelCallOrder.length && nextOrder.every((name, index) => name === modelCallOrder[index]); if ( !settings || saving || modelCallOrderSaving || modelConfigurationSaving || nextOrder.length === 0 || unchanged ) { return; } const previousOrder = [...modelCallOrder]; setModelCallOrder(nextOrder); setModelCallOrderSaving(true); try { const payload = await updateModelCallOrder(client, nextOrder); applyPayload(payload, { preserveAgentForm: true }); onModelNameChange(payload.agent.model || null); setError(null); } catch (err) { setModelCallOrder(previousOrder); setError((err as Error).message); } finally { setModelCallOrderSaving(false); } }; const handleMigrateModelConfigurations = async () => { if (modelMigrationSaving) return; setModelMigrationSaving(true); try { const payload = await migrateModelConfigurations(client); applyPayload(payload); onModelNameChange(payload.agent.model || null); setError(null); } catch (err) { setError((err as Error).message); } finally { setModelMigrationSaving(false); } }; const handleDeleteModelConfiguration = async () => { if ( !modelPresetPendingDelete || saving || modelCallOrderSaving || modelConfigurationSaving ) { return; } setSaving(true); try { const payload = await deleteModelConfiguration(client, modelPresetPendingDelete.name); applyPayload(payload); setModelPresetPendingDelete(null); setError(null); } catch (err) { setError((err as Error).message); } finally { setSaving(false); } }; const saveProvider = async (providerName: string) => { if (providerSaving) return; const provider = settings?.providers.find((item) => item.name === providerName); if (!provider) return; const isOauthProvider = provider.auth_type === "oauth"; const providerForm = providerForms[providerName] ?? providerFormFromRow(provider); const apiKey = providerForm.apiKey.trim(); const apiKeyRequired = provider.api_key_required ?? true; if (!isOauthProvider && !provider.configured && apiKeyRequired && !apiKey) { setError(t("settings.byok.apiKeyRequired")); return; } setProviderSaving(providerName); try { const supportName = providerName === "bedrock" ? "bedrock" : providerName === "azure_openai" ? "azure" : null; if (supportName && !(await installCapabilities([supportName]))) return; const update: ProviderSettingsUpdate = { provider: providerName }; if (!isOauthProvider) { update.apiKey = apiKey || undefined; update.apiBase = providerForm.apiBase.trim(); if (provider.is_custom) update.displayName = providerForm.displayName.trim(); } for (const field of provider.advanced_fields ?? []) { if (field === "api_type") update.apiType = providerForm.apiType; if (field === "proxy") update.proxy = providerForm.proxy.trim(); if (field === "extra_headers") { update.extraHeaders = providerForm.extraHeaders.trim(); } if (field === "extra_body") update.extraBody = providerForm.extraBody.trim(); if (field === "extra_query") update.extraQuery = providerForm.extraQuery.trim(); if (field === "thinking_style") { update.thinkingStyle = providerForm.thinkingStyle.trim(); } if (field === "region") update.region = providerForm.region.trim(); if (field === "profile") update.profile = providerForm.profile.trim(); } const payload = await updateProviderSettings(client, update); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, image: true })); } await maybeRestartHostEngine(payload); setProviderForms((prev) => ({ ...prev, [providerName]: { ...providerForm, displayName: providerForm.displayName.trim(), apiKey: "", apiBase: providerForm.apiBase.trim(), proxy: providerForm.proxy.trim(), thinkingStyle: providerForm.thinkingStyle.trim(), region: providerForm.region.trim(), profile: providerForm.profile.trim(), }, })); setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false })); setEditingProviderKeys((prev) => ({ ...prev, [providerName]: false })); if (!isOauthProvider) setExpandedProvider(null); setError(null); } catch (err) { setError((err as Error).message); } finally { setProviderSaving(null); } }; const createCustomProvider = async (draft: CustomProviderDraft): Promise => { if (providerSaving) return false; setProviderSaving(CUSTOM_PROVIDER_CREATION_KEY); try { const payload = await createProviderSettings(client, { name: draft.name.trim(), apiKey: draft.apiKey.trim() || undefined, apiBase: draft.apiBase.trim(), proxy: draft.proxy.trim(), extraHeaders: draft.extraHeaders.trim(), extraBody: draft.extraBody.trim(), extraQuery: draft.extraQuery.trim(), thinkingStyle: draft.thinkingStyle.trim(), }); applyPayload(payload); setExpandedProvider(null); setError(null); return true; } catch (err) { setError((err as Error).message); return false; } finally { setProviderSaving(null); } }; const runProviderOAuth = async (providerName: string, action: "login" | "logout") => { if (providerSaving) return; let popup: Window | null = null; if ( action === "login" && providerName === "xai_grok" && !remoteBrowserAccess ) { try { popup = window.open("about:blank", "_blank"); if (popup) popup.opener = null; } catch { popup = null; } } setProviderSaving(providerName); try { const payload = action === "login" ? await loginProviderOAuth( client, providerName, providerName === "openai_codex" && remoteBrowserAccess, ) : await logoutProviderOAuth(client, providerName); if (isProviderOAuthAuthorizationRequired(payload)) { try { if (popup && !popup.closed) popup.location.href = payload.authorization_url; } catch { // The dialog keeps the authorization link available when the popup was closed. } providerOAuthFlowRef.current = payload; setProviderOAuthFlow(payload); setProviderOAuthResponse(""); setProviderOAuthDialogError(null); setExpandedProvider(providerName); setError(null); return; } popup?.close(); closeProviderOAuthFlow(); applyPayload(payload); setExpandedProvider(providerName); setError(null); } catch (err) { popup?.close(); setError((err as Error).message); } finally { setProviderSaving(null); } }; const completeProviderOAuthResponse = async () => { const flow = providerOAuthFlowRef.current; const authorizationResponse = providerOAuthResponse.trim(); if (!flow || !authorizationResponse || providerOAuthCompleting) return; setProviderOAuthCompleting(true); setProviderOAuthDialogError(null); try { const payload = await completeProviderOAuth( client, flow.provider, flow.flow_id, authorizationResponse, ); if (providerOAuthFlowRef.current?.flow_id !== flow.flow_id) return; if (isProviderOAuthPending(payload)) return; applyPayload(payload); setExpandedProvider(flow.provider); setError(null); closeProviderOAuthFlow(); } catch (err) { if (providerOAuthFlowRef.current?.flow_id === flow.flow_id) { setProviderOAuthDialogError((err as Error).message); } } finally { setProviderOAuthCompleting(false); } }; const resetProviderDraft = useCallback((providerName: string) => { const provider = settings?.providers.find((item) => item.name === providerName); if (!provider) return; setProviderForms((prev) => ({ ...prev, [providerName]: providerFormFromRow(provider), })); setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false })); setEditingProviderKeys((prev) => ({ ...prev, [providerName]: false })); }, [settings]); const handleToggleProvider = useCallback((providerName: string) => { if (expandedProvider) resetProviderDraft(expandedProvider); setExpandedProvider(expandedProvider === providerName ? null : providerName); }, [expandedProvider, resetProviderDraft]); const toggleProviderKeyVisibility = (providerName: string) => { const isVisible = visibleProviderKeys[providerName]; setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: !isVisible })); }; const toggleProviderKeyEditing = (providerName: string) => { setEditingProviderKeys((prev) => { const nextEditing = !prev[providerName]; if (!nextEditing) { setProviderForms((forms) => ({ ...forms, [providerName]: { ...(forms[providerName] ?? providerFormFromRow( settings?.providers.find((provider) => provider.name === providerName) ?? { name: providerName, label: providerName, configured: false, }, )), apiKey: "", }, })); setVisibleProviderKeys((visible) => ({ ...visible, [providerName]: false })); } return { ...prev, [providerName]: nextEditing }; }); }; return { beginModelPresetCreation, cancelModelPresetCreation, changeModelCallOrder, completeProviderOAuthResponse, createCustomProvider, handleDeleteModelConfiguration, handleMigrateModelConfigurations, handleToggleProvider, resetProviderDraft, runProviderOAuth, saveModelSettings, saveProvider, toggleProviderKeyEditing, toggleProviderKeyVisibility, }; }