mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
* Monaco transparent bg
* started improving input transform form
* always show static/f selector
* fix connecting btn changing size
* pretty Fill Inputs button
* ResizeTransitionWrapper
* Prettier TemplateEditor
* prevent double onpointerdown when clicking button to close
* text hint
* force focus border for TemplateEditor
* styling in js mode
* update select style
* fix jittery fake monaco placeholder
* select nits
* aiproviderpicker + nits
* smaller ${...} badge
* no-default-style
* select dropdown slide
* nit
* Refresh button in flow picker quick
* jsonEditor pretty
* ai provider toggle button more
* change resource edit button pos
* ResourcePicker Add and Refresh btn
* fix scrollbar
* Fix FileInput and S3 Arg Input
* fix textarea styling
* nicer refresh button in Test This Step
* fix togglebutton border in darkmode
* rounded nit
* Fix multiselect styling
* Prevent crash when selecting dyn-multiselect
* missing $derived and $state => reactivity issue when switching between DynSelect and DynMultiselect
* forgot $effect.pre
* fix nested objects
* nits
* prettier json toggle and array inputs
* array input nits
* nit
* fix json toggle appearing in fileinputs
* nit
* started updating PropertyEditor
* (stash) fix select dropdown animation teleporting from bottom to top
* nit
* nit
* resize transition in schemaform
* nit
* nit typo
* nit enableFlyTransition
* shadow nit
* small consistency changes
* user setting nit
* resize transition in module preview form
* more space
* nit readability on hover
* DateTimeInput new style
* nit fix
* remove yPadding in template and simple editor
* nits
* Revert "remove yPadding in template and simple editor"
This reverts commit 8f27c8d0b8.
* nit
* Fix proppicker border
* fix inconsistent spacing btw arginput and input transform form field headers
* consistent add item button
* nit
* s3 settings nits
* RunsFilter fix
* gray ${...} badge
* border fix darkmode
* nit
* nit app editor consistency
* fix step input gen style
* nit fix
* nits
* toggle border
* nit toggle button
* nit font-medium
* nit font-medium
* nit font-medium
* nit font-medium
* nit
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
279 lines
8.0 KiB
Svelte
279 lines
8.0 KiB
Svelte
<script lang="ts" module>
|
|
export function stepInputGenButtonClasses(selected: boolean) {
|
|
return twMerge(
|
|
'text-violet-500 dark:text-violet-400 border',
|
|
selected
|
|
? 'bg-green-50 hover:bg-green-50 dark:bg-green-400/15 dark:hover:bg-green-400/15 text-green-800 border-green-200 dark:border-green-300/60 dark:text-green-400 '
|
|
: 'hover:bg-violet-50 border-violet-100 dark:hover:bg-violet-400/15 dark:border-violet-400/20'
|
|
)
|
|
}
|
|
</script>
|
|
|
|
<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 { Flow, 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.svelte'
|
|
import { dfs } from '../flows/dfs'
|
|
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'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
|
|
|
|
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
|
|
export let btnClass = ''
|
|
|
|
let empty = false
|
|
$: empty =
|
|
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.val.schema?.properties as Record<string, SchemaProperty> | undefined),
|
|
[newFlowInput]: schemaProperty
|
|
}
|
|
const required = [
|
|
...((flowStore.val.schema?.required as string[] | undefined) ?? []),
|
|
newFlowInput
|
|
]
|
|
flowStore.val.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 flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
|
|
const idOrders = dfs(flow.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(flow.value.modules, upToIndex, idOrders), ['lock']) +
|
|
'</flowDetails>'
|
|
try {
|
|
const availableData = {
|
|
results: pickableProperties?.priorIds,
|
|
flow_input: pickableProperties?.flow_input
|
|
}
|
|
const isInsideLoop = availableData.flow_input && 'iter' in availableData.flow_input
|
|
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 or the flow inputs.
|
|
All possibles inputs either start with results. or flow_input. and are followed by the key of the input.
|
|
${
|
|
isInsideLoop
|
|
? 'As the step is in a loop, the iterator value is accessible as flow_input.iter.value.'
|
|
: 'As the step is not in a loop, flow_input.iter.value is not available.'
|
|
}
|
|
Here's a summary of the available data:
|
|
<available>
|
|
${YAML.stringify(availableData)}</available>
|
|
${
|
|
isInsideLoop
|
|
? 'Favor results and flow_input.iter.value over flow inputs.'
|
|
: 'Favor results over flow inputs'
|
|
}
|
|
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.
|
|
Only return the expression without any wrapper.`
|
|
generatedContent = await getNonStreamingCompletion(
|
|
[
|
|
{
|
|
role: 'user',
|
|
content: user
|
|
}
|
|
],
|
|
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 step input: ' + err, true)
|
|
}
|
|
} finally {
|
|
loading = false
|
|
}
|
|
}
|
|
|
|
export function onKeyUp(event: KeyboardEvent) {
|
|
if (!$copilotInfo.enabled || !$stepInputCompletionEnabled) {
|
|
return
|
|
}
|
|
if (event.key === 'Tab') {
|
|
if (!loading && generatedContent) {
|
|
event.preventDefault()
|
|
dispatch('setExpr', generatedContent)
|
|
if (newFlowInput) {
|
|
openInputsModal = true
|
|
}
|
|
generatedContent = ''
|
|
}
|
|
} else {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
const dispatch = createEventDispatcher()
|
|
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
|
|
|
|
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.enabled && $stepInputCompletionEnabled && focused) {
|
|
automaticGeneration()
|
|
}
|
|
|
|
$: dispatchIfMounted('showExpr', generatedContent)
|
|
|
|
$: dispatchIfMounted('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.enabled && $stepInputCompletionEnabled}
|
|
<FlowCopilotInputsModal
|
|
on:confirmed={async () => {
|
|
createFlowInput()
|
|
}}
|
|
bind:open={openInputsModal}
|
|
inputs={[newFlowInput]}
|
|
/>
|
|
<ManualPopover
|
|
showTooltip={showPopup && (generatedContent.length > 0 || !!$generatedExprs?.[argName])}
|
|
placement="bottom"
|
|
class="p-2"
|
|
>
|
|
<Button
|
|
size="xs"
|
|
color="light"
|
|
btnClasses={twMerge(
|
|
stepInputGenButtonClasses(!loading && generatedContent.length > 0),
|
|
btnClass
|
|
)}
|
|
on:click={() => {
|
|
if (!loading && generatedContent.length > 0) {
|
|
dispatch('setExpr', generatedContent)
|
|
if (newFlowInput) {
|
|
openInputsModal = true
|
|
}
|
|
generatedContent = ''
|
|
}
|
|
}}
|
|
on:mouseenter={(ev) => {
|
|
if (out) {
|
|
out = false
|
|
generateStepInput()
|
|
}
|
|
}}
|
|
on:mouseleave={() => {
|
|
out = true
|
|
cancel()
|
|
}}
|
|
endIcon={{
|
|
icon:
|
|
loading || ($stepInputsLoading && empty)
|
|
? Loader2
|
|
: generatedContent.length > 0
|
|
? Check
|
|
: Wand2,
|
|
classes: loading || ($stepInputsLoading && empty) ? 'animate-spin' : ''
|
|
}}
|
|
on:focus={() => {
|
|
btnFocused = true
|
|
}}
|
|
on:blur={() => {
|
|
btnFocused = false
|
|
}}
|
|
>
|
|
{#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 || $generatedExprs?.[argName]}
|
|
</div>
|
|
</svelte:fragment>
|
|
</ManualPopover>
|
|
{/if}
|