feat: add ai for predicates and iterator expressions (#3203)

* feat: add ai for predicates and iterator expressions

* fix: npm run check

* feat: copilot like suggestion
This commit is contained in:
HugoCasa
2024-02-13 15:52:03 +01:00
committed by GitHub
parent 3d0e5c8e57
commit 43e0ceb342
19 changed files with 607 additions and 157 deletions
@@ -340,6 +340,7 @@
<input
{autofocus}
on:focus
on:blur
{disabled}
type="number"
on:keydown={() => {
@@ -608,6 +609,7 @@
<input
{autofocus}
on:focus
on:blur
{disabled}
type="email"
class={valid
@@ -28,10 +28,6 @@
import libStdContent from '$lib/es6.d.ts.txt?raw'
import denoFetchContent from '$lib/deno_fetch.d.ts.txt?raw'
// import nord from '$lib/assets/nord.json'
// import nord from '$lib/assets/nord.json'
import { MonacoLanguageClient } from 'monaco-languageclient'
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'
@@ -218,13 +218,22 @@
bind:this={stepInputGen}
{focused}
{arg}
schemaProperty={schema.properties[argName]}
showPopup={(isStaticTemplate(inputCat) && propertyType == 'static') ||
propertyType === undefined ||
propertyType === 'static' ||
arg?.expr?.length > 0}
on:showExpr={(e) => {
monaco?.setSuggestion(e.detail)
}}
on:setExpr={(e) => {
arg = {
type: 'javascript',
expr: e.detail
}
propertyType = 'javascript'
monaco?.setCode(e.detail)
monaco?.setCode('')
monaco?.insertAtCursor(e.detail)
}}
{pickableProperties}
{argName}
@@ -395,6 +404,7 @@
focused = false
}}
autoHeight
preventTabOnEmpty={enableAi}
/>
</div>
<DynamicInputHelpBox />
@@ -74,6 +74,7 @@
(args[argName].type === 'javascript' && !args[argName].expr))
)
: []}
{schema}
/>
{/if}
{#if keys.length > 0}
@@ -13,8 +13,7 @@
KeyMod,
Uri as mUri,
languages,
type IRange,
type IKeyboardEvent
type IRange
} from 'monaco-editor'
import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution'
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution'
@@ -57,6 +56,7 @@
export let fixedOverflowWidgets = true
export let small = false
export let domLib = false
export let autofocus = false
const dispatch = createEventDispatcher()
@@ -117,12 +117,6 @@
}
}
export function onKeyUp(f: (e: IKeyboardEvent) => void) {
if (editor) {
return editor.onKeyUp(f)
}
}
export function show(): void {
divEl?.classList.remove('hidden')
}
@@ -131,9 +125,17 @@
divEl?.classList.add('hidden')
}
let suggestion = ''
export function setSuggestion(value: string): void {
suggestion = value
}
let width = 0
let initialized = false
let disableTabCond: meditor.IContextKey<boolean> | undefined
$: disableTabCond?.set(!code && !!suggestion)
async function loadMonaco() {
await initializeVscode()
initialized = true
@@ -184,6 +186,7 @@
let timeoutModel: NodeJS.Timeout | undefined = undefined
editor.onDidChangeModelContent((event) => {
suggestion = ''
timeoutModel && clearTimeout(timeoutModel)
timeoutModel = setTimeout(() => {
code = getCode()
@@ -198,6 +201,9 @@
code = getCode()
shouldBindKey && format && format()
})
disableTabCond = editor.createContextKey('disableTabCond', !code)
editor.addCommand(KeyCode.Tab, function () {}, 'disableTabCond')
})
if (autoHeight) {
@@ -332,6 +338,11 @@
if (BROWSER) {
mounted = true
await loadMonaco()
if (autofocus) {
setTimeout(() => {
focus()
}, 0)
}
}
})
@@ -347,6 +358,15 @@
<EditorTheme />
{#if editor && suggestion && code.length === 0}
<div
class="absolute top-[0.05rem] left-[2.05rem] z-10 text-sm text-[#0007] italic font-mono dark:text-[#ffffff56] text-ellipsis overflow-hidden whitespace-nowrap"
style={`max-width: calc(${width}px - 2.05rem)`}
>
{suggestion}
</div>
{/if}
<div bind:this={divEl} class="{$$props.class ?? ''} editor" bind:clientWidth={width} />
<style lang="postcss">
@@ -19,7 +19,7 @@
on:canceled
title="Windmill AI wants to add the following inputs to the flow:"
>
<ul class="text-lg list-disc pl-5">
<ul class=" list-disc pl-5">
{#each inputs as input}
<li>{input}</li>
{/each}
@@ -0,0 +1,181 @@
<script lang="ts">
import { Check, Loader2, Wand2 } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { getNonStreamingCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import type { InputTransform } from '$lib/gen'
import ManualPopover from '../ManualPopover.svelte'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from '../flows/types'
import type { PickableProperties } from '../flows/previousResults'
import YAML from 'yaml'
import { sliceModules } from '../flows/flowStateUtils'
import { dfs } from '../flows/dfs'
import { yamlStringifyExceptKeys } from './utils'
import { copilotInfo, stepInputCompletionEnabled } from '$lib/stores'
let generatedContent = ''
let loading = false
export let focused = false
export let arg: InputTransform | any
export let pickableProperties: PickableProperties | undefined = undefined
let btnFocused = false
let empty = false
$: empty =
Object.keys(arg ?? {}).length === 0 ||
(arg.type === 'static' && !arg.value) ||
(arg.type === 'javascript' && !arg.expr)
let abortController = new AbortController()
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
async function generateIteratorExpr() {
if (generatedContent.length > 0 || loading) {
return
}
abortController = new AbortController()
loading = true
const idOrders = dfs($flowStore.value.modules, (x) => x.id)
const upToIndex = idOrders.indexOf($selectedId)
if (upToIndex === -1) {
throw new Error('Could not find the selected id in the flow')
}
const flowDetails =
'Take into account the following information for never tested results:\n<flowDetails>\n' +
yamlStringifyExceptKeys(sliceModules($flowStore.value.modules, upToIndex, idOrders), [
'lock'
]) +
'</flowDetails>'
try {
const availableData = {
results: pickableProperties?.priorIds,
flow_input: pickableProperties?.flow_input
}
const user = `I'm building a workflow which is a DAG of script steps.
The current step is ${selectedId} and represents a for-loop. You can find the details of all the steps below:
${flowDetails}
Determine the iterator expression to pass either from the previous results or the flow inputs. Here's a summary of the available data:
<available>
${YAML.stringify(availableData)}</available>
Reply with the most probable answer, do not explain or discuss.
Use javascript object dot notation to access the properties.
Only output the expression, do not explain or discuss.`
generatedContent = await getNonStreamingCompletion(
[
{
role: 'user',
content: user
}
],
abortController
)
} catch (err) {
if (!abortController.signal.aborted) {
sendUserToast('Could not generate summary: ' + err, true)
}
} finally {
loading = false
}
}
export function onKeyUp(event: KeyboardEvent) {
if (!$copilotInfo.exists_openai_resource_path || !$stepInputCompletionEnabled) {
return
}
if (event.key === 'Tab') {
if (!loading && generatedContent) {
event.preventDefault()
dispatch('setExpr', generatedContent)
generatedContent = ''
}
} else {
cancel()
}
}
const dispatch = createEventDispatcher()
function automaticGeneration() {
if (empty) {
generateIteratorExpr()
}
}
function cancelOnOutOfFocus() {
setTimeout(() => {
if (!focused && !btnFocused) {
// only cancel if out of focus is not due to click on btn
cancel()
}
}, 150)
}
$: if (!focused) {
cancelOnOutOfFocus()
}
$: if ($copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled && focused) {
automaticGeneration()
}
function cancel() {
abortController.abort()
generatedContent = ''
}
$: dispatch('showExpr', generatedContent)
let out = true // hack to prevent regenerating answer when accepting the answer due to mouseenter on new icon
</script>
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
<ManualPopover showTooltip={!empty && generatedContent.length > 0} placement="bottom" class="p-2">
<Button
size="xs"
color="light"
btnClasses="text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700 dark:hover:bg-surface-hover"
on:click={() => {
if (!loading && generatedContent.length > 0) {
dispatch('setExpr', generatedContent)
generatedContent = ''
}
}}
on:focus={() => {
btnFocused = true
}}
on:blur={() => {
btnFocused = false
}}
on:mouseenter={(ev) => {
if (out) {
out = false
generateIteratorExpr()
}
}}
on:mouseleave={() => {
out = true
cancel()
}}
endIcon={{
icon: loading ? Loader2 : generatedContent.length > 0 ? Check : Wand2,
classes: loading ? 'animate-spin' : ''
}}
>
{#if focused}
{#if loading}
ESC
{:else if generatedContent.length > 0}
TAB
{/if}
{/if}
</Button>
<svelte:fragment slot="content">
<div class="text-sm text-tertiary">
{generatedContent}
</div>
</svelte:fragment>
</ManualPopover>
{/if}
@@ -0,0 +1,135 @@
<script lang="ts">
import { Wand2 } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { getNonStreamingCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from '../flows/types'
import type { PickableProperties } from '../flows/previousResults'
import YAML from 'yaml'
import { sliceModules } from '../flows/flowStateUtils'
import { dfs } from '../flows/dfs'
import { yamlStringifyExceptKeys } from './utils'
import { copilotInfo, stepInputCompletionEnabled } from '$lib/stores'
import Popup from '../common/popup/Popup.svelte'
let loading = false
export let pickableProperties: PickableProperties | undefined = undefined
let instructions = ''
let instructionsField: HTMLInputElement | undefined = undefined
$: instructionsField && setTimeout(() => instructionsField?.focus(), 100)
let abortController = new AbortController()
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
async function generatePredicate() {
abortController = new AbortController()
loading = true
const idOrders = dfs($flowStore.value.modules, (x) => x.id)
const upToIndex = idOrders.indexOf($selectedId)
if (upToIndex === -1) {
throw new Error('Could not find the selected id in the flow')
}
const flowDetails =
'Take into account the following information for never tested results:\n<flowDetails>\n' +
yamlStringifyExceptKeys(sliceModules($flowStore.value.modules, upToIndex, idOrders), [
'lock'
]) +
'</flowDetails>'
try {
const availableData = {
results: pickableProperties?.priorIds,
flow_input: pickableProperties?.flow_input
}
const user = `I'm building a workflow which is a DAG of script steps.
The current step is ${selectedId} and is a branching step (if-else).
The user wants to generate a predicate for the branching condition.
Here's the user's request: ${instructions}
You can find the details of all the steps below:
${flowDetails}
Determine for the user the javascript expression for the branching condition composed of the previous results or the flow inputs.
All inputs start with either results. or flow_input. and are followed by the key of the input.
Here's a summary of the available data:
<available>
${YAML.stringify(availableData)}</available>
If the branching is made inside a for-loop, the iterator value is accessible as flow_input.iter.value
Only return the expression without any wrapper. Do not explain or discuss.`
const result = await getNonStreamingCompletion(
[
{
role: 'user',
content: user
}
],
abortController
)
dispatch('setExpr', result)
} catch (err) {
if (!abortController.signal.aborted) {
sendUserToast('Could not generate summary: ' + err, true)
}
} finally {
loading = false
}
}
</script>
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
let:close
>
<svelte:fragment slot="button">
<Button
color={loading ? 'red' : 'light'}
size="xs"
nonCaptureEvent={!loading}
startIcon={{ icon: Wand2 }}
iconOnly
title="AI Assistant"
btnClasses="min-h-[30px] text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700"
{loading}
clickableWhileLoading
on:click={loading ? () => abortController?.abort() : undefined}
/>
</svelte:fragment>
<div class="flex w-96">
<input
bind:this={instructionsField}
type="text"
placeholder="Predicate description"
bind:value={instructions}
on:keypress={({ key }) => {
if (key === 'Enter' && instructions.length > 0) {
close(instructionsField || null)
generatePredicate()
}
}}
/>
<Button
size="xs"
color="light"
variant="contained"
buttonType="button"
btnClasses="!p-1 !w-[38px] !ml-2 text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700"
title="Generate predicate from prompt"
aria-label="Generate"
iconOnly
on:click={() => {
close(instructionsField || null)
generatePredicate()
}}
disabled={instructions.length == 0}
startIcon={{ icon: Wand2 }}
/>
</div>
</Popup>
{/if}
@@ -14,24 +14,57 @@
import { yamlStringifyExceptKeys } from './utils'
import type { FlowCopilotContext } from './flow'
import { copilotInfo, stepInputCompletionEnabled } from '$lib/stores'
import type { SchemaProperty } from '$lib/common'
import FlowCopilotInputsModal from './FlowCopilotInputsModal.svelte'
let generatedContent = ''
let loading = false
export let focused = false
export let arg: InputTransform | any
export let schemaProperty: SchemaProperty
export let pickableProperties: PickableProperties | undefined = undefined
export let argName: string
export let showPopup: boolean
let empty = false
$: empty =
!arg || (arg.type === 'static' && !arg.value) || (arg.type === 'javascript' && !arg.expr)
Object.keys(arg ?? {}).length === 0 ||
(arg.type === 'static' && !arg.value) ||
(arg.type === 'javascript' && !arg.expr)
let btnFocused = false
let abortController = new AbortController()
let newFlowInput = ''
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const { stepInputsLoading, generatedExprs } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
function createFlowInput() {
if (!newFlowInput) {
return
}
const properties = {
...($flowStore.schema?.properties as Record<string, SchemaProperty> | undefined),
[newFlowInput]: schemaProperty
}
const required = [
...(($flowStore.schema?.required as string[] | undefined) ?? []),
newFlowInput
]
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties,
required,
type: 'object'
}
}
async function generateStepInput() {
if (generatedContent.length > 0 || loading) {
return
}
abortController = new AbortController()
loading = true
const idOrders = dfs($flowStore.value.modules, (x) => x.id)
@@ -43,8 +76,7 @@
const flowDetails =
'Take into account the following information for never tested results:\n<flowDetails>\n' +
yamlStringifyExceptKeys(sliceModules($flowStore.value.modules, upToIndex, idOrders), [
'lock',
'input_transforms'
'lock'
]) +
'</flowDetails>'
try {
@@ -55,13 +87,15 @@
const user = `I'm building a workflow which is a DAG of script steps.
The current step is ${selectedId}, you can find the details for the step and previous ones below:
${flowDetails}
Determine for the input "${argName}", what to pass either from the previous results of the flow inputs. Here's a summary of the available data:
Determine for the input "${argName}", what to pass either from the previous results or the flow inputs.
All possibles inputs either start with results. or flow_input. and are follow by the key of the input.
Here's a summary of the available data:
<available>
${YAML.stringify(availableData)}</available>
If none of the available results are appropriate, are already used or are more appropriate for other inputs, you can also imagine new flow_input properties which we will create programmatically based on what you provide.
Reply with the most probable answer, do not explain or discuss.
Use javascript object dot notation to access the properties.
Return the input element directly: e.g. flow_input.property, results.a, results.a.property, flow_input.iter.value`
Only return the expression without any wrapper.`
generatedContent = await getNonStreamingCompletion(
[
@@ -72,6 +106,17 @@ Return the input element directly: e.g. flow_input.property, results.a, results.
],
abortController
)
if (
pickableProperties &&
generatedContent.startsWith('flow_input.') &&
generatedContent.split('.')[1] &&
!(generatedContent.split('.')[1] in pickableProperties.flow_input)
) {
newFlowInput = generatedContent.split('.')[1]
} else {
newFlowInput = ''
}
} catch (err) {
if (!abortController.signal.aborted) {
sendUserToast('Could not generate summary: ' + err, true)
@@ -89,6 +134,9 @@ Return the input element directly: e.g. flow_input.property, results.a, results.
if (!loading && generatedContent) {
event.preventDefault()
dispatch('setExpr', generatedContent)
if (newFlowInput) {
openInputsModal = true
}
generatedContent = ''
}
} else {
@@ -98,30 +146,52 @@ Return the input element directly: e.g. flow_input.property, results.a, results.
const dispatch = createEventDispatcher()
function cancel() {
abortController.abort()
generatedContent = ''
}
function automaticGeneration() {
if (empty) {
generateStepInput()
}
}
function cancelOnOutOfFocus() {
setTimeout(() => {
if (!focused && !btnFocused) {
// only cancel if out of focus is not due to click on btn
cancel()
}
}, 150)
}
$: if (!focused) {
cancelOnOutOfFocus()
}
$: if ($copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled && focused) {
automaticGeneration()
}
function cancel() {
abortController.abort()
generatedContent = ''
}
$: if (!focused) {
cancel()
}
$: dispatch('showExpr', generatedContent)
$: dispatch('showExpr', $generatedExprs?.[argName] || '')
let out = true // hack to prevent regenerating answer when accepting the answer due to mouseenter on new icon
let openInputsModal = false
</script>
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
<FlowCopilotInputsModal
on:confirmed={async () => {
createFlowInput()
}}
bind:open={openInputsModal}
inputs={[newFlowInput]}
/>
<ManualPopover
showTooltip={generatedContent.length > 0 || !!$generatedExprs?.[argName]}
showTooltip={showPopup && (generatedContent.length > 0 || !!$generatedExprs?.[argName])}
placement="bottom"
class="p-2"
>
@@ -130,17 +200,16 @@ Return the input element directly: e.g. flow_input.property, results.a, results.
color="light"
btnClasses="text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700 dark:hover:bg-surface-hover"
on:click={() => {
if (loading) {
cancel()
} else if (generatedContent.length > 0) {
if (!loading && generatedContent.length > 0) {
dispatch('setExpr', generatedContent)
if (newFlowInput) {
openInputsModal = true
}
generatedContent = ''
} else {
generateStepInput()
}
}}
on:mouseenter={(ev) => {
if (!generatedContent && !loading && out) {
if (out) {
out = false
generateStepInput()
}
@@ -158,6 +227,12 @@ Return the input element directly: e.g. flow_input.property, results.a, results.
: Wand2,
classes: loading || ($stepInputsLoading && empty) ? 'animate-spin' : ''
}}
on:focus={() => {
btnFocused = true
}}
on:blur={() => {
btnFocused = false
}}
>
{#if focused}
{#if loading}
@@ -13,10 +13,13 @@
import { Check, ExternalLink, Loader2, Wand2 } from 'lucide-svelte'
import { copilotInfo, stepInputCompletionEnabled } from '$lib/stores'
import { Popup } from '../common'
import type { SchemaProperty, Schema } from '$lib/common'
import FlowCopilotInputsModal from './FlowCopilotInputsModal.svelte'
let loading = false
export let pickableProperties: PickableProperties | undefined = undefined
export let argNames: string[] = []
export let schema: Schema
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -25,9 +28,13 @@
let generatedContent = ''
let parsedInputs: string[][] = []
let newFlowInputs: string[] = []
let abortController = new AbortController()
async function generateStepInputs() {
if (Object.keys($generatedExprs || {}).length > 0 || loading) {
return
}
abortController = new AbortController()
loading = true
stepInputsLoading?.set(true)
@@ -39,8 +46,7 @@
const flowDetails =
'Take into account the following information for never tested results:\n<flowDetails>\n' +
yamlStringifyExceptKeys(sliceModules($flowStore.value.modules, upToIndex, idOrders), [
'lock',
'input_transforms'
'lock'
]) +
'</flowDetails>'
@@ -53,21 +59,22 @@
The current step is ${selectedId}, you can find the details for the step and previous ones below:
${flowDetails}
Determine for the inputs "${argNames.join(
Determine for all the inputs "${argNames.join(
'", "'
)}", what to pass either from the previous results of the flow inputs. Here's a summary of the available data:
)}", what to pass either from the previous results of the flow inputs.
All possibles inputs either start with results. or flow_input. and are follow by the key of the input.
Here's a summary of the available data:
<available>
${YAML.stringify(availableData)}</available>
If none of the available results are appropriate, are already used or are more appropriate for other inputs, you can also imagine new flow_input properties which we will create programmatically based on what you provide.
Reply with the most probable answer, do not explain or discuss.
Use javascript object dot notation to access the properties.
Return the input element directly: e.g. flow_input.property, results.a, results.a.property, flow_input.iter.value
Your answer has to be in the following format (one line per input):
input_name: expr`
console.log(user)
{input_name1}: {expression1}
{input_name2}: {expression2}
...`
generatedContent = await getNonStreamingCompletion(
[
@@ -82,9 +89,18 @@ input_name: expr`
parsedInputs = generatedContent.split('\n').map((x) => x.split(': '))
const exprs = {}
newFlowInputs = []
for (const [key, value] of parsedInputs) {
if (argNames.includes(key)) {
exprs[key] = value
if (
pickableProperties &&
value.startsWith('flow_input.') &&
value.split('.')[1] &&
!(value.split('.')[1] in pickableProperties.flow_input)
) {
newFlowInputs.push(value.split('.')[1])
}
}
}
generatedExprs?.set(exprs)
@@ -99,6 +115,29 @@ input_name: expr`
}
}
function createFlowInputs() {
if (!newFlowInputs.length) {
return
}
const properties = {
...($flowStore.schema?.properties as Record<string, SchemaProperty> | undefined),
...newFlowInputs.reduce((acc, x) => {
acc[x] = (schema.properties ?? {})[x]
return acc
}, {})
}
const required = [
...(($flowStore.schema?.required as string[] | undefined) ?? []),
...newFlowInputs
]
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties,
required,
type: 'object'
}
}
function applyExprs() {
const argsUpdate = {}
for (const [key, value] of parsedInputs) {
@@ -111,19 +150,30 @@ input_name: expr`
}
exprsToSet?.set(argsUpdate)
generatedExprs?.set({})
if (newFlowInputs.length) {
openInputsModal = true
}
}
let out = true // hack to prevent regenerating answer when accepting the answer due to mouseenter on new icon
let openInputsModal = false
</script>
<div class="flex flex-row justify-end">
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
<FlowCopilotInputsModal
on:confirmed={async () => {
createFlowInputs()
}}
bind:open={openInputsModal}
inputs={newFlowInputs}
/>
<Button
size="xs"
color="light"
btnClasses="text-violet-800 dark:text-violet-400"
on:mouseenter={(ev) => {
if (Object.keys($generatedExprs || {}).length === 0 && !loading && out) {
if (out) {
out = false
generateStepInputs()
}
@@ -134,12 +184,8 @@ input_name: expr`
generatedExprs?.set({})
}}
on:click={() => {
if (loading) {
abortController.abort()
} else if (Object.keys($generatedExprs || {}).length > 0) {
if (!loading && Object.keys($generatedExprs || {}).length > 0) {
applyExprs()
} else if (!loading) {
generateStepInputs()
}
}}
startIcon={{
@@ -188,9 +234,13 @@ input_name: expr`
</a>
{:else}
Enable step input completion in the{' '}
<a href="#user-settings" class="inline-flex flex-row items-center gap-1" on:click={() => {
close(null)
}}>
<a
href="#user-settings"
class="inline-flex flex-row items-center gap-1"
on:click={() => {
close(null)
}}
>
user settings
</a>
{/if}
@@ -47,7 +47,7 @@
</div>
</div>
{:else}
<FlowEditorPanel />
<FlowEditorPanel enableAi />
{/if}
</Pane>
</Splitpanes>
@@ -8,6 +8,7 @@
import type { FlowEditorContext } from '../types'
import Button from '$lib/components/common/button/Button.svelte'
import { Pen } from 'lucide-svelte'
import PredicateGen from '$lib/components/copilot/PredicateGen.svelte'
export let branch: {
summary?: string
@@ -16,6 +17,7 @@
}
export let parentModule: FlowModule
export let previousModule: FlowModule | undefined
export let enableAi = false
const { previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -55,8 +57,19 @@
</PropPickerWrapper>
{:else}
<div class="flex justify-between gap-4 p-2">
<div><pre class="text-sm">{branch.expr}</pre></div><div>
<div><pre class="text-sm">{branch.expr}</pre></div><div
class="flex flex-row gap-2 items-center"
>
{#if enableAi}
<PredicateGen
on:setExpr={(e) => {
branch.expr = e.detail
}}
pickableProperties={stepPropPicker.pickableProperties}
/>
{/if}
<Button
size="xs"
startIcon={{ icon: Pen }}
variant="border"
on:click={() => (open = !open)}
@@ -12,6 +12,7 @@
export let parentModule: FlowModule
export let previousModule: FlowModule | undefined
export let noEditor: boolean
export let enableAi = false
</script>
<div class="h-full flex flex-col">
@@ -21,7 +22,7 @@
</div>
<div class="overflow-hidden flex-grow">
<h3 class="p-2">Predicate expression</h3>
<BranchPredicateEditor {branch} {parentModule} {previousModule} />
<BranchPredicateEditor {branch} {parentModule} {previousModule} {enableAi} />
</div>
</FlowCard>
</div>
@@ -19,6 +19,7 @@
export let flowModule: FlowModule
export let previousModule: FlowModule | undefined
export let noEditor: boolean
export let enableAi = false
let value = flowModule.value as BranchOne
$: value = flowModule.value as BranchOne
@@ -65,7 +66,12 @@
/>
</div>
<div class="w-full border">
<BranchPredicateEditor {branch} parentModule={flowModule} {previousModule} />
<BranchPredicateEditor
{branch}
parentModule={flowModule}
{previousModule}
{enableAi}
/>
</div>
</div>
{/each}
@@ -10,6 +10,7 @@
import type { FlowModule } from '$lib/gen'
export let noEditor = false
export let enableAi = false
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -46,6 +47,7 @@
{noEditor}
bind:flowModule
previousModule={$flowStore.value.modules[index - 1]}
{enableAi}
/>
{/each}
{/key}
@@ -21,6 +21,7 @@
import type { Job } from '$lib/gen'
import FlowLoopIterationPreview from '$lib/components/FlowLoopIterationPreview.svelte'
import FlowModuleDeleteAfterUse from './FlowModuleDeleteAfterUse.svelte'
import IteratorGen from '$lib/components/copilot/IteratorGen.svelte'
const { previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -29,6 +30,7 @@
export let parentModule: FlowModule | undefined
export let previousModule: FlowModule | undefined
export let noEditor: boolean
export let enableAi = false
let editor: SimpleEditor | undefined = undefined
let selected: string = 'early-stop'
@@ -47,6 +49,9 @@
let jobId: string | undefined = undefined
let job: Job | undefined = undefined
let iteratorFieldFocused = false
let iteratorGen: IteratorGen | undefined = undefined
$: previewIterationArgs = $flowStateStore[mod.id]?.previewArgs ?? {}
</script>
@@ -121,14 +126,42 @@
/>
</div>
</div>
<div class="my-2 text-sm font-bold">
Iterator expression
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/flow_loops">
List to iterate over.
</Tooltip>
<div class="my-2 flex flex-row gap-2 items-center">
<div class="text-sm font-bold">
Iterator expression
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/flow_loops">
List to iterate over.
</Tooltip>
</div>
{#if enableAi}
<IteratorGen
bind:this={iteratorGen}
focused={iteratorFieldFocused}
arg={mod.value.iterator}
on:showExpr={(e) => {
editor?.setSuggestion(e.detail)
}}
on:setExpr={(e) => {
if (mod.value.type === 'forloopflow') {
mod.value.iterator = {
type: 'javascript',
expr: e.detail
}
}
editor?.setCode('')
editor?.insertAtCursor(e.detail)
}}
pickableProperties={stepPropPicker.pickableProperties}
/>
{/if}
</div>
{#if mod.value.iterator.type == 'javascript'}
<div class="border w-full" id="flow-editor-iterator-expression">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="border w-full"
id="flow-editor-iterator-expression"
on:keyup={iteratorGen?.onKeyUp}
>
<PropPickerWrapper
notSelectable
pickableProperties={stepPropPicker.pickableProperties}
@@ -140,6 +173,13 @@
>
<SimpleEditor
bind:this={editor}
on:focus={() => {
iteratorFieldFocused = true
}}
on:blur={() => {
iteratorFieldFocused = false
}}
autofocus
lang="javascript"
bind:code={mod.value.iterator.expr}
class="small-editor"
@@ -20,6 +20,7 @@
export let flowModule: FlowModule
export let noEditor: boolean = false
export let enableAi = false
const { selectedId, schedule, flowStateStore } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -64,9 +65,9 @@
{#if flowModule.id === $selectedId}
{#if flowModule.value.type === 'forloopflow'}
<FlowLoop {noEditor} bind:mod={flowModule} {parentModule} {previousModule} />
<FlowLoop {noEditor} bind:mod={flowModule} {parentModule} {previousModule} {enableAi} />
{:else if flowModule.value.type === 'branchone'}
<FlowBranchesOneWrapper {noEditor} {previousModule} bind:flowModule />
<FlowBranchesOneWrapper {noEditor} {previousModule} bind:flowModule {enableAi} />
{:else if flowModule.value.type === 'branchall'}
<FlowBranchesAllWrapper {noEditor} {previousModule} bind:flowModule />
{:else if flowModule.value.type === 'identity'}
@@ -151,6 +152,7 @@
bind:flowModule={submodule}
bind:parentModule={flowModule}
previousModule={flowModule.value.modules[index - 1]}
{enableAi}
/>
{/each}
{:else if flowModule.value.type === 'branchone'}
@@ -165,18 +167,26 @@
bind:flowModule={submodule}
bind:parentModule={flowModule}
previousModule={flowModule.value.default[index - 1]}
{enableAi}
/>
{/each}
{/if}
{#each flowModule.value.branches as branch, branchIndex (branchIndex)}
{#if $selectedId === `${flowModule?.id}-branch-${branchIndex}`}
<FlowBranchOneWrapper {noEditor} bind:branch parentModule={flowModule} {previousModule} />
<FlowBranchOneWrapper
{noEditor}
bind:branch
parentModule={flowModule}
{previousModule}
{enableAi}
/>
{:else}
{#each branch.modules as submodule, index}
<svelte:self
bind:flowModule={submodule}
bind:parentModule={flowModule}
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
{enableAi}
/>
{/each}
{/if}
@@ -191,6 +201,7 @@
bind:flowModule={submodule}
bind:parentModule={flowModule}
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
{enableAi}
/>
{/each}
{/if}
@@ -90,7 +90,7 @@ export async function createLoop(id: string): Promise<[FlowModule, FlowModuleSta
value: {
type: 'forloopflow',
modules: [],
iterator: { type: 'javascript', expr: '["dynamic or static array"]' },
iterator: { type: 'javascript', expr: '' },
skip_failures: true
}
}
@@ -1,93 +0,0 @@
{
"base": "vs-dark",
"inherit": true,
"rules": [
{
"background": "2E3440",
"token": ""
},
{
"foreground": "616e88",
"token": "comment"
},
{
"foreground": "a3be8c",
"token": "string"
},
{
"foreground": "b48ead",
"token": "constant.numeric"
},
{
"foreground": "81a1c1",
"token": "constant.language"
},
{
"foreground": "81a1c1",
"token": "keyword"
},
{
"foreground": "81a1c1",
"token": "storage"
},
{
"foreground": "81a1c1",
"token": "storage.type"
},
{
"foreground": "8fbcbb",
"token": "entity.name.class"
},
{
"foreground": "8fbcbb",
"fontStyle": " bold",
"token": "entity.other.inherited-class"
},
{
"foreground": "88c0d0",
"token": "entity.name.function"
},
{
"foreground": "81a1c1",
"token": "entity.name.tag"
},
{
"foreground": "8fbcbb",
"token": "entity.other.attribute-name"
},
{
"foreground": "88c0d0",
"token": "support.function"
},
{
"foreground": "f8f8f0",
"background": "f92672",
"token": "invalid"
},
{
"foreground": "f8f8f0",
"background": "ae81ff",
"token": "invalid.deprecated"
},
{
"foreground": "b48ead",
"token": "constant.color.other.rgb-value"
},
{
"foreground": "ebcb8b",
"token": "constant.character.escape"
},
{
"foreground": "8fbcbb",
"token": "variable.other.constant"
}
],
"colors": {
"editor.foreground": "#D8DEE9",
"editor.background": "#2E3440",
"editor.selectionBackground": "#434C5ECC",
"editor.lineHighlightBackground": "#3B4252",
"editorCursor.foreground": "#D8DEE9",
"editorWhitespace.foreground": "#434C5ECC"
}
}