mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 16:02:00 -04:00
fix(web): snippet can use input fields
This commit is contained in:
@@ -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' })
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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] : [],
|
||||
}
|
||||
}
|
||||
@@ -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] : []),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user