Merge branch 'zhsama/assemble-var-input' into feat/pull-a-variable

This commit is contained in:
zhsama
2026-01-16 18:54:53 +08:00
7 changed files with 238 additions and 49 deletions

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.91992 1.6875C3.23055 1.68754 3.48242 1.93937 3.48242 2.25C3.48242 2.56063 3.23055 2.81246 2.91992 2.8125C2.63855 2.8125 2.41064 3.04041 2.41064 3.32178V5.46436C2.41061 5.61344 2.35148 5.75637 2.24609 5.86182L2.10791 6L2.24609 6.13818C2.35148 6.24363 2.41061 6.38656 2.41064 6.53564V8.67822C2.41064 8.95959 2.63855 9.1875 2.91992 9.1875C3.23055 9.18754 3.48242 9.43937 3.48242 9.75C3.48242 10.0606 3.23055 10.3125 2.91992 10.3125C2.01723 10.3125 1.28564 9.58091 1.28564 8.67822V6.76855L0.914551 6.39795C0.809062 6.29246 0.75 6.14918 0.75 6C0.75 5.85082 0.809062 5.70754 0.914551 5.60205L1.28564 5.23145V3.32178C1.28564 2.41909 2.01723 1.6875 2.91992 1.6875Z" fill="currentColor"/>
<path d="M9.08008 1.6875C9.98276 1.68751 10.7144 2.41909 10.7144 3.32178V5.23145L11.085 5.60205C11.1904 5.70754 11.25 5.85082 11.25 6C11.25 6.14918 11.1904 6.29246 11.085 6.39795L10.7144 6.76855V8.67822C10.7144 9.58107 9.98213 10.3125 9.08008 10.3125C8.76942 10.3125 8.51758 10.0607 8.51758 9.75C8.51758 9.43934 8.76942 9.1875 9.08008 9.1875C9.36113 9.18749 9.58936 8.95943 9.58936 8.67822V6.53564C9.58939 6.38654 9.64849 6.24363 9.75391 6.13818L9.89209 6L9.75391 5.86182C9.64849 5.75637 9.58939 5.61346 9.58936 5.46436V3.32178C9.58936 3.04041 9.36144 2.81251 9.08008 2.8125C8.76942 2.8125 8.51758 2.56066 8.51758 2.25C8.51758 1.93934 8.76942 1.6875 9.08008 1.6875Z" fill="currentColor"/>
<path d="M5.24707 5.07715C5.36302 5.07715 5.46712 5.14866 5.50879 5.25684L5.8335 6.10059C5.88932 6.24563 6.00388 6.36018 6.14893 6.41602L6.99268 6.74072C7.10086 6.78238 7.17236 6.88648 7.17236 7.00244C7.17229 7.11832 7.10078 7.22202 6.99268 7.26367L6.14893 7.58838C6.00378 7.64424 5.88929 7.75912 5.8335 7.9043L5.50879 8.74756C5.46715 8.8558 5.36307 8.92725 5.24707 8.92725C5.13116 8.92717 5.02746 8.85572 4.98584 8.74756L4.66113 7.9043C4.60526 7.75904 4.49046 7.6442 4.34521 7.58838L3.50195 7.26367C3.39378 7.22205 3.32234 7.11835 3.32227 7.00244C3.32227 6.88645 3.39371 6.78236 3.50195 6.74072L4.34521 6.41602C4.49039 6.36022 4.60523 6.24573 4.66113 6.10059L4.98584 5.25684C5.02749 5.14874 5.13121 5.07723 5.24707 5.07715Z" fill="currentColor"/>
<path d="M6.89746 2.87744C6.98013 2.87754 7.05427 2.92822 7.08398 3.00537L7.29053 3.54297C7.34635 3.68816 7.46125 3.80302 7.60645 3.85889L8.14404 4.06543C8.22123 4.0952 8.27246 4.16966 8.27246 4.25244C8.27236 4.33513 8.22116 4.40922 8.14404 4.43896L7.60645 4.64551C7.46125 4.70138 7.34635 4.81624 7.29053 4.96143L7.08398 5.49902C7.05428 5.57614 6.98014 5.62734 6.89746 5.62744C6.81468 5.62744 6.74019 5.57622 6.71045 5.49902L6.50391 4.96143C6.44808 4.81624 6.33318 4.70138 6.18799 4.64551L5.65039 4.43896C5.57328 4.40922 5.52256 4.33513 5.52246 4.25244C5.52246 4.16966 5.5732 4.0952 5.65039 4.06543L6.18799 3.85889C6.33318 3.80302 6.44808 3.68816 6.50391 3.54297L6.71045 3.00537C6.74019 2.92814 6.81469 2.87744 6.89746 2.87744Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,53 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "12",
"viewBox": "0 0 12 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2.91992 1.6875C3.23055 1.68754 3.48242 1.93937 3.48242 2.25C3.48242 2.56063 3.23055 2.81246 2.91992 2.8125C2.63855 2.8125 2.41064 3.04041 2.41064 3.32178V5.46436C2.41061 5.61344 2.35148 5.75637 2.24609 5.86182L2.10791 6L2.24609 6.13818C2.35148 6.24363 2.41061 6.38656 2.41064 6.53564V8.67822C2.41064 8.95959 2.63855 9.1875 2.91992 9.1875C3.23055 9.18754 3.48242 9.43937 3.48242 9.75C3.48242 10.0606 3.23055 10.3125 2.91992 10.3125C2.01723 10.3125 1.28564 9.58091 1.28564 8.67822V6.76855L0.914551 6.39795C0.809062 6.29246 0.75 6.14918 0.75 6C0.75 5.85082 0.809062 5.70754 0.914551 5.60205L1.28564 5.23145V3.32178C1.28564 2.41909 2.01723 1.6875 2.91992 1.6875Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.08008 1.6875C9.98276 1.68751 10.7144 2.41909 10.7144 3.32178V5.23145L11.085 5.60205C11.1904 5.70754 11.25 5.85082 11.25 6C11.25 6.14918 11.1904 6.29246 11.085 6.39795L10.7144 6.76855V8.67822C10.7144 9.58107 9.98213 10.3125 9.08008 10.3125C8.76942 10.3125 8.51758 10.0607 8.51758 9.75C8.51758 9.43934 8.76942 9.1875 9.08008 9.1875C9.36113 9.18749 9.58936 8.95943 9.58936 8.67822V6.53564C9.58939 6.38654 9.64849 6.24363 9.75391 6.13818L9.89209 6L9.75391 5.86182C9.64849 5.75637 9.58939 5.61346 9.58936 5.46436V3.32178C9.58936 3.04041 9.36144 2.81251 9.08008 2.8125C8.76942 2.8125 8.51758 2.56066 8.51758 2.25C8.51758 1.93934 8.76942 1.6875 9.08008 1.6875Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.24707 5.07715C5.36302 5.07715 5.46712 5.14866 5.50879 5.25684L5.8335 6.10059C5.88932 6.24563 6.00388 6.36018 6.14893 6.41602L6.99268 6.74072C7.10086 6.78238 7.17236 6.88648 7.17236 7.00244C7.17229 7.11832 7.10078 7.22202 6.99268 7.26367L6.14893 7.58838C6.00378 7.64424 5.88929 7.75912 5.8335 7.9043L5.50879 8.74756C5.46715 8.8558 5.36307 8.92725 5.24707 8.92725C5.13116 8.92717 5.02746 8.85572 4.98584 8.74756L4.66113 7.9043C4.60526 7.75904 4.49046 7.6442 4.34521 7.58838L3.50195 7.26367C3.39378 7.22205 3.32234 7.11835 3.32227 7.00244C3.32227 6.88645 3.39371 6.78236 3.50195 6.74072L4.34521 6.41602C4.49039 6.36022 4.60523 6.24573 4.66113 6.10059L4.98584 5.25684C5.02749 5.14874 5.13121 5.07723 5.24707 5.07715Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M6.89746 2.87744C6.98013 2.87754 7.05427 2.92822 7.08398 3.00537L7.29053 3.54297C7.34635 3.68816 7.46125 3.80302 7.60645 3.85889L8.14404 4.06543C8.22123 4.0952 8.27246 4.16966 8.27246 4.25244C8.27236 4.33513 8.22116 4.40922 8.14404 4.43896L7.60645 4.64551C7.46125 4.70138 7.34635 4.81624 7.29053 4.96143L7.08398 5.49902C7.05428 5.57614 6.98014 5.62734 6.89746 5.62744C6.81468 5.62744 6.74019 5.57622 6.71045 5.49902L6.50391 4.96143C6.44808 4.81624 6.33318 4.70138 6.18799 4.64551L5.65039 4.43896C5.57328 4.40922 5.52256 4.33513 5.52246 4.25244C5.52246 4.16966 5.5732 4.0952 5.65039 4.06543L6.18799 3.85889C6.33318 3.80302 6.44808 3.68816 6.50391 3.54297L6.71045 3.00537C6.74019 2.92814 6.81469 2.87744 6.89746 2.87744Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "AssembleVariables"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './AssembleVariables.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AssembleVariables'
export default Icon

View File

@ -1,3 +1,4 @@
export { default as AssembleVariables } from './AssembleVariables'
export { default as AtSign } from './AtSign'
export { default as Bookmark } from './Bookmark'
export { default as Check } from './Check'

View File

@ -9,9 +9,8 @@ import * as React from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
import { AssembleVariables, CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { MagicWand } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import Input from '@/app/components/base/input'
import {
PortalToFollowElem,
@ -414,8 +413,10 @@ const VarReferenceVars: FC<Props> = ({
onClick={handleAssembleVariables}
onMouseDown={e => e.preventDefault()}
>
<MagicWand className="h-3.5 w-3.5 shrink-0 text-text-accent" />
<span className="system-sm-medium ml-1 w-0 grow truncate text-left text-text-secondary" title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}>
<span className="mr-1 flex h-4 w-4 items-center justify-center rounded bg-util-colors-blue-blue-500">
<AssembleVariables className="h-3 w-3 text-text-primary-on-surface" />
</span>
<span className="system-xs-medium truncate" title={t('nodes.tool.assembleVariables', { ns: 'workflow' })}>
{t('nodes.tool.assembleVariables', { ns: 'workflow' })}
</span>
</button>

View File

@ -11,6 +11,7 @@ type AgentHeaderBarProps = {
onRemove: () => void
onViewInternals?: () => void
hasWarning?: boolean
showAtPrefix?: boolean
}
const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
@ -18,6 +19,7 @@ const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
onRemove,
onViewInternals,
hasWarning,
showAtPrefix = true,
}) => {
const { t } = useTranslation()
@ -36,7 +38,7 @@ const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
<Agent className="h-3 w-3 text-text-primary-on-surface" />
</div>
<span className="system-xs-medium text-text-secondary">
@
{showAtPrefix && '@'}
{agentName}
</span>
<button
@ -48,17 +50,19 @@ const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
</button>
</div>
</div>
<button
type="button"
className="flex items-center gap-1 text-text-tertiary hover:text-text-secondary"
onClick={onViewInternals}
>
<RiEqualizer2Line className="h-3.5 w-3.5" />
<span className="system-xs-medium">{t('common.viewInternals', { ns: 'workflow' })}</span>
{hasWarning && (
<AlertTriangle className="h-3.5 w-3.5 text-text-warning-secondary" />
)}
</button>
{onViewInternals && (
<button
type="button"
className="flex items-center gap-1 text-text-tertiary hover:text-text-secondary"
onClick={onViewInternals}
>
<RiEqualizer2Line className="h-3.5 w-3.5" />
<span className="system-xs-medium">{t('common.viewInternals', { ns: 'workflow' })}</span>
{hasWarning && (
<AlertTriangle className="h-3.5 w-3.5 text-text-warning-secondary" />
)}
</button>
)}
</div>
)
}

View File

@ -2,6 +2,7 @@ import type { AgentNode, WorkflowVariableBlockType } from '@/app/components/base
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
import type { MentionConfig, VarKindType } from '@/app/components/workflow/nodes/_base/types'
import type { AgentNodeType } from '@/app/components/workflow/nodes/agent/types'
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type {
CommonNodeType,
@ -24,7 +25,7 @@ import { useNodesMetaData, useNodesSyncDraft } from '@/app/components/workflow/h
import { VarKindType as VarKindTypeEnum } from '@/app/components/workflow/nodes/_base/types'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types'
import { generateNewNode, getNodeCustomTypeByNodeDataType } from '@/app/components/workflow/utils'
import { useGetLanguage } from '@/context/i18n'
import { useStrategyProviders } from '@/service/use-strategy'
@ -38,6 +39,11 @@ import Placeholder from './placeholder'
* Example: {{@agent-123.context@}} -> captures "agent-123"
*/
const AGENT_CONTEXT_VAR_PATTERN = /\{\{[@#]([^.@#]+)\.context[@#]\}\}/g
const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string) => {
if (!toolNodeId || !paramKey)
return ''
return `{{#${toolNodeId}_ext_${paramKey}.result#}}`
}
const DEFAULT_MENTION_CONFIG: MentionConfig = {
extractor_node_id: '',
output_selector: [],
@ -166,6 +172,16 @@ const MixedVariableTextInput = ({
}, {} as Record<string, WorkflowNode>)
}, [availableNodes])
const assemblePlaceholder = useMemo(() => {
return buildAssemblePlaceholder(toolNodeId, paramKey)
}, [paramKey, toolNodeId])
const isAssembleValue = useMemo(() => {
if (!assemblePlaceholder)
return false
return value.trim() === assemblePlaceholder
}, [assemblePlaceholder, value])
const contextNodeIds = useMemo(() => {
const ids = new Set<string>()
availableNodes.forEach((node) => {
@ -182,6 +198,12 @@ const MixedVariableTextInput = ({
}, {} as Record<string, WorkflowNode>)
}, [nodes])
const assembleExtractorNodeId = useMemo(() => {
if (!toolNodeId || !paramKey)
return ''
return `${toolNodeId}_ext_${paramKey}`
}, [paramKey, toolNodeId])
type DetectedAgent = {
nodeId: string
name: string
@ -278,6 +300,12 @@ const MixedVariableTextInput = ({
return agentWarning || extractorWarning
}, [detectedAgentFromValue, getNodeWarning, nodesById, paramKey, toolNodeId])
const hasAssembleWarning = useMemo(() => {
if (!isAssembleValue || !assembleExtractorNodeId)
return false
return getNodeWarning(nodesById[assembleExtractorNodeId])
}, [assembleExtractorNodeId, getNodeWarning, isAssembleValue, nodesById])
const syncExtractorPromptFromText = useCallback((text: string) => {
if (!toolNodeId || !paramKey)
return
@ -408,6 +436,70 @@ const MixedVariableTextInput = ({
setControlPromptEditorRerenderKey(Date.now())
}, [handleSyncWorkflowDraft, nodesMetaDataMap, onChange, paramKey, reactFlowStore, setControlPromptEditorRerenderKey, syncExtractorPromptFromText, toolNodeId, value])
const handleAssembleSelect = useCallback(() => {
if (!onChange || !toolNodeId || !paramKey || !assemblePlaceholder)
return
const defaultValue = nodesMetaDataMap?.[BlockEnum.Code]?.defaultValue as Partial<CodeNodeType> | undefined
if (!defaultValue)
return
const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
const { getNodes, setNodes } = reactFlowStore.getState()
const currentNodes = getNodes()
const existingNode = currentNodes.find(node => node.id === extractorNodeId)
const shouldReplace = existingNode && existingNode.data.type !== BlockEnum.Code
const shouldCreate = !existingNode || shouldReplace
if (shouldCreate) {
const nextNodes = shouldReplace
? currentNodes.filter(node => node.id !== extractorNodeId)
: currentNodes
const { newNode } = generateNewNode({
id: extractorNodeId,
type: getNodeCustomTypeByNodeDataType(BlockEnum.Code),
data: {
...defaultValue,
type: BlockEnum.Code,
title: defaultValue?.title ?? '',
desc: defaultValue?.desc || '',
parent_node_id: toolNodeId,
outputs: {
result: {
type: VarType.string,
children: null,
},
},
},
position: {
x: 0,
y: 0,
},
hidden: true,
})
setNodes([...nextNodes, newNode])
handleSyncWorkflowDraft()
}
const mentionConfigWithOutputSelector: MentionConfig = {
...DEFAULT_MENTION_CONFIG,
extractor_node_id: extractorNodeId,
output_selector: ['result'],
}
onChange(assemblePlaceholder, VarKindTypeEnum.mention, mentionConfigWithOutputSelector)
setControlPromptEditorRerenderKey(Date.now())
}, [assemblePlaceholder, handleSyncWorkflowDraft, nodesMetaDataMap, onChange, paramKey, reactFlowStore, setControlPromptEditorRerenderKey, toolNodeId])
const handleAssembleRemove = useCallback(() => {
if (!onChange || !assemblePlaceholder)
return
const nextValue = value.replace(assemblePlaceholder, '')
removeExtractorNode()
onChange(nextValue, VarKindTypeEnum.mixed, null)
setControlPromptEditorRerenderKey(Date.now())
}, [assemblePlaceholder, onChange, removeExtractorNode, setControlPromptEditorRerenderKey, value])
const handleOpenSubGraphModal = useCallback(() => {
setIsSubGraphModalOpen(true)
}, [])
@ -427,7 +519,15 @@ const MixedVariableTextInput = ({
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
)}
>
{detectedAgentFromValue && (
{isAssembleValue && (
<AgentHeaderBar
agentName={t('nodes.tool.assembleVariables', { ns: 'workflow' })}
onRemove={handleAssembleRemove}
hasWarning={hasAssembleWarning}
showAtPrefix={false}
/>
)}
{!isAssembleValue && detectedAgentFromValue && (
<AgentHeaderBar
agentName={detectedAgentFromValue.name}
onRemove={handleAgentRemove}
@ -435,37 +535,41 @@ const MixedVariableTextInput = ({
hasWarning={hasAgentWarning}
/>
)}
<PromptEditor
key={controlPromptEditorRerenderKey}
wrapperClassName="min-h-8 px-2 py-1"
className="caret:text-text-accent"
editable={!readOnly}
value={value}
workflowVariableBlock={{
show: !disableVariableInsertion,
variables: nodesOutputVars || [],
workflowNodesMap,
showManageInputField,
onManageInputField,
}}
agentBlock={{
show: agentNodes.length > 0 && !detectedAgentFromValue,
agentNodes,
onSelect: handleAgentSelect,
}}
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} hasSelectedAgent={!!detectedAgentFromValue} />}
onChange={(text) => {
const hasPlaceholder = new RegExp(AGENT_CONTEXT_VAR_PATTERN.source).test(text)
if (hasPlaceholder)
syncExtractorPromptFromText(text)
if (detectedAgentFromValue && !hasPlaceholder) {
removeExtractorNode()
onChange?.(text, VarKindTypeEnum.mixed, null)
return
}
onChange?.(text)
}}
/>
{!isAssembleValue && (
<PromptEditor
key={controlPromptEditorRerenderKey}
wrapperClassName="min-h-8 px-2 py-1"
className="caret:text-text-accent"
editable={!readOnly}
value={value}
workflowVariableBlock={{
show: !disableVariableInsertion,
variables: nodesOutputVars || [],
workflowNodesMap,
showManageInputField,
onManageInputField,
showAssembleVariables: !disableVariableInsertion && !!toolNodeId && !!paramKey,
onAssembleVariables: handleAssembleSelect,
}}
agentBlock={{
show: agentNodes.length > 0 && !detectedAgentFromValue,
agentNodes,
onSelect: handleAgentSelect,
}}
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} hasSelectedAgent={!!detectedAgentFromValue} />}
onChange={(text) => {
const hasPlaceholder = new RegExp(AGENT_CONTEXT_VAR_PATTERN.source).test(text)
if (hasPlaceholder)
syncExtractorPromptFromText(text)
if (detectedAgentFromValue && !hasPlaceholder) {
removeExtractorNode()
onChange?.(text, VarKindTypeEnum.mixed, null)
return
}
onChange?.(text)
}}
/>
)}
{toolNodeId && detectedAgentFromValue && sourceVariable && (
<SubGraphModal
isOpen={isSubGraphModalOpen}