From 36cd49fb4135690d6b78396f3f87ed4a7b01ddad Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:48:27 +0200 Subject: [PATCH] feat(aiagent): Store AI provider config in localStorage (#6854) * feat(aiagent): Store AI provider config in localStorage - Added localStorage persistence for AI provider, resource, and model selections - Configuration is loaded as default values on component initialization - Automatically saves whenever selections change - Validates stored provider is still available before loading - Uses storage key: windmill_ai_provider_config Co-authored-by: windmill-internal-app[bot] * better * fix logic * Update toggle option text for default setting --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: windmill-internal-app[bot] Co-authored-by: Ruben Fiszel --- .../lib/components/AIProviderPicker.svelte | 155 ++++++++++++++---- 1 file changed, 123 insertions(+), 32 deletions(-) diff --git a/frontend/src/lib/components/AIProviderPicker.svelte b/frontend/src/lib/components/AIProviderPicker.svelte index c9336f8627..26b78f21f3 100644 --- a/frontend/src/lib/components/AIProviderPicker.svelte +++ b/frontend/src/lib/components/AIProviderPicker.svelte @@ -9,6 +9,7 @@ import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' import ResourcePicker from './ResourcePicker.svelte' import ToggleButtonMore from './common/toggleButton-v2/ToggleButtonMore.svelte' + import Toggle from './Toggle.svelte' interface ProviderValue { kind?: AIProvider @@ -24,22 +25,34 @@ let { value = $bindable(), disabled = false, actions }: Props = $props() - // Initialize value if undefined - if (!value) { - const providers = Object.keys(AI_PROVIDERS) - value = { - kind: providers.length > 0 ? (providers[0] as AIProvider) : undefined, - resource: undefined, - model: undefined - } - } - let loading = $state(false) let availableModels = $state([]) let filterText = $state('') + let useAsDefault = $state(false) let modelsCache = new Map() + const STORAGE_KEY = 'windmill_ai_provider_config' + + // Initialize value if undefined + if (!value) { + const storedConfig = loadStoredConfig() + if (storedConfig) { + value = storedConfig + useAsDefault = true + } else { + const providers = Object.keys(AI_PROVIDERS) + value = { + kind: providers.length > 0 ? (providers[0] as AIProvider) : undefined, + resource: undefined, + model: undefined + } + useAsDefault = false + } + } else { + useAsDefault = isSameAsStoredConfig(value) + } + // Reactive items for the Select component let items = $derived.by(() => { const r = availableModels.map((model) => ({ @@ -61,6 +74,61 @@ label: details.label })) + // Check if the current config is the same as the stored config + function isSameAsStoredConfig(config: ProviderValue): boolean { + const storedConfig = loadStoredConfig() + return ( + storedConfig !== undefined && + storedConfig?.kind === config.kind && + storedConfig?.resource === config.resource && + storedConfig?.model === config.model + ) + } + + // Load stored configuration from localStorage + function loadStoredConfig(): ProviderValue | undefined { + if (typeof localStorage === 'undefined') { + return undefined + } + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + const parsed = JSON.parse(stored) + // Validate that the stored provider is still available + if (parsed.kind && AI_PROVIDERS[parsed.kind]) { + return parsed + } + } + } catch (e) { + console.error('Failed to load AI provider config from localStorage:', e) + } + return undefined + } + + // Save configuration to localStorage + function saveConfig(config: ProviderValue) { + if (typeof localStorage === 'undefined') { + return + } + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)) + } catch (e) { + console.error('Failed to save AI provider config to localStorage:', e) + } + } + + // Remove configuration from localStorage + function removeConfig() { + if (typeof localStorage === 'undefined') { + return + } + try { + localStorage.removeItem(STORAGE_KEY) + } catch (e) { + console.error('Failed to remove AI provider config from localStorage:', e) + } + } + async function loadModels(signal?: AbortSignal) { const provider = value?.kind const resourceValue = value?.resource @@ -99,28 +167,6 @@ } } - // Reload models when provider or resourcePath changes - $effect(() => { - const abortController = new AbortController() - const provider = value?.kind - const resourceValue = value?.resource - const resourcePath = resourceValueToPath(resourceValue) - - filterText = '' - - if (provider && resourcePath) { - loadModels(abortController.signal) - } else { - const defaultModels = provider ? AI_PROVIDERS[provider]?.defaultModels || [] : [] - availableModels = defaultModels - loading = false - } - - return () => { - abortController.abort() - } - }) - // Handle provider selection function onProviderChange(selectedProvider: AIProvider) { if (value) { @@ -153,6 +199,34 @@ return `$res:${path}` } } + + // Reload models when provider or resourcePath changes + $effect(() => { + const abortController = new AbortController() + const provider = value?.kind + const resourceValue = value?.resource + const resourcePath = resourceValueToPath(resourceValue) + + filterText = '' + + if (provider && resourcePath) { + loadModels(abortController.signal) + } else { + const defaultModels = provider ? AI_PROVIDERS[provider]?.defaultModels || [] : [] + availableModels = defaultModels + loading = false + } + + return () => { + abortController.abort() + } + }) + + $effect(() => { + if (useAsDefault && value && value.kind && value.resource && value.model) { + saveConfig(value) + } + })
+ + +
+ { + if (!e.detail) { + removeConfig() + } else { + saveConfig(value) + } + }} + /> +
{@render actions?.()}