Files
windmill/frontend/src/lib/components/flows/content/FlowInput.svelte
T
Diego ImbertandClaude Fable 5 a544dfde9a fix(frontend): preserve top-level flow settings in AI flow tools (#10369)
* fix(frontend): preserve top-level flow settings in AI flow tools

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaNkfh3YH8VoTNebkRuunB

* fix(frontend): treat degenerate agent transforms as unconfigured in chat mode toggle

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaNkfh3YH8VoTNebkRuunB

* fix(frontend): treat persisted static-null agent transforms as unconfigured

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaNkfh3YH8VoTNebkRuunB

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:48:18 +02:00

1048 lines
31 KiB
Svelte

<script lang="ts">
import { Button } from '$lib/components/common'
import { ButtonType } from '$lib/components/common/button/model'
import { getContext, tick, untrack } from 'svelte'
import FlowCard from '../common/FlowCard.svelte'
import type { FlowEditorContext } from '../types'
import JsonInputs from '$lib/components/JsonInputs.svelte'
import { convert } from '@redocly/json-to-json-schema'
import { sendUserToast } from '$lib/toast'
import { workspaceStore } from '$lib/stores'
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
import AddPropertyV2 from '$lib/components/schema/AddPropertyV2.svelte'
import FlowInputViewer from '$lib/components/FlowInputViewer.svelte'
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
import SavedInputsPicker from '$lib/components/SavedInputsPicker.svelte'
import FirstStepInputs from '$lib/components/FirstStepInputs.svelte'
import {
CornerDownLeft,
Pen,
ChevronRight,
Plus,
History,
Braces,
Code,
Save,
X,
Check,
Settings2
} from 'lucide-svelte'
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
import FlowInputEditor from './FlowInputEditor.svelte'
import { twMerge } from 'tailwind-merge'
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
import {
type SchemaDiff,
getFullPath,
setNestedProperty,
getNestedProperty,
schemaFromDiff,
computeDiff,
applyDiff
} from '$lib/components/schema/schemaUtils.svelte'
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
import { isObjectTooBig, readFieldsRecursively } from '$lib/utils'
import { refreshFlowStateStore } from '$lib/components/flows/flowStoreRefresh.svelte'
import type { AiAgent, InputTransform, ScriptLang } from '$lib/gen'
import { deepEqual } from 'fast-equals'
import Toggle from '$lib/components/Toggle.svelte'
import { AI_AGENT_SCHEMA } from '../flowInfers'
import { nextId } from '../flowModuleNextId'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import FlowChat from '../conversations/FlowChat.svelte'
import { SPECIAL_MODULE_IDS } from '$lib/components/copilot/chat/shared'
interface Props {
noEditor: boolean
disabled: boolean
onTestFlow?: (conversationId?: string) => Promise<string | undefined>
previewOpen: boolean
flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default
}
let {
noEditor,
disabled,
onTestFlow,
previewOpen,
flowModuleSchemaMap = undefined
}: Props = $props()
const {
flowStore,
flowStateStore,
previewArgs,
pathStore,
initialPathStore,
fakeInitialPath,
flowInputEditorState,
opWorkspace
} = getContext<FlowEditorContext>('FlowEditorContext')
// Acting workspace when the flow editor runs in an AI session; else the nav workspace.
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
// Get diffManager from the graph
const diffManager = $derived(flowModuleSchemaMap?.getDiffManager())
// Use pending schema from diffManager when in diff mode, otherwise use flowStore
const effectiveSchema = $derived(diffManager?.currentInputSchema ?? flowStore.val.schema)
// Detect if we're in "review mode" (AI has made schema changes that are pending)
const hasAiSchemaChanges = $derived(
Boolean(
diffManager?.moduleActions[SPECIAL_MODULE_IDS.INPUT]?.pending &&
diffManager?.beforeFlow?.schema
)
)
let chatInputEnabled = $state(Boolean(flowStore.val.value?.chat_input_enabled))
let shouldUseStreaming = $derived.by(() => {
const modules = flowStore.val.value?.modules
const lastModule = modules && modules.length > 0 ? modules[modules.length - 1] : undefined
return (
lastModule?.value?.type === 'aiagent' &&
lastModule?.value?.input_transforms?.streaming?.type === 'static' &&
lastModule?.value?.input_transforms?.streaming?.value === true
)
})
let showChatModeWarning = $state(false)
let showAdditionalInputs = $state(false)
let chatInputsEditTab = $state(false)
let chatInputsAddPropertyV2: AddPropertyV2 | undefined = $state(undefined)
let addPropertyV2: AddPropertyV2 | undefined = $state(undefined)
let previewSchema: Record<string, any> | undefined = $state(undefined)
let payloadData: Record<string, any> | undefined = undefined
let dropdownItems: Array<{
label: string
onClick: () => void
disabled?: boolean
}> = $state([])
let diff: Record<string, SchemaDiff> = $state({})
let editPanelSize = $state($flowInputEditorState?.editPanelSize ?? 0)
let selectedSchema: Record<string, any> | undefined = $state(undefined)
let runDisabled: boolean = $state(false)
let editableSchemaForm: EditableSchemaForm | undefined = $state(undefined)
let savedPreviewArgs: Record<string, any> | undefined = $state(undefined)
let isValid = $state(true)
let dynCode: string | undefined = $state(undefined)
let dynLang: ScriptLang | undefined = $state(undefined)
function updateEditPanelSize(size: number | undefined) {
if (!$flowInputEditorState) return
if (!size || size === 0) {
$flowInputEditorState.editPanelSize = undefined
} else {
$flowInputEditorState.editPanelSize = size
}
}
let timeout: number | undefined = undefined
$effect(() => {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
updateEditPanelSize(editPanelSize)
}, 100)
})
$effect(() => {
chatInputEnabled = Boolean(flowStore.val.value?.chat_input_enabled)
})
// Set up review mode when AI has made schema changes that are pending
$effect(() => {
hasAiSchemaChanges
diffManager?.beforeFlow?.schema
flowStore.val.schema
untrack(() => {
if (hasAiSchemaChanges) {
// In review mode, selectedSchema = beforeSchema (what we might revert to)
selectedSchema = structuredClone($state.snapshot(diffManager!.beforeFlow!.schema))
diff = computeDiff(flowStore.val.schema, selectedSchema)
previewSchema = schemaFromDiff(diff, selectedSchema)
runDisabled = true
if (Object.values(diff).every((d) => d.diff === 'same')) {
diffManager?.acceptModule(SPECIAL_MODULE_IDS.INPUT, flowStore)
}
}
})
})
$effect(() => {
if (!hasAiSchemaChanges) {
selectedSchema = undefined
previewSchema = undefined
diff = {}
runDisabled = false
}
})
const getDropdownItems = () => {
return [
{
label: 'Input editor',
onClick: () => {
handleEditSchema('inputEditor')
},
disabled:
!$flowInputEditorState?.selectedTab ||
$flowInputEditorState?.selectedTab === 'inputEditor',
icon: Pen,
selected: $flowInputEditorState?.selectedTab === 'inputEditor'
},
{
label: 'Trigger captures',
onClick: () => {
handleEditSchema('captures')
},
disabled: $flowInputEditorState?.selectedTab === 'captures',
icon: CaptureIcon,
selected: $flowInputEditorState?.selectedTab === 'captures'
},
{
label: 'History',
onClick: () => {
handleEditSchema('history')
},
disabled: $flowInputEditorState?.selectedTab === 'history',
icon: History,
selected: $flowInputEditorState?.selectedTab === 'history'
},
{
label: 'Json payload',
onClick: () => {
handleEditSchema('json')
},
disabled: $flowInputEditorState?.selectedTab === 'json',
icon: Braces,
selected: $flowInputEditorState?.selectedTab === 'json'
},
{
label: "First step's inputs",
onClick: () => {
handleEditSchema('firstStepInputs')
},
icon: Code,
selected: $flowInputEditorState?.selectedTab === 'firstStepInputs'
},
{
label: 'Saved inputs',
onClick: () => {
handleEditSchema('savedInputs')
},
disabled: $flowInputEditorState?.selectedTab === 'savedInputs',
icon: Save,
selected: $flowInputEditorState?.selectedTab === 'savedInputs'
}
]
}
function handleEditSchema(editTab?: any) {
if (!$flowInputEditorState) {
return
}
if (editTab !== undefined) {
$flowInputEditorState.selectedTab = editTab
} else if ($flowInputEditorState.selectedTab === undefined) {
$flowInputEditorState.selectedTab = 'inputEditor'
} else {
$flowInputEditorState.selectedTab = undefined
}
}
function schemaFromPayload(payloadData: any) {
const payload = structuredClone($state.snapshot(payloadData))
const parsed = JSON.parse(JSON.stringify(payload))
if (!parsed) {
sendUserToast('Invalid Schema', true)
return
}
if (Object.keys(parsed).length === 0) {
return {
required: [],
properties: {},
type: 'object',
additionalProperties: false,
order: []
}
}
return { required: [], properties: {}, ...convert(parsed) }
}
function handleKeydown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
if (!previewOpen) {
runPreview()
}
} else if (event.key === 'Enter' && previewSchema && !preventEnter) {
applySchemaAndArgs()
connectFirstNode()
event.stopPropagation()
event.preventDefault()
}
}
async function runPreview() {
await onTestFlow?.(undefined)
}
function updatePreviewSchemaAndArgs(payload: any) {
if (isObjectTooBig(payload)) {
sendUserToast('Payload too big to be used as input', true)
return
}
if (!payload) {
payloadData = undefined
selectedSchema = undefined
updatePreviewArguments(undefined)
updatePreviewSchema(undefined)
return
}
payloadData = structuredClone($state.snapshot(payload))
selectedSchema = schemaFromPayload(payloadData)
updatePreviewSchema(selectedSchema)
updatePreviewArguments(payloadData)
}
async function updatePreviewSchema(newSchema: Record<string, any> | undefined) {
if (!newSchema) {
previewSchema = undefined
if (runDisabled) {
await tick()
runDisabled = false
}
diff = {}
return
}
diff = {}
const diffSchema = computeDiff(newSchema, flowStore.val.schema)
diff = diffSchema
previewSchema = schemaFromDiff(diffSchema, flowStore.val.schema)
runDisabled = true
}
async function applySchemaAndArgs() {
if (hasAiSchemaChanges) {
// Review mode: Accept all AI changes
diffManager?.acceptModule(SPECIAL_MODULE_IDS.INPUT, flowStore)
} else {
// Preview mode: Apply the diff to flowStore
flowStore.val.schema = applyDiff(flowStore.val.schema, diff)
if (previewArgs.val) {
savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val))
}
updatePreviewSchemaAndArgs(undefined)
if ($flowInputEditorState) {
$flowInputEditorState.selectedTab = undefined
}
}
}
function updatePreviewArguments(payloadData: Record<string, any> | undefined) {
if (!payloadData) {
if (savedPreviewArgs) {
previewArgs.val = savedPreviewArgs
}
return
}
savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val))
previewArgs.val = structuredClone($state.snapshot(payloadData))
}
let tabButtonWidth = 0
let connectFirstNode: () => void = $state(() => {})
let init = false
function initPayloadData() {
if ($flowInputEditorState.payloadData && !init) {
init = true
updatePreviewSchemaAndArgs($flowInputEditorState.payloadData)
$flowInputEditorState.payloadData = undefined
}
}
$effect(() => {
if ($flowInputEditorState) {
untrack(() => {
dropdownItems = getDropdownItems()
initPayloadData()
})
}
})
let preventEnter = $state(false)
async function acceptChange(arg: { label: string; nestedParent: any | undefined }) {
if (hasAiSchemaChanges) {
// Review mode: Accept = finalize the AI change (keep current value)
handleChangeInReviewMode(arg, 'accept')
} else {
// Preview mode: Accept = apply the change to flowStore
handleChange(arg, flowStore.val.schema, diff, (newSchema) => {
flowStore.val.schema = newSchema
})
}
}
async function rejectChange(arg: { label: string; nestedParent: any | undefined }) {
if (hasAiSchemaChanges) {
// Review mode: Reject = revert flowStore field to beforeSchema value
handleChangeInReviewMode(arg, 'reject')
} else {
// Preview mode: Reject = remove proposal from selectedSchema
const revertDiff = computeDiff(flowStore.val.schema, selectedSchema)
handleChange(arg, selectedSchema, revertDiff, (newSchema) => {
selectedSchema = newSchema
})
}
}
function handleChange(
arg: { label: string; nestedParent: any | undefined },
currentSchema: Record<string, any> | undefined,
diffSchema: Record<string, SchemaDiff> | undefined,
updateCurrentSchema: (newSchema: Record<string, any> | undefined) => void
) {
if (!diff || !currentSchema) {
return
}
const path = getFullPath(arg)
const parentPath = path.slice(0, -1)
const diffStatus = getNestedProperty({ diff: diffSchema }, path, 'diff')
const schema = getNestedProperty(currentSchema, parentPath, 'properties')
const localDiff = {
[arg.label]: diffStatus
}
const schemaUpdated = applyDiff(schema, localDiff)
if (parentPath.length > 0) {
setNestedProperty(currentSchema, parentPath, schemaUpdated)
} else {
updateCurrentSchema(schemaUpdated)
}
diff = computeDiff(selectedSchema, flowStore.val.schema)
previewSchema = schemaFromDiff(diff, flowStore.val.schema)
}
function handleChangeInReviewMode(
arg: { label: string; nestedParent: any | undefined },
action: 'accept' | 'reject'
) {
// Accept: source=flowStore.val.schema (current), target=beforeSchema
// Reject: source=beforeSchema, target=flowStore.val.schema (current)
const beforeSchema = diffManager?.beforeFlow?.schema
const sourceSchema = action === 'accept' ? flowStore.val.schema : beforeSchema
const targetSchema = action === 'accept' ? beforeSchema : flowStore.val.schema
if (!beforeSchema || !sourceSchema || !targetSchema) return
const path = getFullPath(arg)
const parentPath = path.slice(0, -1)
const getSchemaAtPath = (schema: Record<string, any>) =>
parentPath.length === 0 ? schema : getNestedProperty(schema, parentPath, 'properties')
const getProperties = (schema: Record<string, any>) => getSchemaAtPath(schema)?.properties
const sourceProperties = getProperties(sourceSchema)
const targetProperties = getProperties(targetSchema)
const targetSchemaAtPath = getSchemaAtPath(targetSchema)
const sourceValue = sourceProperties?.[arg.label]
if (sourceValue !== undefined) {
if (targetProperties) {
targetProperties[arg.label] = structuredClone($state.snapshot(sourceValue))
if (targetSchemaAtPath?.order && !targetSchemaAtPath.order.includes(arg.label)) {
targetSchemaAtPath.order.push(arg.label)
}
}
} else {
if (targetProperties && arg.label in targetProperties) {
delete targetProperties[arg.label]
if (targetSchemaAtPath?.order) {
targetSchemaAtPath.order = targetSchemaAtPath.order.filter((x: string) => x !== arg.label)
}
}
}
if (action === 'accept') {
diffManager.beforeFlow.schema = { ...beforeSchema }
} else {
flowStore.val.schema = { ...flowStore.val.schema }
}
}
function resetArgs() {
if (!previewSchema) {
savedPreviewArgs = undefined
}
}
$effect(() => {
if (!previewArgs && savedPreviewArgs != undefined) {
readFieldsRecursively(flowStore.val.schema)
untrack(() => {
resetArgs()
})
}
})
let historicInputs: HistoricInputs | undefined = $state(undefined)
let captureTable: CaptureTable | undefined = $state(undefined)
let savedInputsPicker: SavedInputsPicker | undefined = $state(undefined)
let jsonInputs: JsonInputs | undefined = $state(undefined)
let firstStepInputs: FirstStepInputs | undefined = $state(undefined)
function resetSelected() {
historicInputs?.resetSelected(true)
captureTable?.resetSelected(true)
savedInputsPicker?.resetSelected(true)
jsonInputs?.resetSelected(true)
firstStepInputs?.resetSelected(true)
}
async function runFlowWithMessage(
message: string,
conversationId: string,
additionalInputs?: Record<string, any>
): Promise<string | undefined> {
previewArgs.val = {
user_message: message,
...(additionalInputs ?? {})
}
const jobId = await onTestFlow?.(conversationId)
return jobId
}
function hasOtherInputs(): boolean {
const properties = flowStore.val.schema?.properties
return Boolean(
properties &&
Object.keys(properties).length > 0 &&
!(Object.keys(properties).length === 1 && Object.keys(properties).includes('user_message'))
)
}
function handleToggleChatMode() {
if (!flowStore.val.value?.chat_input_enabled) {
// Check if there are existing inputs
if (hasOtherInputs()) {
showChatModeWarning = true
} else {
enableChatMode()
}
} else {
// Disable chat input - remove from flow.value
if (flowStore.val.value) {
flowStore.val.value.chat_input_enabled = false
}
}
}
function enableChatMode() {
// Enable chat input - set in flow.value
flowStore.val.value.chat_input_enabled = true
// Set up the schema for chat input
flowStore.val.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
properties: {
user_message: {
type: 'string',
description: 'Message from user'
}
},
required: ['user_message']
}
// Find all AI agent modules
const aiAgentModules = flowStore.val.value.modules.filter((m) => m.value.type === 'aiagent')
if (aiAgentModules.length === 0) {
// No AI agent exists, create one with context memory set to 10
const aiAgentId = nextId(flowStateStore.val, flowStore.val)
flowStore.val.value.modules = [
...flowStore.val.value.modules,
{
id: aiAgentId,
value: {
type: 'aiagent',
tools: [],
input_transforms: Object.keys(AI_AGENT_SCHEMA.properties ?? {}).reduce(
(accu, key) => {
if (key === 'user_message') {
accu[key] = { type: 'javascript', expr: 'flow_input.user_message' }
} else if (key === 'memory') {
accu[key] = { type: 'static', value: { kind: 'auto', context_length: 10 } }
} else {
accu[key] = {
type: 'static',
value: undefined
}
}
return accu
},
{} as AiAgent['input_transforms']
)
}
}
]
sendUserToast(
'Chat mode enabled. AI agent created with user message input and context memory set to 10.',
false
)
} else if (aiAgentModules.length === 1) {
// Exactly one AI agent exists: fill in defaults only for inputs the
// user hasn't configured, so re-enabling chat mode on an already
// configured agent doesn't clobber a custom user_message expression
// or a deliberate memory choice (e.g. off).
const aiAgent = aiAgentModules[0]
const value = aiAgent.value as AiAgent
// Degenerate shapes the input form can produce without deliberate
// configuration count as unconfigured: empty static value (undefined
// persists as null through JSON round-trips), blank JS expression
// (the JS toggle seeds a bare backtick pair), or an AI transform
// (meaningless for the chat input).
const isUnconfigured = (transform: InputTransform | undefined) =>
transform === undefined ||
(transform.type === 'static' && (transform.value == null || transform.value === '')) ||
(transform.type === 'javascript' && transform.expr.replaceAll('`', '').trim() === '') ||
transform.type === 'ai'
const applied: string[] = []
if (isUnconfigured(value.input_transforms['user_message'])) {
value.input_transforms['user_message'] = {
type: 'javascript',
expr: 'flow_input.user_message'
}
applied.push('user message input')
}
if (isUnconfigured(value.input_transforms['memory'])) {
value.input_transforms['memory'] = {
type: 'static',
value: { kind: 'auto', context_length: 10 }
}
applied.push('context memory set to 10')
}
sendUserToast(
applied.length > 0
? `Chat mode enabled. AI agent configured with ${applied.join(' and ')}.`
: 'Chat mode enabled. Existing AI agent configuration kept unchanged.',
false
)
}
// If there are multiple AI agents, don't auto-configure (ambiguous which one to configure)
showChatModeWarning = false
}
</script>
<!-- Add svelte:window to listen for keyboard events -->
<svelte:window onkeydown={handleKeydown} />
<ConfirmationModal
open={showChatModeWarning}
title="Enable Chat Mode?"
confirmationText="Continue"
onConfirmed={enableChatMode}
onCanceled={() => {
showChatModeWarning = false
chatInputEnabled = false
}}
>
<p class="text-sm text-secondary">
Enabling Chat Mode will replace all existing flow inputs with a single
<span class="font-mono text-xs bg-surface-secondary px-1 rounded">user_message</span>
parameter.
</p>
<p class="text-sm text-secondary mt-2">
Your current input configuration will be lost. Are you sure you want to continue?
</p>
</ConfirmationModal>
<FlowCard {noEditor} title="Flow Input">
{#snippet action()}
{#if !disabled}
<div class="flex items-center gap-2">
<Toggle
size="sm"
bind:checked={chatInputEnabled}
on:change={() => {
handleToggleChatMode()
}}
options={{
right: 'Chat Mode',
rightTooltip:
'When enabled, the flow execution page will show a chat interface where each message sent runs the flow with the message as "user_message" input parameter. The flow schema will be automatically set to accept only a user_message string input.'
}}
/>
{#if flowStore.val.value?.chat_input_enabled}
<Button
size="xs"
variant="border"
color={showAdditionalInputs ? 'blue' : 'light'}
startIcon={{ icon: Settings2 }}
title="Manage inputs"
on:click={() => (showAdditionalInputs = !showAdditionalInputs)}
>
Manage inputs
</Button>
{/if}
</div>
{/if}
{/snippet}
{#if !disabled}
<div class="flex flex-col h-full">
{#if flowStore.val.value?.chat_input_enabled}
<div class="flex flex-col h-full">
{#if showAdditionalInputs}
<div class="border-b p-2">
<EditableSchemaForm
bind:schema={flowStore.val.schema}
hiddenArgs={['user_message']}
isFlowInput
showSensitiveToggle
editTab={chatInputsEditTab ? 'inputEditor' : undefined}
showDynOpt
bind:dynCode
bind:dynLang
on:delete={(e) => {
chatInputsAddPropertyV2?.handleDeleteArgument([e.detail])
}}
>
{#snippet openEditTab()}
<Button
size="xs"
variant={chatInputsEditTab ? 'contained' : 'border'}
color={chatInputsEditTab ? 'blue' : 'light'}
startIcon={{ icon: chatInputsEditTab ? ChevronRight : Pen }}
title={chatInputsEditTab ? 'Close editor' : 'Edit inputs'}
onClick={() => {
chatInputsEditTab = !chatInputsEditTab
}}
/>
{/snippet}
{#snippet addProperty()}
<AddPropertyV2
bind:this={chatInputsAddPropertyV2}
bind:schema={flowStore.val.schema}
onAddNew={() => {}}
>
{#snippet trigger()}
<Button
size="xs"
color="light"
startIcon={{ icon: Plus }}
title="Add additional input"
>
Add input
</Button>
{/snippet}
</AddPropertyV2>
{/snippet}
</EditableSchemaForm>
</div>
{/if}
<FlowChat
onRunFlow={runFlowWithMessage}
path={$pathStore}
hideSidebar={true}
useStreaming={shouldUseStreaming}
inputSchema={flowStore.val.schema}
/>
</div>
{:else}
<div class="py-2 px-4 flex-1 min-h-0">
<EditableSchemaForm
bind:this={editableSchemaForm}
bind:schema={flowStore.val.schema}
isFlowInput
showSensitiveToggle
on:delete={(e) => {
addPropertyV2?.handleDeleteArgument([e.detail])
}}
showDynOpt
displayWebhookWarning
editTab={$flowInputEditorState?.selectedTab}
{previewSchema}
bind:args={previewArgs.val}
bind:editPanelSize={
() => {
return editPanelSize
},
(v) => {
if (editPanelSize != v) {
editPanelSize = v
}
}
}
editPanelInitialSize={$flowInputEditorState?.editPanelSize}
pannelExtraButtonWidth={$flowInputEditorState?.editPanelSize ? tabButtonWidth : 0}
{diff}
disableDnd={!!previewSchema}
on:rejectChange={(e) => {
const isAiChange = hasAiSchemaChanges
rejectChange(e.detail).then(() => {
if (!isAiChange) {
updatePreviewSchema(selectedSchema)
}
})
}}
on:acceptChange={(e) => {
acceptChange(e.detail).then(() => {
const isAiChange = hasAiSchemaChanges
if (!isAiChange) {
updatePreviewSchema(selectedSchema)
}
})
}}
shouldDispatchChanges={true}
onChange={() => {
if (!previewSchema) {
let args = $state.snapshot(previewArgs.val)
if (!deepEqual(args, savedPreviewArgs)) {
savedPreviewArgs = args
}
}
}}
bind:isValid
bind:dynCode
bind:dynLang
>
{#snippet openEditTab()}
<div class={twMerge('flex flex-row divide-x', ButtonType.ColorVariants.blue.divider)}>
<SideBarTab {dropdownItems} fullMenu={!!$flowInputEditorState?.selectedTab}>
{#snippet close_button()}
<Button
onClick={() => handleEditSchema()}
{...!!$flowInputEditorState?.selectedTab
? {
title: 'Close input editor',
startIcon: { icon: ChevronRight },
btnClasses: 'rounded-none rounded-tl-md'
}
: {
title: 'Open input editor',
startIcon: { icon: Pen }
}}
variant="accent"
iconOnly
wrapperClasses="h-full"
/>
{/snippet}
</SideBarTab>
</div>
{/snippet}
{#snippet addProperty()}
{#if !!previewSchema}
<div class="flex flex-row items-center gap-2 right-2 justify-end">
<Button
size="xs"
color="green"
disabled={!previewSchema}
shortCut={{ Icon: CornerDownLeft, hide: false, withoutModifier: true }}
startIcon={{ icon: Check }}
onClick={() => {
applySchemaAndArgs()
connectFirstNode()
}}
>
{#if hasAiSchemaChanges}
Accept all changes
{:else if Object.values(diff).every((el) => el.diff === 'same')}
Apply args
{:else}
Update schema
{/if}
</Button>
<Button
variant="default"
size="xs"
startIcon={{ icon: X }}
shortCut={{ key: 'esc', withoutModifier: true }}
onClick={() => {
if (hasAiSchemaChanges) {
if (diffManager?.beforeFlow?.schema) {
diffManager.rejectModule(SPECIAL_MODULE_IDS.INPUT, flowStore)
}
} else {
resetSelected()
}
}}
/>
</div>
{:else}
<AddPropertyV2
bind:schema={flowStore.val.schema}
bind:this={addPropertyV2}
onAddNew={(argName) => {
handleEditSchema('inputEditor')
editableSchemaForm?.openField(argName)
refreshFlowStateStore(flowStore)
}}
>
{#snippet trigger()}
<div
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
id="add-flow-input-btn"
>
<Plus size={14} />
</div>
{/snippet}
</AddPropertyV2>
{/if}
{/snippet}
{#snippet extraTab()}
{#if $flowInputEditorState?.selectedTab === 'history'}
<FlowInputEditor
title="History"
on:destroy={() => {
updatePreviewSchemaAndArgs(undefined)
}}
>
<HistoricInputs
bind:this={historicInputs}
workspace={opWs}
runnableId={$initialPathStore ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail?.args ?? undefined)
}}
limitPayloadSize
/>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'captures'}
<FlowInputEditor
on:destroy={() => {
updatePreviewSchemaAndArgs(undefined)
}}
title="Trigger captures"
>
{#snippet action()}
<div class="center-center">
<CaptureButton on:openTriggers small={true} />
</div>
{/snippet}
<div class="h-full">
<CaptureTable
path={$initialPathStore || fakeInitialPath}
workspace={opWs}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
isFlow={true}
headless={true}
addButton={false}
bind:this={captureTable}
limitPayloadSize
/>
</div>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'savedInputs'}
<FlowInputEditor
on:destroy={() => {
updatePreviewSchemaAndArgs(undefined)
}}
title="Saved inputs"
>
<SavedInputsPicker
workspace={opWs}
runnableId={$initialPathStore ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
on:isEditing={(e) => {
preventEnter = e.detail
}}
previewArgs={previewArgs.val}
{isValid}
limitPayloadSize
bind:this={savedInputsPicker}
/>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'json'}
<FlowInputEditor
on:destroy={() => {
updatePreviewSchemaAndArgs(undefined)
}}
title="Json payload"
>
<JsonInputs
on:focus={() => {
preventEnter = true
}}
on:blur={async () => {
preventEnter = false
}}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
selected={!!previewArgs.val}
bind:this={jsonInputs}
/>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'}
<FlowInputEditor
on:destroy={() => {
updatePreviewSchemaAndArgs(undefined)
connectFirstNode = () => {}
}}
title="First step's inputs"
>
<FirstStepInputs
bind:this={firstStepInputs}
on:connectFirstNode={({ detail }) => {
connectFirstNode = detail.connectFirstNode
}}
on:select={(e) => {
if (e.detail) {
const diffSchema = computeDiff(e.detail, flowStore.val.schema)
diff = diffSchema
previewSchema = schemaFromDiff(diffSchema, flowStore.val.schema)
runDisabled = true
} else {
updatePreviewSchemaAndArgs(undefined)
}
}}
/>
</FlowInputEditor>
{/if}
{/snippet}
{#snippet runButton()}
<div class="w-full flex justify-end pr-5">
<Button
variant="accent"
btnClasses="w-fit"
disabled={runDisabled || !isValid}
size="xs"
shortCut={{ Icon: CornerDownLeft, hide: false }}
onClick={() => {
runPreview()
}}
>
Run
</Button>
</div>
{/snippet}
</EditableSchemaForm>
</div>
{/if}
</div>
{:else}
<div class="p-4 border-b">
<FlowInputViewer schema={effectiveSchema} />
</div>
{/if}
</FlowCard>