mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
feat: add sub-graph config panel with variable selection and null
handling
This commit is contained in:
@ -1,85 +1,178 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { WhenOutputNoneOption } from '../types'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
|
||||
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
|
||||
import { RiCheckLine } from '@remixicon/react'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import Tab, { TabType } from '@/app/components/workflow/nodes/_base/components/workflow-panel/tab'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type ConfigPanelProps = {
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
activeTab: 'settings' | 'lastRun'
|
||||
onTabChange: (tab: 'settings' | 'lastRun') => void
|
||||
agentName: string
|
||||
extractorNodeId: string
|
||||
mentionConfig: MentionConfig
|
||||
availableNodes: Node[]
|
||||
availableVars: NodeOutPutVar[]
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
}
|
||||
|
||||
const outputVariables = [
|
||||
{ name: 'text', type: 'string' },
|
||||
{ name: 'structured_output', type: 'object' },
|
||||
]
|
||||
|
||||
const ConfigPanel: FC<ConfigPanelProps> = ({
|
||||
toolNodeId: _toolNodeId,
|
||||
paramKey: _paramKey,
|
||||
activeTab,
|
||||
agentName,
|
||||
extractorNodeId,
|
||||
mentionConfig,
|
||||
availableNodes,
|
||||
availableVars,
|
||||
onMentionConfigChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [whenOutputNone, setWhenOutputNone] = useState<WhenOutputNoneOption>('default')
|
||||
const [tabType, setTabType] = useState<TabType>(TabType.settings)
|
||||
|
||||
const handleWhenOutputNoneChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setWhenOutputNone(e.target.value as WhenOutputNoneOption)
|
||||
}, [])
|
||||
const resolvedExtractorId = mentionConfig.extractor_node_id || extractorNodeId
|
||||
|
||||
if (activeTab === 'lastRun') {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
const selectedOutput = useMemo<ValueSelector>(() => {
|
||||
if (!resolvedExtractorId || !mentionConfig.output_selector?.length)
|
||||
return []
|
||||
|
||||
return [resolvedExtractorId, ...(mentionConfig.output_selector || [])]
|
||||
}, [mentionConfig.output_selector, resolvedExtractorId])
|
||||
|
||||
const handleOutputVarChange = useCallback((value: ValueSelector | string) => {
|
||||
const selector = Array.isArray(value) ? value : []
|
||||
const nextExtractorId = selector[0] || resolvedExtractorId
|
||||
const nextOutputSelector = selector.length > 1 ? selector.slice(1) : []
|
||||
|
||||
onMentionConfigChange({
|
||||
...mentionConfig,
|
||||
extractor_node_id: nextExtractorId,
|
||||
output_selector: nextOutputSelector,
|
||||
})
|
||||
}, [mentionConfig, onMentionConfigChange, resolvedExtractorId])
|
||||
|
||||
const whenOutputNoneOptions = useMemo(() => ([
|
||||
{
|
||||
value: 'raise_error',
|
||||
name: t('subGraphModal.whenOutputNone.error', { ns: 'workflow' }),
|
||||
description: t('subGraphModal.whenOutputNone.errorDesc', { ns: 'workflow' }),
|
||||
},
|
||||
{
|
||||
value: 'use_default',
|
||||
name: t('subGraphModal.whenOutputNone.default', { ns: 'workflow' }),
|
||||
description: t('subGraphModal.whenOutputNone.defaultDesc', { ns: 'workflow' }),
|
||||
},
|
||||
]), [t])
|
||||
|
||||
const handleNullStrategyChange = useCallback((item: Item) => {
|
||||
if (typeof item.value !== 'string')
|
||||
return
|
||||
onMentionConfigChange({
|
||||
...mentionConfig,
|
||||
null_strategy: item.value as MentionConfig['null_strategy'],
|
||||
})
|
||||
}, [mentionConfig, onMentionConfigChange])
|
||||
|
||||
const handleDefaultValueChange = useCallback((value: string) => {
|
||||
const trimmed = value.trim()
|
||||
let nextValue: unknown = value
|
||||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||
try {
|
||||
nextValue = JSON.parse(trimmed)
|
||||
}
|
||||
catch {
|
||||
nextValue = value
|
||||
}
|
||||
}
|
||||
|
||||
onMentionConfigChange({
|
||||
...mentionConfig,
|
||||
default_value: nextValue,
|
||||
})
|
||||
}, [mentionConfig, onMentionConfigChange])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="px-4 pb-2 pt-4">
|
||||
<div className="system-lg-semibold text-text-primary">
|
||||
{t('subGraphModal.internalStructure', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="system-sm-regular text-text-tertiary">
|
||||
{t('subGraphModal.internalStructureDesc', { ns: 'workflow', name: agentName })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 pb-2">
|
||||
<Tab value={tabType} onChange={setTabType} />
|
||||
</div>
|
||||
{tabType === TabType.lastRun && (
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<p className="system-sm-regular text-text-tertiary">
|
||||
{t('subGraphModal.noRunHistory', { ns: 'workflow' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<Field
|
||||
title={t('subGraphModal.outputVariables', { ns: 'workflow' })}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{outputVariables.map(variable => (
|
||||
<div
|
||||
key={variable.name}
|
||||
className="flex items-center justify-between rounded-lg bg-components-input-bg-normal px-3 py-2"
|
||||
>
|
||||
<span className="system-sm-medium text-text-secondary">{variable.name}</span>
|
||||
<span className="system-xs-regular text-text-tertiary">{variable.type}</span>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
{tabType === TabType.settings && (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="space-y-4 px-4 py-4">
|
||||
<Field title={t('subGraphModal.outputVariables', { ns: 'workflow' })}>
|
||||
<VarReferencePicker
|
||||
nodeId={extractorNodeId}
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
value={selectedOutput}
|
||||
onChange={handleOutputVarChange}
|
||||
availableNodes={availableNodes}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="space-y-4 px-4 py-4">
|
||||
<Field title={t('subGraphModal.whenOutputIsNone', { ns: 'workflow' })}>
|
||||
<SimpleSelect
|
||||
items={whenOutputNoneOptions}
|
||||
defaultValue={mentionConfig.null_strategy}
|
||||
allowSearch={false}
|
||||
notClearable
|
||||
onSelect={handleNullStrategyChange}
|
||||
renderOption={({ item, selected }) => (
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
{selected && (
|
||||
<RiCheckLine className="h-4 w-4 text-[14px] text-text-accent" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="system-sm-medium text-text-secondary">{item.name}</div>
|
||||
<div className="system-xs-regular mt-0.5 text-text-tertiary">{item.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
{mentionConfig.null_strategy === 'use_default' && (
|
||||
<div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{t('subGraphModal.defaultValueHint', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className={cn('mt-2 overflow-hidden rounded-lg border border-components-input-border-active bg-components-input-bg-normal p-1')}>
|
||||
<CodeEditor
|
||||
noWrapper
|
||||
language={CodeLanguage.json}
|
||||
value={mentionConfig.default_value ?? ''}
|
||||
onChange={handleDefaultValueChange}
|
||||
isJSONStringifyBeauty
|
||||
className="min-h-[160px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
title={t('subGraphModal.whenOutputIsNone', { ns: 'workflow' })}
|
||||
>
|
||||
<select
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-components-input-border-active bg-components-input-bg-normal px-3 py-2',
|
||||
'system-sm-regular text-text-secondary',
|
||||
'focus:border-primary-600 focus:outline-none',
|
||||
)}
|
||||
value={whenOutputNone}
|
||||
onChange={handleWhenOutputNoneChange}
|
||||
>
|
||||
<option value="default">
|
||||
{t('subGraphModal.whenOutputNone.default', { ns: 'workflow' })}
|
||||
</option>
|
||||
<option value="error">
|
||||
{t('subGraphModal.whenOutputNone.error', { ns: 'workflow' })}
|
||||
</option>
|
||||
</select>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,36 +1,72 @@
|
||||
import type { FC } from 'react'
|
||||
import type { SubGraphConfig } from '../types'
|
||||
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
|
||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useStore as useReactFlowStore } from 'reactflow'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useIsChatMode, useWorkflowVariables } from '@/app/components/workflow/hooks'
|
||||
import { Panel as NodePanel } from '@/app/components/workflow/nodes'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import ConfigPanel from './config-panel'
|
||||
|
||||
type SubGraphChildrenProps = {
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
onConfigChange: (config: Partial<SubGraphConfig>) => void
|
||||
agentName: string
|
||||
extractorNodeId: string
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
}
|
||||
|
||||
const SubGraphChildren: FC<SubGraphChildrenProps> = ({
|
||||
toolNodeId: _toolNodeId,
|
||||
paramKey: _paramKey,
|
||||
onConfigChange: _onConfigChange,
|
||||
agentName,
|
||||
extractorNodeId,
|
||||
mentionConfig,
|
||||
onMentionConfigChange,
|
||||
}) => {
|
||||
const selectedNode = useReactFlowStore(useShallow((s) => {
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
const nodePanelWidth = useStore(s => s.nodePanelWidth)
|
||||
|
||||
const { selectedNode, nodes } = useReactFlowStore(useShallow((s) => {
|
||||
const nodes = s.getNodes()
|
||||
const currentNode = nodes.find(node => node.data.selected)
|
||||
|
||||
if (currentNode?.data.type === BlockEnum.LLM) {
|
||||
return {
|
||||
id: currentNode.id,
|
||||
type: currentNode.type,
|
||||
data: currentNode.data,
|
||||
selectedNode: {
|
||||
id: currentNode.id,
|
||||
type: currentNode.type,
|
||||
data: currentNode.data,
|
||||
},
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
return null
|
||||
return {
|
||||
selectedNode: null,
|
||||
nodes,
|
||||
}
|
||||
}))
|
||||
|
||||
const extractorNode = useMemo(() => {
|
||||
return nodes.find(node => node.data.type === BlockEnum.LLM)
|
||||
}, [nodes])
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
return extractorNode ? [extractorNode] : []
|
||||
}, [extractorNode])
|
||||
|
||||
const availableVars = useMemo<NodeOutPutVar[]>(() => {
|
||||
if (!extractorNode)
|
||||
return []
|
||||
|
||||
const vars = getNodeAvailableVars({
|
||||
beforeNodes: [extractorNode],
|
||||
isChatMode,
|
||||
filterVar: () => true,
|
||||
})
|
||||
return vars.filter(item => item.nodeId === extractorNode.id)
|
||||
}, [extractorNode, getNodeAvailableVars, isChatMode])
|
||||
|
||||
const nodePanel = useMemo(() => {
|
||||
if (!selectedNode)
|
||||
return null
|
||||
@ -46,11 +82,25 @@ const SubGraphChildren: FC<SubGraphChildrenProps> = ({
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 flex">
|
||||
{nodePanel && (
|
||||
<div className="pointer-events-auto">
|
||||
{nodePanel}
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-auto">
|
||||
{nodePanel || (
|
||||
<div className="relative mr-1 h-full">
|
||||
<div
|
||||
className="flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg"
|
||||
style={{ width: `${nodePanelWidth}px` }}
|
||||
>
|
||||
<ConfigPanel
|
||||
agentName={agentName}
|
||||
extractorNodeId={extractorNodeId}
|
||||
mentionConfig={mentionConfig}
|
||||
availableNodes={availableNodes}
|
||||
availableVars={availableVars}
|
||||
onMentionConfigChange={onMentionConfigChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import type { FC } from 'react'
|
||||
import type { Viewport } from 'reactflow'
|
||||
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
|
||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import { useAvailableNodesMetaData, useSubGraphPersistence } from '../hooks'
|
||||
import { useAvailableNodesMetaData } from '../hooks'
|
||||
import SubGraphChildren from './sub-graph-children'
|
||||
|
||||
type SubGraphMainProps = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport: Viewport
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
agentName: string
|
||||
extractorNodeId: string
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
onSave?: (nodes: Node[], edges: Edge[]) => void
|
||||
}
|
||||
|
||||
@ -20,13 +23,14 @@ const SubGraphMain: FC<SubGraphMainProps> = ({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
agentName,
|
||||
extractorNodeId,
|
||||
mentionConfig,
|
||||
onMentionConfigChange,
|
||||
onSave,
|
||||
}) => {
|
||||
const reactFlowStore = useStoreApi()
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
const { updateSubGraphConfig } = useSubGraphPersistence({ toolNodeId, paramKey })
|
||||
|
||||
const handleSyncSubGraphDraft = useCallback(() => {
|
||||
const { getNodes, edges } = reactFlowStore.getState()
|
||||
@ -53,9 +57,10 @@ const SubGraphMain: FC<SubGraphMainProps> = ({
|
||||
interactionMode="subgraph"
|
||||
>
|
||||
<SubGraphChildren
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
onConfigChange={updateSubGraphConfig}
|
||||
agentName={agentName}
|
||||
extractorNodeId={extractorNodeId}
|
||||
mentionConfig={mentionConfig}
|
||||
onMentionConfigChange={onMentionConfigChange}
|
||||
/>
|
||||
</WorkflowWithInnerContext>
|
||||
)
|
||||
|
||||
@ -5,16 +5,27 @@ import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store
|
||||
import type { PromptItem } from '@/app/components/workflow/types'
|
||||
import { memo, useMemo } from 'react'
|
||||
import WorkflowWithDefaultContext from '@/app/components/workflow'
|
||||
import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION } from '@/app/components/workflow/constants'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { BlockEnum, EditionType, PromptRole } from '@/app/components/workflow/types'
|
||||
import SubGraphMain from './components/sub-graph-main'
|
||||
import { useSubGraphNodes } from './hooks'
|
||||
import { createSubGraphSlice } from './store'
|
||||
|
||||
const SUB_GRAPH_EDGE_GAP = 180
|
||||
const SUB_GRAPH_ENTRY_POSITION = {
|
||||
x: START_INITIAL_POSITION.x,
|
||||
y: 150,
|
||||
}
|
||||
const SUB_GRAPH_LLM_POSITION = {
|
||||
x: SUB_GRAPH_ENTRY_POSITION.x + NODE_WIDTH_X_OFFSET - SUB_GRAPH_EDGE_GAP,
|
||||
y: SUB_GRAPH_ENTRY_POSITION.y,
|
||||
}
|
||||
|
||||
const defaultViewport: Viewport = {
|
||||
x: 50,
|
||||
x: SUB_GRAPH_EDGE_GAP,
|
||||
y: 50,
|
||||
zoom: 1,
|
||||
zoom: 1.3,
|
||||
}
|
||||
|
||||
const SubGraph: FC<SubGraphProps> = (props) => {
|
||||
@ -23,6 +34,8 @@ const SubGraph: FC<SubGraphProps> = (props) => {
|
||||
paramKey,
|
||||
agentName,
|
||||
agentNodeId,
|
||||
mentionConfig,
|
||||
onMentionConfigChange,
|
||||
extractorNode,
|
||||
toolParamValue,
|
||||
onSave,
|
||||
@ -41,7 +54,7 @@ const SubGraph: FC<SubGraphProps> = (props) => {
|
||||
return {
|
||||
id: 'subgraph-source',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 150 },
|
||||
position: SUB_GRAPH_ENTRY_POSITION,
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: agentName,
|
||||
@ -50,8 +63,10 @@ const SubGraph: FC<SubGraphProps> = (props) => {
|
||||
_connectedTargetHandleIds: [],
|
||||
_subGraphEntry: true,
|
||||
_iconTypeOverride: BlockEnum.Agent,
|
||||
selected: false,
|
||||
variables: [],
|
||||
},
|
||||
selected: false,
|
||||
selectable: false,
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
@ -110,9 +125,11 @@ const SubGraph: FC<SubGraphProps> = (props) => {
|
||||
return {
|
||||
...extractorNode,
|
||||
hidden: false,
|
||||
position: { x: 320, y: 150 },
|
||||
selected: false,
|
||||
position: SUB_GRAPH_LLM_POSITION,
|
||||
data: {
|
||||
...extractorNode.data,
|
||||
selected: false,
|
||||
prompt_template: nextPromptTemplate,
|
||||
},
|
||||
}
|
||||
@ -159,8 +176,10 @@ const SubGraph: FC<SubGraphProps> = (props) => {
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={defaultViewport}
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
agentName={agentName}
|
||||
extractorNodeId={`${toolNodeId}_ext_${paramKey}`}
|
||||
mentionConfig={mentionConfig}
|
||||
onMentionConfigChange={onMentionConfigChange}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</WorkflowWithDefaultContext>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { Edge, Node, NodeOutPutVar, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
@ -26,6 +27,8 @@ export type SubGraphProps = {
|
||||
sourceVariable: ValueSelector
|
||||
agentNodeId: string
|
||||
agentName: string
|
||||
mentionConfig: MentionConfig
|
||||
onMentionConfigChange: (config: MentionConfig) => void
|
||||
extractorNode?: Node<LLMNodeType>
|
||||
toolParamValue?: string
|
||||
onSave?: (nodes: Node[], edges: Edge[]) => void
|
||||
|
||||
Reference in New Issue
Block a user