fix(web): snippet can use input fields

This commit is contained in:
JzoNg
2026-04-28 15:30:35 +08:00
parent 735e88f673
commit d95d4335bf
4 changed files with 212 additions and 30 deletions

View File

@@ -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' })

View File

@@ -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,
}

View File

@@ -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] : [],
}
}

View File

@@ -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] : []),
],
}