mirror of
https://github.com/langgenius/dify.git
synced 2026-01-14 06:07:33 +08:00
feat: enhance agent integration in prompt editor and mixed-variable text input
This commit is contained in:
parent
831eba8b1c
commit
1aed585a19
@ -5,6 +5,7 @@ import type {
|
||||
} from 'lexical'
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
AgentBlockType,
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
@ -103,6 +104,7 @@ export type PromptEditorProps = {
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
agentBlock?: AgentBlockType
|
||||
isSupportFileVar?: boolean
|
||||
}
|
||||
|
||||
@ -128,6 +130,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
agentBlock,
|
||||
isSupportFileVar,
|
||||
}) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@ -139,6 +142,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
{
|
||||
replace: TextNode,
|
||||
with: (node: TextNode) => new CustomTextNode(node.__text),
|
||||
withKlass: CustomTextNode,
|
||||
},
|
||||
ContextBlockNode,
|
||||
HistoryBlockNode,
|
||||
@ -212,19 +216,22 @@ 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}
|
||||
/>
|
||||
{(!agentBlock || agentBlock.show) && (
|
||||
<ComponentPickerBlock
|
||||
triggerString="@"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
agentBlock={agentBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
)}
|
||||
<ComponentPickerBlock
|
||||
triggerString="{"
|
||||
contextBlock={contextBlock}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import type { TextNode } from 'lexical'
|
||||
import type {
|
||||
AgentBlockType,
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
@ -29,7 +30,9 @@ import {
|
||||
} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import AgentNodeList from '@/app/components/workflow/nodes/_base/components/agent-node-list'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../hooks'
|
||||
import { $splitNodeContainingQuery } from '../../utils'
|
||||
@ -51,6 +54,7 @@ type ComponentPickerProps = {
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
agentBlock?: AgentBlockType
|
||||
isSupportFileVar?: boolean
|
||||
}
|
||||
const ComponentPicker = ({
|
||||
@ -64,6 +68,7 @@ const ComponentPicker = ({
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
agentBlock,
|
||||
isSupportFileVar,
|
||||
}: ComponentPickerProps) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@ -151,12 +156,31 @@ const ComponentPicker = ({
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent)
|
||||
}, [editor])
|
||||
|
||||
const handleSelectAgent = useCallback((agent: { id: string, title: string }) => {
|
||||
editor.update(() => {
|
||||
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
})
|
||||
agentBlock?.onSelect?.(agent)
|
||||
handleClose()
|
||||
}, [editor, checkForTriggerMatch, triggerString, agentBlock, handleClose])
|
||||
|
||||
const isAgentTrigger = triggerString === '@' && agentBlock?.show
|
||||
const agentNodes = agentBlock?.agentNodes || []
|
||||
|
||||
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>((
|
||||
anchorElementRef,
|
||||
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||
) => {
|
||||
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||
return null
|
||||
if (isAgentTrigger) {
|
||||
if (!(anchorElementRef.current && agentNodes.length))
|
||||
return null
|
||||
}
|
||||
else {
|
||||
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||
return null
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (anchorElementRef.current)
|
||||
@ -167,9 +191,6 @@ const ComponentPicker = ({
|
||||
<>
|
||||
{
|
||||
ReactDOM.createPortal(
|
||||
// The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
|
||||
// Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
|
||||
// See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
|
||||
<div className="h-0 w-0">
|
||||
<div
|
||||
className="w-[260px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg"
|
||||
@ -179,56 +200,73 @@ const ComponentPicker = ({
|
||||
}}
|
||||
ref={refs.setFloating}
|
||||
>
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<div className="p-1">
|
||||
<VarReferenceVars
|
||||
searchBoxClassName="mt-1"
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
{isAgentTrigger
|
||||
? (
|
||||
<AgentNodeList
|
||||
nodes={agentNodes.map(node => ({
|
||||
...node,
|
||||
type: BlockEnum.Agent,
|
||||
}))}
|
||||
onSelect={handleSelectAgent}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
showManageInputField={workflowVariableBlock.showManageInputField}
|
||||
onManageInputField={workflowVariableBlock.onManageInputField}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && !!options.length && (
|
||||
<div className="my-1 h-px w-full -translate-x-1 bg-divider-subtle"></div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{
|
||||
// Divider
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
workflowVariableBlock?.show && (
|
||||
<div className="p-1">
|
||||
<VarReferenceVars
|
||||
searchBoxClassName="mt-1"
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
showManageInputField={workflowVariableBlock.showManageInputField}
|
||||
onManageInputField={workflowVariableBlock.onManageInputField}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && !!options.length && (
|
||||
<div className="my-1 h-px w-full -translate-x-1 bg-divider-subtle"></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
{
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className="my-1 h-px w-full -translate-x-1 bg-divider-subtle"></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
@ -236,7 +274,7 @@ const ComponentPicker = ({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
||||
}, [isAgentTrigger, agentNodes, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, handleSelectAgent, handleClose, workflowVariableOptions, isSupportFileVar, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
|
||||
@ -73,6 +73,17 @@ export type WorkflowVariableBlockType = {
|
||||
onManageInputField?: () => void
|
||||
}
|
||||
|
||||
export type AgentNode = {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type AgentBlockType = {
|
||||
show?: boolean
|
||||
agentNodes?: AgentNode[]
|
||||
onSelect?: (agent: AgentNode) => void
|
||||
}
|
||||
|
||||
export type MenuTextMatch = {
|
||||
leadOffset: number
|
||||
matchingString: string
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { AgentBlockType } from '@/app/components/base/prompt-editor/types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
@ -74,19 +76,44 @@ const MixedVariableTextInput = ({
|
||||
return null
|
||||
}, [value, nodesByIdMap])
|
||||
|
||||
const [selectedAgent, setSelectedAgent] = useState<{ id: string, title: string } | null>(null)
|
||||
|
||||
const agentNodes = useMemo(() => {
|
||||
return availableNodes
|
||||
.filter(node => node.data.type === BlockEnum.Agent)
|
||||
.map(node => ({
|
||||
id: node.id,
|
||||
title: node.data.title,
|
||||
}))
|
||||
}, [availableNodes])
|
||||
|
||||
const handleAgentSelect = useCallback((agent: { id: string, title: string }) => {
|
||||
setSelectedAgent(agent)
|
||||
if (onChange) {
|
||||
const agentVar = `{{#${agent.id}.text#}}`
|
||||
const newValue = value ? `${agentVar}${value}` : agentVar
|
||||
onChange(newValue)
|
||||
setControlPromptEditorRerenderKey(Date.now())
|
||||
}
|
||||
}, [onChange, value, setControlPromptEditorRerenderKey])
|
||||
|
||||
const handleAgentRemove = useCallback(() => {
|
||||
if (!detectedAgentFromValue || !onChange)
|
||||
const agentNodeId = detectedAgentFromValue?.nodeId || selectedAgent?.id
|
||||
if (!agentNodeId || !onChange)
|
||||
return
|
||||
|
||||
const pattern = /\{\{#([^#]+)#\}\}/g
|
||||
const valueWithoutAgentVars = value.replace(pattern, (match, variablePath) => {
|
||||
const nodeId = variablePath.split('.')[0]
|
||||
return nodeId === detectedAgentFromValue.nodeId ? '' : match
|
||||
return nodeId === agentNodeId ? '' : match
|
||||
}).trim()
|
||||
|
||||
onChange(valueWithoutAgentVars)
|
||||
setSelectedAgent(null)
|
||||
setControlPromptEditorRerenderKey(Date.now())
|
||||
}, [detectedAgentFromValue, value, onChange, setControlPromptEditorRerenderKey])
|
||||
}, [detectedAgentFromValue?.nodeId, selectedAgent?.id, value, onChange, setControlPromptEditorRerenderKey])
|
||||
|
||||
const displayedAgent = detectedAgentFromValue || (selectedAgent ? { nodeId: selectedAgent.id, name: selectedAgent.title } : null)
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
@ -95,9 +122,9 @@ const MixedVariableTextInput = ({
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
)}
|
||||
>
|
||||
{detectedAgentFromValue && (
|
||||
{displayedAgent && (
|
||||
<AgentHeaderBar
|
||||
agentName={detectedAgentFromValue.name}
|
||||
agentName={displayedAgent.name}
|
||||
onRemove={handleAgentRemove}
|
||||
onViewInternals={onViewInternals}
|
||||
/>
|
||||
@ -127,7 +154,12 @@ const MixedVariableTextInput = ({
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
}}
|
||||
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} />}
|
||||
agentBlock={{
|
||||
show: agentNodes.length > 0 && !displayedAgent,
|
||||
agentNodes,
|
||||
onSelect: handleAgentSelect,
|
||||
} as AgentBlockType}
|
||||
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} hasSelectedAgent={!!displayedAgent} />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -7,9 +7,10 @@ import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/cust
|
||||
|
||||
type PlaceholderProps = {
|
||||
disableVariableInsertion?: boolean
|
||||
hasSelectedAgent?: boolean
|
||||
}
|
||||
|
||||
const Placeholder = ({ disableVariableInsertion = false }: PlaceholderProps) => {
|
||||
const Placeholder = ({ disableVariableInsertion = false, hasSelectedAgent = false }: PlaceholderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
@ -44,17 +45,21 @@ 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>
|
||||
{!hasSelectedAgent && (
|
||||
<>
|
||||
<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>
|
||||
|
||||
@ -173,6 +173,7 @@
|
||||
"common.needConnectTip": "This step is not connected to anything",
|
||||
"common.needOutputNode": "The Output node must be added",
|
||||
"common.needStartNode": "At least one start node must be added",
|
||||
"common.noAgentNodes": "No agent nodes available",
|
||||
"common.noHistory": "No History",
|
||||
"common.noVar": "No variable",
|
||||
"common.notRunning": "Not running yet",
|
||||
|
||||
@ -171,6 +171,7 @@
|
||||
"common.needConnectTip": "接続されていないステップがあります",
|
||||
"common.needOutputNode": "出力ノードを追加する必要があります",
|
||||
"common.needStartNode": "少なくとも1つのスタートノードを追加する必要があります",
|
||||
"common.noAgentNodes": "利用可能なエージェントノードがありません",
|
||||
"common.noHistory": "履歴がありません",
|
||||
"common.noVar": "変数がありません",
|
||||
"common.notRunning": "まだ実行されていません",
|
||||
|
||||
@ -171,6 +171,7 @@
|
||||
"common.needConnectTip": "此节点尚未连接到其他节点",
|
||||
"common.needOutputNode": "必须添加输出节点",
|
||||
"common.needStartNode": "必须添加至少一个开始节点",
|
||||
"common.noAgentNodes": "没有可用的代理节点",
|
||||
"common.noHistory": "没有历史版本",
|
||||
"common.noVar": "没有变量",
|
||||
"common.notRunning": "尚未运行",
|
||||
|
||||
@ -171,6 +171,7 @@
|
||||
"common.needConnectTip": "此節點尚未連接到其他節點",
|
||||
"common.needOutputNode": "必須新增輸出節點",
|
||||
"common.needStartNode": "至少必須新增一個起始節點",
|
||||
"common.noAgentNodes": "沒有可用的代理節點",
|
||||
"common.noHistory": "無歷史記錄",
|
||||
"common.noVar": "沒有變數",
|
||||
"common.notRunning": "尚未運行",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user