diff --git a/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts b/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts index 55db395f2e..1f5654ebda 100644 --- a/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts @@ -1,5 +1,7 @@ import type { Node, NodeOutPutVar, Var } from '../../types' import { renderHook } from '@testing-library/react' +import { useSnippetDetailStore } from '@/app/components/snippets/store' +import { PipelineInputVarType } from '@/models/pipeline' import { BlockEnum, VarType } from '../../types' import useNodesAvailableVarList, { useGetNodesAvailableVarList } from '../use-nodes-available-var-list' @@ -42,6 +44,8 @@ const outputVars: NodeOutPutVar[] = [{ describe('useNodesAvailableVarList', () => { beforeEach(() => { vi.clearAllMocks() + globalThis.history.pushState({}, '', '/') + useSnippetDetailStore.getState().reset() mockGetBeforeNodesInSameBranchIncludeParent.mockImplementation((nodeId: string) => [createNode({ id: `before-${nodeId}` })]) mockGetTreeLeafNodes.mockImplementation((nodeId: string) => [createNode({ id: `leaf-${nodeId}` })]) mockGetNodeAvailableVars.mockReturnValue(outputVars) @@ -76,7 +80,7 @@ describe('useNodesAvailableVarList', () => { expect(mockGetBeforeNodesInSameBranchIncludeParent).toHaveBeenCalledWith('loop-1') expect(mockGetBeforeNodesInSameBranchIncludeParent).toHaveBeenCalledWith('child-1') expect(result.current['loop-1']?.availableNodes.map(node => node.id)).toEqual(['before-loop-1', 'loop-1']) - expect(result.current['child-1']?.availableVars).toBe(outputVars) + expect(result.current['child-1']?.availableVars).toEqual(outputVars) expect(mockGetNodeAvailableVars).toHaveBeenNthCalledWith(2, expect.objectContaining({ parentNode: loopNode, isChatMode: true, @@ -86,6 +90,37 @@ describe('useNodesAvailableVarList', () => { })) }) + it('adds snippet input fields as virtual start variables on snippet canvases', () => { + globalThis.history.pushState({}, '', '/snippets/snippet-1/orchestrate') + useSnippetDetailStore.getState().setFields([{ + type: PipelineInputVarType.textInput, + label: 'Topic', + variable: 'topic', + required: true, + }]) + + const currentNode = createNode({ id: 'node-a' }) + + const { result } = renderHook(() => useNodesAvailableVarList([currentNode], { + filterVar: () => true, + })) + + expect(result.current['node-a']?.availableNodes[0]).toEqual(expect.objectContaining({ + id: 'start', + data: expect.objectContaining({ + type: BlockEnum.Start, + }), + })) + expect(result.current['node-a']?.availableVars[0]).toEqual(expect.objectContaining({ + nodeId: 'start', + isStartNode: true, + vars: [expect.objectContaining({ + variable: 'topic', + type: VarType.string, + })], + })) + }) + it('returns a callback version that can use leaf nodes or caller-provided nodes', () => { const firstNode = createNode({ id: 'node-a' }) const secondNode = createNode({ id: 'node-b' }) diff --git a/web/app/components/workflow/hooks/use-nodes-available-var-list.ts b/web/app/components/workflow/hooks/use-nodes-available-var-list.ts index cb04b43002..23a77ba849 100644 --- a/web/app/components/workflow/hooks/use-nodes-available-var-list.ts +++ b/web/app/components/workflow/hooks/use-nodes-available-var-list.ts @@ -1,10 +1,15 @@ import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useSnippetDetailStore } from '@/app/components/snippets/store' import { useIsChatMode, useWorkflow, useWorkflowVariables, } from '@/app/components/workflow/hooks' +import { + appendSnippetInputFieldVars, +} from '@/app/components/workflow/nodes/_base/hooks/snippet-input-field-vars' import { BlockEnum } from '@/app/components/workflow/types' type Params = { @@ -41,6 +46,8 @@ const useNodesAvailableVarList = (nodes: Node[], { onlyLeafNodeVar: false, filterVar: () => true, }) => { + const { t } = useTranslation() + const snippetInputFields = useSnippetDetailStore(s => s.fields) const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() const { getNodeAvailableVars } = useWorkflowVariables() const isChatMode = useIsChatMode() @@ -52,23 +59,31 @@ const useNodesAvailableVarList = (nodes: Node[], { const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId)) if (node.data.type === BlockEnum.Loop) availableNodes.push(node) + const snippetInputFieldAvailability = appendSnippetInputFieldVars({ + availableNodes, + fields: snippetInputFields, + title: t('panelTitle', { ns: 'snippet' }), + }) const { parentNode: iterationNode, } = getNodeInfo(nodeId, nodes) - const availableVars = getNodeAvailableVars({ - parentNode: iterationNode, - beforeNodes: availableNodes, - isChatMode, - filterVar, - hideEnv, - hideChatVar, - }) + const availableVars = [ + ...snippetInputFieldAvailability.availableVars, + ...getNodeAvailableVars({ + parentNode: iterationNode, + beforeNodes: availableNodes, + isChatMode, + filterVar, + hideEnv, + hideChatVar, + }), + ] const result = { node, availableVars, - availableNodes, + availableNodes: snippetInputFieldAvailability.availableNodes, } nodeAvailabilityMap[nodeId] = result }) @@ -76,6 +91,8 @@ const useNodesAvailableVarList = (nodes: Node[], { } export const useGetNodesAvailableVarList = () => { + const { t } = useTranslation() + const snippetInputFields = useSnippetDetailStore(s => s.fields) const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() const { getNodeAvailableVars } = useWorkflowVariables() const isChatMode = useIsChatMode() @@ -96,28 +113,36 @@ export const useGetNodesAvailableVarList = () => { const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId)) if (node.data.type === BlockEnum.Loop) availableNodes.push(node) + const snippetInputFieldAvailability = appendSnippetInputFieldVars({ + availableNodes, + fields: snippetInputFields, + title: t('panelTitle', { ns: 'snippet' }), + }) const { parentNode: iterationNode, } = getNodeInfo(nodeId, nodes) - const availableVars = getNodeAvailableVars({ - parentNode: iterationNode, - beforeNodes: availableNodes, - isChatMode, - filterVar, - hideEnv, - hideChatVar, - }) + const availableVars = [ + ...snippetInputFieldAvailability.availableVars, + ...getNodeAvailableVars({ + parentNode: iterationNode, + beforeNodes: availableNodes, + isChatMode, + filterVar, + hideEnv, + hideChatVar, + }), + ] const result = { node, availableVars, - availableNodes, + availableNodes: snippetInputFieldAvailability.availableNodes, } nodeAvailabilityMap[nodeId] = result }) return nodeAvailabilityMap - }, [getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode]) + }, [getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode, snippetInputFields, t]) return { getNodesAvailableVarList, } diff --git a/web/app/components/workflow/nodes/_base/hooks/snippet-input-field-vars.ts b/web/app/components/workflow/nodes/_base/hooks/snippet-input-field-vars.ts new file mode 100644 index 0000000000..a0c84b7495 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/hooks/snippet-input-field-vars.ts @@ -0,0 +1,108 @@ +import type { InputVarType, Node, NodeOutPutVar } from '@/app/components/workflow/types' +import type { SnippetInputField } from '@/models/snippet' +import { NODE_WIDTH } from '@/app/components/workflow/constants' +import { BlockEnum } from '@/app/components/workflow/types' +import { PipelineInputVarType } from '@/models/pipeline' +import { inputVarTypeToVarType } from '../../data-source/utils' + +export const SNIPPET_INPUT_FIELD_NODE_ID = 'start' + +export const isSnippetCanvas = () => { + if (typeof globalThis.location === 'undefined') + return false + + return /^\/snippets\/[^/]+\/orchestrate/.test(globalThis.location.pathname) +} + +const toWorkflowInputType = (type: SnippetInputField['type']) => type as unknown as InputVarType + +export const buildSnippetInputFieldNode = ( + fields: SnippetInputField[], + title: string, +): Node | undefined => { + const variables = fields.filter(field => !!field.variable) + + if (!variables.length) + return undefined + + return { + id: SNIPPET_INPUT_FIELD_NODE_ID, + type: 'custom', + position: { x: 0, y: 0 }, + width: NODE_WIDTH, + height: 80, + data: { + title, + desc: '', + type: BlockEnum.Start, + variables: variables.map(field => ({ + type: toWorkflowInputType(field.type), + label: field.label, + variable: field.variable, + max_length: field.max_length, + default: field.default_value, + required: field.required, + options: field.options, + placeholder: field.placeholder, + unit: field.unit, + allowed_file_upload_methods: field.allowed_file_upload_methods, + allowed_file_types: field.allowed_file_types, + allowed_file_extensions: field.allowed_file_extensions, + })), + }, + } as Node +} + +export const buildSnippetInputFieldVars = ( + fields: SnippetInputField[], + title: string, +): NodeOutPutVar | undefined => { + const vars = fields + .filter(field => !!field.variable) + .map(field => ({ + variable: field.variable, + type: inputVarTypeToVarType(field.type as PipelineInputVarType), + isParagraph: field.type === PipelineInputVarType.paragraph, + isSelect: field.type === PipelineInputVarType.select, + options: field.options, + required: field.required, + des: field.label, + })) + + if (!vars.length) + return undefined + + return { + nodeId: SNIPPET_INPUT_FIELD_NODE_ID, + title, + vars, + isStartNode: true, + } +} + +export const appendSnippetInputFieldVars = ({ + availableNodes, + fields, + title, +}: { + availableNodes: Node[] + fields: SnippetInputField[] + title: string +}) => { + const shouldAppendSnippetInputFields = isSnippetCanvas() + && fields.length > 0 + && !availableNodes.some(node => node.data.type === BlockEnum.Start) + const snippetInputFieldNode = shouldAppendSnippetInputFields + ? buildSnippetInputFieldNode(fields, title) + : undefined + const snippetInputFieldVars = shouldAppendSnippetInputFields + ? buildSnippetInputFieldVars(fields, title) + : undefined + + return { + availableNodes: snippetInputFieldNode + ? [snippetInputFieldNode, ...availableNodes] + : availableNodes, + availableVars: snippetInputFieldVars ? [snippetInputFieldVars] : [], + } +} diff --git a/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts b/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts index f226900899..e94f94916f 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts @@ -1,4 +1,6 @@ import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' +import { useTranslation } from 'react-i18next' +import { useSnippetDetailStore } from '@/app/components/snippets/store' import { useIsChatMode, useWorkflow, @@ -7,6 +9,7 @@ import { import { useStore as useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' import { inputVarTypeToVarType } from '../../data-source/utils' +import { appendSnippetInputFieldVars } from './snippet-input-field-vars' import useNodeInfo from './use-node-info' type Params = { @@ -28,10 +31,17 @@ const useAvailableVarList = (nodeId: string, { onlyLeafNodeVar: false, filterVar: () => true, }) => { + const { t } = useTranslation() + const snippetInputFields = useSnippetDetailStore(s => s.fields) const { getTreeLeafNodes, getNodeById, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() const { getNodeAvailableVars } = useWorkflowVariables() const isChatMode = useIsChatMode() const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId)) + const snippetInputFieldAvailability = appendSnippetInputFieldVars({ + availableNodes, + fields: snippetInputFields, + title: t('panelTitle', { ns: 'snippet' }), + }) const { parentNode: iterationNode, } = useNodeInfo(nodeId) @@ -63,20 +73,24 @@ const useAvailableVarList = (nodeId: string, { }) } } - const availableVars = [...getNodeAvailableVars({ - parentNode: iterationNode, - beforeNodes: availableNodes, - isChatMode, - filterVar, - hideEnv, - hideChatVar, - }), ...dataSourceRagVars] + const availableVars = [ + ...snippetInputFieldAvailability.availableVars, + ...getNodeAvailableVars({ + parentNode: iterationNode, + beforeNodes: availableNodes, + isChatMode, + filterVar, + hideEnv, + hideChatVar, + }), + ...dataSourceRagVars, + ] return { availableVars, - availableNodes, + availableNodes: snippetInputFieldAvailability.availableNodes, availableNodesWithParent: [ - ...availableNodes, + ...snippetInputFieldAvailability.availableNodes, ...(isDataSourceNode ? [currNode] : []), ], }