mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 19:16:51 +08:00
Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
330 lines
8.9 KiB
TypeScript
330 lines
8.9 KiB
TypeScript
import type { Edge, Node, ValueSelector } from '@/app/components/workflow/types'
|
|
import type { SnippetCanvasData, SnippetInputField } from '@/models/snippet'
|
|
import { useCallback, useState } from 'react'
|
|
import { getNodesBounds } from 'reactflow'
|
|
import CreateSnippetDialog from '@/app/components/snippets/create-snippet-dialog'
|
|
import { PipelineInputVarType } from '@/models/pipeline'
|
|
import { useCreateSnippet } from './use-create-snippet'
|
|
|
|
const DEFAULT_SNIPPET_VIEWPORT = { x: 0, y: 0, zoom: 1 }
|
|
const SNIPPET_INPUT_FIELD_NODE_ID = 'start'
|
|
const SNIPPET_VIEWPORT_WIDTH = 1200
|
|
const SNIPPET_VIEWPORT_HEIGHT = 800
|
|
const SNIPPET_VIEWPORT_PADDING = 160
|
|
const VARIABLE_REFERENCE_REGEX = /\{\{#([^#{}]+)#\}\}/g
|
|
const RESERVED_VARIABLE_PREFIXES = new Set(['rag'])
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
}
|
|
|
|
const isValueSelector = (value: unknown): value is ValueSelector => {
|
|
return Array.isArray(value)
|
|
&& value.length > 0
|
|
&& value.every(item => typeof item === 'string')
|
|
}
|
|
|
|
const isSelectorKey = (key?: string) => {
|
|
return key === 'selector' || !!key?.endsWith('_selector')
|
|
}
|
|
|
|
const isValueSelectorListKey = (key?: string) => {
|
|
return key === 'variables'
|
|
}
|
|
|
|
const isValueSelectorList = (value: unknown[]) => {
|
|
return value.length > 0 && value.every(isValueSelector)
|
|
}
|
|
|
|
const isContextPlaceholderSelector = (selector: ValueSelector) => {
|
|
return (selector.length === 1 && selector[0] === 'context')
|
|
|| selector.at(-1) === '#context#'
|
|
}
|
|
|
|
const getCenteredViewport = (nodes: Node[]) => {
|
|
if (!nodes.length)
|
|
return DEFAULT_SNIPPET_VIEWPORT
|
|
|
|
const bounds = getNodesBounds(nodes)
|
|
if (!bounds.width || !bounds.height)
|
|
return DEFAULT_SNIPPET_VIEWPORT
|
|
|
|
const zoom = Math.min(
|
|
(SNIPPET_VIEWPORT_WIDTH - SNIPPET_VIEWPORT_PADDING * 2) / bounds.width,
|
|
(SNIPPET_VIEWPORT_HEIGHT - SNIPPET_VIEWPORT_PADDING * 2) / bounds.height,
|
|
1,
|
|
)
|
|
const centerX = bounds.x + bounds.width / 2
|
|
const centerY = bounds.y + bounds.height / 2
|
|
|
|
return {
|
|
x: SNIPPET_VIEWPORT_WIDTH / 2 - centerX * zoom,
|
|
y: SNIPPET_VIEWPORT_HEIGHT / 2 - centerY * zoom,
|
|
zoom,
|
|
}
|
|
}
|
|
|
|
const collectSelectorsFromText = (value: string, selectors: ValueSelector[]) => {
|
|
for (const match of value.matchAll(VARIABLE_REFERENCE_REGEX)) {
|
|
const variablePath = match[1]
|
|
if (!variablePath)
|
|
continue
|
|
|
|
const selector = variablePath.split('.').filter(Boolean)
|
|
if (selector.length > 0 && !isContextPlaceholderSelector(selector))
|
|
selectors.push(selector)
|
|
}
|
|
}
|
|
|
|
const collectVariableSelectors = (
|
|
value: unknown,
|
|
selectors: ValueSelector[],
|
|
key?: string,
|
|
) => {
|
|
if (typeof value === 'string') {
|
|
collectSelectorsFromText(value, selectors)
|
|
return
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
if (isSelectorKey(key) && isValueSelector(value))
|
|
selectors.push(value)
|
|
|
|
if (isValueSelectorListKey(key) && isValueSelectorList(value)) {
|
|
value.forEach(selector => selectors.push(selector))
|
|
return
|
|
}
|
|
|
|
value.forEach(item => collectVariableSelectors(item, selectors))
|
|
return
|
|
}
|
|
|
|
if (!isRecord(value))
|
|
return
|
|
|
|
Object.entries(value).forEach(([currentKey, currentValue]) => {
|
|
collectVariableSelectors(currentValue, selectors, currentKey)
|
|
})
|
|
}
|
|
|
|
const isExternalVariableSelector = (
|
|
selector: ValueSelector,
|
|
selectedNodeIds: Set<string>,
|
|
) => {
|
|
const nodeId = selector[0]
|
|
if (!nodeId)
|
|
return false
|
|
|
|
if (nodeId.startsWith('$'))
|
|
return false
|
|
|
|
if (isContextPlaceholderSelector(selector))
|
|
return false
|
|
|
|
if (selectedNodeIds.has(nodeId))
|
|
return false
|
|
|
|
return !RESERVED_VARIABLE_PREFIXES.has(nodeId)
|
|
}
|
|
|
|
const sanitizeInputFieldVariable = (variable: string) => {
|
|
const sanitized = variable.replace(/\W/g, '_')
|
|
if (!sanitized)
|
|
return 'input'
|
|
|
|
if (/^\d/.test(sanitized))
|
|
return `input_${sanitized}`
|
|
|
|
return sanitized
|
|
}
|
|
|
|
const getUniqueInputFieldVariable = (
|
|
selector: ValueSelector,
|
|
usedVariables: Set<string>,
|
|
) => {
|
|
const baseVariable = sanitizeInputFieldVariable(selector.at(-1) ?? 'input')
|
|
let variable = baseVariable
|
|
let index = 2
|
|
|
|
while (usedVariables.has(variable)) {
|
|
variable = `${baseVariable}_${index}`
|
|
index += 1
|
|
}
|
|
|
|
usedVariables.add(variable)
|
|
return variable
|
|
}
|
|
|
|
const getInputFieldType = (selector: ValueSelector) => {
|
|
const variable = selector.at(-1)
|
|
if (variable === 'files')
|
|
return PipelineInputVarType.multiFiles
|
|
|
|
return PipelineInputVarType.textInput
|
|
}
|
|
|
|
const getExternalVariableInputFields = (
|
|
nodes: Node[],
|
|
selectedNodeIds: Set<string>,
|
|
) => {
|
|
const selectors: ValueSelector[] = []
|
|
nodes.forEach(node => collectVariableSelectors(node.data, selectors))
|
|
|
|
const usedVariables = new Set<string>()
|
|
const fieldBySelector = new Map<string, SnippetInputField>()
|
|
|
|
selectors.forEach((selector) => {
|
|
if (!isExternalVariableSelector(selector, selectedNodeIds))
|
|
return
|
|
|
|
const selectorKey = selector.join('.')
|
|
if (fieldBySelector.has(selectorKey))
|
|
return
|
|
|
|
const variable = getUniqueInputFieldVariable(selector, usedVariables)
|
|
fieldBySelector.set(selectorKey, {
|
|
label: variable,
|
|
variable,
|
|
type: getInputFieldType(selector),
|
|
required: true,
|
|
})
|
|
})
|
|
|
|
return {
|
|
inputFields: [...fieldBySelector.values()],
|
|
selectorMap: new Map(
|
|
[...fieldBySelector.entries()].map(([selectorKey, field]) => [
|
|
selectorKey,
|
|
[SNIPPET_INPUT_FIELD_NODE_ID, field.variable] satisfies ValueSelector,
|
|
]),
|
|
),
|
|
}
|
|
}
|
|
|
|
const rewriteVariableReferences = (
|
|
value: unknown,
|
|
selectorMap: Map<string, ValueSelector>,
|
|
key?: string,
|
|
): unknown => {
|
|
if (typeof value === 'string') {
|
|
return value.replace(VARIABLE_REFERENCE_REGEX, (match, variablePath: string) => {
|
|
const nextSelector = selectorMap.get(variablePath)
|
|
if (!nextSelector)
|
|
return match
|
|
|
|
return `{{#${nextSelector.join('.')}#}}`
|
|
})
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
if (isSelectorKey(key) && isValueSelector(value)) {
|
|
const nextSelector = selectorMap.get(value.join('.'))
|
|
if (nextSelector)
|
|
return nextSelector
|
|
}
|
|
|
|
if (isValueSelectorListKey(key) && isValueSelectorList(value)) {
|
|
return value.map((selector) => {
|
|
const nextSelector = selectorMap.get(selector.join('.'))
|
|
return nextSelector || selector
|
|
})
|
|
}
|
|
|
|
return value.map(item => rewriteVariableReferences(item, selectorMap))
|
|
}
|
|
|
|
if (!isRecord(value))
|
|
return value
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([currentKey, currentValue]) => [
|
|
currentKey,
|
|
rewriteVariableReferences(currentValue, selectorMap, currentKey),
|
|
]),
|
|
)
|
|
}
|
|
|
|
const getSelectedSnippetGraph = (selectedNodes: Node[], edges: Edge[]) => {
|
|
const selectedNodeIds = new Set(selectedNodes.map(node => node.id))
|
|
const {
|
|
inputFields,
|
|
selectorMap,
|
|
} = getExternalVariableInputFields(selectedNodes, selectedNodeIds)
|
|
const nodes = selectedNodes.map(node => ({
|
|
...node,
|
|
data: rewriteVariableReferences(node.data, selectorMap) as Node['data'],
|
|
selected: false,
|
|
}))
|
|
|
|
return {
|
|
graph: {
|
|
nodes,
|
|
edges: edges
|
|
.filter(edge => selectedNodeIds.has(edge.source) && selectedNodeIds.has(edge.target))
|
|
.map(edge => ({
|
|
...edge,
|
|
selected: false,
|
|
})),
|
|
viewport: getCenteredViewport(nodes),
|
|
} satisfies SnippetCanvasData,
|
|
inputFields,
|
|
}
|
|
}
|
|
|
|
type UseCreateSnippetFromSelectionParams = {
|
|
edges: Edge[]
|
|
selectedNodes: Node[]
|
|
onClose: () => void
|
|
}
|
|
|
|
export const useCreateSnippetFromSelection = ({
|
|
edges,
|
|
selectedNodes,
|
|
onClose,
|
|
}: UseCreateSnippetFromSelectionParams) => {
|
|
const [selectedSnippetGraph, setSelectedSnippetGraph] = useState<SnippetCanvasData>()
|
|
const [selectedSnippetInputFields, setSelectedSnippetInputFields] = useState<SnippetInputField[]>([])
|
|
const {
|
|
createSnippetMutation,
|
|
handleCloseCreateSnippetDialog,
|
|
handleCreateSnippet,
|
|
handleOpenCreateSnippetDialog,
|
|
isCreateSnippetDialogOpen,
|
|
isCreatingSnippet,
|
|
} = useCreateSnippet()
|
|
|
|
const handleOpenCreateSnippet = useCallback(() => {
|
|
const {
|
|
graph,
|
|
inputFields,
|
|
} = getSelectedSnippetGraph(selectedNodes, edges)
|
|
setSelectedSnippetGraph(graph)
|
|
setSelectedSnippetInputFields(inputFields)
|
|
handleOpenCreateSnippetDialog()
|
|
onClose()
|
|
}, [edges, handleOpenCreateSnippetDialog, onClose, selectedNodes])
|
|
|
|
const handleCloseCreateSnippet = useCallback(() => {
|
|
setSelectedSnippetGraph(undefined)
|
|
setSelectedSnippetInputFields([])
|
|
handleCloseCreateSnippetDialog()
|
|
}, [handleCloseCreateSnippetDialog])
|
|
|
|
const createSnippetDialog = (
|
|
<CreateSnippetDialog
|
|
isOpen={isCreateSnippetDialogOpen}
|
|
selectedGraph={selectedSnippetGraph}
|
|
inputFields={selectedSnippetInputFields}
|
|
isSubmitting={isCreatingSnippet || createSnippetMutation.isPending}
|
|
onClose={handleCloseCreateSnippet}
|
|
onConfirm={handleCreateSnippet}
|
|
/>
|
|
)
|
|
|
|
return {
|
|
createSnippetDialog,
|
|
handleOpenCreateSnippet,
|
|
isCreateSnippetDialogOpen,
|
|
}
|
|
}
|