feat: Add sub-graph component for workflow

This commit is contained in:
zhsama
2026-01-12 14:56:53 +08:00
parent f925266c1b
commit cab7cd37b8
21 changed files with 1046 additions and 19 deletions

View File

@ -337,6 +337,8 @@ const FormInputItem: FC<Props> = ({
showManageInputField={showManageInputField}
onManageInputField={onManageInputField}
disableVariableInsertion={disableVariableInsertion}
toolNodeId={nodeId}
paramKey={variable}
/>
)}
{isNumber && isConstant && (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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