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] <windmill-internal-app[bot]@users.noreply.github.com>

* 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] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
centdix
2025-10-17 11:48:27 +00:00
committed by GitHub
co-authored by windmill-internal-app[bot] claude[bot] Ruben Fiszel
parent deb4fc6ede
commit 36cd49fb41
@@ -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<string[]>([])
let filterText = $state('')
let useAsDefault = $state(false)
let modelsCache = new Map<AIProvider, string[]>()
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)
}
})
</script>
<div
@@ -222,6 +296,23 @@
inputClass="min-h-10 !bg-surface disabled:!bg-surface-disabled"
/>
</div>
<!-- Use as Default Checkbox -->
<div class="flex justify-end pt-1">
<Toggle
disabled={disabled || !value?.kind || !value?.resource || !value?.model}
bind:checked={useAsDefault}
options={{ right: 'Use as personal default for other new agents' }}
size="xs"
on:change={(e) => {
if (!e.detail) {
removeConfig()
} else {
saveConfig(value)
}
}}
/>
</div>
</div>
{@render actions?.()}