mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 16:02:24 +00:00
db5e03610d
* 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>
750 lines
21 KiB
Svelte
750 lines
21 KiB
Svelte
<script lang="ts">
|
|
import { run } from 'svelte/legacy'
|
|
|
|
import { goto } from '$lib/navigation'
|
|
import { base } from '$lib/base'
|
|
import {
|
|
JobService,
|
|
ResourceService,
|
|
SettingService,
|
|
UserService,
|
|
VariableService,
|
|
WorkspaceService,
|
|
type AIProvider,
|
|
type CompletedJob,
|
|
type GetCopilotInfoResponse
|
|
} from '$lib/gen'
|
|
import { validateUsername } from '$lib/utils'
|
|
import { logoutWithRedirect } from '$lib/logoutKit'
|
|
import { page } from '$app/state'
|
|
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
|
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
|
import { Button } from '$lib/components/common'
|
|
import Toggle from '$lib/components/Toggle.svelte'
|
|
import Tooltip from '$lib/components/Tooltip.svelte'
|
|
import { onMount } from 'svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import TestAIKey from '$lib/components/copilot/TestAIKey.svelte'
|
|
import { switchWorkspace } from '$lib/storeUtils'
|
|
import { isCloudHosted } from '$lib/cloud'
|
|
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
|
import { AI_PROVIDERS } from '$lib/components/copilot/lib'
|
|
import { GitFork, LoaderCircle } from 'lucide-svelte'
|
|
import PrefixedInput from '../PrefixedInput.svelte'
|
|
import TextInput from '../text_input/TextInput.svelte'
|
|
import { jobManager } from '$lib/services/JobManager'
|
|
import Alert from '../common/alert/Alert.svelte'
|
|
|
|
interface Props {
|
|
isFork?: boolean
|
|
}
|
|
|
|
let { isFork = false }: Props = $props()
|
|
|
|
const rd = page.url.searchParams.get('rd')
|
|
|
|
let id = $state('')
|
|
let name = $state('')
|
|
let username = $state('')
|
|
|
|
let errorId = $state('')
|
|
let errorUser = $state('')
|
|
let aiKey = $state('')
|
|
let codeCompletionEnabled = $state(true)
|
|
let checking = $state(false)
|
|
let createLoading = $state(false)
|
|
let aiSetupLoading = $state(false)
|
|
let creationStep = $state<'details' | 'ai'>('details')
|
|
let createdWorkspaceId: string | undefined = $state(undefined)
|
|
|
|
let workspaceColor: string | undefined = $state(undefined)
|
|
let colorEnabled = $state(false)
|
|
|
|
function generateRandomColor() {
|
|
const randomColor =
|
|
'#' +
|
|
Math.floor(Math.random() * 16777215)
|
|
.toString(16)
|
|
.padStart(6, '0')
|
|
workspaceColor = randomColor
|
|
}
|
|
|
|
async function validateName(id: string): Promise<void> {
|
|
checking = true
|
|
let exists = await WorkspaceService.existsWorkspace({ requestBody: { id } })
|
|
if (exists) {
|
|
errorId = 'ID already exists'
|
|
} else if (id != '' && !/^\w+(-\w+)*$/.test(id)) {
|
|
errorId = 'ID can only contain letters, numbers and dashes and must not finish by a dash'
|
|
} else {
|
|
errorId = ''
|
|
}
|
|
checking = false
|
|
}
|
|
|
|
const WM_FORK_PREFIX = 'wm-fork-'
|
|
|
|
let forkCreationLoading = $state(false)
|
|
let forkCreationError = $state('')
|
|
let errorMsgs: string[] = $state([])
|
|
let failedSyncJobs: string[] = $state([])
|
|
|
|
function getErrorMessage(error: any): string {
|
|
return (
|
|
error?.body?.error?.message ||
|
|
error?.body?.message ||
|
|
(typeof error?.body === 'string' ? error.body : null) ||
|
|
error?.message ||
|
|
'Unknown error'
|
|
)
|
|
}
|
|
|
|
function hasEffectiveAi(copilotInfo: GetCopilotInfoResponse): boolean {
|
|
return Object.keys(copilotInfo.providers ?? {}).length > 0
|
|
}
|
|
|
|
async function finishWorkspaceSetup(workspaceId: string): Promise<void> {
|
|
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
|
switchWorkspace(workspaceId)
|
|
goto(rd ?? '/')
|
|
}
|
|
|
|
async function getWorkspaceUsername(workspaceId: string): Promise<string> {
|
|
if (!automateUsernameCreation) {
|
|
return username
|
|
}
|
|
|
|
const user = await UserService.whoami({
|
|
workspace: workspaceId
|
|
})
|
|
return user.username
|
|
}
|
|
|
|
async function maybeShowAiSetupStep(workspaceId: string): Promise<void> {
|
|
try {
|
|
const copilotInfo = await WorkspaceService.getCopilotInfo({
|
|
workspace: workspaceId
|
|
})
|
|
|
|
if (hasEffectiveAi(copilotInfo)) {
|
|
await finishWorkspaceSetup(workspaceId)
|
|
return
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to check effective AI configuration for new workspace', error)
|
|
sendUserToast(
|
|
'Workspace created, but Windmill AI availability could not be verified. You can configure it later in Workspace settings.',
|
|
true
|
|
)
|
|
await finishWorkspaceSetup(workspaceId)
|
|
return
|
|
}
|
|
|
|
createdWorkspaceId = workspaceId
|
|
creationStep = 'ai'
|
|
aiKey = ''
|
|
codeCompletionEnabled = true
|
|
selected = 'openai'
|
|
}
|
|
|
|
async function fetchFailedSyncJobs(jobs: string[]): Promise<CompletedJob[]> {
|
|
let ret: CompletedJob[] = []
|
|
for (const job of jobs) {
|
|
let j = await JobService.getCompletedJob({
|
|
id: job,
|
|
workspace: $workspaceStore!
|
|
})
|
|
ret.push(j)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
function isPathVersionLessThan(path: string | undefined, version: number): boolean {
|
|
if (!path || !path.startsWith('hub/')) {
|
|
return false
|
|
}
|
|
|
|
const parts = path.split('/')
|
|
|
|
if (parts.length < 2) {
|
|
return false
|
|
}
|
|
|
|
const embeddedVersion = parseInt(parts[1], 10)
|
|
|
|
if (isNaN(embeddedVersion)) {
|
|
return false
|
|
}
|
|
|
|
return embeddedVersion < version
|
|
}
|
|
|
|
async function createOrForkWorkspace() {
|
|
const prefixed_id = `${WM_FORK_PREFIX}${id}`
|
|
if (isFork) {
|
|
if ($workspaceStore) {
|
|
forkCreationLoading = true
|
|
errorMsgs = []
|
|
failedSyncJobs = []
|
|
forkCreationError = ''
|
|
|
|
let gitSyncJobIds = await WorkspaceService.createWorkspaceForkGitBranch({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
id: prefixed_id,
|
|
name,
|
|
color: colorEnabled && workspaceColor ? workspaceColor : undefined
|
|
}
|
|
})
|
|
|
|
try {
|
|
await Promise.all(
|
|
gitSyncJobIds.map((jobId) =>
|
|
jobManager.runWithProgress(() => Promise.resolve(jobId), {
|
|
workspace: $workspaceStore,
|
|
timeout: 60000,
|
|
timeoutMessage: `Deploy fork job timed out after 60s`,
|
|
onProgress: (status) => {
|
|
if (status.status === 'failure') {
|
|
errorMsgs.push(status.error ?? 'Deploy fork job failed')
|
|
failedSyncJobs.push(jobId)
|
|
}
|
|
}
|
|
})
|
|
)
|
|
)
|
|
} catch (error) {
|
|
forkCreationLoading = false
|
|
sendUserToast(
|
|
`Could not fork workspace ${$workspaceStore} because branch creation failed: ${errorMsgs} - ${error}`,
|
|
true
|
|
)
|
|
return
|
|
}
|
|
if (errorMsgs.length != 0) {
|
|
forkCreationError = 'Failed to create a branch for this fork on the git sync repo(s)'
|
|
forkCreationLoading = false
|
|
sendUserToast(
|
|
`Could not fork workspace ${$workspaceStore} because branch creation failed: ${errorMsgs}`,
|
|
true
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
await WorkspaceService.createWorkspaceFork({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
id: prefixed_id,
|
|
name,
|
|
color: colorEnabled && workspaceColor ? workspaceColor : undefined
|
|
}
|
|
})
|
|
} catch (e) {
|
|
forkCreationError = `Failed to create fork '${prefixed_id}'`
|
|
errorMsgs.push(e?.body ?? e ?? 'Unknown error')
|
|
forkCreationLoading = false
|
|
sendUserToast(`Could not create fork '${prefixed_id}' ${e}`, true)
|
|
return
|
|
}
|
|
|
|
forkCreationLoading = false
|
|
sendUserToast(`Successfully forked workspace ${$workspaceStore} as: wm-fork-${id}`)
|
|
await finishWorkspaceSetup(prefixed_id)
|
|
} else {
|
|
sendUserToast('No workspace selected, cannot fork non-existent workspace', true)
|
|
}
|
|
} else {
|
|
createLoading = true
|
|
try {
|
|
const workspaceId = await createWorkspace()
|
|
await maybeShowAiSetupStep(workspaceId)
|
|
} finally {
|
|
createLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createWorkspace(): Promise<string> {
|
|
await WorkspaceService.createWorkspace({
|
|
requestBody: {
|
|
id,
|
|
name,
|
|
color: colorEnabled && workspaceColor ? workspaceColor : undefined,
|
|
username: automateUsernameCreation ? undefined : username
|
|
}
|
|
})
|
|
if (auto_invite) {
|
|
await WorkspaceService.editAutoInvite({
|
|
workspace: id,
|
|
requestBody: { operator: operatorOnly, invite_all: !isCloudHosted(), auto_add: autoAdd }
|
|
})
|
|
}
|
|
|
|
sendUserToast(`Created workspace id: ${id}`)
|
|
return id
|
|
}
|
|
|
|
async function saveWorkspaceAiSetup(): Promise<void> {
|
|
if (!createdWorkspaceId || !aiKey) {
|
|
return
|
|
}
|
|
|
|
aiSetupLoading = true
|
|
try {
|
|
const actualUsername = await getWorkspaceUsername(createdWorkspaceId)
|
|
const path = `u/${actualUsername}/${selected}_windmill_codegen`
|
|
|
|
await VariableService.createVariable({
|
|
workspace: createdWorkspaceId,
|
|
requestBody: {
|
|
path,
|
|
value: aiKey,
|
|
is_secret: true,
|
|
description: 'Ai token'
|
|
}
|
|
})
|
|
await ResourceService.createResource({
|
|
workspace: createdWorkspaceId,
|
|
requestBody: {
|
|
path,
|
|
value: {
|
|
api_key: '$var:' + path
|
|
},
|
|
resource_type: selected
|
|
}
|
|
})
|
|
await WorkspaceService.editCopilotConfig({
|
|
workspace: createdWorkspaceId,
|
|
requestBody: {
|
|
providers: {
|
|
[selected]: {
|
|
resource_path: path,
|
|
models: [AI_PROVIDERS[selected].defaultModels[0]]
|
|
}
|
|
},
|
|
default_model: {
|
|
model: AI_PROVIDERS[selected].defaultModels[0],
|
|
provider: selected
|
|
},
|
|
code_completion_model: codeCompletionEnabled
|
|
? { model: AI_PROVIDERS[selected].defaultModels[0], provider: selected }
|
|
: undefined
|
|
}
|
|
})
|
|
|
|
sendUserToast('Windmill AI configured')
|
|
await finishWorkspaceSetup(createdWorkspaceId)
|
|
} catch (error) {
|
|
sendUserToast(`Failed to configure Windmill AI: ${getErrorMessage(error)}`, true)
|
|
} finally {
|
|
aiSetupLoading = false
|
|
}
|
|
}
|
|
|
|
function handleCreateKeyUp(event: KeyboardEvent) {
|
|
const key = event.key
|
|
if (key === 'Enter') {
|
|
event.preventDefault()
|
|
createOrForkWorkspace()
|
|
}
|
|
}
|
|
|
|
function handleAiKeyUp(event: KeyboardEvent) {
|
|
const key = event.key
|
|
if (key === 'Enter' && aiKey) {
|
|
event.preventDefault()
|
|
saveWorkspaceAiSetup()
|
|
}
|
|
}
|
|
|
|
async function loadWorkspaces() {
|
|
if (!$usersWorkspaceStore) {
|
|
try {
|
|
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
|
} catch {}
|
|
}
|
|
if (!$usersWorkspaceStore) {
|
|
const url = page.url
|
|
console.log('logout 2')
|
|
await logoutWithRedirect(url.href.replace(url.origin, ''))
|
|
}
|
|
}
|
|
|
|
let automateUsernameCreation = $state(true)
|
|
async function getAutomateUsernameCreationSetting() {
|
|
automateUsernameCreation =
|
|
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? true
|
|
|
|
if (!automateUsernameCreation) {
|
|
UserService.globalWhoami().then((x) => {
|
|
let uname = ''
|
|
if (x.name) {
|
|
uname = x.name.split(' ')[0]
|
|
} else {
|
|
uname = x.email.split('@')[0]
|
|
}
|
|
uname = uname.replace(/\./gi, '')
|
|
username = uname.toLowerCase()
|
|
})
|
|
}
|
|
}
|
|
getAutomateUsernameCreationSetting()
|
|
|
|
onMount(() => {
|
|
loadWorkspaces()
|
|
|
|
WorkspaceService.isDomainAllowed().then((x) => {
|
|
isDomainAllowed = x
|
|
})
|
|
})
|
|
|
|
let isDomainAllowed: undefined | boolean = $state(undefined)
|
|
|
|
let auto_invite = $state(false)
|
|
let operatorOnly = $state(false)
|
|
let autoAdd = $state(true)
|
|
let selected: Exclude<AIProvider, 'customai'> = $state('openai')
|
|
let modalTitle = $derived(
|
|
isFork ? 'Fork Workspace' : creationStep === 'ai' ? 'Set up Windmill AI' : 'New Workspace'
|
|
)
|
|
run(() => {
|
|
id = name.toLowerCase().replace(/\s/gi, '-')
|
|
})
|
|
run(() => {
|
|
validateName(id)
|
|
})
|
|
run(() => {
|
|
errorUser = validateUsername(username)
|
|
})
|
|
run(() => {
|
|
colorEnabled && !workspaceColor && generateRandomColor()
|
|
})
|
|
let domain = $derived($usersWorkspaceStore?.email.split('@')[1])
|
|
</script>
|
|
|
|
<CenteredModal title={modalTitle} centerVertically={false}>
|
|
<div class="flex flex-col gap-8">
|
|
{#if isFork}
|
|
<div class="flex flex-block gap-2">
|
|
<GitFork size={16} />
|
|
<span class="text-xs text-normal">Forking </span>
|
|
<span class="text-xs text-emphasis font-semibold">
|
|
{$workspaceStore}
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
{#if errorMsgs.length != 0}
|
|
<Alert class="p-2" title={forkCreationError} type="error">
|
|
<ul class="pl-2 pr-4 break-words pb-5">
|
|
{#each errorMsgs as errorMsg}
|
|
<li><pre class="whitespace-pre-wrap">- {errorMsg}</pre></li>
|
|
{/each}
|
|
</ul>
|
|
{#if failedSyncJobs.length != 0}
|
|
More details on the jobs that failed:
|
|
{#await fetchFailedSyncJobs(failedSyncJobs)}
|
|
<LoaderCircle class="animate-spin" />
|
|
{:then failedJobs}
|
|
<ul class="pl-2 pr-4 break-words">
|
|
{#each failedJobs as job}
|
|
<li>
|
|
-
|
|
<a
|
|
target="_blank"
|
|
class="underline"
|
|
href={`/run/${job.id}?workspace=${$workspaceStore}`}
|
|
>
|
|
{job.id}
|
|
</a>
|
|
</li>
|
|
<!-- This 28073 is the version where git sync on fork was introduced -->
|
|
{#if isPathVersionLessThan(job.script_path, 28073)}
|
|
<div class="font-bold">
|
|
This job was not running the latest version of the git sync script available on
|
|
the hub. You might be able to solve this issue by going to `Workspace Settings`
|
|
-> `Git Sync` and updating the script.
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</ul>
|
|
{:catch error}
|
|
Tried to fetch jobs to get more information, but failed: {error}. Here are the failed
|
|
job ids:
|
|
<ul class="pl-2 pr-4 break-words">
|
|
{#each failedSyncJobs as jobId}
|
|
<li>
|
|
-
|
|
<a
|
|
target="_blank"
|
|
class="underline"
|
|
href={`/run/${jobId}?workspace=${$workspaceStore}`}
|
|
>
|
|
{jobId}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/await}
|
|
{/if}
|
|
</Alert>
|
|
{/if}
|
|
|
|
{#if isFork || creationStep === 'details'}
|
|
<label class="flex flex-col gap-1">
|
|
{#if isFork}
|
|
<span class="text-xs font-semibold text-emphasis">Fork name</span>
|
|
<span class="text-xs text-secondary">Displayable name of the forked workspace</span>
|
|
{:else}
|
|
<span class="text-xs font-semibold text-emphasis">Workspace name</span>
|
|
<span class="text-xs text-secondary">Displayable name</span>
|
|
{/if}
|
|
<!-- svelte-ignore a11y_autofocus -->
|
|
<TextInput inputProps={{ autofocus: true }} bind:value={name} />
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Workspace ID</span>
|
|
{#if isFork}
|
|
<span class="text-xs text-secondary"
|
|
>Slug to uniquely identify your fork (this will also set the branch name)</span
|
|
>
|
|
{:else}
|
|
<span class="text-xs text-secondary">Slug to uniquely identify your workspace</span>
|
|
{/if}
|
|
|
|
{#if isFork}
|
|
<PrefixedInput
|
|
prefix={WM_FORK_PREFIX}
|
|
type="text"
|
|
bind:value={id}
|
|
placeholder="example.com"
|
|
class={errorId != '' ? 'input-error' : ''}
|
|
/>
|
|
{:else}
|
|
<TextInput bind:value={id} error={errorId} />
|
|
{/if}
|
|
{#if errorId}
|
|
<span class="text-red-500 text-2xs font-normal">{errorId}</span>
|
|
{/if}
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Workspace color</span>
|
|
<span class="text-xs text-secondary"
|
|
>Color to identify the current workspace in the list of workspaces</span
|
|
>
|
|
<div class="flex items-center gap-4">
|
|
<Toggle bind:checked={colorEnabled} options={{ right: 'Enable' }} />
|
|
{#if colorEnabled}
|
|
<div class="flex items-center gap-1 grow">
|
|
<input
|
|
class="grow min-w-10"
|
|
type="color"
|
|
bind:value={workspaceColor}
|
|
disabled={!colorEnabled}
|
|
/>
|
|
|
|
<TextInput
|
|
class="w-24"
|
|
bind:value={workspaceColor}
|
|
inputProps={{ disabled: !colorEnabled }}
|
|
/>
|
|
<Button
|
|
on:click={generateRandomColor}
|
|
size="xs"
|
|
variant="default"
|
|
disabled={!colorEnabled}>Random</Button
|
|
>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</label>
|
|
{#if !automateUsernameCreation}
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Your username in that workspace</span>
|
|
<TextInput
|
|
bind:value={username}
|
|
inputProps={{ onkeyup: handleCreateKeyUp }}
|
|
error={errorUser}
|
|
/>
|
|
{#if errorUser}
|
|
<span class="text-red-500 text-2xs">{errorUser}</span>
|
|
{/if}
|
|
</label>
|
|
{/if}
|
|
{#if !isFork}
|
|
<div class="flex flex-col gap-1">
|
|
<label for="auto-invite" class="text-xs font-semibold text-emphasis"
|
|
>{isCloudHosted()
|
|
? `Auto-${autoAdd ? 'add' : 'invite'} anyone from ${domain}`
|
|
: `Auto-${autoAdd ? 'add' : 'invite'} anyone joining the instance`}</label
|
|
>
|
|
<Toggle
|
|
id="auto-invite"
|
|
disabled={isCloudHosted() && !isDomainAllowed}
|
|
bind:checked={auto_invite}
|
|
/>
|
|
{#if isCloudHosted() && isDomainAllowed == false}
|
|
<div class="text-secondary text-2xs">{domain} domain not allowed for auto-invite</div>
|
|
{/if}
|
|
|
|
{#if auto_invite}
|
|
<div class="bg-surface-tertiary p-4 rounded-md flex flex-col gap-8">
|
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
{#if isCloudHosted()}
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Mode</span>
|
|
<span class="text-xs text-secondary font-normal"
|
|
>Whether to invite or add users directly to the workspace.</span
|
|
>
|
|
<ToggleButtonGroup
|
|
selected={autoAdd ? 'add' : 'invite'}
|
|
on:selected={async (e) => {
|
|
autoAdd = e.detail === 'add'
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="invite" label="Auto-invite" {item} />
|
|
<ToggleButton value="add" label="Auto-add" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</label>
|
|
{/if}
|
|
|
|
<label class="font-semibold flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Role</span>
|
|
<span class="text-xs text-secondary font-normal">Role of the auto-invited users</span>
|
|
<ToggleButtonGroup
|
|
selected={operatorOnly ? 'operator' : 'developer'}
|
|
on:selected={(e) => {
|
|
operatorOnly = e.detail == 'operator'
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="operator" label="Operator" {item} />
|
|
<ToggleButton value="developer" label="Developer" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</label>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex flex-wrap flex-row justify-between gap-4 pt-4">
|
|
<Button
|
|
disabled={forkCreationLoading || createLoading}
|
|
variant="default"
|
|
size="sm"
|
|
href="{base}/user/workspaces">← Back to workspaces</Button
|
|
>
|
|
{#if !forkCreationLoading}
|
|
<Button
|
|
variant="accent"
|
|
loading={createLoading}
|
|
disabled={createLoading ||
|
|
checking ||
|
|
errorId != '' ||
|
|
!name ||
|
|
(!automateUsernameCreation && (errorUser != '' || !username)) ||
|
|
!id}
|
|
on:click={createOrForkWorkspace}
|
|
>
|
|
{#if isFork}
|
|
Fork workspace
|
|
{:else}
|
|
Create workspace
|
|
{/if}
|
|
</Button>
|
|
{:else}
|
|
<Button variant="accent" disabled={true}>
|
|
<LoaderCircle class="animate-spin" /> Creating branch
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-1">
|
|
<label for="ai-key" class="flex flex-row gap-2">
|
|
<span class="text-xs font-semibold text-emphasis">
|
|
Set up Windmill AI
|
|
<Tooltip>
|
|
Find out how it can help you <a
|
|
href="https://www.windmill.dev/docs/core_concepts/ai_generation"
|
|
target="_blank"
|
|
rel="noopener noreferrer">in the docs</a
|
|
>
|
|
</Tooltip>
|
|
</span>
|
|
</label>
|
|
<span class="text-xs text-secondary">
|
|
Windmill AI powers the chat, code generation, flow creation, and code completion. Set
|
|
it up now or configure it later in Workspace settings.
|
|
<a
|
|
href="https://www.windmill.dev/docs/core_concepts/ai_generation"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="underline"
|
|
>
|
|
Learn more
|
|
</a>
|
|
</span>
|
|
|
|
<ToggleButtonGroup bind:selected class="mt-4">
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="openai" label="OpenAI" {item} />
|
|
<ToggleButton value="anthropic" label="Anthropic" {item} />
|
|
<ToggleButton value="mistral" label="Mistral" {item} />
|
|
<ToggleButton value="deepseek" label="DeepSeek" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
<div class="flex flex-row gap-1">
|
|
<input
|
|
id="ai-key"
|
|
type="password"
|
|
autocomplete="new-password"
|
|
bind:value={aiKey}
|
|
onkeyup={handleAiKeyUp}
|
|
/>
|
|
<TestAIKey
|
|
apiKey={aiKey}
|
|
disabled={!aiKey}
|
|
aiProvider={selected}
|
|
model={AI_PROVIDERS[selected].defaultModels[0]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{#if aiKey}
|
|
<div class="flex flex-col gap-2">
|
|
<Toggle
|
|
disabled={!aiKey}
|
|
bind:checked={codeCompletionEnabled}
|
|
options={{ right: 'Enable code completion' }}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex flex-wrap flex-row justify-between gap-4 pt-4">
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
disabled={aiSetupLoading || !createdWorkspaceId}
|
|
on:click={() => createdWorkspaceId && finishWorkspaceSetup(createdWorkspaceId)}
|
|
>
|
|
Skip for now
|
|
</Button>
|
|
<Button
|
|
variant="accent"
|
|
loading={aiSetupLoading}
|
|
disabled={aiSetupLoading || !aiKey}
|
|
on:click={saveWorkspaceAiSetup}
|
|
>
|
|
Set up AI
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</CenteredModal>
|