mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
feat: violet ai branding + flow summary (#3171)
* feat: violet ai branding + flow summary * fix: npm run check
This commit is contained in:
@@ -35,7 +35,6 @@
|
||||
Settings,
|
||||
X
|
||||
} from 'lucide-svelte'
|
||||
import autosize from 'svelte-autosize'
|
||||
import { SCRIPT_SHOW_BASH, SCRIPT_SHOW_GO } from '$lib/consts'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -369,8 +368,6 @@
|
||||
let dirtyPath = false
|
||||
|
||||
let selectedTab: 'metadata' | 'runtime' | 'ui' = 'metadata'
|
||||
|
||||
let descriptionTextArea: HTMLTextAreaElement | undefined
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
@@ -411,38 +408,31 @@
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col gap-4">
|
||||
<MetadataGen
|
||||
label="Summary"
|
||||
bind:content={script.summary}
|
||||
lang={script.language}
|
||||
code={script.content}
|
||||
configName="summary"
|
||||
let:updateFocus
|
||||
let:active
|
||||
let:classNames
|
||||
focused
|
||||
on:change={() => {
|
||||
if (initialPath == '' && script.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
script.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
autofocus
|
||||
class={classNames}
|
||||
on:focus={() => updateFocus(true)}
|
||||
on:blur={() => updateFocus(false)}
|
||||
bind:value={script.summary}
|
||||
placeholder={!active ? 'Short summary to be displayed when listed' : ''}
|
||||
<Label label="Summary">
|
||||
<MetadataGen
|
||||
label="Summary"
|
||||
bind:content={script.summary}
|
||||
lang={script.language}
|
||||
code={script.content}
|
||||
promptConfigName="summary"
|
||||
generateOnAppear
|
||||
on:change={() => {
|
||||
if (initialPath == '' && script.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
script.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
elementProps={{
|
||||
type: 'text',
|
||||
placeholder: 'Short summary to be displayed when listed'
|
||||
}}
|
||||
/>
|
||||
</MetadataGen>
|
||||
</Label>
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:this={path}
|
||||
@@ -455,27 +445,18 @@
|
||||
kind="script"
|
||||
/>
|
||||
</Label>
|
||||
<MetadataGen
|
||||
bind:content={script.description}
|
||||
lang={script.language}
|
||||
code={script.content}
|
||||
configName="description"
|
||||
el={descriptionTextArea}
|
||||
label="Description"
|
||||
let:updateFocus
|
||||
let:active
|
||||
let:classNames
|
||||
>
|
||||
<textarea
|
||||
on:focus={() => updateFocus(true)}
|
||||
on:blur={() => updateFocus(false)}
|
||||
class={classNames}
|
||||
use:autosize
|
||||
bind:this={descriptionTextArea}
|
||||
bind:value={script.description}
|
||||
placeholder={!active ? 'Description displayed in the details page' : ''}
|
||||
<Label label="Description">
|
||||
<MetadataGen
|
||||
bind:content={script.description}
|
||||
lang={script.language}
|
||||
code={script.content}
|
||||
promptConfigName="description"
|
||||
elementType="textarea"
|
||||
elementProps={{
|
||||
placeholder: 'Description displayed'
|
||||
}}
|
||||
/>
|
||||
</MetadataGen>
|
||||
</Label>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
|
||||
@@ -33,15 +33,12 @@
|
||||
: {
|
||||
icon: $codeCompletionSessionEnabled ? SparklesIcon : SparklesOffIcon,
|
||||
classes: $codeCompletionSessionEnabled
|
||||
? 'text-sky-900 dark:text-sky-200'
|
||||
: 'text-sky-900/50 dark:text-sky-200/50'
|
||||
? 'text-violet-800 dark:text-violet-400'
|
||||
: 'text-violet-800/50 dark:text-violet-400/50'
|
||||
}}
|
||||
on:click={() => {
|
||||
storeSetting()
|
||||
}}
|
||||
btnClasses={$codeCompletionSessionEnabled
|
||||
? 'bg-sky-50 dark:bg-gray-700 dark:hover:bg-surface-hover'
|
||||
: ''}
|
||||
/>
|
||||
</Popover>
|
||||
{:else}
|
||||
@@ -54,7 +51,7 @@
|
||||
startIcon={{
|
||||
icon: SparklesOffIcon
|
||||
}}
|
||||
disabled
|
||||
btnClasses="text-violet-800/50 dark:text-violet-400/50"
|
||||
/>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
@@ -56,13 +56,13 @@
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
color={genLoading ? 'red' : 'dark'}
|
||||
color={genLoading ? 'red' : 'light'}
|
||||
size="xs"
|
||||
nonCaptureEvent={!genLoading}
|
||||
startIcon={{ icon: Wand2 }}
|
||||
iconOnly
|
||||
title="AI Assistant"
|
||||
btnClasses="min-h-[30px] text-sky-900 dark:text-sky-200 bg-sky-100 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover"
|
||||
btnClasses="min-h-[30px] text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700"
|
||||
loading={genLoading}
|
||||
clickableWhileLoading
|
||||
on:click={genLoading ? () => abortController?.abort() : undefined}
|
||||
@@ -84,10 +84,10 @@
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
color="light"
|
||||
variant="contained"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[38px] !ml-2 text-sky-900 dark:text-sky-200 bg-sky-100 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover"
|
||||
btnClasses="!p-1 !w-[38px] !ml-2 text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700"
|
||||
title="Generate CRON schedule from prompt"
|
||||
aria-label="Generate"
|
||||
iconOnly
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
$currentStepStore !== undefined ? 'z-[901]' : '',
|
||||
copilotLoading || ($currentStepStore !== undefined && $currentStepStore !== 'Input')
|
||||
? ''
|
||||
: 'bg-sky-100 dark:bg-gray-700 text-sky-900 dark:text-sky-200 dark:hover:bg-surface-hover'
|
||||
: '!text-violet-800 !dark:text-violet-400'
|
||||
)}
|
||||
on:click={() => {
|
||||
if (copilotLoading || ($currentStepStore !== undefined && $currentStepStore !== 'Input')) {
|
||||
@@ -72,7 +72,7 @@
|
||||
? 'Stop'
|
||||
: $currentStepStore !== undefined && $currentStepStore !== 'Input'
|
||||
? 'Exit'
|
||||
: 'AI Flow Builder'}
|
||||
: 'AI Builder'}
|
||||
</Button>
|
||||
<div slot="content" class="text-sm flex flex-row items-center z-[901]">
|
||||
<span class="font-semibold">
|
||||
@@ -125,12 +125,12 @@
|
||||
startIcon={{
|
||||
icon: Wand2
|
||||
}}
|
||||
btnClasses="bg-sky-100 dark:bg-gray-700 text-sky-900 dark:text-sky-200 dark:hover:bg-surface-hover"
|
||||
btnClasses="text-violet-800 dark:text-violet-400"
|
||||
color={'light'}
|
||||
variant={'border'}
|
||||
nonCaptureEvent
|
||||
>
|
||||
AI
|
||||
AI Builder
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="block text-primary">
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { getCompletion } from './lib'
|
||||
import type { NewScript } from '$lib/gen'
|
||||
import { isInitialCode } from '$lib/script_helpers'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { Check, Loader2, Wand2 } from 'lucide-svelte'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources'
|
||||
import { copilotInfo, metadataCompletionEnabled } from '$lib/stores'
|
||||
import Label from '../Label.svelte'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import autosize from 'svelte-autosize'
|
||||
import type { FlowValue } from '$lib/gen'
|
||||
import YAML from 'yaml'
|
||||
|
||||
type Config = {
|
||||
type PromptConfig = {
|
||||
system: string
|
||||
user: string
|
||||
mode: 'automatic' | 'manual'
|
||||
placeholderName: 'code' | 'flow'
|
||||
}
|
||||
|
||||
const configs: {
|
||||
summary: Config
|
||||
description: Config
|
||||
const promptConfigs: {
|
||||
summary: PromptConfig
|
||||
description: PromptConfig
|
||||
flowSummary: PromptConfig
|
||||
flowDescription: PromptConfig
|
||||
} = {
|
||||
summary: {
|
||||
system: `
|
||||
@@ -26,56 +30,94 @@ The summaries need to be as short as possible (maximum 8 words) and only give a
|
||||
Examples: List the commits of a GitHub repository, Divide a number by 16, etc..
|
||||
`,
|
||||
user: `
|
||||
Generate a very short summary for the script below:
|
||||
\'\'\'{lang}
|
||||
Generate a very short summary for the script below:
|
||||
\'\'\'code
|
||||
{code}
|
||||
\`\`\`
|
||||
`,
|
||||
mode: 'automatic'
|
||||
placeholderName: 'code'
|
||||
},
|
||||
description: {
|
||||
system: `
|
||||
You are a helpful AI assistant. You generate descriptions from scripts.
|
||||
These descriptions are used to explain to other users what the script does, on particular on the input and what it returns.
|
||||
Descriptions should contain a maximum of 4-5 sentences.
|
||||
The description should focus on what it does and should not contain what concepts it uses (e.g. function named main, export an async function, etc...)
|
||||
These descriptions are used to explain to other users what the script does and how to use it.
|
||||
Be as short as possible to give a global idea, maximum 3-4 sentences.
|
||||
All scripts export an asynchronous function called main, do not include it in the description.
|
||||
Do not describe how to call it either.
|
||||
`,
|
||||
user: `
|
||||
Generate a description for the script below:
|
||||
\'\'\'{lang}
|
||||
Generate a description for the script below:
|
||||
\'\'\'code
|
||||
{code}
|
||||
\`\`\`
|
||||
`,
|
||||
mode: 'manual'
|
||||
placeholderName: 'code'
|
||||
},
|
||||
flowSummary: {
|
||||
system: `
|
||||
You are a helpful AI assistant. You generate very brief summaries from scripts.
|
||||
The summaries need to be as short as possible (maximum 8 words) and only give a global idea. Do not use any punctation. Avoid using prepositions and articles.
|
||||
`,
|
||||
user: `
|
||||
Summarize the flow below in one very short sentence without punctation:
|
||||
{flow}`,
|
||||
placeholderName: 'flow'
|
||||
},
|
||||
flowDescription: {
|
||||
system: `
|
||||
You are a helpful AI assistant. You generate descriptions from flow.
|
||||
These descriptions are used to explain to other users what the flow does and how to use it.
|
||||
Be as short as possible to give a global idea, maximum 3-4 sentences.
|
||||
Do not include line breaks.
|
||||
`,
|
||||
user: `
|
||||
Generate a description for the flow below:
|
||||
{flow}`,
|
||||
placeholderName: 'flow'
|
||||
}
|
||||
}
|
||||
|
||||
function removeLocks(obj: any) {
|
||||
if (obj && typeof obj === 'object') {
|
||||
delete obj.lock
|
||||
for (const key in obj) {
|
||||
removeLocks(obj[key])
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export let content: string | undefined
|
||||
export let code: string
|
||||
export let lang: NewScript.language
|
||||
export let configName: keyof typeof configs
|
||||
|
||||
export let el: HTMLElement | undefined = undefined
|
||||
export let label: string
|
||||
export let code: string | undefined = undefined
|
||||
export let flow: FlowValue | undefined = undefined
|
||||
export let promptConfigName: keyof typeof promptConfigs
|
||||
export let generateOnAppear: boolean = false
|
||||
export let elementType: 'input' | 'textarea' = 'input'
|
||||
export let elementProps: Record<string, any> = {}
|
||||
|
||||
let el: HTMLElement | undefined
|
||||
let generatedContent = ''
|
||||
let genEl: HTMLElement | undefined = undefined
|
||||
let active = false
|
||||
let loading = false
|
||||
let abortController = new AbortController()
|
||||
let manualDisabled = false
|
||||
let width = 0
|
||||
let genHeight = 0
|
||||
|
||||
export let focused = false
|
||||
const updateFocus = (val) => {
|
||||
focused = val
|
||||
}
|
||||
|
||||
let config: Config = configs[configName]
|
||||
let focused = false
|
||||
let config: PromptConfig = promptConfigs[promptConfigName]
|
||||
|
||||
async function generateContent(automatic = false) {
|
||||
abortController = new AbortController()
|
||||
loading = true
|
||||
try {
|
||||
const placeholderContent = flow ? YAML.stringify(removeLocks(flow)) : code
|
||||
|
||||
if (!placeholderContent) {
|
||||
sendUserToast('Could not generate summary: no content to generate from', true)
|
||||
return
|
||||
}
|
||||
// return
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -83,7 +125,7 @@ The description should focus on what it does and should not contain what concept
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: config.user.replace('{lang}', lang).replace('{code}', code)
|
||||
content: config.user.replace(`{${config.placeholderName}}`, placeholderContent)
|
||||
}
|
||||
]
|
||||
const response = await getCompletion(messages, abortController)
|
||||
@@ -91,9 +133,6 @@ The description should focus on what it does and should not contain what concept
|
||||
for await (const chunk of response) {
|
||||
const toks = chunk.choices[0]?.delta?.content || ''
|
||||
generatedContent += toks
|
||||
if (el !== undefined && genEl !== undefined) {
|
||||
el.style.height = Math.max(genEl.scrollHeight + 34, 58) + 'px'
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
@@ -111,11 +150,14 @@ The description should focus on what it does and should not contain what concept
|
||||
if (
|
||||
$copilotInfo.exists_openai_resource_path &&
|
||||
$metadataCompletionEnabled &&
|
||||
config.mode === 'automatic' &&
|
||||
code &&
|
||||
generateOnAppear &&
|
||||
!content &&
|
||||
code &&
|
||||
!isInitialCode(code)
|
||||
) {
|
||||
setTimeout(() => {
|
||||
el?.focus()
|
||||
}, 0)
|
||||
generateContent(true)
|
||||
}
|
||||
|
||||
@@ -130,81 +172,120 @@ The description should focus on what it does and should not contain what concept
|
||||
$metadataCompletionEnabled &&
|
||||
!content &&
|
||||
(loading || focused || !!generatedContent) &&
|
||||
!manualDisabled
|
||||
!manualDisabled &&
|
||||
((config.placeholderName === 'code' && !!code) ||
|
||||
(config.placeholderName === 'flow' &&
|
||||
!!flow &&
|
||||
Array.isArray(flow.modules) &&
|
||||
flow.modules.length > 0))
|
||||
|
||||
$: focused && (manualDisabled = false)
|
||||
|
||||
$: if (content) {
|
||||
abortController.abort()
|
||||
generatedContent = ''
|
||||
} else {
|
||||
manualDisabled = false
|
||||
}
|
||||
|
||||
$: if (elementType === 'textarea' && el !== undefined && !content) {
|
||||
el.style.height = Math.max(genHeight + 34, 58) + 'px'
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
abortController.abort()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<div class="flex flex-row" />
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<Label {label}>
|
||||
<div
|
||||
class="relative"
|
||||
on:keydown={(event) => {
|
||||
if (!$copilotInfo.exists_openai_resource_path || !$metadataCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
if (!loading && generatedContent) {
|
||||
event.preventDefault()
|
||||
content = generatedContent
|
||||
generatedContent = ''
|
||||
} else if (!loading && !content) {
|
||||
event.preventDefault()
|
||||
generateContent()
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (loading) {
|
||||
abortController.abort()
|
||||
} else {
|
||||
manualDisabled = true
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={'absolute left-0.5 flex flex-row pl-1 gap-2 items-start top-[0.3rem] pointer-events-none'}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={twMerge('relative', $$props.class)}
|
||||
bind:clientWidth={width}
|
||||
on:keydown={(event) => {
|
||||
if (!$copilotInfo.exists_openai_resource_path || !$metadataCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
if (manualDisabled) {
|
||||
event.preventDefault()
|
||||
manualDisabled = false
|
||||
} else if (!loading && generatedContent) {
|
||||
event.preventDefault()
|
||||
content = generatedContent
|
||||
generatedContent = ''
|
||||
} else if (!loading && !content) {
|
||||
event.preventDefault()
|
||||
generateContent()
|
||||
}
|
||||
} else if (event.key === 'Escape' && !manualDisabled) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (loading) {
|
||||
abortController.abort()
|
||||
} else {
|
||||
manualDisabled = true
|
||||
generatedContent = ''
|
||||
}
|
||||
} else if (event.key === 'Backspace' && !loading && !content) {
|
||||
manualDisabled = true
|
||||
generatedContent = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={'absolute left-[0.5rem] top-[0.3rem] flex flex-row gap-2 items-start pointer-events-none'}
|
||||
>
|
||||
{#if active}
|
||||
<span
|
||||
class="absolute text-xs bg-violet-100 text-violet-800 dark:bg-gray-700 dark:text-violet-400 px-1 py-0.5 rounded-md flex flex-row items-center justify-center gap-2 transition-all shrink-0"
|
||||
>
|
||||
{#if active}
|
||||
<span
|
||||
class="absolute text-xs text-sky-900 bg-sky-100 dark:text-sky-200 dark:bg-gray-700 p-1 rounded-md flex flex-row items-center justify-center gap-2 w-32 shrink-0"
|
||||
>
|
||||
{#if loading}
|
||||
<Loader2 class="animate-spin text-gray-500 dark:text-gray-400" size={16} />
|
||||
{/if}
|
||||
<div>
|
||||
<span class="px-1 py-0.5 rounded-md text-2xs text-bold bg-white dark:bg-surface">
|
||||
{#if loading}
|
||||
ESC
|
||||
{:else}
|
||||
TAB
|
||||
{/if}
|
||||
</span>
|
||||
{#if loading}
|
||||
to cancel
|
||||
{:else if generatedContent}
|
||||
to accept
|
||||
{:else}
|
||||
to generate
|
||||
{/if}
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="text-sm leading-6 indent-[8.5rem] text-gray-500 dark:text-gray-400 pr-1"
|
||||
bind:this={genEl}
|
||||
>
|
||||
{generatedContent}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="px-0.5 py-0.5 rounded-md text-2xs text-bold flex flex-row items-center gap-1">
|
||||
{#if loading}
|
||||
ESC
|
||||
{:else}
|
||||
TAB
|
||||
{/if}
|
||||
{#if loading}
|
||||
<Loader2 class="animate-spin" size={12} />
|
||||
{:else if generatedContent}
|
||||
<Check size={12} />
|
||||
{:else}
|
||||
<Wand2 size={12} />
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
bind:clientHeight={genHeight}
|
||||
class={twMerge(
|
||||
'text-sm leading-6 indent-[3.5rem] text-gray-500 dark:text-gray-400 pr-1',
|
||||
elementType === 'input' ? 'text-ellipsis overflow-hidden whitespace-nowrap' : ''
|
||||
)}
|
||||
style={elementType === 'input' ? `max-width: calc(${width}px - 0.5rem)` : ''}
|
||||
>
|
||||
{generatedContent}
|
||||
</div>
|
||||
<slot {updateFocus} {active} classNames={active && !generatedContent ? '!pl-[8.7rem]' : ''} />
|
||||
</div>
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
{#if elementType === 'textarea'}
|
||||
<textarea
|
||||
bind:this={el}
|
||||
bind:value={content}
|
||||
use:autosize
|
||||
{...elementProps}
|
||||
placeholder={!active ? elementProps.placeholder : ''}
|
||||
class={active ? '!indent-[3.5rem]' : ''}
|
||||
on:focus={() => (focused = true)}
|
||||
on:blur={() => (focused = false)}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={el}
|
||||
bind:value={content}
|
||||
placeholder={!active ? elementProps.placeholder : ''}
|
||||
class={active ? '!indent-[3.5rem]' : ''}
|
||||
on:focus={() => (focused = true)}
|
||||
on:blur={() => (focused = false)}
|
||||
/>
|
||||
{/if}
|
||||
<!-- <slot {updateFocus} {active} {generatedContent} classNames={active ? '!indent-[8.8rem]' : ''} /> -->
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ExternalLink, Sparkles, ZapOffIcon } from 'lucide-svelte'
|
||||
import { ExternalLink, Sparkles } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { copilotInfo, metadataCompletionEnabled } from '$lib/stores'
|
||||
import Popover from '../Popover.svelte'
|
||||
@@ -22,7 +22,7 @@
|
||||
{#if $copilotInfo.exists_openai_resource_path}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text"
|
||||
>Metadata completion: {$metadataCompletionEnabled ? 'disabled' : 'enabled'} (applies only to you)</svelte:fragment
|
||||
>Metadata completion: {$metadataCompletionEnabled ? 'enabled' : 'disabled'} (applies only to you)</svelte:fragment
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -30,13 +30,13 @@
|
||||
startIcon={{
|
||||
icon: $metadataCompletionEnabled ? Sparkles : SparklesOffIcon,
|
||||
classes: $metadataCompletionEnabled
|
||||
? 'text-sky-900 dark:text-sky-200'
|
||||
: 'text-sky-900/50 dark:text-sky-200/50'
|
||||
? 'text-violet-800 dark:text-violet-400'
|
||||
: 'text-violet-800/50 dark:text-violet-400/50'
|
||||
}}
|
||||
on:click={() => {
|
||||
storeSetting()
|
||||
}}
|
||||
btnClasses={$metadataCompletionEnabled ? 'bg-sky-100 dark:bg-gray-700' : ''}
|
||||
btnClasses="p-1.5"
|
||||
/>
|
||||
</Popover>
|
||||
{:else}
|
||||
@@ -52,10 +52,12 @@
|
||||
</svelte:fragment>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
class="text-violet-800/50 dark:text-violet-400/50"
|
||||
btnClasses="p-1.5"
|
||||
startIcon={{
|
||||
icon: ZapOffIcon
|
||||
icon: SparklesOffIcon
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
@@ -162,14 +162,14 @@
|
||||
<Button
|
||||
title="Fix code"
|
||||
size="xs"
|
||||
color={genLoading ? 'red' : 'blue'}
|
||||
color={genLoading ? 'red' : 'light'}
|
||||
spacingSize="xs2"
|
||||
startIcon={genLoading ? undefined : { icon: Wand2 }}
|
||||
nonCaptureEvent={!genLoading}
|
||||
on:click={genLoading ? () => abortController?.abort() : undefined}
|
||||
btnClasses={genLoading
|
||||
? ''
|
||||
: 'text-sky-900 bg-sky-100 dark:text-sky-200 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover'}
|
||||
: 'text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700'}
|
||||
>{#if genLoading}
|
||||
<WindmillIcon
|
||||
white
|
||||
|
||||
@@ -270,9 +270,7 @@
|
||||
<Button
|
||||
size="xs"
|
||||
color={genLoading ? 'red' : 'light'}
|
||||
btnClasses={genLoading
|
||||
? '!px-3 z-[5000]'
|
||||
: '!px-2 bg-sky-50 dark:bg-gray-700 dark:hover:bg-surface-hover'}
|
||||
btnClasses={genLoading ? '!px-3 z-[5000]' : '!px-2'}
|
||||
nonCaptureEvent={!genLoading}
|
||||
on:click={genLoading ? () => abortController?.abort() : undefined}
|
||||
bind:element={button}
|
||||
@@ -280,23 +278,19 @@
|
||||
title="Generate code from Prompt"
|
||||
startIcon={genLoading
|
||||
? { icon: Ban }
|
||||
: { icon: Wand2, classes: 'text-sky-900 dark:text-sky-200 ' }}
|
||||
: { icon: Wand2, classes: 'text-violet-800 dark:text-violet-400' }}
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
title="Generate code from prompt"
|
||||
btnClasses={twMerge(
|
||||
'!font-medium',
|
||||
genLoading
|
||||
? 'z-[5000]'
|
||||
: 'text-sky-900 dark:text-sky-200 bg-sky-50 dark:bg-gray-700 dark:hover:bg-surface-hover'
|
||||
genLoading ? 'z-[5000]' : 'text-violet-800 dark:text-violet-400'
|
||||
)}
|
||||
size="xs"
|
||||
color={genLoading ? 'red' : 'light'}
|
||||
spacingSize="md"
|
||||
startIcon={genLoading
|
||||
? undefined
|
||||
: { icon: Wand2, classes: 'text-sky-900 dark:text-sky-200' }}
|
||||
startIcon={genLoading ? undefined : { icon: Wand2 }}
|
||||
propagateEvent
|
||||
on:click={genLoading
|
||||
? () => abortController?.abort()
|
||||
@@ -366,9 +360,9 @@
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
color="light"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[38px] !ml-2 text-sky-900 dark:text-sky-200 bg-sky-100 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover"
|
||||
btnClasses="!p-1 !w-[38px] !ml-2 text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700"
|
||||
title="Generate code from prompt"
|
||||
aria-label="Generate"
|
||||
on:click={() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
import MetadataGen from '$lib/components/copilot/MetadataGen.svelte'
|
||||
import IconedPath from '$lib/components/IconedPath.svelte'
|
||||
import { ScriptService, type FlowModule, type PathScript } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -41,7 +42,15 @@
|
||||
<div class="mx-0.5">
|
||||
<LanguageIcon lang={flowModule.value.language} width={20} height={20} />
|
||||
</div>
|
||||
<input bind:value={flowModule.summary} placeholder={'Summary'} class="w-full grow" />
|
||||
<MetadataGen
|
||||
bind:content={flowModule.summary}
|
||||
promptConfigName="summary"
|
||||
code={flowModule.value.content}
|
||||
class="w-full"
|
||||
elementProps={{
|
||||
placeholder: 'Summary'
|
||||
}}
|
||||
/>
|
||||
{:else if flowModule?.value.type === 'script' && 'path' in flowModule.value && flowModule.value.path}
|
||||
<IconedPath path={flowModule.value.path} hash={flowModule.value.hash} class="grow" />
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
import { Alert, Button, SecondsInput } from '$lib/components/common'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import autosize from 'svelte-autosize'
|
||||
import Slider from '$lib/components/Slider.svelte'
|
||||
import { enterpriseLicense, workerTags, workspaceStore } from '$lib/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
@@ -25,6 +24,8 @@
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import ErrorHandlerToggleButton from '$lib/components/details/ErrorHandlerToggleButton.svelte'
|
||||
import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte'
|
||||
import MetadataGen from '$lib/components/copilot/MetadataGen.svelte'
|
||||
import MetadataGenToggle from '$lib/components/copilot/MetadataGenToggle.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
|
||||
@@ -69,192 +70,202 @@
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="settings-metadata" class="p-4 h-full overflow-auto">
|
||||
<div class="h-full gap-8 flex flex-col">
|
||||
<Label label="Summary">
|
||||
<input
|
||||
type="text"
|
||||
autofocus
|
||||
bind:value={$flowStore.summary}
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
id="flow-summary"
|
||||
on:keyup={() => {
|
||||
if (initialPath == '' && $flowStore.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
$flowStore.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{#if !noEditor}
|
||||
<Label label="Path">
|
||||
<Path
|
||||
autofocus={false}
|
||||
bind:this={path}
|
||||
bind:dirty={dirtyPath}
|
||||
bind:path={$pathStore}
|
||||
{initialPath}
|
||||
namePlaceholder="flow"
|
||||
kind="flow"
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<Label label="Description">
|
||||
<textarea
|
||||
use:autosize
|
||||
type="text"
|
||||
class="text-sm"
|
||||
id="inp"
|
||||
bind:value={$flowStore.description}
|
||||
placeholder="What this flow does and how to use it."
|
||||
rows="3"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<!-- TODO: Add EE-only badge when we have it -->
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={$flowStore.value.priority !== undefined && $flowStore.value.priority > 0}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority) {
|
||||
$flowStore.value.priority = undefined
|
||||
} else {
|
||||
$flowStore.value.priority = 100
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: `Label as high priority`,
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense
|
||||
? 'This is a feature only available on enterprise edition.'
|
||||
: ''
|
||||
}`
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="right">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-16 ml-4"
|
||||
disabled={$flowStore.value.priority === undefined}
|
||||
bind:value={$flowStore.value.priority}
|
||||
on:focus
|
||||
<Section label="Metadata">
|
||||
<svelte:fragment slot="header">
|
||||
<MetadataGenToggle />
|
||||
</svelte:fragment>
|
||||
<div class="h-full gap-8 flex flex-col">
|
||||
<Label label="Summary">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.summary}
|
||||
promptConfigName="flowSummary"
|
||||
flow={$flowStore.value}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority && $flowStore.value.priority > 100) {
|
||||
$flowStore.value.priority = 100
|
||||
} else if ($flowStore.value.priority && $flowStore.value.priority < 0) {
|
||||
$flowStore.value.priority = 0
|
||||
if (initialPath == '' && $flowStore.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
$flowStore.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
elementProps={{
|
||||
type: 'text',
|
||||
id: 'flow-summary',
|
||||
placeholder: 'Short summary to be displayed when listed'
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Toggle>
|
||||
</Label>
|
||||
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<ErrorHandlerToggleButton
|
||||
kind="flow"
|
||||
scriptOrFlowPath={$pathStore}
|
||||
bind:errorHandlerMuted={$flowStore.ws_error_handler_muted}
|
||||
iconOnly={false}
|
||||
/>
|
||||
</div>
|
||||
{#if !noEditor}
|
||||
<Label label="Path">
|
||||
<Path
|
||||
autofocus={false}
|
||||
bind:this={path}
|
||||
bind:dirty={dirtyPath}
|
||||
bind:path={$pathStore}
|
||||
{initialPath}
|
||||
namePlaceholder="flow"
|
||||
kind="flow"
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<Slider text="How to trigger flows?">
|
||||
<div class="text-sm text-tertiary border p-4 mb-20">
|
||||
On-demand:
|
||||
<ul class="pt-4">
|
||||
<li>
|
||||
1. <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/auto_generated_uis"
|
||||
target="_blank">Auto-generated UIs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
2. <a href="/apps/add?nodraft=true" target="_blank"> App Editor</a> for customized-UIs
|
||||
</li>
|
||||
<li>
|
||||
3. <a href="/schedules" target="_blank">Scheduling</a>
|
||||
</li>
|
||||
<li>
|
||||
4. <a href="https://www.windmill.dev/docs/advanced/cli" target="_blank"
|
||||
>Windmill CLI</a
|
||||
>
|
||||
</li>
|
||||
<br />
|
||||
<li class="mt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p> From external events: </p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
5. Send a <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/webhooks"
|
||||
target="_blank">webhook</a
|
||||
>
|
||||
after each event:
|
||||
<ul class="list-disc pl-4"
|
||||
><li
|
||||
>Async <Tooltip
|
||||
>Return an uuid instantly that you can use to fetch status and result</Tooltip
|
||||
>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(url)
|
||||
}}
|
||||
href={url}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{url}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
>Sync <Tooltip>Wait for result within a timeout of 20s</Tooltip>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(syncedUrl)
|
||||
}}
|
||||
href={syncedUrl}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{syncedUrl}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul></li
|
||||
>
|
||||
<br />
|
||||
<li>
|
||||
6. Use a <a
|
||||
href="https://www.windmill.dev/docs/flows/flow_trigger"
|
||||
target="_blank">trigger script</a
|
||||
>
|
||||
and schedule this flow to run as frequently as needed and compare a state persisted
|
||||
in Windmill to the state of the external system. If a difference is detected, then
|
||||
the rest of the flow is triggered. Oftentimes, the second step of a flow is a for-loop
|
||||
that will iterate over every elements. When using a trigger, a default schedule
|
||||
will be created.
|
||||
<img
|
||||
class="shadow-lg border rounded"
|
||||
alt="static button"
|
||||
src="/trigger_button.png"
|
||||
/>
|
||||
</li></ul
|
||||
>
|
||||
<Label label="Description">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.description}
|
||||
promptConfigName="flowDescription"
|
||||
flow={$flowStore.value}
|
||||
class="w-full"
|
||||
elementType="textarea"
|
||||
elementProps={{
|
||||
id: 'inp',
|
||||
placeholder: 'What this flow does and how to use it.'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<!-- TODO: Add EE-only badge when we have it -->
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={$flowStore.value.priority !== undefined && $flowStore.value.priority > 0}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority) {
|
||||
$flowStore.value.priority = undefined
|
||||
} else {
|
||||
$flowStore.value.priority = 100
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: `Label as high priority`,
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense
|
||||
? 'This is a feature only available on enterprise edition.'
|
||||
: ''
|
||||
}`
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="right">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-16 ml-4"
|
||||
disabled={$flowStore.value.priority === undefined}
|
||||
bind:value={$flowStore.value.priority}
|
||||
on:focus
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority && $flowStore.value.priority > 100) {
|
||||
$flowStore.value.priority = 100
|
||||
} else if ($flowStore.value.priority && $flowStore.value.priority < 0) {
|
||||
$flowStore.value.priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Toggle>
|
||||
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<ErrorHandlerToggleButton
|
||||
kind="flow"
|
||||
scriptOrFlowPath={$pathStore}
|
||||
bind:errorHandlerMuted={$flowStore.ws_error_handler_muted}
|
||||
iconOnly={false}
|
||||
/>
|
||||
</div>
|
||||
</Slider>
|
||||
</div>
|
||||
|
||||
<Slider text="How to trigger flows?">
|
||||
<div class="text-sm text-tertiary border p-4 mb-20">
|
||||
On-demand:
|
||||
<ul class="pt-4">
|
||||
<li>
|
||||
1. <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/auto_generated_uis"
|
||||
target="_blank">Auto-generated UIs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
2. <a href="/apps/add?nodraft=true" target="_blank"> App Editor</a> for customized-UIs
|
||||
</li>
|
||||
<li>
|
||||
3. <a href="/schedules" target="_blank">Scheduling</a>
|
||||
</li>
|
||||
<li>
|
||||
4. <a href="https://www.windmill.dev/docs/advanced/cli" target="_blank"
|
||||
>Windmill CLI</a
|
||||
>
|
||||
</li>
|
||||
<br />
|
||||
<li class="mt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p> From external events: </p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
5. Send a <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/webhooks"
|
||||
target="_blank">webhook</a
|
||||
>
|
||||
after each event:
|
||||
<ul class="list-disc pl-4"
|
||||
><li
|
||||
>Async <Tooltip
|
||||
>Return an uuid instantly that you can use to fetch status and result</Tooltip
|
||||
>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(url)
|
||||
}}
|
||||
href={url}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{url}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
>Sync <Tooltip>Wait for result within a timeout of 20s</Tooltip>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(syncedUrl)
|
||||
}}
|
||||
href={syncedUrl}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{syncedUrl}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul></li
|
||||
>
|
||||
<br />
|
||||
<li>
|
||||
6. Use a <a
|
||||
href="https://www.windmill.dev/docs/flows/flow_trigger"
|
||||
target="_blank">trigger script</a
|
||||
>
|
||||
and schedule this flow to run as frequently as needed and compare a state persisted
|
||||
in Windmill to the state of the external system. If a difference is detected,
|
||||
then the rest of the flow is triggered. Oftentimes, the second step of a flow
|
||||
is a for-loop that will iterate over every elements. When using a trigger, a
|
||||
default schedule will be created.
|
||||
<img
|
||||
class="shadow-lg border rounded"
|
||||
alt="static button"
|
||||
src="/trigger_button.png"
|
||||
/>
|
||||
</li></ul
|
||||
>
|
||||
</div>
|
||||
</Slider>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<TabContent value="settings-schedule" class="p-4 h-full overflow-scroll">
|
||||
<Alert
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
: undefined}
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class=" bg-sky-100 dark:bg-gray-700 text-sky-900 dark:text-sky-200 dark:hover:bg-surface-hover border mx-0.5 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-8 h-8 flex items-center justify-center"
|
||||
class=" bg-surface text-violet-800 dark:text-violet-400 border mx-0.5 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-8 h-8 flex items-center justify-center"
|
||||
>
|
||||
<Wand2 size={16} />
|
||||
</button>
|
||||
|
||||
@@ -188,7 +188,6 @@ components:
|
||||
- $ref: "#/components/schemas/BranchOne"
|
||||
- $ref: "#/components/schemas/BranchAll"
|
||||
- $ref: "#/components/schemas/Identity"
|
||||
- $ref: "#/components/schemas/Graphql"
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
@@ -199,7 +198,6 @@ components:
|
||||
branchone: "#/components/schemas/BranchOne"
|
||||
branchall: "#/components/schemas/BranchAll"
|
||||
identity: "#/components/schemas/Identity"
|
||||
graphql: "#/components/schemas/Graphql"
|
||||
|
||||
RawScript:
|
||||
type: object
|
||||
@@ -385,26 +383,6 @@ components:
|
||||
required:
|
||||
- type
|
||||
|
||||
Http:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- http
|
||||
required:
|
||||
- type
|
||||
|
||||
Graphql:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- graphql
|
||||
required:
|
||||
- type
|
||||
|
||||
FlowStatus:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user