feat: update agent functionality in mixed-variable text input

This commit is contained in:
zhsama
2026-01-08 16:59:09 +08:00
parent 8b8e521c4e
commit 831eba8b1c
10 changed files with 317 additions and 31 deletions

2
.nvmrc
View File

@ -1 +1 @@
22.11.0
24

View File

@ -212,6 +212,19 @@ const PromptEditor: FC<PromptEditorProps> = ({
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
<ComponentPickerBlock
triggerString="@"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
<ComponentPickerBlock
triggerString="{"
contextBlock={contextBlock}

View File

@ -0,0 +1,135 @@
'use client'
import type { FC } from 'react'
import type { BlockEnum } from '@/app/components/workflow/types'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import BlockIcon from '@/app/components/workflow/block-icon'
import { cn } from '@/utils/classnames'
export type AgentNode = {
id: string
title: string
type: BlockEnum
}
type ItemProps = {
node: AgentNode
onSelect: (node: AgentNode) => void
}
const Item: FC<ItemProps> = ({ node, onSelect }) => {
const [isHovering, setIsHovering] = useState(false)
return (
<button
type="button"
className={cn(
'relative flex h-6 w-full cursor-pointer items-center rounded-md border-none bg-transparent px-3 text-left',
isHovering && 'bg-state-base-hover',
)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onClick={() => onSelect(node)}
onMouseDown={e => e.preventDefault()}
>
<BlockIcon
className="mr-1 shrink-0"
type={node.type}
size="xs"
/>
<span
className="system-sm-medium truncate text-text-secondary"
title={node.title}
>
{node.title}
</span>
</button>
)
}
type Props = {
nodes: AgentNode[]
onSelect: (node: AgentNode) => void
onClose?: () => void
onBlur?: () => void
hideSearch?: boolean
searchBoxClassName?: string
maxHeightClass?: string
autoFocus?: boolean
}
const AgentNodeList: FC<Props> = ({
nodes,
onSelect,
onClose,
onBlur,
hideSearch,
searchBoxClassName,
maxHeightClass,
autoFocus = true,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault()
onClose?.()
}
}
const filteredNodes = nodes.filter((node) => {
if (!searchText)
return true
return node.title.toLowerCase().includes(searchText.toLowerCase())
})
return (
<>
{!hideSearch && (
<>
<div className={cn('mx-2 mb-2 mt-2', searchBoxClassName)}>
<Input
showLeftIcon
showClearIcon
value={searchText}
placeholder={t('common.searchAgent', { ns: 'workflow' })}
onChange={e => setSearchText(e.target.value)}
onClick={e => e.stopPropagation()}
onKeyDown={handleKeyDown}
onClear={() => setSearchText('')}
onBlur={onBlur}
autoFocus={autoFocus}
/>
</div>
<div
className="relative left-[-4px] h-[0.5px] bg-black/5"
style={{ width: 'calc(100% + 8px)' }}
/>
</>
)}
{filteredNodes.length > 0
? (
<div className={cn('max-h-[85vh] overflow-y-auto py-1', maxHeightClass)}>
{filteredNodes.map(node => (
<Item
key={node.id}
node={node}
onSelect={onSelect}
/>
))}
</div>
)
: (
<div className="py-2 pl-3 text-xs font-medium text-text-tertiary">
{t('common.noAgentNodes', { ns: 'workflow' })}
</div>
)}
</>
)
}
export default React.memo(AgentNodeList)

View File

@ -0,0 +1,52 @@
import type { FC } from 'react'
import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
type AgentHeaderBarProps = {
agentName: string
onRemove: () => void
onViewInternals?: () => void
}
const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
agentName,
onRemove,
onViewInternals,
}) => {
const { t } = useTranslation()
return (
<div className="flex items-center justify-between px-2 py-1">
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark px-1.5 py-0.5 shadow-xs">
<div className="flex h-4 w-4 items-center justify-center rounded bg-util-colors-indigo-indigo-500">
<Agent className="h-3 w-3 text-text-primary-on-surface" />
</div>
<span className="system-xs-medium text-text-secondary">
@
{agentName}
</span>
<button
type="button"
className="flex h-4 w-4 items-center justify-center rounded hover:bg-state-base-hover"
onClick={onRemove}
>
<RiCloseLine className="h-3 w-3 text-text-tertiary" />
</button>
</div>
</div>
<button
type="button"
className="flex items-center gap-0.5 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>
</button>
</div>
)
}
export default memo(AgentHeaderBar)

View File

@ -4,14 +4,23 @@ import type {
} from '@/app/components/workflow/types'
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
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 AgentHeaderBar from './agent-header-bar'
import Placeholder from './placeholder'
/**
* Matches workflow variable syntax: {{#nodeId.varName#}}
* Example: {{#agent-123.text#}} -> captures "agent-123.text"
*/
const WORKFLOW_VAR_PATTERN = /\{\{#([^#]+)#\}\}/g
type MixedVariableTextInputProps = {
readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[]
@ -21,7 +30,9 @@ type MixedVariableTextInputProps = {
showManageInputField?: boolean
onManageInputField?: () => void
disableVariableInsertion?: boolean
onViewInternals?: () => void
}
const MixedVariableTextInput = ({
readOnly = false,
nodesOutputVars,
@ -31,43 +42,95 @@ const MixedVariableTextInput = ({
showManageInputField,
onManageInputField,
disableVariableInsertion = false,
onViewInternals,
}: MixedVariableTextInputProps) => {
const { t } = useTranslation()
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey)
const nodesByIdMap = useMemo(() => {
return availableNodes.reduce((acc, node) => {
acc[node.id] = node
return acc
}, {} as Record<string, Node>)
}, [availableNodes])
const detectedAgentFromValue = useMemo(() => {
if (!value)
return null
const matches = value.matchAll(WORKFLOW_VAR_PATTERN)
for (const match of matches) {
const variablePath = match[1]
const nodeId = variablePath.split('.')[0]
const node = nodesByIdMap[nodeId]
if (node?.data.type === BlockEnum.Agent) {
return {
nodeId,
name: node.data.title,
}
}
}
return null
}, [value, nodesByIdMap])
const handleAgentRemove = useCallback(() => {
if (!detectedAgentFromValue || !onChange)
return
const pattern = /\{\{#([^#]+)#\}\}/g
const valueWithoutAgentVars = value.replace(pattern, (match, variablePath) => {
const nodeId = variablePath.split('.')[0]
return nodeId === detectedAgentFromValue.nodeId ? '' : match
}).trim()
onChange(valueWithoutAgentVars)
setControlPromptEditorRerenderKey(Date.now())
}, [detectedAgentFromValue, value, onChange, setControlPromptEditorRerenderKey])
return (
<PromptEditor
key={controlPromptEditorRerenderKey}
wrapperClassName={cn(
'min-h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
<div className={cn(
'w-full rounded-lg border border-transparent bg-components-input-bg-normal',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
)}
>
{detectedAgentFromValue && (
<AgentHeaderBar
agentName={detectedAgentFromValue.name}
onRemove={handleAgentRemove}
onViewInternals={onViewInternals}
/>
)}
className="caret:text-text-accent"
editable={!readOnly}
value={value}
workflowVariableBlock={{
show: !disableVariableInsertion,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('blocks.start', { ns: 'workflow' }),
type: BlockEnum.Start,
<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: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
}
return acc
}, {} as any),
showManageInputField,
onManageInputField,
}}
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} />}
onChange={onChange}
/>
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('blocks.start', { ns: 'workflow' }),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
showManageInputField,
onManageInputField,
}}
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} />}
onChange={onChange}
/>
</div>
)
}

View File

@ -44,6 +44,17 @@ const Placeholder = ({ disableVariableInsertion = false }: PlaceholderProps) =>
>
{t('nodes.tool.insertPlaceholder2', { ns: 'workflow' })}
</div>
<div className="system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder">@</div>
<div
className="system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary"
onMouseDown={((e) => {
e.preventDefault()
e.stopPropagation()
handleInsert('@')
})}
>
{t('nodes.tool.insertPlaceholder3', { ns: 'workflow' })}
</div>
</>
)}
</div>

View File

@ -204,6 +204,7 @@
"common.runApp": "Run App",
"common.runHistory": "Run History",
"common.running": "Running",
"common.searchAgent": "Search agent",
"common.searchVar": "Search variable",
"common.setVarValuePlaceholder": "Set variable",
"common.showRunHistory": "Show Run History",
@ -215,6 +216,7 @@
"common.variableNamePlaceholder": "Variable name",
"common.versionHistory": "Version History",
"common.viewDetailInTracingPanel": "View details",
"common.viewInternals": "View internals",
"common.viewOnly": "View Only",
"common.viewRunHistory": "View run history",
"common.workflowAsTool": "Workflow as Tool",
@ -764,6 +766,7 @@
"nodes.tool.inputVars": "Input Variables",
"nodes.tool.insertPlaceholder1": "Type or press",
"nodes.tool.insertPlaceholder2": "insert variable",
"nodes.tool.insertPlaceholder3": "add agent",
"nodes.tool.outputVars.files.title": "tool generated files",
"nodes.tool.outputVars.files.transfer_method": "Transfer method.Value is remote_url or local_file",
"nodes.tool.outputVars.files.type": "Support type. Now only support image",

View File

@ -202,6 +202,7 @@
"common.runApp": "アプリを実行",
"common.runHistory": "実行履歴",
"common.running": "実行中",
"common.searchAgent": "エージェントを検索",
"common.searchVar": "変数を検索",
"common.setVarValuePlaceholder": "変数値を設定",
"common.showRunHistory": "実行履歴を表示",
@ -213,6 +214,7 @@
"common.variableNamePlaceholder": "変数名を入力",
"common.versionHistory": "バージョン履歴",
"common.viewDetailInTracingPanel": "詳細を表示",
"common.viewInternals": "内部を表示",
"common.viewOnly": "閲覧のみ",
"common.viewRunHistory": "実行履歴を表示",
"common.workflowAsTool": "ワークフローをツールとして公開する",
@ -762,6 +764,7 @@
"nodes.tool.inputVars": "入力変数",
"nodes.tool.insertPlaceholder1": "タイプするか押してください",
"nodes.tool.insertPlaceholder2": "変数を挿入する",
"nodes.tool.insertPlaceholder3": "エージェントを追加",
"nodes.tool.outputVars.files.title": "ツールが生成したファイル",
"nodes.tool.outputVars.files.transfer_method": "転送方法。値は remote_url または local_file です",
"nodes.tool.outputVars.files.type": "サポートタイプ。現在は画像のみサポートされています",

View File

@ -202,6 +202,7 @@
"common.runApp": "运行",
"common.runHistory": "运行历史",
"common.running": "运行中",
"common.searchAgent": "搜索代理",
"common.searchVar": "搜索变量",
"common.setVarValuePlaceholder": "设置变量值",
"common.showRunHistory": "显示运行历史",
@ -213,6 +214,7 @@
"common.variableNamePlaceholder": "变量名",
"common.versionHistory": "版本历史",
"common.viewDetailInTracingPanel": "查看详细信息",
"common.viewInternals": "查看内部",
"common.viewOnly": "只读",
"common.viewRunHistory": "查看运行历史",
"common.workflowAsTool": "发布为工具",
@ -762,6 +764,7 @@
"nodes.tool.inputVars": "输入变量",
"nodes.tool.insertPlaceholder1": "键入",
"nodes.tool.insertPlaceholder2": "插入变量",
"nodes.tool.insertPlaceholder3": "添加代理",
"nodes.tool.outputVars.files.title": "工具生成的文件",
"nodes.tool.outputVars.files.transfer_method": "传输方式。值为 remote_url 或 local_file",
"nodes.tool.outputVars.files.type": "支持类型。现在只支持图片",

View File

@ -202,6 +202,7 @@
"common.runApp": "運行",
"common.runHistory": "運行歷史",
"common.running": "運行中",
"common.searchAgent": "搜索代理",
"common.searchVar": "搜索變數",
"common.setVarValuePlaceholder": "設置變數值",
"common.showRunHistory": "顯示運行歷史",
@ -213,6 +214,7 @@
"common.variableNamePlaceholder": "變數名",
"common.versionHistory": "版本歷史",
"common.viewDetailInTracingPanel": "查看詳細信息",
"common.viewInternals": "查看內部",
"common.viewOnly": "只讀",
"common.viewRunHistory": "查看運行歷史",
"common.workflowAsTool": "發佈為工具",
@ -762,6 +764,7 @@
"nodes.tool.inputVars": "輸入變數",
"nodes.tool.insertPlaceholder1": "輸入或按壓",
"nodes.tool.insertPlaceholder2": "插入變數",
"nodes.tool.insertPlaceholder3": "添加代理",
"nodes.tool.outputVars.files.title": "工具生成的文件",
"nodes.tool.outputVars.files.transfer_method": "傳輸方式。值為 remote_url 或 local_file",
"nodes.tool.outputVars.files.type": "支持類型。現在只支持圖片",