feat: Enhance sub-graph components with context handling and variable management

This commit is contained in:
zhsama
2026-01-14 23:23:09 +08:00
parent 4828348532
commit 810f9eaaad
19 changed files with 528 additions and 103 deletions

View File

@ -67,6 +67,9 @@ const ConfigPanel: FC<ConfigPanelProps> = ({
description: t('subGraphModal.whenOutputNone.defaultDesc', { ns: 'workflow' }),
},
]), [t])
const selectedWhenOutputNoneOption = useMemo(() => (
whenOutputNoneOptions.find(item => item.value === mentionConfig.null_strategy) ?? whenOutputNoneOptions[0]
), [mentionConfig.null_strategy, whenOutputNoneOptions])
const handleNullStrategyChange = useCallback((item: Item) => {
if (typeof item.value !== 'string')
@ -94,6 +97,8 @@ const ConfigPanel: FC<ConfigPanelProps> = ({
default_value: nextValue,
})
}, [mentionConfig, onMentionConfigChange])
const defaultValue = mentionConfig.default_value ?? ''
const shouldFormatDefaultValue = typeof defaultValue !== 'string'
return (
<div className="flex h-full flex-col">
@ -131,45 +136,54 @@ const ConfigPanel: FC<ConfigPanelProps> = ({
</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]"
<Field
title={t('subGraphModal.whenOutputIsNone', { ns: 'workflow' })}
operations={(
<div className="flex items-center">
<SimpleSelect
items={whenOutputNoneOptions}
defaultValue={mentionConfig.null_strategy}
allowSearch={false}
notClearable
wrapperClassName="min-w-[160px]"
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>
)}
/>
</div>
)}
>
<div className="space-y-2">
{selectedWhenOutputNoneOption?.description && (
<div className="system-xs-regular text-text-tertiary">
{selectedWhenOutputNoneOption.description}
</div>
)}
{mentionConfig.null_strategy === 'use_default' && (
<div className={cn('overflow-hidden rounded-lg border border-components-input-border-active bg-components-input-bg-normal p-1')}>
<CodeEditor
noWrapper
language={CodeLanguage.json}
value={defaultValue}
onChange={handleDefaultValueChange}
isJSONStringifyBeauty={shouldFormatDefaultValue}
className="min-h-[160px]"
/>
</div>
)}
</div>
)}
</Field>
</div>
</div>
)}

View File

@ -2,12 +2,13 @@ import type { FC } from 'react'
import type { Viewport } from 'reactflow'
import type { SubGraphProps } from './types'
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
import type { PromptItem } from '@/app/components/workflow/types'
import { memo, useMemo } from 'react'
import type { PromptItem, PromptTemplateItem } from '@/app/components/workflow/types'
import { memo, useEffect, 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 { useStore } from '@/app/components/workflow/store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types'
import SubGraphMain from './components/sub-graph-main'
import { useSubGraphNodes } from './hooks'
import { createSubGraphSlice } from './store'
@ -38,9 +39,19 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
onMentionConfigChange,
extractorNode,
toolParamValue,
parentAvailableNodes,
parentAvailableVars,
onSave,
} = props
const setParentAvailableVars = useStore(state => state.setParentAvailableVars)
const setParentAvailableNodes = useStore(state => state.setParentAvailableNodes)
useEffect(() => {
setParentAvailableVars?.(parentAvailableVars || [])
setParentAvailableNodes?.(parentAvailableNodes || [])
}, [parentAvailableNodes, parentAvailableVars, setParentAvailableNodes, setParentAvailableVars])
const promptText = useMemo(() => {
if (!toolParamValue)
return ''
@ -95,16 +106,18 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
if (!Array.isArray(template))
return applyPromptText(template as PromptItem)
const userIndex = template.findIndex(item => item.role === PromptRole.user)
const promptItems = template.filter((item): item is PromptItem => !isPromptMessageContext(item))
const userIndex = promptItems.findIndex(item => item.role === PromptRole.user)
if (userIndex >= 0) {
return template.map((item, index) => {
return promptItems.map((item, index) => {
if (index !== userIndex)
return item
return applyPromptText(item)
})
}) as PromptTemplateItem[]
}
const useJinja = template.some((item: PromptItem) => item.edition_type === EditionType.jinja2)
const useJinja = promptItems.some((item: PromptItem) => item.edition_type === EditionType.jinja2)
const defaultUserPrompt: PromptItem = useJinja
? {
role: PromptRole.user,
@ -113,13 +126,13 @@ const SubGraphContent: FC<SubGraphProps> = (props) => {
edition_type: EditionType.jinja2,
}
: { role: PromptRole.user, text: promptText }
const systemIndex = template.findIndex(item => item.role === PromptRole.system)
const nextTemplate = [...template]
const systemIndex = promptItems.findIndex(item => item.role === PromptRole.system)
const nextTemplate = [...promptItems]
if (systemIndex >= 0)
nextTemplate.splice(systemIndex + 1, 0, defaultUserPrompt)
else
nextTemplate.unshift(defaultUserPrompt)
return nextTemplate
return nextTemplate as PromptTemplateItem[]
})()
return {

View File

@ -1,6 +1,6 @@
import type { CreateSubGraphSlice, SubGraphSliceShape } from '../types'
const initialState: Omit<SubGraphSliceShape, 'setSubGraphContext' | 'setSubGraphNodes' | 'setSubGraphEdges' | 'setSelectedOutputVar' | 'setWhenOutputNone' | 'setDefaultValue' | 'setShowDebugPanel' | 'setIsRunning' | 'setParentAvailableVars' | 'resetSubGraph'> = {
const initialState: Omit<SubGraphSliceShape, 'setSubGraphContext' | 'setSubGraphNodes' | 'setSubGraphEdges' | 'setSelectedOutputVar' | 'setWhenOutputNone' | 'setDefaultValue' | 'setShowDebugPanel' | 'setIsRunning' | 'setParentAvailableVars' | 'setParentAvailableNodes' | 'resetSubGraph'> = {
parentToolNodeId: '',
parameterKey: '',
sourceAgentNodeId: '',
@ -18,6 +18,7 @@ const initialState: Omit<SubGraphSliceShape, 'setSubGraphContext' | 'setSubGraph
isRunning: false,
parentAvailableVars: [],
parentAvailableNodes: [],
}
export const createSubGraphSlice: CreateSubGraphSlice = set => ({
@ -46,5 +47,7 @@ export const createSubGraphSlice: CreateSubGraphSlice = set => ({
setParentAvailableVars: vars => set(() => ({ parentAvailableVars: vars })),
setParentAvailableNodes: nodes => set(() => ({ parentAvailableNodes: nodes })),
resetSubGraph: () => set(() => ({ ...initialState })),
})

View File

@ -31,6 +31,8 @@ export type SubGraphProps = {
onMentionConfigChange: (config: MentionConfig) => void
extractorNode?: Node<LLMNodeType>
toolParamValue?: string
parentAvailableNodes?: Node[]
parentAvailableVars?: NodeOutPutVar[]
onSave?: (nodes: Node[], edges: Edge[]) => void
}
@ -52,6 +54,7 @@ export type SubGraphSliceShape = {
isRunning: boolean
parentAvailableVars: NodeOutPutVar[]
parentAvailableNodes: Node[]
setSubGraphContext: (context: {
parentToolNodeId: string
@ -67,6 +70,7 @@ export type SubGraphSliceShape = {
setShowDebugPanel: (show: boolean) => void
setIsRunning: (running: boolean) => void
setParentAvailableVars: (vars: NodeOutPutVar[]) => void
setParentAvailableNodes: (nodes: Node[]) => void
resetSubGraph: () => void
}

View File

@ -84,6 +84,7 @@ type Props = {
currentTool?: Tool
currentProvider?: ToolWithProvider | TriggerWithProvider
preferSchemaType?: boolean
hideSearch?: boolean
}
const DEFAULT_VALUE_SELECTOR: Props['value'] = []
@ -117,6 +118,7 @@ const VarReferencePicker: FC<Props> = ({
currentTool,
currentProvider,
preferSchemaType,
hideSearch,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
@ -636,6 +638,7 @@ const VarReferencePicker: FC<Props> = ({
isSupportFileVar={isSupportFileVar}
zIndex={zIndex}
preferSchemaType={preferSchemaType}
hideSearch={hideSearch}
/>
)}
</PortalToFollowElemContent>

View File

@ -15,6 +15,7 @@ type Props = {
onChange: (value: ValueSelector, varDetail: Var) => void
itemWidth?: number
isSupportFileVar?: boolean
hideSearch?: boolean
zIndex?: number
preferSchemaType?: boolean
}
@ -24,6 +25,7 @@ const VarReferencePopup: FC<Props> = ({
onChange,
itemWidth,
isSupportFileVar = true,
hideSearch,
zIndex,
preferSchemaType,
}) => {
@ -35,7 +37,7 @@ const VarReferencePopup: FC<Props> = ({
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
return (
<div
className="space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg"
className="space-y-1 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg"
style={{
width: itemWidth || 228,
}}
@ -84,6 +86,7 @@ const VarReferencePopup: FC<Props> = ({
showManageInputField={showManageRagInputFields}
onManageInputField={() => setShowInputFieldPanel?.(true)}
preferSchemaType={preferSchemaType}
hideSearch={hideSearch}
/>
)}
</div>

View File

@ -0,0 +1,129 @@
'use client'
import type { FC } from 'react'
import type { PromptMessageContext, ValueSelector } from '../../../types'
import type { Node, NodeOutPutVar, Var } from '@/app/components/workflow/types'
import { RiArrowDownSLine, RiDeleteBinLine } from '@remixicon/react'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import VariableLabelInSelect from '@/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-select'
import { BlockEnum } from '@/app/components/workflow/types'
import { cn } from '@/utils/classnames'
type Props = {
readOnly: boolean
payload: PromptMessageContext
contextVars: NodeOutPutVar[]
availableNodes: Node[]
onChange: (value: ValueSelector) => void
onRemove: () => void
}
const ConfigContextItem: FC<Props> = ({
readOnly,
payload,
contextVars,
availableNodes,
onChange,
onRemove,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const selectedNodeId = Array.isArray(payload.$context) ? payload.$context[0] : ''
const selectedNode = useMemo(() => {
return availableNodes.find(node => node.id === selectedNodeId)
}, [availableNodes, selectedNodeId])
const hasOptions = contextVars.length > 0
const handleChange = useCallback((value: ValueSelector, _item?: Var) => {
onChange(value)
setOpen(false)
}, [onChange])
const handleToggle = useCallback(() => {
if (readOnly)
return
setOpen(prev => !prev)
}, [readOnly])
const handleRemove = useCallback(() => {
onRemove()
setOpen(false)
}, [onRemove])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement="bottom-start"
offset={6}
>
<PortalToFollowElemTrigger asChild onClick={handleToggle}>
<button
type="button"
disabled={readOnly}
className={cn(
'flex w-full items-center justify-between rounded-xl bg-components-panel-on-panel-item-bg px-3 py-2',
!readOnly && 'cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover',
)}
>
<div className="system-xs-semibold-uppercase text-text-tertiary">
{t('nodes.llm.context', { ns: 'workflow' })}
</div>
<div className="flex items-center gap-1">
<VariableLabelInSelect
nodeType={selectedNode?.data.type || BlockEnum.Agent}
nodeTitle={selectedNode?.data.title}
variables={payload.$context}
/>
<RiArrowDownSLine className="h-4 w-4 text-text-tertiary" />
</div>
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<div className="w-[260px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
{hasOptions
? (
<VarReferenceVars
vars={contextVars}
onChange={handleChange}
hideSearch
maxHeightClass="max-h-[34vh]"
onClose={() => setOpen(false)}
onBlur={() => setOpen(false)}
autoFocus={false}
preferSchemaType
/>
)
: (
<div className="system-xs-regular px-3 py-2 text-center text-text-tertiary">
{t('common.noAgentNodes', { ns: 'workflow' })}
</div>
)}
{!readOnly && (
<div className="mt-1 border-t border-divider-subtle pt-1">
<button
type="button"
className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-text-secondary hover:bg-state-base-hover"
onClick={handleRemove}
>
<RiDeleteBinLine className="h-4 w-4" />
<span className="system-sm-regular">
{t('nodes.llm.removeContext', { ns: 'workflow' })}
</span>
</button>
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ConfigContextItem)

View File

@ -1,19 +1,28 @@
'use client'
import type { FC } from 'react'
import type { ModelConfig, PromptItem, ValueSelector, Var, Variable } from '../../../types'
import type { ModelConfig, Node, NodeOutPutVar, PromptItem, PromptMessageContext, PromptTemplateItem, ValueSelector, Var, Variable } from '../../../types'
import { produce } from 'immer'
import * as React from 'react'
import { useCallback } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ReactSortable } from 'react-sortablejs'
import { useStoreApi } from 'reactflow'
import { v4 as uuid4 } from 'uuid'
import { DragHandle } from '@/app/components/base/icons/src/vender/line/others'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import AddButton from '@/app/components/workflow/nodes/_base/components/add-button'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { cn } from '@/utils/classnames'
import { useWorkflowStore } from '../../../store'
import { EditionType, PromptRole } from '../../../types'
import { useWorkflow } from '../../../hooks'
import { useStore, useWorkflowStore } from '../../../store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '../../../types'
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
import ConfigContextItem from './config-context-item'
import ConfigPromptItem from './config-prompt-item'
const i18nPrefix = 'nodes.llm'
@ -24,8 +33,8 @@ type Props = {
filterVar: (payload: Var, selector: ValueSelector) => boolean
isChatModel: boolean
isChatApp: boolean
payload: PromptItem | PromptItem[]
onChange: (payload: PromptItem | PromptItem[]) => void
payload: PromptItem | PromptTemplateItem[]
onChange: (payload: PromptItem | PromptTemplateItem[]) => void
isShowContext: boolean
hasSetBlockStatus: {
context: boolean
@ -56,6 +65,13 @@ const ConfigPrompt: FC<Props> = ({
const {
setControlPromptEditorRerenderKey,
} = workflowStore.getState()
const store = useStoreApi()
const { getBeforeNodesInSameBranch } = useWorkflow()
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
const contextMenuTriggerRef = useRef<HTMLDivElement>(null)
const payloadWithIds = (isChatModel && Array.isArray(payload))
? payload.map((item) => {
const id = uuid4()
@ -75,11 +91,78 @@ const ConfigPrompt: FC<Props> = ({
onlyLeafNodeVar: false,
filterVar,
})
const parentAvailableVars = useStore(state => state.parentAvailableVars) || []
const parentAvailableNodes = useStore(state => state.parentAvailableNodes) || []
const mergedAvailableVars = useMemo(() => {
if (!parentAvailableVars.length)
return availableVars
const merged = new Map<string, NodeOutPutVar>()
availableVars.forEach((item) => {
merged.set(item.nodeId, item)
})
parentAvailableVars.forEach((item) => {
if (!merged.has(item.nodeId))
merged.set(item.nodeId, item)
})
return Array.from(merged.values())
}, [availableVars, parentAvailableVars])
const mergedAvailableNodesWithParent = useMemo(() => {
if (!parentAvailableNodes.length)
return availableNodesWithParent
const merged = new Map<string, Node>()
availableNodesWithParent.forEach((node) => {
merged.set(node.id, node)
})
parentAvailableNodes.forEach((node) => {
if (!merged.has(node.id))
merged.set(node.id, node)
})
return Array.from(merged.values())
}, [availableNodesWithParent, parentAvailableNodes])
const contextAgentNodes = useMemo(() => {
const agentNodes = mergedAvailableNodesWithParent
.filter(node => node.data.type === BlockEnum.Agent)
const { getNodes } = store.getState()
const allNodes = getNodes()
const currentNode = allNodes.find(n => n.id === nodeId)
const parentNodeId = currentNode?.parentId
if (parentNodeId) {
const beforeNodes = getBeforeNodesInSameBranch(parentNodeId)
const parentAgentNodes = beforeNodes
.filter(node => node.data.type === BlockEnum.Agent)
.filter(node => !agentNodes.some(n => n.id === node.id))
agentNodes.unshift(...parentAgentNodes)
}
return agentNodes
}, [mergedAvailableNodesWithParent, nodeId, store, getBeforeNodesInSameBranch])
const contextVarOptions = useMemo<NodeOutPutVar[]>(() => {
return contextAgentNodes.map(node => ({
nodeId: node.id,
title: node.data.title,
vars: [
{
variable: 'context',
type: VarType.arrayObject,
schemaType: 'List[promptMessage]',
},
],
}))
}, [contextAgentNodes])
const handleChatModePromptChange = useCallback((index: number) => {
return (prompt: string) => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
draft[index][draft[index].edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
const newPrompt = produce(payload as PromptTemplateItem[], (draft) => {
const item = draft[index]
if (!isPromptMessageContext(item))
item[item.edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
})
onChange(newPrompt)
}
@ -87,8 +170,10 @@ const ConfigPrompt: FC<Props> = ({
const handleChatModeEditionTypeChange = useCallback((index: number) => {
return (editionType: EditionType) => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
draft[index].edition_type = editionType
const newPrompt = produce(payload as PromptTemplateItem[], (draft) => {
const item = draft[index]
if (!isPromptMessageContext(item))
item.edition_type = editionType
})
onChange(newPrompt)
}
@ -96,29 +181,80 @@ const ConfigPrompt: FC<Props> = ({
const handleChatModeMessageRoleChange = useCallback((index: number) => {
return (role: PromptRole) => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
draft[index].role = role
const newPrompt = produce(payload as PromptTemplateItem[], (draft) => {
const item = draft[index]
if (!isPromptMessageContext(item))
item.role = role
})
onChange(newPrompt)
}
}, [onChange, payload])
const handleAddPrompt = useCallback(() => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
const newPrompt = produce(payload as PromptTemplateItem[], (draft) => {
if (draft.length === 0) {
draft.push({ role: PromptRole.system, text: '', id: uuid4() })
return
}
const isLastItemUser = draft[draft.length - 1].role === PromptRole.user
const lastPromptItem = [...draft].reverse().find(item => !isPromptMessageContext(item)) as PromptItem | undefined
const isLastItemUser = lastPromptItem?.role === PromptRole.user
draft.push({ role: isLastItemUser ? PromptRole.assistant : PromptRole.user, text: '', id: uuid4() })
})
onChange(newPrompt)
}, [onChange, payload])
const handleAddContext = useCallback((agentNodeId: string) => {
const newPrompt = produce(payload as PromptTemplateItem[], (draft) => {
const contextItem: PromptMessageContext = {
id: uuid4(),
$context: [agentNodeId, 'context'],
}
const lastUserIndex = draft
.map((item, idx) => ({ item, idx }))
.reverse()
.find(({ item }) => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user)
?.idx
if (lastUserIndex !== undefined) {
draft.splice(lastUserIndex, 0, contextItem)
return
}
const promptItems = draft.filter(item => !isPromptMessageContext(item)) as PromptItem[]
const hasOnlySystem = promptItems.length === 1 && promptItems[0].role === PromptRole.system
if (hasOnlySystem) {
draft.push({ role: PromptRole.user, text: '', id: uuid4() })
draft.splice(draft.length - 1, 0, contextItem)
return
}
draft.push(contextItem)
})
onChange(newPrompt)
setIsContextMenuOpen(false)
}, [onChange, payload])
const handleAddContextVar = useCallback((value: ValueSelector, _item?: Var) => {
if (!Array.isArray(value) || value.length < 2)
return
handleAddContext(value[0])
}, [handleAddContext])
const handleContextChange = useCallback((index: number) => {
return (value: ValueSelector) => {
const newPrompt = produce(payload as PromptTemplateItem[], (draft) => {
const item = draft[index]
if (isPromptMessageContext(item))
item.$context = value
})
onChange(newPrompt)
}
}, [onChange, payload])
const handleRemove = useCallback((index: number) => {
return () => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
const newPrompt = produce(payload as PromptTemplateItem[], (draft) => {
draft.splice(index, 1)
})
onChange(newPrompt)
@ -145,11 +281,12 @@ const ConfigPrompt: FC<Props> = ({
}, [onChange, payload])
const canChooseSystemRole = (() => {
if (isChatModel && Array.isArray(payload))
return !payload.find(item => item.role === PromptRole.system)
if (isChatModel && Array.isArray(payload)) {
return !payload.find(item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.system)
}
return false
})()
return (
<div>
{(isChatModel && Array.isArray(payload))
@ -160,9 +297,12 @@ const ConfigPrompt: FC<Props> = ({
className="space-y-1"
list={payloadWithIds}
setList={(list) => {
if ((payload as PromptItem[])?.[0]?.role === PromptRole.system && list[0].p?.role !== PromptRole.system)
return
const firstItem = (payload as PromptTemplateItem[])?.[0]
if (firstItem && !isPromptMessageContext(firstItem) && firstItem.role === PromptRole.system) {
const newFirstItem = list[0]?.p
if (newFirstItem && !isPromptMessageContext(newFirstItem) && newFirstItem.role !== PromptRole.system)
return
}
onChange(list.map(item => item.p))
}}
handle=".handle"
@ -170,7 +310,23 @@ const ConfigPrompt: FC<Props> = ({
animation={150}
>
{
(payload as PromptItem[]).map((item, index) => {
(payload as PromptTemplateItem[]).map((item, index) => {
if (isPromptMessageContext(item)) {
return (
<div key={item.id || index} className="group relative">
{!readOnly && <DragHandle className="handle absolute left-[-14px] top-2 hidden h-3.5 w-3.5 cursor-grab text-text-quaternary group-hover:block" />}
<ConfigContextItem
readOnly={readOnly}
payload={item}
contextVars={contextVarOptions}
availableNodes={mergedAvailableNodesWithParent}
onChange={handleContextChange(index)}
onRemove={handleRemove(index)}
/>
</div>
)
}
const canDrag = (() => {
if (readOnly)
return false
@ -182,7 +338,7 @@ const ConfigPrompt: FC<Props> = ({
})()
return (
<div key={item.id || index} className="group relative">
{canDrag && <DragHandle className="absolute left-[-14px] top-2 hidden h-3.5 w-3.5 text-text-quaternary group-hover:block" />}
{canDrag && <DragHandle className="handle absolute left-[-14px] top-2 hidden h-3.5 w-3.5 cursor-grab text-text-quaternary group-hover:block" />}
<ConfigPromptItem
instanceId={item.role === PromptRole.system ? `${nodeId}-chat-workflow-llm-prompt-editor` : `${nodeId}-chat-workflow-llm-prompt-editor-${index}`}
className={cn(canDrag && 'handle')}
@ -201,8 +357,8 @@ const ConfigPrompt: FC<Props> = ({
onRemove={handleRemove(index)}
isShowContext={isShowContext}
hasSetBlockStatus={hasSetBlockStatus}
availableVars={availableVars}
availableNodes={availableNodesWithParent}
availableVars={mergedAvailableVars}
availableNodes={mergedAvailableNodesWithParent}
varList={varList}
handleAddVariable={handleAddVariable}
modelConfig={modelConfig}
@ -213,11 +369,48 @@ const ConfigPrompt: FC<Props> = ({
}
</ReactSortable>
</div>
<AddButton
className="mt-2"
text={t(`${i18nPrefix}.addMessage`, { ns: 'workflow' })}
onClick={handleAddPrompt}
/>
<div className="mt-2 grid grid-cols-[11fr_9fr] gap-2">
<AddButton
text={t(`${i18nPrefix}.addMessage`, { ns: 'workflow' })}
onClick={handleAddPrompt}
/>
<PortalToFollowElem
open={isContextMenuOpen}
onOpenChange={setIsContextMenuOpen}
placement="bottom-start"
>
<PortalToFollowElemTrigger className="w-full" onClick={() => setIsContextMenuOpen(!isContextMenuOpen)}>
<div ref={contextMenuTriggerRef}>
<AddButton
text={t(`${i18nPrefix}.addContext`, { ns: 'workflow' })}
onClick={() => {}}
/>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<div className="w-[260px] rounded-lg border border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
{contextVarOptions.length > 0
? (
<VarReferenceVars
vars={contextVarOptions}
onChange={handleAddContextVar}
hideSearch
maxHeightClass="max-h-[34vh]"
onClose={() => setIsContextMenuOpen(false)}
onBlur={() => setIsContextMenuOpen(false)}
autoFocus={false}
preferSchemaType
/>
)
: (
<div className="system-xs-regular px-3 py-2 text-center text-text-tertiary">
{t('common.noAgentNodes', { ns: 'workflow' })}
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
</div>
)
: (
@ -232,8 +425,8 @@ const ConfigPrompt: FC<Props> = ({
isChatApp={isChatApp}
isShowContext={isShowContext}
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
nodesOutputVars={mergedAvailableVars}
availableNodes={mergedAvailableNodesWithParent}
isSupportPromptGenerator
isSupportJinja
editionType={(payload as PromptItem).edition_type}

View File

@ -5,6 +5,7 @@ import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import AddButton2 from '@/app/components/base/button/add-button'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
@ -119,7 +120,12 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
{/* knowledge */}
<Field
title={t(`${i18nPrefix}.context`, { ns: 'workflow' })}
title={(
<div className="flex items-center">
<div>{t(`${i18nPrefix}.context`, { ns: 'workflow' })}</div>
<Badge className="ml-2" uppercase>LEGACY</Badge>
</div>
)}
tooltip={t(`${i18nPrefix}.contextTooltip`, { ns: 'workflow' })!}
>
<>
@ -130,6 +136,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
value={inputs.context?.variable_selector || []}
onChange={handleContextVarChange}
filterVar={filterVar}
hideSearch
/>
{shouldShowContextTip && (
<div className="text-xs font-normal leading-[18px] text-[#DC6803]">{t(`${i18nPrefix}.notSetContextInPromptTip`, { ns: 'workflow' })}</div>

View File

@ -1,8 +1,8 @@
import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types'
import type { CommonNodeType, Memory, ModelConfig, PromptItem, PromptTemplateItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types'
export type LLMNodeType = CommonNodeType & {
model: ModelConfig
prompt_template: PromptItem[] | PromptItem
prompt_template: PromptTemplateItem[] | PromptItem
prompt_config?: {
jinja2_variables?: Variable[]
}

View File

@ -1,4 +1,4 @@
import type { Memory, PromptItem, ValueSelector, Var, Variable } from '../../types'
import type { Memory, PromptItem, PromptTemplateItem, ValueSelector, Var, Variable } from '../../types'
import type { LLMNodeType, StructuredOutput } from './types'
import { produce } from 'immer'
import { useCallback, useEffect, useRef, useState } from 'react'
@ -249,7 +249,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
setInputs(newInputs)
}, [setInputs])
const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => {
const handlePromptChange = useCallback((newPrompt: PromptTemplateItem[] | PromptItem) => {
const newInputs = produce(inputRef.current, (draft) => {
draft.prompt_template = newPrompt
})

View File

@ -10,12 +10,12 @@ import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { Fragment, memo, useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
import { useIsChatMode, useNodesSyncDraft, useWorkflow, useWorkflowVariables } from '@/app/components/workflow/hooks'
import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
import { useStore } from '@/app/components/workflow/store'
import { EditionType, PromptRole } from '@/app/components/workflow/types'
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum, EditionType, PromptRole } from '@/app/components/workflow/types'
import SubGraphCanvas from './sub-graph-canvas'
const SubGraphModal: FC<SubGraphModalProps> = ({
@ -29,9 +29,13 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
}) => {
const { t } = useTranslation()
const reactflowStore = useStoreApi()
const workflowNodes = useStore(state => state.nodes)
const setControlPromptEditorRerenderKey = useStore(state => state.setControlPromptEditorRerenderKey)
const workflowNodes = useWorkflowStore(state => state.nodes)
const workflowEdges = useReactFlowStore(state => state.edges)
const setControlPromptEditorRerenderKey = useWorkflowStore(state => state.setControlPromptEditorRerenderKey)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getBeforeNodesInSameBranch } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
const extractorNode = useMemo(() => {
@ -43,6 +47,28 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
const toolParam = (toolNode?.data as ToolNodeType | undefined)?.tool_parameters?.[paramKey]
const toolParamValue = toolParam?.value as string | undefined
const parentAgentNodes = useMemo(() => {
if (!isOpen)
return []
const beforeNodes = getBeforeNodesInSameBranch(toolNodeId, workflowNodes, workflowEdges)
return beforeNodes.filter(node => node.data.type === BlockEnum.Agent)
}, [getBeforeNodesInSameBranch, isOpen, toolNodeId, workflowEdges, workflowNodes])
const parentAgentNodeIds = useMemo(() => {
return parentAgentNodes.map(node => node.id)
}, [parentAgentNodes])
const parentAvailableVars = useMemo(() => {
if (!parentAgentNodeIds.length)
return []
const vars = getNodeAvailableVars({
beforeNodes: parentAgentNodes,
isChatMode,
filterVar: () => true,
})
return vars.filter(nodeVar => parentAgentNodeIds.includes(nodeVar.nodeId))
}, [getNodeAvailableVars, isChatMode, parentAgentNodeIds, parentAgentNodes])
const mentionConfig = useMemo<MentionConfig>(() => {
const current = toolParam?.mention_config
const rawSelector = Array.isArray(current?.output_selector) ? current!.output_selector : []
@ -115,8 +141,7 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
const userPrompt = promptTemplate.find(item => item.role === PromptRole.user)
if (userPrompt)
return resolveText(userPrompt)
const systemPrompt = promptTemplate.find(item => item.role === PromptRole.system)
return resolveText(systemPrompt)
return ''
}
return resolveText(promptTemplate)
}, [])
@ -212,6 +237,8 @@ const SubGraphModal: FC<SubGraphModalProps> = ({
onMentionConfigChange={handleMentionConfigChange}
extractorNode={extractorNode}
toolParamValue={toolParamValue}
parentAvailableNodes={parentAgentNodes}
parentAvailableVars={parentAvailableVars}
onSave={handleSave}
/>
</div>

View File

@ -14,6 +14,8 @@ const SubGraphCanvas: FC<SubGraphCanvasProps> = ({
onMentionConfigChange,
extractorNode,
toolParamValue,
parentAvailableNodes,
parentAvailableVars,
onSave,
}) => {
return (
@ -28,6 +30,8 @@ const SubGraphCanvas: FC<SubGraphCanvasProps> = ({
onMentionConfigChange={onMentionConfigChange}
extractorNode={extractorNode}
toolParamValue={toolParamValue}
parentAvailableNodes={parentAvailableNodes}
parentAvailableVars={parentAvailableVars}
onSave={onSave}
/>
</div>

View File

@ -1,6 +1,6 @@
import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types'
import type { NodeOutPutVar, Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types'
type WorkflowValueSelector = string[]
@ -24,5 +24,7 @@ export type SubGraphCanvasProps = {
onMentionConfigChange: (config: MentionConfig) => void
extractorNode?: WorkflowNode<LLMNodeType>
toolParamValue?: string
parentAvailableNodes?: WorkflowNode[]
parentAvailableVars?: NodeOutPutVar[]
onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void
}

View File

@ -255,6 +255,17 @@ export type PromptItem = {
jinja2_text?: string
}
export type PromptMessageContext = {
id?: string
$context: ValueSelector
}
export type PromptTemplateItem = PromptItem | PromptMessageContext
export const isPromptMessageContext = (item: PromptTemplateItem): item is PromptMessageContext => {
return '$context' in item
}
export enum MemoryRole {
user = 'user',
assistant = 'assistant',

View File

@ -625,8 +625,10 @@
"nodes.listFilter.outputVars.last_record": "Last record",
"nodes.listFilter.outputVars.result": "Filter result",
"nodes.listFilter.selectVariableKeyPlaceholder": "Select sub variable key",
"nodes.llm.addContext": "Add Context",
"nodes.llm.addMessage": "Add Message",
"nodes.llm.context": "context",
"nodes.llm.contextBlock": "Context Block",
"nodes.llm.contextTooltip": "You can import Knowledge as context",
"nodes.llm.files": "Files",
"nodes.llm.jsonSchema.addChildField": "Add Child Field",
@ -663,6 +665,7 @@
"nodes.llm.reasoningFormat.tagged": "Keep think tags",
"nodes.llm.reasoningFormat.title": "Enable reasoning tag separation",
"nodes.llm.reasoningFormat.tooltip": "Extract content from think tags and store it in the reasoning_content field.",
"nodes.llm.removeContext": "Remove context",
"nodes.llm.resolution.high": "High",
"nodes.llm.resolution.low": "Low",
"nodes.llm.resolution.name": "Resolution",
@ -999,8 +1002,8 @@
"subGraphModal.sourceNode": "SOURCE",
"subGraphModal.title": "INTERNAL STRUCTURE",
"subGraphModal.whenOutputIsNone": "WHEN OUTPUT IS NONE",
"subGraphModal.whenOutputNone.default": "Use default value",
"subGraphModal.whenOutputNone.defaultDesc": "Continue with a default value",
"subGraphModal.whenOutputNone.default": "Default value",
"subGraphModal.whenOutputNone.defaultDesc": "Returns the value below",
"subGraphModal.whenOutputNone.error": "Raise an error",
"subGraphModal.whenOutputNone.errorDesc": "Pass the error to the outer workflow",
"subGraphModal.whenOutputNone.skip": "Skip this step",

View File

@ -623,8 +623,10 @@
"nodes.listFilter.outputVars.last_record": "最後のレコード",
"nodes.listFilter.outputVars.result": "フィルター結果",
"nodes.listFilter.selectVariableKeyPlaceholder": "サブ変数キーを選択する",
"nodes.llm.addContext": "コンテキスト追加",
"nodes.llm.addMessage": "メッセージ追加",
"nodes.llm.context": "コンテキスト",
"nodes.llm.contextBlock": "コンテキストブロック",
"nodes.llm.contextTooltip": "ナレッジベースをコンテキストとして利用",
"nodes.llm.files": "ファイル",
"nodes.llm.jsonSchema.addChildField": "サブフィールドを追加",
@ -661,6 +663,7 @@
"nodes.llm.reasoningFormat.tagged": "タグを考え続けてください",
"nodes.llm.reasoningFormat.title": "推論タグの分離を有効にする",
"nodes.llm.reasoningFormat.tooltip": "thinkタグから内容を抽出し、それをreasoning_contentフィールドに保存します。",
"nodes.llm.removeContext": "コンテキストを削除",
"nodes.llm.resolution.high": "高",
"nodes.llm.resolution.low": "低",
"nodes.llm.resolution.name": "解像度",
@ -996,8 +999,8 @@
"subGraphModal.sourceNode": "ソース",
"subGraphModal.title": "内部構造",
"subGraphModal.whenOutputIsNone": "出力が空の場合",
"subGraphModal.whenOutputNone.default": "デフォルト値を使用",
"subGraphModal.whenOutputNone.defaultDesc": "デフォルト値で続行",
"subGraphModal.whenOutputNone.default": "デフォルト値",
"subGraphModal.whenOutputNone.defaultDesc": "以下の値を返す",
"subGraphModal.whenOutputNone.error": "エラーを発生させる",
"subGraphModal.whenOutputNone.errorDesc": "エラーを外部ワークフローに渡す",
"subGraphModal.whenOutputNone.skip": "このステップをスキップ",

View File

@ -623,8 +623,10 @@
"nodes.listFilter.outputVars.last_record": "最后一条记录",
"nodes.listFilter.outputVars.result": "过滤结果",
"nodes.listFilter.selectVariableKeyPlaceholder": "选择子变量的 Key",
"nodes.llm.addContext": "添加上下文",
"nodes.llm.addMessage": "添加消息",
"nodes.llm.context": "上下文",
"nodes.llm.contextBlock": "上下文块",
"nodes.llm.contextTooltip": "您可以导入知识库作为上下文",
"nodes.llm.files": "文件",
"nodes.llm.jsonSchema.addChildField": "添加子字段",
@ -661,6 +663,7 @@
"nodes.llm.reasoningFormat.tagged": "保持思考标签",
"nodes.llm.reasoningFormat.title": "启用推理标签分离",
"nodes.llm.reasoningFormat.tooltip": "从think标签中提取内容并将其存储在reasoning_content字段中。",
"nodes.llm.removeContext": "删除上下文",
"nodes.llm.resolution.high": "高",
"nodes.llm.resolution.low": "低",
"nodes.llm.resolution.name": "分辨率",
@ -997,8 +1000,8 @@
"subGraphModal.sourceNode": "来源",
"subGraphModal.title": "内部结构",
"subGraphModal.whenOutputIsNone": "当输出为空时",
"subGraphModal.whenOutputNone.default": "使用默认值",
"subGraphModal.whenOutputNone.defaultDesc": "使用默认值继续执行",
"subGraphModal.whenOutputNone.default": "默认值",
"subGraphModal.whenOutputNone.defaultDesc": "返回以下值",
"subGraphModal.whenOutputNone.error": "抛出错误",
"subGraphModal.whenOutputNone.errorDesc": "将错误传递给外部工作流",
"subGraphModal.whenOutputNone.skip": "跳过此步骤",

View File

@ -623,8 +623,10 @@
"nodes.listFilter.outputVars.last_record": "最後一條記錄",
"nodes.listFilter.outputVars.result": "篩選結果",
"nodes.listFilter.selectVariableKeyPlaceholder": "Select sub variable key選擇子變數鍵",
"nodes.llm.addContext": "新增上下文",
"nodes.llm.addMessage": "新增消息",
"nodes.llm.context": "上下文",
"nodes.llm.contextBlock": "上下文區塊",
"nodes.llm.contextTooltip": "您可以導入知識庫作為上下文",
"nodes.llm.files": "文件",
"nodes.llm.jsonSchema.addChildField": "新增子欄位",
@ -661,6 +663,7 @@
"nodes.llm.reasoningFormat.tagged": "保持思考標籤",
"nodes.llm.reasoningFormat.title": "啟用推理標籤分離",
"nodes.llm.reasoningFormat.tooltip": "從 think 標籤中提取內容並將其存儲在 reasoning_content 欄位中。",
"nodes.llm.removeContext": "刪除上下文",
"nodes.llm.resolution.high": "高",
"nodes.llm.resolution.low": "低",
"nodes.llm.resolution.name": "分辨率",
@ -996,8 +999,8 @@
"subGraphModal.sourceNode": "來源",
"subGraphModal.title": "內部結構",
"subGraphModal.whenOutputIsNone": "當輸出為空時",
"subGraphModal.whenOutputNone.default": "使用預設值",
"subGraphModal.whenOutputNone.defaultDesc": "使用預設值繼續執行",
"subGraphModal.whenOutputNone.default": "預設值",
"subGraphModal.whenOutputNone.defaultDesc": "返回以下值",
"subGraphModal.whenOutputNone.error": "拋出錯誤",
"subGraphModal.whenOutputNone.errorDesc": "將錯誤傳遞給外部工作流程",
"subGraphModal.whenOutputNone.skip": "跳過此步驟",