feat: add assemble variables icon

This commit is contained in:
zhsama
2026-01-16 18:45:28 +08:00
parent 4ee49552ce
commit 8d643e4b85
7 changed files with 236 additions and 48 deletions

View File

@ -9,8 +9,7 @@ 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 { MagicWand } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
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 Input from '@/app/components/base/input'
import {
@ -414,8 +413,8 @@ const VarReferenceVars: FC<Props> = ({
onClick={handleAssembleVariables}
onMouseDown={e => e.preventDefault()}
>
<span className="mr-1 flex h-4 w-4 items-center justify-center rounded bg-util-colors-indigo-indigo-500/10">
<MagicWand className="h-3.5 w-3.5 text-util-colors-indigo-indigo-500" />
<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' })}

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,
@ -10,6 +11,7 @@ import type {
PromptTemplateItem,
ValueSelector,
Node as WorkflowNode,
VarType,
} from '@/app/components/workflow/types'
import {
memo,
@ -38,6 +40,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 +173,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 +199,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 +301,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 +437,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 +520,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 +536,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}