mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
feat: add assemble variables icon
This commit is contained in:
@ -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' })}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
Reference in New Issue
Block a user