mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
* 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>
1048 lines
31 KiB
Svelte
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>
|