feat: improve ai flow (#2270)

* feat: improve ai flow

* fix: copilot status popup placement

* fix: step only approve flow inputs additions + nits

* fix: nits
This commit is contained in:
HugoCasa
2023-09-13 14:24:27 +02:00
committed by GitHub
parent 3118e144d0
commit 6a809bdca0
15 changed files with 899 additions and 520 deletions
+228 -142
View File
@@ -6,17 +6,23 @@
type FlowModule,
DraftService,
type PathScript,
RawScript,
ScriptService
ScriptService,
Script
} from '$lib/gen'
import { initHistory, redo, undo } from '$lib/history'
import { enterpriseLicense, hubScripts, userStore, workspaceStore } from '$lib/stores'
import { initHistory, push, redo, undo } from '$lib/history'
import {
enterpriseLicense,
existsOpenaiResourcePath,
hubScripts,
userStore,
workspaceStore
} from '$lib/stores'
import { encodeState, formatCron, sleep } from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import type { Drawer } from '$lib/components/common'
import { faCalendarAlt, faSave } from '@fortawesome/free-solid-svg-icons'
import { setContext } from 'svelte'
import { setContext, tick } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Badge, Button, Kbd, UndoRedo } from './common'
@@ -24,12 +30,13 @@
import FlowEditor from './flows/FlowEditor.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import type { FlowState } from './flows/flowState'
import { dfs } from './flows/flowStore'
import { dfs as dfsApply } from './flows/flowStore'
import { dfs, getPreviousIds } from './flows/previousResults'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext } from './flows/types'
import { cleanInputs } from './flows/utils'
import { cleanInputs, emptyFlowModuleState } from './flows/utils'
import { Pen } from 'lucide-svelte'
import { loadHubScripts } from '$lib/scripts'
import { createEventDispatcher } from 'svelte'
@@ -41,11 +48,12 @@
glueCopilot,
type FlowCopilotContext
} from './copilot/flow'
import { numberToChars } from './flows/idUtils'
import type { Schema, SchemaProperty } from '$lib/common'
import FlowCopilotDrawer from './copilot/FlowCopilotDrawer.svelte'
import FlowCopilotStatus from './copilot/FlowCopilotStatus.svelte'
import { fade } from 'svelte/transition'
import { loadFlowModuleState } from './flows/flowStateUtils'
import FlowCopilotInputsModal from './copilot/FlowCopilotInputsModal.svelte'
export let initialPath: string = ''
export let selectedId: string | undefined
@@ -342,7 +350,7 @@
return [
'settings-metadata',
'constants',
...dfs($flowStore.value.modules, (module) => module.id)
...dfsApply($flowStore.value.modules, (module) => module.id)
]
}
@@ -366,7 +374,8 @@
let flowCopilotContext: FlowCopilotContext = {
drawerStore: writable<Drawer | undefined>(undefined),
modulesStore: writable<FlowCopilotModule[]>([]),
currentStepStore: writable<string | undefined>(undefined)
currentStepStore: writable<string | undefined>(undefined),
genFlow: undefined
}
setContext('FlowCopilotContext', flowCopilotContext)
@@ -378,7 +387,7 @@
} = flowCopilotContext
let doneTs = 0
async function hubCompletions(text: string, idx: number, type: 'trigger' | 'script') {
async function getHubCompletions(text: string, idx: number, type: 'trigger' | 'script') {
try {
// make sure we display the results of the last request last
const ts = Date.now()
@@ -414,9 +423,12 @@
let copilotLoading = false
let flowCopilotMode: 'trigger' | 'sequence' = 'trigger'
let copilotStatus: string = ''
let copilotFlowInputs: Record<string, SchemaProperty> = {}
let copilotFlowRequiredInputs: string[] = []
let openCopilotInputsModal = false
function getInitCopilotModules(mode: typeof flowCopilotMode): FlowCopilotModule[] {
return [
function setInitCopilotModules(mode: typeof flowCopilotMode) {
$copilotModulesStore = [
{
id: 'a',
type: mode === 'trigger' ? 'trigger' : 'script',
@@ -424,7 +436,8 @@
code: '',
hubCompletions: [],
selectedCompletion: undefined,
source: undefined
source: undefined,
lang: undefined
},
{
id: 'b',
@@ -433,38 +446,60 @@
code: '',
hubCompletions: [],
selectedCompletion: undefined,
source: undefined
source: undefined,
lang: undefined
}
]
}
$: {
copilotModulesStore.set(getInitCopilotModules(flowCopilotMode))
$: setInitCopilotModules(flowCopilotMode)
function applyCopilotFlowInputs() {
const properties = {
...($flowStore.schema?.properties as Record<string, SchemaProperty> | undefined),
...copilotFlowInputs
}
const required = [
...(($flowStore.schema?.required as string[] | undefined) ?? []),
...copilotFlowRequiredInputs
]
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties,
required,
type: 'object'
}
copilotFlowInputs = {}
copilotFlowRequiredInputs = []
}
async function genFlow(i: number) {
copilotLoading = true
copilotStatus = "Generating code for step '" + numberToChars(i) + "'..."
$copilotCurrentStepStore = numberToChars(i)
async function genFlow(idx: number, flowModules: FlowModule[], stepOnly = false) {
try {
abortController = new AbortController()
push(history, $flowStore)
let module = stepOnly ? $copilotModulesStore[0] : $copilotModulesStore[idx]
$flowStore.value.modules = $flowStore.value.modules.slice(0, i)
let prevCode = ''
if (i === 0) {
prevCode = ''
copilotLoading = true
copilotStatus = "Generating code for step '" + module.id + "'..."
$copilotCurrentStepStore = module.id
focusCopilot()
if (!stepOnly && flowModules.length > idx) {
select('')
await tick()
flowModules.splice(idx, flowModules.length - idx)
$flowStore = $flowStore
focusCopilot()
}
if (idx === 0 && !stepOnly) {
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {},
required: [],
type: 'object'
}
} else {
prevCode = ($flowStore.value.modules[i - 1].value as RawScript).content
}
let module = $copilotModulesStore[i]
if (module.type === 'trigger') {
if (!$scheduleStore.cron) {
$scheduleStore.cron = '0 */15 * * *'
@@ -472,8 +507,24 @@
$scheduleStore.enabled = true
}
let hubScript:
| {
content: string
lockfile?: string | undefined
schema?: any
language: string
summary?: string | undefined
}
| undefined = undefined
if (module.source === 'hub' && module.selectedCompletion) {
hubScript = await ScriptService.getHubScriptByPath({
path: module.selectedCompletion.path
})
}
const flowModule = {
id: numberToChars(i),
id: module.id,
stop_after_if:
module.type === 'trigger'
? {
@@ -484,16 +535,18 @@
value: {
input_transforms: {},
content: '',
language: RawScript.language.BUN,
language: (hubScript ? hubScript.language : module.lang ?? 'bun') as Script.language,
type: 'rawscript' as const
},
summary:
$copilotModulesStore[i].selectedCompletion?.summary ?? $copilotModulesStore[i].description
summary: module.selectedCompletion?.summary ?? module.description
}
if (i === 1 && $copilotModulesStore[i - 1].type === 'trigger') {
$flowStateStore[module.id] = emptyFlowModuleState()
if (stepOnly) {
flowModules.splice(idx, 0, flowModule)
} else if (idx === 1 && $copilotModulesStore[idx - 1].type === 'trigger') {
const loopModule: FlowModule = {
id: numberToChars(i) + '_loop',
id: module.id + '_loop',
value: {
type: 'forloopflow',
iterator: {
@@ -504,152 +557,167 @@
modules: [flowModule]
}
}
$flowStore.value.modules.push(loopModule)
const loopState = await loadFlowModuleState(loopModule)
$flowStateStore[loopModule.id] = loopState
flowModules.push(loopModule)
} else {
$flowStore.value.modules.push(flowModule)
flowModules.push(flowModule)
}
$copilotDrawerStore?.closeDrawer()
select(numberToChars(i))
await sleep(200)
select(module.id)
await tick()
focusCopilot()
$copilotModulesStore[i].editor?.setCode('')
const deltaStore = writable<string>('')
const unsubscribe = deltaStore.subscribe(async (delta) => {
$copilotModulesStore[i].editor?.append(delta)
})
await stepCopilot(module, deltaStore, prevCode, abortController)
unsubscribe()
let isFirstInLoop = false
const parents = dfs(module.id, $flowStore).slice(1)
if (
parents[0]?.value.type === 'forloopflow' &&
parents[0].value.modules[0].id === module.id
) {
isFirstInLoop = true
}
const prevNodeId = getPreviousIds(module.id, $flowStore, false)[0]
const pastModule: FlowModule | undefined = dfs(prevNodeId, $flowStore, false)[0]
copilotStatus = "Generating inputs for step '" + numberToChars(i) + "'..."
if (hubScript) {
module.editor?.setCode(hubScript.content)
} else if (module.source === 'custom') {
module.editor?.setCode('')
const deltaStore = writable<string>('')
const unsubscribe = deltaStore.subscribe(async (delta) => {
module.editor?.append(delta)
})
abortController = new AbortController()
await stepCopilot(
module,
deltaStore,
pastModule?.value.type === 'rawscript' ? pastModule.value.content : '',
pastModule?.value.type === 'rawscript' ? pastModule.value.language : undefined,
pastModule === undefined,
isFirstInLoop,
abortController
)
unsubscribe()
} else {
throw new Error('Invalid copilot module source')
}
copilotStatus = "Generating inputs for step '" + module.id + "'..."
await sleep(500) // make sure code was parsed
try {
let currentFlowModule = $flowStore.value.modules[i]
if (currentFlowModule.value.type === 'forloopflow') {
currentFlowModule = currentFlowModule.value.modules[0]
}
if (currentFlowModule.value.type === 'rawscript') {
if (flowModule.value.type === 'rawscript') {
const stepSchema: Schema = JSON.parse(JSON.stringify($flowStateStore[module.id].schema)) // deep copy
if (module.source === 'hub' && i >= 1) {
if (module.source === 'hub' && pastModule !== undefined && $existsOpenaiResourcePath) {
// ask AI to set step inputs
const pastModule = $flowStore.value.modules[i - 1]
abortController = new AbortController()
const inputs = await glueCopilot(
Object.keys(currentFlowModule.value.input_transforms),
Object.keys(flowModule.value.input_transforms),
pastModule.value.type === 'rawscript' ? pastModule.value.content : '',
i === 1 && $copilotModulesStore[i - 1].type === 'trigger',
pastModule.value.type === 'rawscript' ? pastModule.value.language : undefined,
isFirstInLoop,
abortController
)
// create flow inputs used by AI for autocompletion
Object.entries(inputs)
.filter(
([key, expr]) =>
key in stepSchema.properties &&
expr.startsWith('flow_inputs.') &&
!expr.startsWith('flow_inputs.iter')
)
.map(([key, _]) => {
const inputSchemaProperty = stepSchema.properties[key]
const isRequired = stepSchema.required.includes(key)
if ($flowStore.schema) {
$flowStore.schema.properties[key] = inputSchemaProperty
if (isRequired) {
$flowStore.schema.required.push(key)
}
} else {
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {
[key]: inputSchemaProperty
},
required: isRequired ? [key] : [],
type: 'object'
}
copilotFlowInputs = {}
copilotFlowRequiredInputs = []
Object.entries(inputs).forEach(([key, expr]) => {
if (
key in stepSchema.properties &&
expr.startsWith('flow_input.') &&
!expr.startsWith('flow_input.iter') &&
(!$flowStore.schema || !(key in $flowStore.schema.properties)) // prevent overriding flow inputs
) {
copilotFlowInputs[key] = stepSchema.properties[key]
if (stepSchema.required.includes(key)) {
copilotFlowRequiredInputs.push(key)
}
$flowStore.schema
})
flowModule.value.input_transforms = Object.entries(inputs).reduce(
(acc, [key, expr]) => {
acc[key] = {
type: 'javascript',
expr
}
return acc
},
{}
)
} else {
// create possible flow inputs for autocompletion
delete stepSchema.properties.prev_output
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {
...$flowStore.schema?.properties,
...stepSchema.properties
},
required: Array.from(
new Set([...$flowStore.schema?.required, ...stepSchema.required])
),
type: 'object'
}
})
if (!stepOnly) {
applyCopilotFlowInputs()
}
// programatically set step inputs
for (const key of Object.keys(currentFlowModule.value.input_transforms)) {
// set step inputs
Object.entries(inputs).forEach(([key, expr]) => {
flowModule.value.input_transforms[key] = {
type: 'javascript',
expr
}
})
} else {
if (module.source === 'hub' && pastModule !== undefined && !$existsOpenaiResourcePath) {
sendUserToast(
'For better input generation, enable Windmill AI in the workspace settings',
true
)
}
// create possible flow inputs for autocompletion
copilotFlowInputs = {}
copilotFlowRequiredInputs = []
Object.keys(flowModule.value.input_transforms).forEach((key) => {
if (key !== 'prev_output') {
const schema = $flowStateStore[module.id].schema
const schemaProperty = Object.entries(schema.properties).find(
(x) => x[0] === key
)?.[1]
if (schemaProperty) {
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {
...$flowStore.schema?.properties,
[key]: schemaProperty
},
required: schemaProperty.required
? Array.from(new Set([...$flowStore.schema?.required, key]))
: $flowStore.schema?.required,
type: 'object'
if (
schemaProperty &&
(!$flowStore.schema || !(key in $flowStore.schema.properties)) // prevent overriding flow inputs
) {
copilotFlowInputs[key] = schemaProperty
if (schema.required.includes(key)) {
copilotFlowRequiredInputs.push(key)
}
}
}
})
if (!stepOnly) {
applyCopilotFlowInputs()
}
// programatically set step inputs
for (const key of Object.keys(flowModule.value.input_transforms)) {
flowModule.value.input_transforms[key] = {
type: 'javascript',
expr:
key === 'prev_output'
? $copilotModulesStore[i - 1].type === 'trigger'
? isFirstInLoop
? 'flow_input.iter.value'
: 'results.' + $copilotModulesStore[i - 1].id
: pastModule
? 'results.' + pastModule.id
: 'flow_input.' + key
: 'flow_input.' + key
}
}
}
const wrappingFlowModule = $flowStore.value.modules[i]
if (wrappingFlowModule.value.type === 'forloopflow') {
wrappingFlowModule.value = {
...wrappingFlowModule.value,
modules: [flowModule]
}
$flowStore.value.modules[i] = wrappingFlowModule
} else {
$flowStore.value.modules[i] = flowModule
}
$flowStore = $flowStore // force rerendering
}
} catch (err) {
console.error(err)
}
copilotStatus =
"Waiting for the user to validate code and inputs of step '" + numberToChars(i) + "'"
if (stepOnly) {
openCopilotInputsModal = true
$copilotCurrentStepStore = undefined
copilotLoading = false
setInitCopilotModules(flowCopilotMode)
copilotStatus = ''
} else {
copilotStatus =
"Waiting for the user to validate code and inputs of step '" + module.id + "'"
}
} catch (err) {
if (stepOnly) {
copilotStatus = ''
$copilotCurrentStepStore = undefined
setInitCopilotModules(flowCopilotMode)
}
if (err?.message) {
sendUserToast('Failed to generate code: ' + err.message, true)
} else {
@@ -661,7 +729,9 @@
}
}
async function handleFlowGenInputs() {
flowCopilotContext.genFlow = genFlow
async function handleFlowCopilotInputs() {
copilotLoading = true
select('Input')
$copilotCurrentStepStore = 'Input'
@@ -734,14 +804,30 @@
})
}
$: $copilotCurrentStepStore !== undefined ? focusCopilot() : blurCopilot()
$: $copilotCurrentStepStore === undefined && blurCopilot()
</script>
<svelte:window on:keydown={onKeyDown} />
<FlowCopilotDrawer {hubCompletions} {genFlow} bind:flowCopilotMode />
{#if !$userStore?.operator}
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
<FlowCopilotInputsModal
on:confirmed={async () => {
applyCopilotFlowInputs()
copilotStatus = "Done! Just check the step's inputs and you're good to go!"
await sleep(3000)
copilotStatus = ''
}}
on:canceled={async () => {
copilotFlowInputs = {}
copilotFlowRequiredInputs = []
copilotStatus = "Done! Just check the step's inputs and you're good to go!"
await sleep(3000)
copilotStatus = ''
}}
bind:open={openCopilotInputsModal}
inputs={Object.keys(copilotFlowInputs)}
/>
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
<div class="flex flex-col flex-1 h-screen">
@@ -825,7 +911,7 @@
{copilotLoading}
bind:copilotStatus
{genFlow}
{handleFlowGenInputs}
{handleFlowCopilotInputs}
{abortController}
/>
@@ -6,7 +6,11 @@
export let placement: PopoverPlacement = 'bottom'
const [popperRef, popperContent] = createPopperActions({ placement })
const [popperRef, popperContent, getInstance] = createPopperActions({ placement })
export async function refresh() {
await getInstance()?.update()
}
let showTooltip = false
export function open() {
-110
View File
@@ -1,110 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let open: boolean = false
export let z = 'z-30'
const dispatch = createEventDispatcher()
export function closeDrawer(): void {
document.body.style.overflow = 'auto'
open = false
dispatch('close')
}
export function openDrawer(): void {
document.body.style.overflow = 'hidden'
open = true
dispatch('open')
}
function handleKeyUp(event: KeyboardEvent): void {
const key = event.key
if (key === 'Escape' || key === 'Esc') {
if (open) {
event.preventDefault()
closeDrawer()
}
}
}
</script>
<svelte:window on:keyup={handleKeyUp} />
{#if open}
<div class="blurred-background" />
<div class="fixed top-0 w-screen h-screen {z}">
<div
class="fixed right-0 top-0 flex flex-col w-3/4 sm:w-2/3 lg:w-1/2 h-screen border border-gray-300 shadow-xl"
>
{#if open}
<div class="flex flex-row justify-between p-2 bg-surface border-b border-gray-200">
<button
on:click={() => {
open = false
closeDrawer()
}}
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<p class="font-semibold text-gray-800"><slot name="title" /></p>
<div />
</div>
<div class="flex flex-col bg-gray-50 pt-3 px-6 grow overflow-y-auto">
<slot name="content" />
</div>
<div class="flex flex-col bg-surface border-gray-200 p-2">
<div class="flex flex-row justify-between p-2">
<button
on:click={() => {
closeDrawer()
}}
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<span class="mr-4"><slot name="submission">&nbsp;</slot></span>
</div>
</div>
{/if}
</div>
</div>
{/if}
<style lang="postcss">
.blurred-background {
/* @apply absolute sm:top-6 lg:top-8 left-28 sm:left-40 md:left-48; */ /* If we wanted to make the navbars visible */
@apply fixed top-0 left-0;
@apply bg-gray-400 opacity-75;
@apply w-screen;
@apply h-screen;
z-index: 10;
}
</style>
@@ -76,6 +76,7 @@
</div>
</div>
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
<slot name="actions" />
<Button
on:click={() => {
dispatch('canceled')
@@ -9,11 +9,16 @@
import { APP_TO_ICON_COMPONENT } from '../icons'
import { charsToNumber, numberToChars } from '../flows/idUtils'
import type { FlowCopilotContext } from './flow'
import Alert from '../common/alert/Alert.svelte'
import type { FlowEditorContext } from '../flows/types'
import type { FlowModule } from '$lib/gen'
export let hubCompletions: (text: string, idx: number, type: 'trigger' | 'script') => void
export let genFlow: (index: number) => void
export let getHubCompletions: (text: string, idx: number, type: 'trigger' | 'script') => void
export let genFlow: (index: number, modules: FlowModule[], stepOnly?: boolean) => void
export let flowCopilotMode: 'trigger' | 'sequence'
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
const { drawerStore, modulesStore, currentStepStore } =
getContext<FlowCopilotContext>('FlowCopilotContext')
</script>
@@ -21,7 +26,17 @@
<Drawer bind:this={$drawerStore}>
<DrawerContent on:close={$drawerStore.closeDrawer} title="AI Flow Builder">
<div class="flex flex-col gap-6">
<ToggleButtonGroup bind:selected={flowCopilotMode}>
{#if $flowStore.value.modules.length > 0 && $currentStepStore === undefined}
<Alert type="error" title="Flow not empty">All flow steps will be overriden</Alert>
{/if}
<ToggleButtonGroup
bind:selected={flowCopilotMode}
on:selected={() => {
if ($currentStepStore !== undefined) {
$currentStepStore = numberToChars(0)
}
}}
>
<ToggleButton value="trigger" label="Trigger" />
<ToggleButton value="sequence" label="Sequence" />
</ToggleButtonGroup>
@@ -30,7 +45,7 @@
{#if i === 1 && $modulesStore[i - 1].type === 'trigger'}
<div class="flex flex-row items-center mb-4 gap-1">
<p class="text-sm font-semibold">For loop</p>
<Badge color="indigo">{numberToChars(i)}_loop</Badge>
<Badge color="indigo">{copilotModule.id}_loop</Badge>
</div>
{/if}
<div class={i === 1 && $modulesStore[i - 1].type === 'trigger' ? 'pl-4' : ''}>
@@ -40,13 +55,19 @@
<p class="text-sm font-semibold"
>{copilotModule.type === 'trigger' ? 'Trigger' : 'Action'}</p
>
<Badge color="indigo">{numberToChars(i)}</Badge>
<Badge color="indigo">{copilotModule.id}</Badge>
</div>
{#if flowCopilotMode === 'sequence' && i >= 1}
<button
on:click={() => {
if ($currentStepStore !== undefined) {
$currentStepStore = numberToChars(i < $modulesStore.length - 1 ? i : i - 1)
}
modulesStore.update((prev) => {
prev.splice(i, 1)
prev.forEach((m, idx) => {
m.id = numberToChars(idx)
})
return prev
})
}}
@@ -60,9 +81,7 @@
<div
class={classNames(
'p-4 gap-4 flex flex-row grow transition-all items-center rounded-md justify-between border',
$currentStepStore !== undefined &&
$currentStepStore !== 'Input' &&
i < charsToNumber($currentStepStore)
$currentStepStore !== undefined && i < charsToNumber($currentStepStore)
? 'bg-gray-700/10'
: 'bg-surface'
)}
@@ -84,7 +103,9 @@
<div class="text-primary flex-wrap text-sm font-medium">
{copilotModule.source === 'hub' && copilotModule.selectedCompletion
? copilotModule.selectedCompletion.summary
: copilotModule.description}
: `Generate "${copilotModule.description}" in ${
copilotModule.lang === 'bun' ? 'TypeScript' : 'Python'
}`}
</div>
</div>
</div>
@@ -96,13 +117,13 @@
<button
on:click={() => {
copilotModule.selectedCompletion = undefined
copilotModule.source = undefined
if (
$currentStepStore !== undefined &&
$currentStepStore !== 'Input' &&
i < charsToNumber($currentStepStore)
) {
copilotModule = {
...copilotModule,
selectedCompletion: undefined,
source: undefined,
lang: undefined
}
if ($currentStepStore !== undefined && i < charsToNumber($currentStepStore)) {
$currentStepStore = numberToChars(i)
}
}}
@@ -110,7 +131,7 @@
<Icon data={faClose} />
</button>
</div>
{#if $currentStepStore !== undefined && $currentStepStore !== 'Input' && i < charsToNumber($currentStepStore)}
{#if $currentStepStore !== undefined && i < charsToNumber($currentStepStore)}
<p class="font-semibold text-sm text-green-600"
>Already generated, edit step to regenerate from this point</p
>
@@ -125,7 +146,7 @@
bind:value={copilotModule.description}
on:input={() => {
if (copilotModule.description.length > 2) {
hubCompletions(copilotModule.description, i, copilotModule.type)
getHubCompletions(copilotModule.description, i, copilotModule.type)
} else {
copilotModule.hubCompletions = []
}
@@ -133,27 +154,58 @@
/>
{/if}
{#if copilotModule.description.length > 2 && copilotModule.source === undefined}
<button
class="mt-2 p-4 gap-4 flex flex-row hover:bg-surface-hover bg-surface transition-all items-center rounded-md justify-between w-full border"
on:click={() => {
copilotModule.source = 'custom'
copilotModule.selectedCompletion = undefined
}}
>
<div class="flex items-center gap-4">
<div
class="rounded-md p-1 flex justify-center items-center bg-surface border w-6 h-6"
>
<Icon data={faMagicWandSparkles} />
</div>
<div class="divide-y border rounded-md transition-all mt-2">
<button
class="p-4 gap-4 flex flex-row hover:bg-surface-hover bg-surface transition-all items-center rounded-md justify-between w-full"
on:click={() => {
copilotModule = {
...copilotModule,
source: 'custom',
selectedCompletion: undefined,
lang: 'bun'
}
}}
>
<div class="flex items-center gap-4">
<div
class="rounded-md p-1 flex justify-center items-center bg-surface border w-6 h-6"
>
<Icon data={faMagicWandSparkles} />
</div>
<div class="w-full text-left text-sm">
<div class="text-primary flex-wrap font-medium">
Generate step from scratch using AI
<div class="w-full text-left text-sm">
<div class="text-primary flex-wrap font-medium">
Generate "{copilotModule.description}" in TypeScript
</div>
</div>
</div>
</div>
</button>
</button>
<button
class="p-4 gap-4 flex flex-row hover:bg-surface-hover bg-surface transition-all items-center rounded-md justify-between w-full"
on:click={() => {
copilotModule = {
...copilotModule,
source: 'custom',
selectedCompletion: undefined,
lang: 'python3'
}
}}
>
<div class="flex items-center gap-4">
<div
class="rounded-md p-1 flex justify-center items-center bg-surface border w-6 h-6"
>
<Icon data={faMagicWandSparkles} />
</div>
<div class="w-full text-left text-sm">
<div class="text-primary flex-wrap font-medium">
Generate "{copilotModule.description}" in Python
</div>
</div>
</div>
</button>
</div>
{#if copilotModule.hubCompletions.length > 0}
<p class="mt-2 font-semibold text-sm">Hub scripts</p>
<ul class="divide-y border rounded-md transition-all mt-1">
@@ -162,8 +214,12 @@
<button
class="p-4 gap-4 flex flex-row hover:bg-surface-hover bg-surface transition-all items-center rounded-md justify-between w-full"
on:click={() => {
copilotModule.source = 'hub'
copilotModule.selectedCompletion = item
copilotModule = {
...copilotModule,
source: 'hub',
selectedCompletion: item,
lang: undefined
}
}}
>
<div class="flex items-center gap-4">
@@ -207,7 +263,8 @@
code: '',
source: undefined,
hubCompletions: [],
selectedCompletion: undefined
selectedCompletion: undefined,
lang: undefined
}
])}>Add step</Button
>
@@ -216,14 +273,14 @@
<Button
on:click={() =>
$currentStepStore !== undefined && $currentStepStore !== 'Input'
? genFlow(charsToNumber($currentStepStore))
: genFlow(0)}
$currentStepStore !== undefined
? genFlow(charsToNumber($currentStepStore), $flowStore.value.modules)
: genFlow(0, $flowStore.value.modules)}
spacingSize="md"
startIcon={{ icon: faMagicWandSparkles }}
disabled={$modulesStore.find((m) => m.source === undefined) !== undefined}
>
{$currentStepStore !== undefined && $currentStepStore !== 'Input'
{$currentStepStore !== undefined
? `Regenerate flow from step '${$currentStepStore}'`
: 'Build flow'}
</Button>
@@ -0,0 +1,39 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Button, Badge } from '../common'
import Modal from '../common/modal/Modal.svelte'
export let open = false
export let inputs: string[] = []
const dispatch = createEventDispatcher()
</script>
<Modal
{open}
on:confirmed={() => {
open = false
dispatch('confirmed')
}}
on:canceled
title="Windmill AI wants to add the following inputs to the flow:"
>
<ul class="text-lg list-disc pl-5">
{#each inputs as input}
<li>{input}</li>
{/each}
</ul>
<Button
slot="actions"
on:click={() => {
open = false
dispatch('confirmed')
}}
color="light"
size="sm"
>
<span class="inline-flex gap-2">Add <Badge color="dark-green">Enter</Badge></span>
</Button>
</Modal>
@@ -8,19 +8,26 @@
import { charsToNumber } from '../flows/idUtils'
import { existsOpenaiResourcePath } from '$lib/stores'
import Popup from '../common/popup/Popup.svelte'
import type { FlowModule } from '$lib/gen'
import type { FlowEditorContext } from '../flows/types'
import { ExternalLink } from 'lucide-svelte'
export let copilotLoading: boolean
export let copilotStatus: string
export let abortController: AbortController | undefined
export let genFlow: (index: number) => void
export let handleFlowGenInputs: () => void
export let genFlow: (index: number, modules: FlowModule[], stepOnly?: boolean) => void
export let handleFlowCopilotInputs: () => void
let copilotPopover: ManualPopover | undefined = undefined
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
const { modulesStore, drawerStore, currentStepStore } =
getContext<FlowCopilotContext>('FlowCopilotContext')
$: copilotStatus.length > 0 ? copilotPopover?.open() : copilotPopover?.close()
$: copilotStatus && copilotPopover?.refresh()
</script>
{#if $existsOpenaiResourcePath}
@@ -62,8 +69,8 @@
? 'Exit'
: 'AI Flow Builder'}
</Button>
<div slot="content" class="text-sm flex flex-row items-center z-[901]"
><span class="font-semibold">
<div slot="content" class="text-sm flex flex-row items-center z-[901]">
<span class="font-semibold">
{copilotStatus}
</span>
{#if !copilotLoading && $currentStepStore !== undefined && $currentStepStore !== 'Input'}
@@ -85,9 +92,9 @@
}
const stepNb = charsToNumber($currentStepStore)
if (stepNb >= $modulesStore.length - 1) {
handleFlowGenInputs()
handleFlowCopilotInputs()
} else {
genFlow(stepNb + 1)
genFlow(stepNb + 1, $flowStore.value.modules)
}
}}
>
@@ -95,11 +102,16 @@
? 'Flow inputs'
: 'Next step'}
</Button>
{/if}</div
>
{/if}
</div>
</ManualPopover>
{:else}
<Popup>
<Popup
floatingConfig={{
strategy: 'absolute',
placement: 'bottom'
}}
>
<svelte:fragment slot="button">
<Button
size="xs"
@@ -116,7 +128,11 @@
</svelte:fragment>
<div class="block text-primary">
<p class="text-sm"
>Enable Windmill AI in the <a href="/workspace_settings?tab=openai">workspace settings.</a
>Enable Windmill AI in the <a
href="/workspace_settings?tab=openai"
target="_blank"
class="inline-flex flex-row items-center gap-1"
>workspace settings <ExternalLink size={16} /></a
></p
>
</div>
@@ -181,28 +181,35 @@
</Button>
</svelte:fragment>
{@const fixAction = (_) => {
onFix(() => close(null))
if ($existsOpenaiResourcePath) {
onFix(() => close(null))
}
}}
<div use:fixAction>
<div class="w-[42rem] min-h-[3rem] max-h-[34rem] overflow-y-scroll">
{#if $generatedCode.length > 0}
<div class="overflow-x-scroll">
<HighlightCode language={lang} code={$generatedCode} />
</div>
{#if $generatedExplanation.length > 0}
<p class="text-sm mt-2"
><span class="font-bold">Explanation:</span> {$generatedExplanation}</p
>
{#if $existsOpenaiResourcePath}
<div class="w-[42rem] min-h-[3rem] max-h-[34rem] overflow-y-scroll">
{#if $generatedCode.length > 0}
<div class="overflow-x-scroll">
<HighlightCode language={lang} code={$generatedCode} />
</div>
{#if $generatedExplanation.length > 0}
<p class="text-sm mt-2"
><span class="font-bold">Explanation:</span> {$generatedExplanation}</p
>
{/if}
{:else}
<LoadingIcon />
{/if}
{:else}
<LoadingIcon />
{/if}
</div>
{#if !$existsOpenaiResourcePath}
<p class="text-sm"
>Enable Windmill AI in the <a href="/workspace_settings?tab=openai"
>workspace settings.</a
></p
</div>
{:else}
<div class="w-80">
<p class="text-sm"
>Enable Windmill AI in the <a
class="inline-flex flex-row items-center gap-1"
href="/workspace_settings?tab=openai"
target="_blank">workspace settings</a
></p
></div
>
{/if}
</div>
@@ -27,6 +27,7 @@
import LoadingIcon from '../apps/svelte-select/lib/LoadingIcon.svelte'
import { sleep } from '$lib/utils'
import { autoPlacement } from '@floating-ui/core'
import { ExternalLink } from 'lucide-svelte'
// props
export let iconOnly: boolean = false
@@ -309,8 +310,12 @@
{/if}
{:else}
<p class="text-sm"
>Enable Windmill AI in the <a href="/workspace_settings?tab=openai">workspace settings.</a
></p
>Enable Windmill AI in the <a
href="/workspace_settings?tab=openai"
target="_blank"
class="inline-flex flex-row items-center gap-1"
>workspace settings <ExternalLink size={16} />
</a></p
>
{/if}
</div>
@@ -0,0 +1,184 @@
<script lang="ts">
import { faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
import { Icon } from 'svelte-awesome'
import { existsOpenaiResourcePath, hubScripts } from '$lib/stores'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../flows/types'
import type { FlowCopilotContext, FlowCopilotModule } from './flow'
import { nextId } from '../flows/flowStateUtils'
import { ScriptService, type FlowModule } from '$lib/gen'
import { APP_TO_ICON_COMPONENT } from '../icons'
import { sendUserToast } from '$lib/toast'
export let index: number
export let open: boolean | undefined
export let close: () => void
export let funcDesc: string
export let modules: FlowModule[]
// state
let input: HTMLInputElement | undefined
let hubCompletions: FlowCopilotModule['hubCompletions'] = []
let selectedCompletion: FlowCopilotModule['selectedCompletion'] = undefined
let lang: FlowCopilotModule['lang'] = undefined
const { flowStore, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
//
const { modulesStore: copilotModulesStore, genFlow } =
getContext<FlowCopilotContext>('FlowCopilotContext')
let doneTs = 0
async function getHubCompletions(text: string) {
try {
// make sure we display the results of the last request last
const ts = Date.now()
const scriptIds = await ScriptService.queryHubScripts({
text: `${text}`,
limit: 3
})
if (ts < doneTs) return
doneTs = ts
const scripts = scriptIds
.map((qs) => {
const s = $hubScripts?.find((hs) => hs.ask_id === Number(qs.id))
return s
})
.filter((s) => !!s)
hubCompletions = scripts as FlowCopilotModule['hubCompletions']
} catch (err) {
if (err.name !== 'CancelError') throw err
}
}
async function onGenerate() {
if (!selectedCompletion && !$existsOpenaiResourcePath) {
sendUserToast(
'Windmill AI is not enabled, you can activate it in the workspace settings',
true
)
return
}
$copilotModulesStore = [
{
id: nextId($flowStateStore, $flowStore),
type: 'script',
description: funcDesc,
code: '',
source: selectedCompletion ? 'hub' : 'custom',
hubCompletions,
selectedCompletion,
editor: undefined,
lang
}
]
genFlow?.(index, modules, true)
}
$: {
if (open) {
setTimeout(() => {
input?.focus()
}, 0)
}
}
</script>
<div class="text-primary transition-all {funcDesc.length > 0 ? 'w-96' : 'w-60'}">
<div>
<div class="flex p-2">
<input
type="text"
bind:this={input}
bind:value={funcDesc}
on:input={() => {
if (funcDesc.length > 2) {
getHubCompletions(funcDesc)
} else {
hubCompletions = []
}
}}
placeholder="AI Gen &#xf0d0; or search hub scripts"
style="font-family:Inter, FontAwesome"
/>
</div>
{#if funcDesc.length > 0}
<ul class="transition-all divide-y">
<li>
<button
class="py-2 gap-4 flex flex-row hover:bg-surface-hover transition-all items-center justify-between w-full"
on:click={() => {
lang = 'bun'
onGenerate()
close()
}}
>
<div class="flex items-center gap-2.5 px-2">
<div
class="rounded-md p-1 flex justify-center items-center bg-surface border w-6 h-6"
>
<Icon data={faMagicWandSparkles} scale={0.8} />
</div>
<div class="text-left text-xs text-secondary">
Generate "{funcDesc}" in TypeScript
</div>
</div>
</button>
</li>
<li>
<button
class="py-2 gap-4 flex flex-row hover:bg-surface-hover transition-all items-center justify-between w-full"
on:click={() => {
lang = 'python3'
onGenerate()
close()
}}
>
<div class="flex items-center gap-2.5 px-2">
<div
class="rounded-md p-1 flex justify-center items-center bg-surface border w-6 h-6"
>
<Icon data={faMagicWandSparkles} scale={0.8} />
</div>
<div class="text-left text-xs text-secondary">
Generate "{funcDesc}" in Python
</div>
</div>
</button>
</li>
</ul>
{/if}
{#if hubCompletions.length > 0}
<div class="text-left mt-2">
<p class="text-xs text-secondary ml-2">Hub Scripts</p>
<ul class="transition-all divide-y">
{#each hubCompletions as item (item.path)}
<li>
<button
class="py-2 gap-4 flex flex-row hover:bg-surface-hover transition-all items-center justify-between w-full"
on:click={() => {
selectedCompletion = item
close()
onGenerate()
}}
>
<div class="flex items-center gap-2.5 px-2">
<div
class="rounded-md p-1 flex justify-center items-center bg-surface border w-6 h-6"
>
<svelte:component this={APP_TO_ICON_COMPONENT[item['app']]} />
</div>
<div class="text-left text-xs text-secondary">
{item.summary ?? ''}
</div>
</div>
</button>
</li>
{/each}
</ul>
</div>
{/if}
</div>
</div>
+108 -55
View File
@@ -1,8 +1,9 @@
import { ScriptService, type Script } from '$lib/gen'
import type { Script, FlowModule } from '$lib/gen'
import { addResourceTypes, deltaCodeCompletion, getNonStreamingCompletion } from './lib'
import type { Writable } from 'svelte/store'
import type Editor from '../Editor.svelte'
import type { Drawer } from '../common'
import { scriptLangToEditorLang } from '$lib/scripts'
export type FlowCopilotModule = {
id: string
@@ -10,6 +11,7 @@ export type FlowCopilotModule = {
description: string
code: string
source: 'hub' | 'custom' | undefined
lang: 'bun' | 'python3' | undefined
hubCompletions: {
path: string
summary: string
@@ -35,6 +37,7 @@ export type FlowCopilotContext = {
drawerStore: Writable<Drawer | undefined>
modulesStore: Writable<FlowCopilotModule[]>
currentStepStore: Writable<string | undefined>
genFlow: ((i: number, modules: FlowModule[], stepOnly?: boolean) => Promise<void>) | undefined
}
const systemPrompt = `You write code as instructed by the user. Only output code. Wrap the code in a code block.
@@ -42,35 +45,67 @@ Put explanations directly in the code as comments.
Here's how interactions have to look like:
user: {sample_question}
assistant: \`\`\`typescript
assistant: \`\`\`{codeLang}
{code}
\`\`\``
const additionalInformation = `Additional information: We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
const additionalInfos: {
bun: string
python3: string
} = {
bun: `Additional information: We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
You have access to the following resource types, if you need them, you have to define the type exactly as specified and add them as parameters: {resourceTypes}
Only use the ones you need. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.`
Only use the ones you need. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.`,
python3: `Additional information: We have to export a "main" function and specify the parameter types but do not call it.
You have access to the following resource types, if you need them, you have to define the TypedDict exactly as specified (class name has to be IN LOWERCASE) and add them as parameters: {resourceTypes}
Only use the ones you need. If the TypedDict name conflicts with the imported object, rename the imported object NOT THE TYPE.`
}
const triggerPrompt = `I'm building a workflow which is a sequence of script steps. Write the first script in typescript which should check for {description} and return an array.
const triggerPrompts: {
bun: string
python3: string
} = {
bun: `I'm building a workflow which is a sequence of script steps. Write the first script in {codeLang} which should check for {description} and return an array.
You can use "const {state_name}: {state_type} = getState(...)" and "setState(...)" from "npm:windmill-client@1" to maintain state across runs.
${additionalInformation}`
{additionalInformation}`,
python3: `I'm building a workflow which is a sequence of script steps. Write the first script in {codeLang} which should check for {description} and return an array.
You can use get_state and set_state from wmill to maintain state across runs.
const firstActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in typescript which should {description}.
{additionalInformation}`
}
const firstActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}.
Return the script's output.
${additionalInformation}`
{additionalInformation}`
const actionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in typescript which should {description} using as a parameter called "prev_output" the output of the previous script.
Infer the type of "prev_output" from the previous script: \`\`\`typescript\n{prevCode}\n\`\`\`.
const inferTypePrompt =
'Infer the type of "prev_output" from the previous\'s step code: ```{codeLang}\n{prevCode}\n```'
const actionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}. It should take a parameter called "prev_output" which contains the output of the previous script.
{inferTypePrompt}
Return the script's output.
${additionalInformation}`
{additionalInformation}`
const inferTypeLoopPrompt =
'Infer the type of "prev_output" from the previous\'s step code: ```{codeLang}\n{prevCode}\n```, keeping in mind that it is ONE ELEMENT of the output of the previous step.'
const loopActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}. It should take a parameter called "prev_output" which contains ONE ELEMEMT of the output of the previous script.
{inferTypePrompt}
Return the script's output.
{additionalInformation}`
const inferTypeGluePrompt =
"Infer its type from the previous's step code: ```{codeLang}\n{prevCode}\n```"
const loopGluePrompt = `I'm building a workflow which is a sequence of script steps.
My current step code has the following inputs: {inputs}.
Determine what to pass as inputs. You can only use the following:
- \`flow_input\` (javascript object): general inputs that are passed to the workflow, you can assume any object properties.
- \`flow_input.iter.value\` (javascript object): it is ONE ELEMENT of the output of the previous step. Infer its type from the previous's step code: \`\`\`typescript\n{prevCode}\n\`\`\`
- \`flow_input.iter.value\` (javascript object): it is ONE ELEMENT of the output of the previous step. {inferTypeGluePrompt}
Reply in the following format:
input_name: expr`
@@ -79,7 +114,7 @@ const gluePrompt = `I'm building a workflow which is a sequence of script steps.
My current step code has the following inputs: {inputs}.
Determine what to pass as inputs. You can only use the following:
- \`flow_input\` (javascript object): general inputs that are passed to the workflow, you can assume any object properties.
- \`prev_output\` (javascript object): previous output is the output of the previous step. Infer its type from the previous's step code: \`\`\`typescript\n{prevCode}\n\`\`\`
- \`prev_output\` (javascript object): previous output is the output of the previous step. {inferTypeGluePrompt}
Reply in the following format:
input_name: expr`
@@ -88,65 +123,83 @@ export async function stepCopilot(
module: FlowCopilotModule,
deltaCodeStore: Writable<string>,
prevCode: string,
prevLang: Script.language | undefined,
isFirstAction: boolean,
isFirstInLoop: boolean,
abortController: AbortController
) {
if (module.source === undefined) {
throw new Error('Module not configured')
if (module.source !== 'custom') {
throw new Error('Not a custom module')
}
if (module.source === 'hub' && module.selectedCompletion) {
const hubScript = await ScriptService.getHubScriptByPath({
path: module.selectedCompletion.path
})
deltaCodeStore.set(hubScript.content)
return hubScript.content
} else {
let prompt =
module.type === 'trigger'
? triggerPrompt
: prevCode.length > 0
? actionPrompt
: firstActionPrompt
prompt = prompt.replace('{description}', module.description).replace('{prevCode}', prevCode)
prompt = await addResourceTypes(
const lang = module.lang ?? 'bun'
const codeLang = lang === 'python3' ? 'python' : 'typescript (Node.js)'
let prompt =
module.type === 'trigger'
? triggerPrompts[lang]
: isFirstAction
? firstActionPrompt
: isFirstInLoop
? loopActionPrompt
: actionPrompt
prompt = prompt
.replace('{codeLang}', codeLang)
.replace(
'{inferTypePrompt}',
prevCode.length > 0 && prevLang
? (isFirstInLoop ? inferTypeLoopPrompt : inferTypePrompt)
.replace('{prevCode}', prevCode)
.replace('{codeLang}', scriptLangToEditorLang(prevLang))
: ''
)
.replace('{additionalInformation}', additionalInfos[lang])
.replace('{description}', module.description)
prompt = await addResourceTypes(
{
type: 'gen',
language: lang as Script.language,
description: module.description,
dbSchema: undefined
},
prompt
)
const code = await deltaCodeCompletion(
[
{
type: 'gen',
language: 'bun' as Script.language,
description: module.description,
dbSchema: undefined
role: 'system',
content: systemPrompt
},
prompt
)
const code = await deltaCodeCompletion(
[
{
role: 'system',
content: systemPrompt
},
{
role: 'user',
content: prompt
}
],
deltaCodeStore,
abortController
)
return code
}
{
role: 'user',
content: prompt
}
],
deltaCodeStore,
abortController
)
return code
}
export async function glueCopilot(
inputs: string[],
prevCode: string,
isLoop: boolean,
prevLang: Script.language | undefined,
isFirstInLoop: boolean,
abortController: AbortController
) {
let response = await getNonStreamingCompletion(
[
{
role: 'user',
content: (isLoop ? loopGluePrompt : gluePrompt)
content: (isFirstInLoop ? loopGluePrompt : gluePrompt)
.replace('{inputs}', inputs.join(', '))
.replace('{prevCode}', prevCode)
.replace(
'{inferTypeGluePrompt}',
prevCode.length > 0 && prevLang
? inferTypeGluePrompt
.replace('{prevCode}', prevCode)
.replace('{codeLang}', scriptLangToEditorLang(prevLang))
: ''
)
}
],
abortController
@@ -10,13 +10,27 @@
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { Cross, Repeat, Square } from 'lucide-svelte'
import StepGen from '$lib/components/copilot/StepGen.svelte'
import type { FlowModule } from '$lib/gen'
const dispatch = createEventDispatcher()
export let trigger = false
export let stop = false
export let open: boolean | undefined = undefined
export let index: number
export let funcDesc = ''
export let modules: FlowModule[]
$: !open && (funcDesc = '')
</script>
<svelte:head>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
rel="stylesheet"
/>
</svelte:head>
<Menu
transitionDuration={0}
pointerDown
@@ -33,107 +47,110 @@
>
<Cross size={12} />
</button>
<div class="font-mono divide-y text-xs w-40 text-secondary">
<button
class="w-full text-left p-2 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'script')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faCode} scale={0.8} class="mr-2" />
Action
</button>
{#if trigger}
<StepGen {index} bind:funcDesc bind:open {close} {modules} />
{#if funcDesc.length === 0}
<div class="font-mono divide-y text-xs w-full text-secondary">
<button
class="w-full text-left p-2 hover:bg-surface-hover"
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'trigger')
dispatch('new', 'script')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faBolt} scale={0.8} class="mr-2" />
Trigger
<Icon data={faCode} scale={0.8} class="mr-2" />
Action
</button>
{/if}
<button
class="w-full text-left gap-1 p-2 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'approval')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faCheck} class="mr-1.5" scale={0.8} />
Approval
</button>
<button
class="w-full inline-flex text-left p-2 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'forloop')
}}
role="menuitem"
>
<span class="mr-3">
<Repeat size={14} />
</span>
For Loop
</button>
<button
class="w-full text-left p-2 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'branchone')
}}
role="menuitem"
>
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
Branch to one
</button>
<button
class="w-full text-left p-2 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'branchall')
}}
role="menuitem"
>
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
Branch to all
</button>
<button
class="w-full text-left p-2 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'flow')
}}
role="menuitem"
>
<Icon data={faBarsStaggered} scale={0.8} class="mr-2" />
Flow
</button>
{#if stop}
{#if trigger}
<button
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'trigger')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faBolt} scale={0.8} class="mr-2" />
Trigger
</button>
{/if}
<button
class="w-full text-left p-2 hover:bg-surface-hover inline-flex gap-2.5"
class="w-full text-left gap-1 py-2 px-3 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'end')
dispatch('new', 'approval')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faCheck} class="mr-1.5" scale={0.8} />
Approval
</button>
<button
class="w-full inline-flex text-left py-2 px-3 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'forloop')
}}
role="menuitem"
>
<Square size={14} />
End Flow
<span class="mr-3">
<Repeat size={14} />
</span>
For Loop
</button>
{/if}
</div>
<button
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'branchone')
}}
role="menuitem"
>
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
Branch to one
</button>
<button
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'branchall')
}}
role="menuitem"
>
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
Branch to all
</button>
<button
class="w-full text-left py-2 px-3 hover:bg-surface-hover rounded-none"
on:pointerdown={() => {
close()
dispatch('new', 'flow')
}}
role="menuitem"
>
<Icon data={faBarsStaggered} scale={0.8} class="mr-2" />
Flow
</button>
{#if stop}
<button
class="w-full text-left py-2 px-3 hover:bg-surface-hover inline-flex gap-2.5"
on:pointerdown={() => {
close()
dispatch('new', 'end')
}}
role="menuitem"
>
<Square size={14} />
End Flow
</button>
{/if}
</div>
{/if}
</Menu>
@@ -78,6 +78,8 @@
on:new={(e) => {
dispatch('insert', { modules, index: idx, detail: e.detail })
}}
index={idx}
{modules}
/>
{/if}
</div>
@@ -212,6 +214,8 @@
on:new={(e) => {
dispatch('insert', { modules, index: idx + 1, detail: e.detail })
}}
index={idx + 1}
{modules}
/>
{/if}
</div>
@@ -3,13 +3,13 @@
import type { FlowModule } from '$lib/gen'
import { classNames } from '$lib/utils'
import { faBolt, faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
import { ClipboardCopy, X } from 'lucide-svelte'
import { ClipboardCopy, ExternalLink, X } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
import InsertModuleButton from './InsertModuleButton.svelte'
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import { existsOpenaiResourcePath } from '$lib/stores'
import Popup from '$lib/components/common/popup/Popup.svelte'
import Menu from '$lib/components/common/menu/Menu.svelte'
export let label: string
export let modules: FlowModule[] | undefined
@@ -34,6 +34,7 @@
deleteBranch: { module: FlowModule; index: number }
}>()
let openMenu = false
let openNoCopilot = false
const { drawerStore: copilotDrawerStore, currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
@@ -127,12 +128,53 @@
})
}
}}
index={whereInsert == 'after' ? index : index - 1}
modules={modules ?? []}
/>
{/if}
</div>
{/if}
{#if insertable && modules && label == 'Input'}
<div
class="{openNoCopilot
? 'z-10'
: ''} w-9 absolute -top-10 left-[50%] right-[50%] -translate-x-1/2"
>
<Menu pointerDown noMinW placement="bottom-center" let:close bind:show={openNoCopilot}>
<button
title="AI Flow Builder"
on:pointerdown={$existsOpenaiResourcePath
? (ev) => {
ev.preventDefault()
ev.stopPropagation()
$copilotDrawerStore?.openDrawer()
}
: undefined}
slot="trigger"
type="button"
class="text-primary bg-surface 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"
>
<Icon data={faMagicWandSparkles} scale={1} />
</button>
{#if !$existsOpenaiResourcePath}
<div class="text-primary p-4">
<p class="text-sm w-80"
>Enable Windmill AI in the <a
href="/workspace_settings?tab=openai"
target="_blank"
class="inline-flex flex-row items-center gap-1"
on:click={() => {
close()
}}
>workspace settings
<ExternalLink size={16} /></a
></p
>
</div>
{/if}
</Menu>
</div>
<div class="w-7 absolute top-12 left-[65%] right-[35%] -translate-x-1/2">
<button
title="Add a Trigger"
@@ -147,35 +189,4 @@
<Icon data={faBolt} scale={0.8} />
</button>
</div>
<div class="w-7 absolute top-12 left-[80%] -translate-x-1/2">
<Popup let:close>
<svelte:fragment slot="button">
<button
title="AI Flow Builder"
on:click={$existsOpenaiResourcePath
? (ev) => {
ev.preventDefault()
ev.stopPropagation()
$copilotDrawerStore?.openDrawer()
}
: undefined}
type="button"
class="text-primary bg-surface 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-6 h-6 flex items-center justify-center"
>
<Icon data={faMagicWandSparkles} scale={0.8} />
</button>
</svelte:fragment>
<div class="block text-primary">
<p class="text-sm"
>Enable Windmill AI in the <a
href="/workspace_settings?tab=openai"
on:click={() => {
close(null)
}}>workspace settings.</a
></p
>
</div>
</Popup>
</div>
{/if}
@@ -18,7 +18,7 @@ type StepPropPicker = {
type ModuleBranches = FlowModule[][]
function dfs(id: string | undefined, flow: Flow, getParents: boolean = true): FlowModule[] {
export function dfs(id: string | undefined, flow: Flow, getParents: boolean = true): FlowModule[] {
if (id === undefined) {
return []
}
@@ -80,22 +80,7 @@ function getFlowInput(
}
}
export function getStepPropPicker(
flowState: FlowState,
parentModule: FlowModule | undefined,
previousModule: FlowModule | undefined,
id: string,
flow: Flow,
args: any,
include_node: boolean
): StepPropPicker {
const flowInput = getFlowInput(
dfs(parentModule?.id, flow),
flowState,
args,
flow.schema as Schema
)
export function getPreviousIds(id: string, flow: Flow, include_node: boolean): string[] {
const previousIds = dfs(id, flow, false)
.map((x) => {
let submodules = getAllSubmodules(x)
@@ -112,6 +97,26 @@ export function getStepPropPicker(
if (!include_node) {
previousIds.shift()
}
return previousIds
}
export function getStepPropPicker(
flowState: FlowState,
parentModule: FlowModule | undefined,
previousModule: FlowModule | undefined,
id: string,
flow: Flow,
args: any,
include_node: boolean
): StepPropPicker {
const flowInput = getFlowInput(
dfs(parentModule?.id, flow),
flowState,
args,
flow.schema as Schema
)
const previousIds = getPreviousIds(id, flow, include_node)
let priorIds = Object.fromEntries(
previousIds.map((id) => [id, flowState[id]?.previewResult ?? {}]).reverse()