From 2650ceb0a65e3dc5bbce104f587c811c0f0a5ada Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 20 Jan 2026 13:56:26 +0800 Subject: [PATCH] feat: support picker vars files ui in editor --- .../components/base/prompt-editor/index.tsx | 10 +- .../plugins/component-picker-block/index.tsx | 243 ++++++++++++------ .../nodes/_base/components/prompt/editor.tsx | 3 + .../llm/components/config-prompt-item.tsx | 3 + .../nodes/llm/components/config-prompt.tsx | 5 + .../plugins/file-picker-panel.tsx | 34 ++- 6 files changed, 217 insertions(+), 81 deletions(-) diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 59227c3d8e..78159f8fc3 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -29,6 +29,8 @@ import { } from 'lexical' import * as React from 'react' import { useEffect } from 'react' +import { FileReferenceNode } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/node' +import FileReferenceReplacementBlock from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/replacement-block' import { useEventEmitterContextContext } from '@/context/event-emitter' import { cn } from '@/utils/classnames' import { @@ -41,13 +43,13 @@ import { ContextBlockNode, ContextBlockReplacementBlock, } from './plugins/context-block' + import { CurrentBlock, CurrentBlockNode, CurrentBlockReplacementBlock, } from './plugins/current-block' import { CustomTextNode } from './plugins/custom-text/node' - import { ErrorMessageBlock, ErrorMessageBlockNode, @@ -106,6 +108,7 @@ export type PromptEditorProps = { lastRunBlock?: LastRunBlockType agentBlock?: AgentBlockType isSupportFileVar?: boolean + isSupportSandbox?: boolean } const PromptEditor: FC = ({ @@ -132,6 +135,7 @@ const PromptEditor: FC = ({ lastRunBlock, agentBlock, isSupportFileVar, + isSupportSandbox, }) => { const { eventEmitter } = useEventEmitterContextContext() const initialConfig = { @@ -152,6 +156,7 @@ const PromptEditor: FC = ({ CurrentBlockNode, ErrorMessageBlockNode, LastRunBlockNode, // LastRunBlockNode is used for error message block replacement + ...(isSupportSandbox ? [FileReferenceNode] : []), ], editorState: textToEditorState(value || ''), onError: (error: Error) => { @@ -215,6 +220,7 @@ const PromptEditor: FC = ({ errorMessageBlock={errorMessageBlock} lastRunBlock={lastRunBlock} isSupportFileVar={isSupportFileVar} + isSupportSandbox={isSupportSandbox} /> {(!agentBlock || agentBlock.show) && ( = ({ errorMessageBlock={errorMessageBlock} lastRunBlock={lastRunBlock} isSupportFileVar={isSupportFileVar} + isSupportSandbox={isSupportSandbox} /> { contextBlock?.show && ( @@ -285,6 +292,7 @@ const PromptEditor: FC = ({ ) } + {isSupportSandbox && } { currentBlock?.show && ( <> diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx index 78b7f1e753..673231c06d 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx @@ -38,9 +38,13 @@ import { useState, } from 'react' import ReactDOM from 'react-dom' +import { useTranslation } from 'react-i18next' import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types' +import { SegmentedControl } from '@/app/components/base/segmented-control' 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 { FilePickerPanel } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-panel' +import { $createFileReferenceNode } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/node' import { BlockEnum } from '@/app/components/workflow/types' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useBasicTypeaheadTriggerMatch } from '../../hooks' @@ -66,6 +70,7 @@ type ComponentPickerProps = { lastRunBlock?: LastRunBlockType agentBlock?: AgentBlockType isSupportFileVar?: boolean + isSupportSandbox?: boolean } const ComponentPicker = ({ triggerString, @@ -80,7 +85,9 @@ const ComponentPicker = ({ lastRunBlock, agentBlock, isSupportFileVar, + isSupportSandbox, }: ComponentPickerProps) => { + const { t } = useTranslation() const { eventEmitter } = useEventEmitterContextContext() const { refs, floatingStyles, isPositioned } = useFloating({ placement: 'bottom-start', @@ -114,6 +121,7 @@ const ComponentPicker = ({ }, [checkForTriggerMatch, editor]) const [queryString, setQueryString] = useState(null) + const [activeTab, setActiveTab] = useState<'variables' | 'files'>('variables') eventEmitter?.useSubscription((v: any) => { if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND) @@ -153,6 +161,17 @@ const ComponentPicker = ({ [editor], ) + const handleSelectFileReference = useCallback((resourceId: string) => { + editor.update(() => { + const match = checkForTriggerMatch(triggerString, editor) + const nodeToRemove = match ? $splitNodeContainingQuery(match) : null + if (nodeToRemove) + nodeToRemove.remove() + + $insertNodes([$createFileReferenceNode({ resourceId })]) + }) + }, [checkForTriggerMatch, editor, triggerString]) + const handleSelectWorkflowVariable = useCallback((variables: string[]) => { editor.update(() => { const match = getMatchFromSelection() @@ -227,6 +246,10 @@ const ComponentPicker = ({ const isAgentTrigger = triggerString === '@' && agentBlock?.show const showAssembleVariables = triggerString === '/' const agentNodes: AgentNode[] = useMemo(() => agentBlock?.agentNodes || [], [agentBlock?.agentNodes]) + const handleOpen = useCallback(() => { + if (isSupportSandbox && triggerString === '/') + setActiveTab('variables') + }, [isSupportSandbox, triggerString]) const renderMenu = useCallback>(( anchorElementRef, @@ -240,12 +263,87 @@ const ComponentPicker = ({ if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) return null } + const isSandboxMenu = isSupportSandbox && triggerString === '/' + + if (!(anchorElementRef.current && (isSandboxMenu || allFlattenOptions.length || workflowVariableBlock?.show))) + return null setTimeout(() => { if (anchorElementRef.current) refs.setReference(anchorElementRef.current) }, 0) + if (isSandboxMenu) { + return ( + <> + {ReactDOM.createPortal( +
+
+ +
+ {activeTab === 'variables' && ( + { + handleSelectWorkflowVariable(variables) + handleClose() + }} + maxHeightClass="max-h-[34vh]" + isSupportFileVar={isSupportFileVar} + onClose={handleClose} + onBlur={handleClose} + showManageInputField={workflowVariableBlock?.showManageInputField} + onManageInputField={workflowVariableBlock?.onManageInputField} + autoFocus={false} + isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code} + /> + )} + {activeTab === 'files' && ( + { + handleSelectFileReference(node.id) + handleClose() + }} + className="w-full border-0 bg-transparent p-0 shadow-none" + contentClassName="px-0" + showHeader={false} + /> + )} +
+
+
, + anchorElementRef.current, + )} + + ) + } + return ( <> { @@ -261,79 +359,79 @@ const ComponentPicker = ({ > {isAgentTrigger ? ( - ({ - ...node, - type: BlockEnum.Agent || BlockEnum.LLM, - }))} - onSelect={handleSelectAgent} - onClose={handleClose} - onBlur={handleClose} - maxHeightClass="max-h-[34vh]" - autoFocus={false} - hideSearch={useExternalSearch} - externalSearchText={useExternalSearch ? (queryString ?? '') : undefined} - enableKeyboardNavigation={useExternalSearch} - /> - ) + ({ + ...node, + type: BlockEnum.Agent || BlockEnum.LLM, + }))} + onSelect={handleSelectAgent} + onClose={handleClose} + onBlur={handleClose} + maxHeightClass="max-h-[34vh]" + autoFocus={false} + hideSearch={useExternalSearch} + externalSearchText={useExternalSearch ? (queryString ?? '') : undefined} + enableKeyboardNavigation={useExternalSearch} + /> + ) : ( - <> + <> + { + workflowVariableBlock?.show && ( +
+ { + handleSelectWorkflowVariable(variables) + }} + maxHeightClass="max-h-[34vh]" + isSupportFileVar={isSupportFileVar} + onClose={handleClose} + onBlur={handleClose} + showManageInputField={workflowVariableBlock.showManageInputField} + onManageInputField={workflowVariableBlock.onManageInputField} + showAssembleVariables={showAssembleVariables} + onAssembleVariables={showAssembleVariables ? handleSelectAssembleVariables : undefined} + autoFocus={false} + isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code} + hideSearch={useExternalSearch} + externalSearchText={useExternalSearch ? (queryString ?? '') : undefined} + enableKeyboardNavigation={useExternalSearch} + /> +
+ ) + } + { + workflowVariableBlock?.show && !!options.length && ( +
+ ) + } +
{ - workflowVariableBlock?.show && ( -
- { - handleSelectWorkflowVariable(variables) - }} - maxHeightClass="max-h-[34vh]" - isSupportFileVar={isSupportFileVar} - onClose={handleClose} - onBlur={handleClose} - showManageInputField={workflowVariableBlock.showManageInputField} - onManageInputField={workflowVariableBlock.onManageInputField} - showAssembleVariables={showAssembleVariables} - onAssembleVariables={showAssembleVariables ? handleSelectAssembleVariables : undefined} - autoFocus={false} - isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code} - hideSearch={useExternalSearch} - externalSearchText={useExternalSearch ? (queryString ?? '') : undefined} - enableKeyboardNavigation={useExternalSearch} - /> -
- ) + options.map((option, index) => ( + + { + index !== 0 && options.at(index - 1)?.group !== option.group && ( +
+ ) + } + {option.renderMenuOption({ + queryString, + isSelected: selectedIndex === index, + onSelect: () => { + selectOptionAndCleanUp(option) + }, + onSetHighlight: () => { + setHighlightedIndex(index) + }, + })} +
+ )) } - { - workflowVariableBlock?.show && !!options.length && ( -
- ) - } -
- { - options.map((option, index) => ( - - { - index !== 0 && options.at(index - 1)?.group !== option.group && ( -
- ) - } - {option.renderMenuOption({ - queryString, - isSelected: selectedIndex === index, - onSelect: () => { - selectOptionAndCleanUp(option) - }, - onSetHighlight: () => { - setHighlightedIndex(index) - }, - })} -
- )) - } -
- - )} +
+ + )} , anchorElementRef.current, @@ -341,13 +439,14 @@ const ComponentPicker = ({ } ) - }, [isAgentTrigger, agentNodes, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, handleSelectAgent, handleClose, workflowVariableOptions, isSupportFileVar, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField, showAssembleVariables, handleSelectAssembleVariables, useExternalSearch]) + }, [isAgentTrigger, isSupportSandbox, triggerString, allFlattenOptions.length, workflowVariableBlock?.show, workflowVariableBlock.showManageInputField, workflowVariableBlock.onManageInputField, floatingStyles, isPositioned, refs, agentNodes, handleSelectAgent, handleClose, useExternalSearch, queryString, workflowVariableOptions, isSupportFileVar, showAssembleVariables, handleSelectAssembleVariables, currentBlock?.generatorType, t, activeTab, handleSelectWorkflowVariable, handleSelectFileReference]) return ( void modelConfig?: ModelConfig @@ -102,6 +103,7 @@ const Editor: FC = ({ nodesOutputVars, availableNodes = [], isSupportFileVar, + isSupportSandbox, isSupportPromptGenerator, isSupportJinja, editionType, @@ -295,6 +297,7 @@ const Editor: FC = ({ onFocus={setFocus} editable={!readOnly} isSupportFileVar={isSupportFileVar} + isSupportSandbox={isSupportSandbox} /> {/* to patch Editor not support dynamic change editable status */} {readOnly &&
} diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx index 118e3a7c0a..01c5f8ff54 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx +++ b/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx @@ -40,6 +40,7 @@ type Props = { varList: Variable[] handleAddVariable: (payload: any) => void modelConfig?: ModelConfig + isSupportSandbox?: boolean } const roleOptions = [ @@ -82,6 +83,7 @@ const ConfigPromptItem: FC = ({ varList, handleAddVariable, modelConfig, + isSupportSandbox, }) => { const { t } = useTranslation() const workflowStore = useWorkflowStore() @@ -149,6 +151,7 @@ const ConfigPromptItem: FC = ({ varList={varList} handleAddVariable={handleAddVariable} isSupportFileVar + isSupportSandbox={isSupportSandbox} /> ) } diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt.tsx index d88ec95f34..3bfaae7cd4 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx +++ b/web/app/components/workflow/nodes/llm/components/config-prompt.tsx @@ -7,6 +7,7 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { v4 as uuid4 } from 'uuid' +import { useFeatures } from '@/app/components/base/features/hooks' import { DragHandle } from '@/app/components/base/icons/src/vender/line/others' import { PortalToFollowElem, @@ -59,6 +60,8 @@ const ConfigPrompt: FC = ({ }) => { const { t } = useTranslation() const workflowStore = useWorkflowStore() + const features = useFeatures(s => s.features) + const isSupportSandbox = !!features.sandbox?.enabled const { setControlPromptEditorRerenderKey, } = workflowStore.getState() @@ -337,6 +340,7 @@ const ConfigPrompt: FC = ({ varList={varList} handleAddVariable={handleAddVariable} modelConfig={modelConfig} + isSupportSandbox={isSupportSandbox} /> ) @@ -410,6 +414,7 @@ const ConfigPrompt: FC = ({ handleAddVariable={handleAddVariable} onGenerated={handleGenerated} modelConfig={modelConfig} + isSupportSandbox={isSupportSandbox} /> )} diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-panel.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-panel.tsx index bc6eda50d3..961d0e5108 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-panel.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-panel.tsx @@ -109,11 +109,17 @@ FilePickerTreeNode.displayName = 'FilePickerTreeNode' type FilePickerPanelProps = { onSelectNode: (node: TreeNodeData) => void focusNodeId?: string + className?: string + contentClassName?: string + showHeader?: boolean } const FilePickerPanel: FC = ({ onSelectNode, focusNodeId, + className, + contentClassName, + showHeader = true, }) => { const { t } = useTranslation('workflow') const { data: treeData, isLoading, error } = useSkillAssetTreeData() @@ -141,7 +147,10 @@ const FilePickerPanel: FC = ({ return (
{ const target = e.target as HTMLElement if (target.closest('input, textarea, select')) @@ -149,13 +158,22 @@ const FilePickerPanel: FC = ({ e.preventDefault() }} > -
- - {t('skillEditor.referenceFiles')} - -
-
+ {showHeader && ( +
+ + {t('skillEditor.referenceFiles')} + +
+ )} +
{isLoading && (