mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
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:
@@ -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() {
|
||||
|
||||
@@ -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"> </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  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>
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user