mirror of
https://github.com/langgenius/dify.git
synced 2026-03-07 00:26:36 +08:00
feat: update agent functionality in mixed-variable text input
This commit is contained in:
@ -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}
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "サポートタイプ。現在は画像のみサポートされています",
|
||||
|
||||
@ -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": "支持类型。现在只支持图片",
|
||||
|
||||
@ -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": "支持類型。現在只支持圖片",
|
||||
|
||||
Reference in New Issue
Block a user