mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 15:08:06 +08:00
feat: Add sub-graph component for workflow
This commit is contained in:
@ -337,6 +337,8 @@ const FormInputItem: FC<Props> = ({
|
||||
showManageInputField={showManageInputField}
|
||||
onManageInputField={onManageInputField}
|
||||
disableVariableInsertion={disableVariableInsertion}
|
||||
toolNodeId={nodeId}
|
||||
paramKey={variable}
|
||||
/>
|
||||
)}
|
||||
{isNumber && isConstant && (
|
||||
|
||||
@ -2,11 +2,11 @@ import type { AgentBlockType } from '@/app/components/base/prompt-editor/types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
@ -15,6 +15,7 @@ import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import SubGraphModal from '../sub-graph-modal'
|
||||
import AgentHeaderBar from './agent-header-bar'
|
||||
import Placeholder from './placeholder'
|
||||
|
||||
@ -33,7 +34,8 @@ type MixedVariableTextInputProps = {
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
disableVariableInsertion?: boolean
|
||||
onViewInternals?: () => void
|
||||
toolNodeId?: string
|
||||
paramKey?: string
|
||||
}
|
||||
|
||||
const MixedVariableTextInput = ({
|
||||
@ -45,11 +47,13 @@ const MixedVariableTextInput = ({
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
disableVariableInsertion = false,
|
||||
onViewInternals,
|
||||
toolNodeId,
|
||||
paramKey = '',
|
||||
}: MixedVariableTextInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
|
||||
const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey)
|
||||
const [isSubGraphModalOpen, setIsSubGraphModalOpen] = useState(false)
|
||||
|
||||
const nodesByIdMap = useMemo(() => {
|
||||
return availableNodes.reduce((acc, node) => {
|
||||
@ -79,11 +83,6 @@ const MixedVariableTextInput = ({
|
||||
|
||||
const [selectedAgent, setSelectedAgent] = useState<{ id: string, title: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!detectedAgentFromValue && selectedAgent)
|
||||
setSelectedAgent(null)
|
||||
}, [detectedAgentFromValue, selectedAgent])
|
||||
|
||||
const agentNodes = useMemo(() => {
|
||||
return availableNodes
|
||||
.filter(node => node.data.type === BlockEnum.Agent)
|
||||
@ -115,6 +114,18 @@ const MixedVariableTextInput = ({
|
||||
|
||||
const displayedAgent = detectedAgentFromValue || (selectedAgent ? { nodeId: selectedAgent.id, name: selectedAgent.title } : null)
|
||||
|
||||
const handleOpenSubGraphModal = useCallback(() => {
|
||||
setIsSubGraphModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseSubGraphModal = useCallback(() => {
|
||||
setIsSubGraphModalOpen(false)
|
||||
}, [])
|
||||
|
||||
const sourceVariable: ValueSelector | undefined = displayedAgent
|
||||
? [displayedAgent.nodeId, 'text']
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'w-full rounded-lg border border-transparent bg-components-input-bg-normal',
|
||||
@ -126,7 +137,7 @@ const MixedVariableTextInput = ({
|
||||
<AgentHeaderBar
|
||||
agentName={displayedAgent.name}
|
||||
onRemove={handleAgentRemove}
|
||||
onViewInternals={onViewInternals}
|
||||
onViewInternals={handleOpenSubGraphModal}
|
||||
/>
|
||||
)}
|
||||
<PromptEditor
|
||||
@ -162,6 +173,17 @@ const MixedVariableTextInput = ({
|
||||
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} hasSelectedAgent={!!displayedAgent} />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{toolNodeId && displayedAgent && sourceVariable && (
|
||||
<SubGraphModal
|
||||
isOpen={isSubGraphModalOpen}
|
||||
onClose={handleCloseSubGraphModal}
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
sourceVariable={sourceVariable}
|
||||
agentName={displayedAgent.name}
|
||||
agentNodeId={displayedAgent.nodeId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ConfigPanelProps, WhenOutputNoneOption } from './types'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const outputVariables = [
|
||||
{ name: 'text', type: 'string' },
|
||||
{ name: 'structured_output', type: 'object' },
|
||||
]
|
||||
|
||||
const ConfigPanel: FC<ConfigPanelProps> = ({
|
||||
toolNodeId: _toolNodeId,
|
||||
paramKey: _paramKey,
|
||||
activeTab,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [whenOutputNone, setWhenOutputNone] = useState<WhenOutputNoneOption>('skip')
|
||||
|
||||
const handleWhenOutputNoneChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setWhenOutputNone(e.target.value as WhenOutputNoneOption)
|
||||
}, [])
|
||||
|
||||
if (activeTab === 'lastRun') {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
<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>
|
||||
))}
|
||||
</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="skip">
|
||||
{t('subGraphModal.whenOutputNone.skip', { ns: 'workflow' })}
|
||||
</option>
|
||||
<option value="error">
|
||||
{t('subGraphModal.whenOutputNone.error', { ns: 'workflow' })}
|
||||
</option>
|
||||
<option value="default">
|
||||
{t('subGraphModal.whenOutputNone.default', { ns: 'workflow' })}
|
||||
</option>
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConfigPanel)
|
||||
@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { SubGraphModalProps } from './types'
|
||||
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { Fragment, memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import SubGraphCanvas from './sub-graph-canvas'
|
||||
|
||||
const SubGraphModal: FC<SubGraphModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
sourceVariable,
|
||||
agentName,
|
||||
agentNodeId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-[60]" onClose={noop}>
|
||||
<TransitionChild>
|
||||
<div className="fixed inset-0 bg-background-overlay duration-300 ease-in data-[closed]:opacity-0 data-[enter]:opacity-100 data-[leave]:opacity-0" />
|
||||
</TransitionChild>
|
||||
<div className="fixed inset-0 overflow-hidden">
|
||||
<div className="flex h-full w-full items-center justify-center px-[10px] pb-[4px] pt-[24px]">
|
||||
<TransitionChild>
|
||||
<DialogPanel className="flex h-full w-full flex-col overflow-hidden rounded-2xl bg-components-panel-bg shadow-xl duration-100 ease-in data-[closed]:scale-95 data-[enter]:scale-100 data-[leave]:scale-95 data-[closed]:opacity-0 data-[enter]:opacity-100 data-[leave]:opacity-0">
|
||||
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-subtle px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-util-colors-indigo-indigo-500">
|
||||
<Agent className="h-4 w-4 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<span className="system-md-semibold text-text-primary">
|
||||
@
|
||||
{agentName}
|
||||
{' '}
|
||||
{t('subGraphModal.title', { ns: 'workflow' })}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-state-base-hover"
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-workflow-canvas-wrapper relative flex-1 overflow-hidden">
|
||||
<SubGraphCanvas
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
sourceVariable={sourceVariable}
|
||||
agentNodeId={agentNodeId}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SubGraphModal)
|
||||
@ -0,0 +1,49 @@
|
||||
import type { CreateSubGraphSlice, SubGraphSliceShape } from './types'
|
||||
|
||||
const initialState: Omit<SubGraphSliceShape, 'setSubGraphContext' | 'setSubGraphNodes' | 'setSubGraphEdges' | 'setSelectedOutputVar' | 'setWhenOutputNone' | 'setDefaultValue' | 'setShowDebugPanel' | 'setIsRunning' | 'setParentAvailableVars' | 'resetSubGraph'> = {
|
||||
parentToolNodeId: '',
|
||||
parameterKey: '',
|
||||
sourceAgentNodeId: '',
|
||||
sourceVariable: [],
|
||||
|
||||
subGraphNodes: [],
|
||||
subGraphEdges: [],
|
||||
|
||||
selectedOutputVar: [],
|
||||
whenOutputNone: 'skip',
|
||||
defaultValue: '',
|
||||
|
||||
showDebugPanel: false,
|
||||
isRunning: false,
|
||||
|
||||
parentAvailableVars: [],
|
||||
}
|
||||
|
||||
export const createSubGraphSlice: CreateSubGraphSlice = set => ({
|
||||
...initialState,
|
||||
|
||||
setSubGraphContext: context => set(() => ({
|
||||
parentToolNodeId: context.parentToolNodeId,
|
||||
parameterKey: context.parameterKey,
|
||||
sourceAgentNodeId: context.sourceAgentNodeId,
|
||||
sourceVariable: context.sourceVariable,
|
||||
})),
|
||||
|
||||
setSubGraphNodes: nodes => set(() => ({ subGraphNodes: nodes })),
|
||||
|
||||
setSubGraphEdges: edges => set(() => ({ subGraphEdges: edges })),
|
||||
|
||||
setSelectedOutputVar: selector => set(() => ({ selectedOutputVar: selector })),
|
||||
|
||||
setWhenOutputNone: option => set(() => ({ whenOutputNone: option })),
|
||||
|
||||
setDefaultValue: value => set(() => ({ defaultValue: value })),
|
||||
|
||||
setShowDebugPanel: show => set(() => ({ showDebugPanel: show })),
|
||||
|
||||
setIsRunning: running => set(() => ({ isRunning: running })),
|
||||
|
||||
setParentAvailableVars: vars => set(() => ({ parentAvailableVars: vars })),
|
||||
|
||||
resetSubGraph: () => set(() => ({ ...initialState })),
|
||||
})
|
||||
@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { SubGraphCanvasProps } from './types'
|
||||
import { memo } from 'react'
|
||||
import SubGraph from '@/app/components/sub-graph'
|
||||
|
||||
const SubGraphCanvas: FC<SubGraphCanvasProps> = ({
|
||||
toolNodeId,
|
||||
paramKey,
|
||||
sourceVariable,
|
||||
agentNodeId,
|
||||
agentName,
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<SubGraph
|
||||
toolNodeId={toolNodeId}
|
||||
paramKey={paramKey}
|
||||
sourceVariable={sourceVariable}
|
||||
agentNodeId={agentNodeId}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SubGraphCanvas)
|
||||
@ -0,0 +1,103 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { Edge, Node, NodeOutPutVar, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export type SubGraphNodeData = {
|
||||
isInSubGraph: boolean
|
||||
subGraph_id: string
|
||||
subGraphParamKey: string
|
||||
}
|
||||
|
||||
export type SubGraphNode = Node & {
|
||||
data: Node['data'] & SubGraphNodeData
|
||||
}
|
||||
|
||||
export type SubGraphSourceNodeData = {
|
||||
title: string
|
||||
sourceAgentNodeId: string
|
||||
sourceVariable: ValueSelector
|
||||
sourceVarType: VarType
|
||||
isReadOnly: true
|
||||
isInSubGraph: true
|
||||
subGraph_id: string
|
||||
subGraphParamKey: string
|
||||
}
|
||||
|
||||
export type WhenOutputNoneOption = 'skip' | 'error' | 'default'
|
||||
|
||||
export type SubGraphConfig = {
|
||||
enabled: boolean
|
||||
startNodeId: string
|
||||
selectedOutputVar: ValueSelector
|
||||
whenOutputNone: WhenOutputNoneOption
|
||||
defaultValue?: string
|
||||
}
|
||||
|
||||
export type SubGraphOutputVariable = {
|
||||
nodeId: string
|
||||
nodeName: string
|
||||
variable: string
|
||||
type: VarType
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type SubGraphModalProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
sourceVariable: ValueSelector
|
||||
agentName: string
|
||||
agentNodeId: string
|
||||
}
|
||||
|
||||
export type ConfigPanelProps = {
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
activeTab: 'settings' | 'lastRun'
|
||||
onTabChange: (tab: 'settings' | 'lastRun') => void
|
||||
}
|
||||
|
||||
export type SubGraphCanvasProps = {
|
||||
toolNodeId: string
|
||||
paramKey: string
|
||||
sourceVariable: ValueSelector
|
||||
agentNodeId: string
|
||||
agentName: string
|
||||
}
|
||||
|
||||
export type SubGraphSliceShape = {
|
||||
parentToolNodeId: string
|
||||
parameterKey: string
|
||||
sourceAgentNodeId: string
|
||||
sourceVariable: ValueSelector
|
||||
|
||||
subGraphNodes: SubGraphNode[]
|
||||
subGraphEdges: Edge[]
|
||||
|
||||
selectedOutputVar: ValueSelector
|
||||
whenOutputNone: WhenOutputNoneOption
|
||||
defaultValue: string
|
||||
|
||||
showDebugPanel: boolean
|
||||
isRunning: boolean
|
||||
|
||||
parentAvailableVars: NodeOutPutVar[]
|
||||
|
||||
setSubGraphContext: (context: {
|
||||
parentToolNodeId: string
|
||||
parameterKey: string
|
||||
sourceAgentNodeId: string
|
||||
sourceVariable: ValueSelector
|
||||
}) => void
|
||||
setSubGraphNodes: (nodes: SubGraphNode[]) => void
|
||||
setSubGraphEdges: (edges: Edge[]) => void
|
||||
setSelectedOutputVar: (selector: ValueSelector) => void
|
||||
setWhenOutputNone: (option: WhenOutputNoneOption) => void
|
||||
setDefaultValue: (value: string) => void
|
||||
setShowDebugPanel: (show: boolean) => void
|
||||
setIsRunning: (running: boolean) => void
|
||||
setParentAvailableVars: (vars: NodeOutPutVar[]) => void
|
||||
resetSubGraph: () => void
|
||||
}
|
||||
|
||||
export type CreateSubGraphSlice = StateCreator<SubGraphSliceShape>
|
||||
Reference in New Issue
Block a user