mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
651 lines
22 KiB
Svelte
651 lines
22 KiB
Svelte
<script lang="ts">
|
|
import { untrack } from 'svelte'
|
|
import { Sparkles, Plus, List, Ban, ExternalLinkIcon, Loader2 } from 'lucide-svelte'
|
|
import type { Policy } from '$lib/gen'
|
|
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
|
import { base } from '$lib/base'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
|
import Modal from '$lib/components/common/modal/Modal.svelte'
|
|
import Button from '$lib/components/common/button/Button.svelte'
|
|
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
|
import Select from '$lib/components/select/Select.svelte'
|
|
import Toggle from '$lib/components/Toggle.svelte'
|
|
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
|
import { Alert } from '$lib/components/common'
|
|
import { AIBtnClasses } from '$lib/components/copilot/chat/AIButtonStyle'
|
|
import { prefersSessionHandoff } from '$lib/components/copilot/chat/global/gate'
|
|
import { copilotInfo, copilotWorkspace } from '$lib/aiStore'
|
|
import { loadCopilot } from '$lib/components/copilot/loadCopilot'
|
|
import { react18Template, react19Template, svelte5Template } from './templates'
|
|
import type { Runnable } from './rawAppPolicy'
|
|
import {
|
|
type DataTableRef,
|
|
type RawAppData,
|
|
formatDataTableRef,
|
|
withAppDatatableRole
|
|
} from './dataTableRefUtils'
|
|
import {
|
|
createDatatableAccessResource,
|
|
createDatatablesResource,
|
|
createRolesResource,
|
|
rolesWorthPicking,
|
|
toDatatableItems,
|
|
toSchemaItems
|
|
} from './datatableUtils.svelte'
|
|
import RawAppDataTableList from './RawAppDataTableList.svelte'
|
|
import RawAppDataTableDrawer from './RawAppDataTableDrawer.svelte'
|
|
import FileEditorIcon from './FileEditorIcon.svelte'
|
|
|
|
export type RawAppTemplatePickerResult = {
|
|
files: Record<string, string>
|
|
runnables: Record<string, Runnable>
|
|
data: RawAppData
|
|
summary: string
|
|
policy: Policy
|
|
prompt?: string
|
|
}
|
|
|
|
let {
|
|
open = $bindable(),
|
|
onStart
|
|
}: {
|
|
open: boolean
|
|
onStart: (result: RawAppTemplatePickerResult, withPrompt: boolean) => void
|
|
} = $props()
|
|
|
|
const templates = [
|
|
{ name: 'React 19', icon: 'tsx', files: react19Template },
|
|
{ name: 'React 18', icon: 'tsx', files: react18Template },
|
|
{ name: 'Svelte 5', icon: 'svelte', files: svelte5Template }
|
|
]
|
|
|
|
let selectedTemplateIndex = $state(0)
|
|
let tableCreationEnabled = $state(true)
|
|
let selectedDatatable = $state<string | undefined>(undefined)
|
|
let schemaMode = $state<'none' | 'new' | 'existing'>('new')
|
|
let selectedSchema = $state<string | undefined>(undefined)
|
|
let newSchemaName = $state('')
|
|
let appSummary = $state('')
|
|
let initialPrompt = $state('')
|
|
let preWhitelistedTables = $state<DataTableRef[]>([])
|
|
/** The role each pre-whitelisted table's data table was browsed as. */
|
|
let preWhitelistedRoles = $state<Record<string, string>>({})
|
|
let dataTableDrawer: RawAppDataTableDrawer | undefined = $state()
|
|
|
|
const getOpWs = getRawAppOperatingWorkspace()
|
|
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
|
|
|
const datatables = createDatatablesResource(() => opWs)
|
|
const roles = createRolesResource(
|
|
() => selectedDatatable,
|
|
() => opWs
|
|
)
|
|
let selectedRole = $state<string | undefined>(undefined)
|
|
|
|
// Every reader waits for an answer stamped with the current selection: until then `current`
|
|
// belongs to the previous data table or role.
|
|
const loadedRoles = $derived(
|
|
roles.current.datatable === selectedDatatable ? roles.current.roles : []
|
|
)
|
|
const showRolePicker = $derived(rolesWorthPicking(loadedRoles))
|
|
// Saved explicitly rather than left to resolve: "whatever the default is then" moves the app
|
|
// the day an admin changes the default.
|
|
const effectiveRole = $derived(
|
|
selectedRole !== undefined && loadedRoles.includes(selectedRole) ? selectedRole : undefined
|
|
)
|
|
// An app uses one role per data table: the one picked above is what the table drawer browses
|
|
// as and what the list shows, or tables would be added under a role the app is not saved with.
|
|
const pickerRoles = $derived(
|
|
selectedDatatable !== undefined && effectiveRole !== undefined
|
|
? withAppDatatableRole(preWhitelistedRoles, selectedDatatable, effectiveRole)
|
|
: preWhitelistedRoles
|
|
)
|
|
|
|
const availableDatatables = $derived(datatables.current)
|
|
// `undefined` while the list loads, so this is false until it has answered.
|
|
const hasNoDatatables = $derived(availableDatatables?.length === 0)
|
|
|
|
const rolesSettled = $derived(
|
|
hasNoDatatables ||
|
|
(selectedDatatable !== undefined && roles.current.datatable === selectedDatatable)
|
|
)
|
|
|
|
// A role is picked on one data table: two data tables can both define an `analyst` that
|
|
// means something different, so a name surviving the switch is not the role surviving it.
|
|
let rolesPickedOn = $state<string | undefined>(undefined)
|
|
$effect(() => {
|
|
const loaded = roles.current
|
|
if (loaded.datatable !== selectedDatatable) return
|
|
const switched = untrack(() => rolesPickedOn) !== selectedDatatable
|
|
const current = untrack(() => selectedRole)
|
|
if (switched || current === undefined || !loaded.roles.includes(current)) {
|
|
// Tables already picked on this data table were browsed as a role: keep that one.
|
|
const browsed = selectedDatatable
|
|
? untrack(() => preWhitelistedRoles)[selectedDatatable]
|
|
: undefined
|
|
pickRole(
|
|
browsed !== undefined && loaded.roles.includes(browsed)
|
|
? browsed
|
|
: loaded.roles.includes(loaded.defaultRole)
|
|
? loaded.defaultRole
|
|
: loaded.roles[0]
|
|
)
|
|
rolesPickedOn = selectedDatatable
|
|
}
|
|
})
|
|
|
|
/** Picks the app's role on the selected data table. Tables picked on it under another role are
|
|
* dropped: that role may reach them where this one does not. */
|
|
function pickRole(role: string | undefined) {
|
|
selectedRole = role
|
|
const dt = selectedDatatable
|
|
const browsed = dt ? preWhitelistedRoles[dt] : undefined
|
|
if (dt === undefined || role === undefined || browsed === undefined || browsed === role) return
|
|
preWhitelistedTables = preWhitelistedTables.filter((t) => t.datatable !== dt)
|
|
const { [dt]: _, ...rest } = preWhitelistedRoles
|
|
preWhitelistedRoles = rest
|
|
}
|
|
|
|
const access = createDatatableAccessResource(
|
|
() => selectedDatatable,
|
|
() => effectiveRole,
|
|
() => opWs,
|
|
() => rolesSettled && (loadedRoles.length === 0 || effectiveRole !== undefined)
|
|
)
|
|
// Under roles, and this caller may use none of them: the app would be saved with queries the
|
|
// server refuses.
|
|
const noUsableRole = $derived(
|
|
rolesSettled &&
|
|
selectedDatatable !== undefined &&
|
|
roles.current.permissioned &&
|
|
loadedRoles.length === 0
|
|
)
|
|
const rolesUnknown = $derived(rolesSettled && roles.current.failed)
|
|
const accessSettled = $derived(
|
|
hasNoDatatables ||
|
|
(rolesSettled &&
|
|
selectedDatatable !== undefined &&
|
|
access.current.datatable === selectedDatatable &&
|
|
access.current.role === effectiveRole)
|
|
)
|
|
const accessUnknown = $derived(accessSettled && access.current.failed)
|
|
const availableSchemas = $derived(accessSettled ? access.current.schemas : [])
|
|
const canCreateSchema = $derived(accessSettled && access.current.canCreateSchema)
|
|
|
|
// Only an app that keeps the data table is held back by it: with table creation off nothing
|
|
// saves it.
|
|
const blockedByRole = $derived(
|
|
(noUsableRole || rolesUnknown || accessUnknown) && tableCreationEnabled
|
|
)
|
|
|
|
// A role that cannot create schemas has nothing to name, so the mode goes back to the one
|
|
// every role has, once that is an answer.
|
|
$effect(() => {
|
|
if (accessSettled && !accessUnknown && schemaMode === 'new' && !canCreateSchema) {
|
|
schemaMode = 'none'
|
|
}
|
|
})
|
|
// Likewise an existing schema the current role no longer reaches is unpicked, so the select
|
|
// does not keep showing it.
|
|
$effect(() => {
|
|
if (
|
|
accessSettled &&
|
|
selectedSchema !== undefined &&
|
|
!availableSchemas.includes(selectedSchema)
|
|
) {
|
|
selectedSchema = undefined
|
|
}
|
|
})
|
|
|
|
let hasAutoSelected = false
|
|
$effect(() => {
|
|
if (availableDatatables?.length > 0 && !hasAutoSelected) {
|
|
hasAutoSelected = true
|
|
selectedDatatable = availableDatatables.includes('main') ? 'main' : availableDatatables[0]
|
|
}
|
|
})
|
|
|
|
function generateUniqueSchemaName(existingSchemas: string[]): string {
|
|
let num = 1
|
|
while (existingSchemas.includes(`app${num}`)) {
|
|
num++
|
|
}
|
|
return `app${num}`
|
|
}
|
|
|
|
const newSchemaAlreadyExists = $derived(
|
|
schemaMode === 'new' &&
|
|
newSchemaName.trim() !== '' &&
|
|
(availableSchemas ?? []).includes(newSchemaName.trim())
|
|
)
|
|
|
|
let userEditedSchemaName = $state(false)
|
|
|
|
$effect(() => {
|
|
const schemas = availableSchemas ?? []
|
|
if (schemaMode === 'new') {
|
|
if (!newSchemaName) {
|
|
newSchemaName = generateUniqueSchemaName(schemas)
|
|
userEditedSchemaName = false
|
|
} else if (!userEditedSchemaName && schemas.includes(newSchemaName)) {
|
|
newSchemaName = generateUniqueSchemaName(schemas)
|
|
}
|
|
}
|
|
})
|
|
|
|
const datatableItems = $derived(toDatatableItems(availableDatatables))
|
|
const schemaItems = $derived(toSchemaItems(availableSchemas))
|
|
|
|
// An existing schema counts only while the current role reaches it: one picked under another
|
|
// role would save an app that creates its tables where it cannot.
|
|
const effectiveSchema = $derived(
|
|
schemaMode === 'new'
|
|
? newSchemaName
|
|
: schemaMode === 'existing' &&
|
|
selectedSchema !== undefined &&
|
|
availableSchemas.includes(selectedSchema)
|
|
? selectedSchema
|
|
: undefined
|
|
)
|
|
|
|
// copilotInfo is a global that stays empty until some ancestor's fetch lands, so
|
|
// `enabled` alone cannot tell "no providers" from "not loaded yet" and the modal
|
|
// would announce AI as unconfigured while it is merely unknown. Gate on the
|
|
// config describing opWs, and load it here so the claim owns its own evidence.
|
|
const aiConfigLoaded = $derived(!!opWs && $copilotWorkspace === opWs)
|
|
// Say where the button leads: the route hands this prompt to a fresh AI
|
|
// session for everyone who has one, and drives the docked chat for the rest.
|
|
const handsOffToSession = $derived(prefersSessionHandoff($userStore?.operator))
|
|
const isAiEnabled = $derived(aiConfigLoaded && $copilotInfo.enabled)
|
|
|
|
$effect(() => {
|
|
if (open && opWs && !aiConfigLoaded) {
|
|
loadCopilot(opWs)
|
|
}
|
|
})
|
|
|
|
async function start(withPrompt: boolean) {
|
|
const template = templates[selectedTemplateIndex]
|
|
|
|
if (
|
|
tableCreationEnabled &&
|
|
schemaMode === 'new' &&
|
|
newSchemaName &&
|
|
selectedDatatable &&
|
|
opWs
|
|
) {
|
|
try {
|
|
const { dbSchemaOpsWithPreviewScripts } = await import('$lib/components/dbOps')
|
|
const dbOps = dbSchemaOpsWithPreviewScripts({
|
|
workspace: opWs,
|
|
input: {
|
|
type: 'database',
|
|
resourceType: 'postgresql',
|
|
resourcePath: `datatable://${selectedDatatable}`,
|
|
role: effectiveRole
|
|
}
|
|
})
|
|
await dbOps.onCreateSchema({ schema: newSchemaName })
|
|
} catch (e) {
|
|
console.error('Failed to create schema:', e)
|
|
sendUserToast(`Failed to create schema: ${e}`, true)
|
|
}
|
|
}
|
|
|
|
const formattedTables = preWhitelistedTables.map(formatDataTableRef)
|
|
const keepsDatatable = tableCreationEnabled && selectedDatatable !== undefined
|
|
// The roles shown, for the data tables the app ends up using.
|
|
const usedDatatables = new Set(preWhitelistedTables.map((t) => t.datatable))
|
|
if (keepsDatatable) usedDatatables.add(selectedDatatable!)
|
|
const shownRoles = Object.entries(pickerRoles ?? {}).filter(([dt]) => usedDatatables.has(dt))
|
|
const appRoles = shownRoles.length > 0 ? Object.fromEntries(shownRoles) : undefined
|
|
const data: RawAppData = keepsDatatable
|
|
? {
|
|
tables: formattedTables,
|
|
datatable: selectedDatatable,
|
|
schema: effectiveSchema,
|
|
roles: appRoles
|
|
}
|
|
: { tables: formattedTables, datatable: undefined, schema: undefined, roles: appRoles }
|
|
|
|
const policy: Policy = {
|
|
on_behalf_of: $userStore?.username.includes('@')
|
|
? $userStore?.username
|
|
: `u/${$userStore?.username}`,
|
|
on_behalf_of_email: $userStore?.email,
|
|
execution_mode: 'publisher'
|
|
}
|
|
|
|
open = false
|
|
onStart(
|
|
{
|
|
files: template.files,
|
|
runnables: {},
|
|
data,
|
|
summary: appSummary.trim(),
|
|
policy,
|
|
prompt: withPrompt ? initialPrompt.trim() : undefined
|
|
},
|
|
withPrompt
|
|
)
|
|
}
|
|
</script>
|
|
|
|
{#if open}
|
|
<!-- `bind:open` (not `open`) so the inner Modal's X / Esc / click-
|
|
outside dismissal propagates back to the parent. Without it the
|
|
Modal closes its own UI but the picker's `open` prop stays true,
|
|
so the route's `templatePicker → false` watcher never fires and
|
|
autosave stays suspended after the dismissal. -->
|
|
<Modal kind="X" bind:open title="New App setup">
|
|
<div class="flex flex-col gap-6 min-w-sm">
|
|
<div>
|
|
<h2 class="text-xs font-semibold text-emphasis mb-1">Summary</h2>
|
|
<TextInput
|
|
bind:value={appSummary}
|
|
inputProps={{
|
|
placeholder: "Brief description of the app (e.g., 'Todo list with authentication')"
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="pt-6">
|
|
<h2 class="text-xs font-semibold text-emphasis mb-1">Framework</h2>
|
|
<div class="flex flex-wrap gap-3">
|
|
{#each templates as t, i}
|
|
<button
|
|
onclick={() => (selectedTemplateIndex = i)}
|
|
class="w-24 h-24 flex justify-between py-5 flex-col {selectedTemplateIndex === i
|
|
? 'bg-surface-accent-selected border border-accent'
|
|
: ''} hover:bg-surface-hover border rounded-lg transition-all"
|
|
>
|
|
<div class="w-full flex items-center justify-center">
|
|
<FileEditorIcon file={'.' + t.icon} size={32} />
|
|
</div>
|
|
<div class="center-center w-full text-sm text-secondary">{t.name}</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-6">
|
|
<h2 class="text-xs font-semibold text-emphasis mb-1">Data configuration</h2>
|
|
|
|
{#if hasNoDatatables}
|
|
<Alert type="warning" title="No datatables configured.">
|
|
You can still create an app, but for data storage you won't be able to use data tables
|
|
which are <b>highly recommended</b>.
|
|
<br />
|
|
{#if $userStore?.is_admin}
|
|
Configure datatables in
|
|
<a
|
|
href="/workspace_settings?tab=windmill_data_tables"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-1"
|
|
>workspace settings <ExternalLinkIcon size={16} />
|
|
</a> to enable this feature.
|
|
{:else}
|
|
Ask your workspace admin to configure datatables in workspace settings to enable this
|
|
feature.
|
|
{/if}
|
|
</Alert>
|
|
{:else}
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-xs text-secondary mb-1 block">Default settings for new tables</span>
|
|
<div class="flex flex-col gap-4 rounded-md p-4 border">
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-1">
|
|
<label class="text-xs text-emphasis font-semibold" for="datatable"
|
|
>Datatable</label
|
|
>
|
|
<div class="flex flex-row items-center gap-2">
|
|
<Select
|
|
id="datatable"
|
|
disablePortal
|
|
items={datatableItems}
|
|
bind:value={selectedDatatable}
|
|
placeholder="Datatable"
|
|
size="sm"
|
|
class="w-40"
|
|
/>
|
|
{#if showRolePicker}
|
|
<!-- Reads as one phrase, "main as analyst", so the role needs no label. -->
|
|
<span class="text-xs text-secondary">as</span>
|
|
<Select
|
|
id="datatable-role"
|
|
disablePortal
|
|
items={loadedRoles.map((r) => ({ value: r, label: r }))}
|
|
bind:value={() => selectedRole, pickRole}
|
|
clearable={false}
|
|
placeholder="Role"
|
|
size="sm"
|
|
class="w-40"
|
|
/>
|
|
{/if}
|
|
{#if noUsableRole || rolesUnknown || accessUnknown}
|
|
<span
|
|
class="text-xs text-red-600 dark:text-red-400"
|
|
title={access.current.error}
|
|
>
|
|
{rolesUnknown
|
|
? 'could not read its roles'
|
|
: noUsableRole
|
|
? 'no role you can use'
|
|
: 'could not reach it'}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-emphasis font-semibold">Schema</span>
|
|
<div class="flex flex-row gap-1 w-full items-center">
|
|
<div>
|
|
<ToggleButtonGroup bind:selected={schemaMode} noWFull>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="none" label="None" icon={Ban} {item} size="sm" />
|
|
<ToggleButton
|
|
value="new"
|
|
label="New"
|
|
icon={Plus}
|
|
disabled={!canCreateSchema}
|
|
tooltip={canCreateSchema
|
|
? undefined
|
|
: noUsableRole
|
|
? `You can use no role of ${selectedDatatable}`
|
|
: accessUnknown
|
|
? `Could not read what may be created in ${selectedDatatable}`
|
|
: `${effectiveRole ?? 'This connection'} cannot create schemas in ${selectedDatatable}`}
|
|
{item}
|
|
size="sm"
|
|
/>
|
|
<ToggleButton
|
|
value="existing"
|
|
label="Existing"
|
|
icon={List}
|
|
{item}
|
|
size="sm"
|
|
/>
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</div>
|
|
{#if schemaMode === 'new'}
|
|
<TextInput
|
|
bind:value={newSchemaName}
|
|
inputProps={{
|
|
placeholder: 'Schema name',
|
|
oninput: () => (userEditedSchemaName = true)
|
|
}}
|
|
class="flex-1"
|
|
error={newSchemaAlreadyExists}
|
|
size="sm"
|
|
/>
|
|
{:else if schemaMode === 'existing'}
|
|
<div class="flex-1">
|
|
<Select
|
|
disablePortal
|
|
items={schemaItems}
|
|
bind:value={selectedSchema}
|
|
placeholder="Schema"
|
|
size="sm"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{#if newSchemaAlreadyExists}
|
|
<span class="text-xs text-red-500"
|
|
>Schema "{newSchemaName}" already exists</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<Toggle
|
|
size="sm"
|
|
bind:checked={tableCreationEnabled}
|
|
options={{ right: 'Allow AI to create new tables' }}
|
|
/>
|
|
</div>
|
|
|
|
<div class="pt-6">
|
|
<RawAppDataTableList
|
|
dataTableRefs={preWhitelistedTables}
|
|
defaultDatatable={selectedDatatable}
|
|
defaultSchema={effectiveSchema}
|
|
roles={pickerRoles}
|
|
standalone
|
|
hideDefaultSelector
|
|
onAdd={() => dataTableDrawer?.openDrawer()}
|
|
onRemove={(index) => {
|
|
preWhitelistedTables = preWhitelistedTables.filter((_, i) => i !== index)
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if !$copilotInfo.workspaceDisabled}
|
|
<div class="pt-6">
|
|
<h2 class="text-xs font-semibold text-emphasis mb-1 flex items-center gap-2">
|
|
<Sparkles size={16} class="text-ai" />
|
|
Start with AI
|
|
<span class="text-xs font-normal text-tertiary">(optional)</span>
|
|
</h2>
|
|
|
|
{#if !aiConfigLoaded}
|
|
<div class="flex items-center gap-2 text-xs text-tertiary">
|
|
<Loader2 size={14} class="animate-spin" />
|
|
Loading AI settings...
|
|
</div>
|
|
{:else if !isAiEnabled}
|
|
<Alert type="info" title="AI is not configured.">
|
|
You can still create an app manually but using AI is highly recommended.
|
|
<br />
|
|
{#if $userStore?.is_admin}
|
|
Configure AI in
|
|
<a
|
|
href="{base}/workspace_settings?tab=ai"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-1 font-semibold"
|
|
>workspace settings <ExternalLinkIcon size={16} />
|
|
</a>
|
|
{#if $superadmin}
|
|
or
|
|
<a
|
|
href="{base}/?workspace=admins#superadmin-settings"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-1 font-semibold"
|
|
>instance settings <ExternalLinkIcon size={16} />
|
|
</a>
|
|
{/if} to enable this feature.
|
|
{:else if $superadmin}
|
|
Configure AI in
|
|
<a
|
|
href="{base}/?workspace=admins#superadmin-settings"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-1 font-semibold"
|
|
>instance settings <ExternalLinkIcon size={16} />
|
|
</a> to enable this feature.
|
|
{:else}
|
|
Ask your workspace admin to configure AI in workspace settings to enable this
|
|
feature.
|
|
{/if}
|
|
</Alert>
|
|
{:else}
|
|
<div class="flex flex-col gap-2">
|
|
<TextInput
|
|
underlyingInputEl="textarea"
|
|
bind:value={initialPrompt}
|
|
inputProps={{
|
|
rows: 3,
|
|
placeholder:
|
|
"Describe what you want to build... (e.g., 'Create a todo list app with user authentication')"
|
|
}}
|
|
/>
|
|
<p class="text-xs text-tertiary">
|
|
{handsOffToSession
|
|
? 'Leave empty to start with a blank template, or describe your app to open an AI session that builds it.'
|
|
: 'Leave empty to start with a blank template, or describe your app to get AI assistance right away.'}
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="pt-6 flex justify-end gap-3">
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
on:click={() => start(false)}
|
|
disabled={!templates[selectedTemplateIndex] ||
|
|
newSchemaAlreadyExists ||
|
|
!rolesSettled ||
|
|
!accessSettled ||
|
|
blockedByRole}
|
|
>
|
|
{$copilotInfo.workspaceDisabled ? 'Start' : 'Start without AI'}
|
|
</Button>
|
|
{#if isAiEnabled}
|
|
<Button
|
|
variant="accent"
|
|
on:click={() => start(true)}
|
|
disabled={!rolesSettled ||
|
|
!accessSettled ||
|
|
blockedByRole ||
|
|
!templates[selectedTemplateIndex] ||
|
|
!initialPrompt.trim() ||
|
|
newSchemaAlreadyExists}
|
|
startIcon={{ icon: Sparkles }}
|
|
btnClasses={AIBtnClasses('accent')}
|
|
>
|
|
{handsOffToSession ? 'Start in AI session' : 'Start with AI'}
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
{/if}
|
|
|
|
<RawAppDataTableDrawer
|
|
bind:this={dataTableDrawer}
|
|
offset={10000}
|
|
existingRefs={preWhitelistedTables}
|
|
roles={pickerRoles}
|
|
onAdd={(refs, browsedRoles, roleChanged) => {
|
|
preWhitelistedTables = [
|
|
...preWhitelistedTables.filter((t) => !roleChanged.has(t.datatable)),
|
|
...refs
|
|
]
|
|
preWhitelistedRoles = { ...preWhitelistedRoles, ...browsedRoles }
|
|
if (selectedDatatable !== undefined && browsedRoles[selectedDatatable] !== undefined) {
|
|
selectedRole = browsedRoles[selectedDatatable]
|
|
}
|
|
}}
|
|
/>
|