Files
windmill/frontend/src/lib/components/SuperadminSettings.svelte
T
centdixandClaude Opus 4.6 db5e03610d feat: add instance-level AI settings (#8453)
* feat: add instance-level AI settings with workspace fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add AI step to onboarding setup wizard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: thread workspace prop through resource editor and disable chat offset

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: thread workspace prop through resource editor and disable chat offset"

This reverts commit 9fea9cc0c239f6432d1fef1487c45e74ab752e21.

* fix: set workspace store and disable chat offset during AI setup step

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: thread workspace and disableChatOffset props through resource editors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: populate workspace and user stores for AI step path component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: initialize AI clients for test key during onboarding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract AI config state into InstanceAISettings component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move AI config state ownership into AISettings component

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Persist instance AI settings before navigation

* Reload effective workspace AI state after save

* Scope AI key tests to the rendered workspace

* Add post-create AI onboarding for new workspaces

* Unify instance AI settings header

* Fix instance AI drawer offset on workspace selection

* Add instance AI fallback settings behavior

* Update sqlx metadata

* Update sqlx metadata

* Clarify active instance AI in workspace settings

* Refresh workspace AI state after instance AI save

* Declare instance AI summary in API schema

* Normalize empty instance AI config handling

* Clean up workspace AI settings UI

* Unify AI config provider checks

* Split AI settings metadata from effective config

* Propagate instance AI cache invalidation across servers

* Fix AI settings dirty state tracking

* Update sqlx metadata

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 19:18:36 +00:00

216 lines
5.8 KiB
Svelte

<script lang="ts">
import { Drawer, DrawerContent, Button } from '$lib/components/common'
import SuperadminSettingsInner from './SuperadminSettingsInner.svelte'
import Version from './Version.svelte'
import MeltTooltip from './meltComponents/Tooltip.svelte'
import Toggle from './Toggle.svelte'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import SaveButton from './SaveButton.svelte'
import { X, FileDiff, Loader2 } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { SettingsService } from '$lib/gen'
import { isCloudHosted } from '$lib/cloud'
interface Props {
disableChatOffset?: boolean
}
let { disableChatOffset = false }: Props = $props()
let drawer: Drawer | undefined = $state()
let diffDrawer: Drawer | undefined = $state()
let innerComponent: SuperadminSettingsInner | undefined = $state()
let uptodateVersion: string | undefined = $state(undefined)
let yamlMode = $state(false)
let hasUnsavedChanges = $state(false)
let hasAnyInvalid = $state(false)
let showCloseConfirmModal = $state(false)
let diffData: { original: string; modified: string } = $state({ original: '', modified: '' })
let inlineDiff = $state(false)
async function loadUptodate() {
try {
const res = await SettingsService.backendUptodate()
if (res != 'yes') {
const parts = res.split(' -> ')
uptodateVersion = parts.length > 1 ? parts[parts.length - 1] : res
}
} catch {}
}
loadUptodate()
// When true, the next Drawer 'close' event is intentional and should not be intercepted
let bypassCloseCheck = false
export function openDrawer() {
drawer?.openDrawer()
}
export function closeDrawer() {
bypassCloseCheck = true
drawer?.closeDrawer()
}
function handleClose() {
if (hasUnsavedChanges) {
showCloseConfirmModal = true
} else {
closeDrawer()
}
}
/** Catches click-away and escape closes from the Drawer/Disposable layer */
function handleDrawerClose() {
if (!bypassCloseCheck && hasUnsavedChanges) {
drawer?.openDrawer()
showCloseConfirmModal = true
}
bypassCloseCheck = false
}
async function handleSave(): Promise<void> {
if (!innerComponent?.syncBeforeDiff()) throw new Error('YAML sync failed')
await innerComponent?.saveSettings()
}
async function handleSaveAndCloseDiff(): Promise<void> {
await handleSave()
diffDrawer?.closeDrawer()
}
function handleDiscard() {
innerComponent?.discardAll()
}
function handleReviewChanges() {
if (!innerComponent?.syncBeforeDiff()) return
diffData = innerComponent?.buildFullDiff() ?? { original: '', modified: '' }
diffDrawer?.openDrawer()
}
</script>
<Drawer bind:this={drawer} size="1200px" {disableChatOffset} on:close={handleDrawerClose}>
<DrawerContent noPadding overflow_y={false} title="Instance settings" on:close={handleClose}>
{#snippet titleExtra()}
<MeltTooltip disablePopup={!uptodateVersion}>
<div class="text-xs text-secondary flex items-center gap-1 ml-6">
<Version />
{#if uptodateVersion}
<span class="text-accent">{uptodateVersion}</span>
{/if}
</div>
{#snippet text()}
{#if isCloudHosted()}
The cloud version is updated daily.
{:else}
How to update?<br />
- docker: <code>docker compose up -d</code><br />
- <a href="https://github.com/windmill-labs/windmill-helm-charts#install">helm</a>
{/if}
{/snippet}
</MeltTooltip>
{/snippet}
{#snippet actions()}
<div class="flex items-center gap-2">
{#if hasUnsavedChanges}
<div transition:fade={{ duration: 150 }}>
<Button
variant="default"
size="xs"
startIcon={{ icon: X }}
onClick={handleDiscard}
>
Discard
</Button>
</div>
{/if}
<Button
variant="accent"
size="xs"
startIcon={{ icon: FileDiff }}
onClick={handleReviewChanges}
disabled={!hasUnsavedChanges}
>
Review changes
</Button>
<Toggle
bind:checked={yamlMode}
options={{ right: 'YAML' }}
size="sm"
/>
</div>
{/snippet}
<SuperadminSettingsInner
bind:this={innerComponent}
closeDrawer={handleClose}
showHeaderInfo={false}
{disableChatOffset}
bind:yamlMode
bind:hasUnsavedChanges
bind:hasAnyInvalid
/>
</DrawerContent>
</Drawer>
<Drawer bind:this={diffDrawer} size="1200px">
<DrawerContent title="Review changes" on:close={() => diffDrawer?.closeDrawer()}>
{#snippet actions()}
<Toggle
bind:checked={inlineDiff}
options={{ right: 'Unified' }}
size="xs"
/>
<SaveButton onSave={handleSaveAndCloseDiff} disabled={!hasUnsavedChanges || hasAnyInvalid} size="xs" />
{/snippet}
<!-- DiffEditor reacts to inlineDiff changes via $effect — no {#key} needed -->
<div class="h-full">
{#await import('$lib/components/DiffEditor.svelte')}
<Loader2 class="animate-spin m-4" />
{:then Module}
<Module.default
open={true}
className="!h-full"
defaultLang="yaml"
defaultOriginal={diffData.original}
defaultModified={diffData.modified}
readOnly
{inlineDiff}
/>
{/await}
</div>
</DrawerContent>
</Drawer>
{#if showCloseConfirmModal}
<ConfirmationModal
open={showCloseConfirmModal}
title="Unsaved changes"
confirmationText="Discard & close"
on:canceled={() => {
showCloseConfirmModal = false
}}
on:confirmed={() => {
innerComponent?.discardAll()
showCloseConfirmModal = false
closeDrawer()
}}
>
<div class="flex flex-col w-full space-y-4">
<span>You have unsaved changes. Are you sure you want to discard them and close?</span>
<Button
variant="default"
size="sm"
startIcon={{ icon: FileDiff }}
onClick={() => {
showCloseConfirmModal = false
handleReviewChanges()
}}
>
Review changes
</Button>
</div>
</ConfirmationModal>
{/if}