diff --git a/web/app/components/workflow/header/run-and-history.tsx b/web/app/components/workflow/header/run-and-history.tsx index aba6c0ea67..f93459f15b 100644 --- a/web/app/components/workflow/header/run-and-history.tsx +++ b/web/app/components/workflow/header/run-and-history.tsx @@ -38,6 +38,7 @@ const RunMode = memo(() => { const { doSyncWorkflowDraft, } = useNodesSyncDraft() + const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() const workflowRunningData = useStore(s => s.workflowRunningData) const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running @@ -55,10 +56,16 @@ const RunMode = memo(() => { const startVariables = startNode?.data.variables || [] const fileSettings = featuresStore!.getState().features.file const { + showDebugAndPreviewPanel, setShowDebugAndPreviewPanel, setShowInputsPanel, } = workflowStore.getState() + if (showDebugAndPreviewPanel) { + handleCancelDebugAndPreviewPanel() + return + } + if (!startVariables.length && !fileSettings?.image?.enabled) { await doSyncWorkflowDraft() handleRun({ inputs: {}, files: [] }) @@ -75,6 +82,7 @@ const RunMode = memo(() => { doSyncWorkflowDraft, store, featuresStore, + handleCancelDebugAndPreviewPanel, ]) return ( diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index 267ffa91b7..332f4ffcd1 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -21,6 +21,7 @@ export const useWorkflowInteractions = () => { const handleCancelDebugAndPreviewPanel = useCallback(() => { workflowStore.setState({ showDebugAndPreviewPanel: false, + workflowRunningData: undefined, }) handleNodeCancelRunningStatus() handleEdgeCancelRunningStatus() diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx new file mode 100644 index 0000000000..d3f5547781 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx @@ -0,0 +1,173 @@ +'use client' +import type { FC } from 'react' +import React, { useEffect, useRef, useState } from 'react' +import { useBoolean } from 'ahooks' +import { useTranslation } from 'react-i18next' +import type { Props as EditorProps } from '.' +import Editor from '.' +import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' +import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' +import type { Variable } from '@/app/components/workflow/types' + +const TO_WINDOW_OFFSET = 8 + +type Props = { + nodeId: string + varList: Variable[] + onAddVar: (payload: Variable) => void +} & EditorProps + +const CodeEditor: FC = ({ + nodeId, + varList, + onAddVar, + ...editorProps +}) => { + const { t } = useTranslation() + + const { availableVars } = useAvailableVarList(nodeId, { + onlyLeafNodeVar: false, + filterVar: () => true, + }) + + const isLeftBraceRef = useRef(false) + + const editorRef = useRef(null) + const monacoRef = useRef(null) + + const popupRef = useRef(null) + const [isShowVarPicker, { + setTrue: showVarPicker, + setFalse: hideVarPicker, + }] = useBoolean(false) + + const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 }) + + // Listen for cursor position changes + const handleCursorPositionChange = (event: any) => { + const editor: any = editorRef.current + const { position } = event + const text = editor.getModel().getLineContent(position.lineNumber) + const charBefore = text[position.column - 2] + if (['/', '{'].includes(charBefore)) { + isLeftBraceRef.current = charBefore === '{' + const editorRect = editor.getDomNode().getBoundingClientRect() + const cursorCoords = editor.getScrolledVisiblePosition(position) + + const popupX = editorRect.left + cursorCoords.left + const popupY = editorRect.top + cursorCoords.top + 20 // Adjust the vertical position as needed + + setPopupPosition({ x: popupX, y: popupY }) + showVarPicker() + } + else { + hideVarPicker() + } + } + + useEffect(() => { + if (isShowVarPicker && popupRef.current) { + const windowWidth = window.innerWidth + const { width, height } = popupRef.current!.getBoundingClientRect() + const newPopupPosition = { ...popupPosition } + if (popupPosition.x + width > windowWidth - TO_WINDOW_OFFSET) + newPopupPosition.x = windowWidth - width - TO_WINDOW_OFFSET + + if (popupPosition.y + height > window.innerHeight - TO_WINDOW_OFFSET) + newPopupPosition.y = window.innerHeight - height - TO_WINDOW_OFFSET + + setPopupPosition(newPopupPosition) + } + }, [isShowVarPicker, popupPosition]) + + const onEditorMounted = (editor: any, monaco: any) => { + editorRef.current = editor + monacoRef.current = monaco + editor.onDidChangeCursorPosition(handleCursorPositionChange) + } + + const getUniqVarName = (varName: string) => { + if (varList.find(v => v.variable === varName)) { + const match = varName.match(/_([0-9]+)$/) + + const index = (() => { + if (match) + return parseInt(match[1]!) + 1 + + return 1 + })() + return getUniqVarName(`${varName.replace(/_([0-9]+)$/, '')}_${index}`) + } + return varName + } + + const getVarName = (varValue: string[]) => { + const existVar = varList.find(v => Array.isArray(v.value_selector) && v.value_selector.join('@@@') === varValue.join('@@@')) + if (existVar) { + return { + name: existVar.variable, + isExist: true, + } + } + const varName = varValue.slice(-1)[0] + return { + name: getUniqVarName(varName), + isExist: false, + } + } + + const handleSelectVar = (varValue: string[]) => { + const { name, isExist } = getVarName(varValue) + if (!isExist) { + const newVar: Variable = { + variable: name, + value_selector: varValue, + } + + onAddVar(newVar) + } + const editor: any = editorRef.current + const monaco: any = monacoRef.current + const position = editor?.getPosition() + + // Insert the content at the cursor position + editor?.executeEdits('', [ + { + // position.column - 1 to remove the text before the cursor + range: new monaco.Range(position.lineNumber, position.column - 1, position.lineNumber, position.column), + text: `{{ ${name} }${!isLeftBraceRef.current ? '}' : ''}`, // left brace would auto add one right brace + }, + ]) + + hideVarPicker() + } + + return ( +
+ + {isShowVarPicker && ( +
+ +
+ )} +
+ ) +} +export default React.memo(CodeEditor) diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index fd19c91bb7..e87e93100c 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -1,6 +1,7 @@ 'use client' import type { FC } from 'react' import Editor, { loader } from '@monaco-editor/react' + import React, { useRef } from 'react' import Base from '../base' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' @@ -9,8 +10,9 @@ import './style.css' // load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482 loader.config({ paths: { vs: '/vs' } }) -type Props = { +export type Props = { value?: string | object + placeholder?: string onChange?: (value: string) => void title: JSX.Element language: CodeLanguage @@ -19,6 +21,7 @@ type Props = { isJSONStringifyBeauty?: boolean height?: number isInNode?: boolean + onMount?: (editor: any, monaco: any) => void } const languageMap = { @@ -29,6 +32,7 @@ const languageMap = { const CodeEditor: FC = ({ value = '', + placeholder = '', onChange = () => { }, title, headerRight, @@ -37,6 +41,7 @@ const CodeEditor: FC = ({ isJSONStringifyBeauty, height, isInNode, + onMount, }) => { const [isFocus, setIsFocus] = React.useState(false) @@ -47,6 +52,7 @@ const CodeEditor: FC = ({ const editorRef = useRef(null) const handleEditorDidMount = (editor: any, monaco: any) => { editorRef.current = editor + editor.onDidFocusEditorText(() => { setIsFocus(true) }) @@ -71,6 +77,8 @@ const CodeEditor: FC = ({ 'editor.background': '#ffffff', }, }) + + onMount?.(editor, monaco) } const outPutValue = (() => { @@ -87,6 +95,7 @@ const CodeEditor: FC = ({ return (
= ({ }} onMount={handleEditorDidMount} /> + {!outPutValue &&
{placeholder}
}
diff --git a/web/app/components/workflow/nodes/template-transform/panel.tsx b/web/app/components/workflow/nodes/template-transform/panel.tsx index 384a9283f0..a20565b767 100644 --- a/web/app/components/workflow/nodes/template-transform/panel.tsx +++ b/web/app/components/workflow/nodes/template-transform/panel.tsx @@ -8,7 +8,7 @@ import VarList from '@/app/components/workflow/nodes/_base/components/variable/v import AddButton from '@/app/components/base/button/add-button' import Field from '@/app/components/workflow/nodes/_base/components/field' import Split from '@/app/components/workflow/nodes/_base/components/split' -import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general' import type { NodePanelProps } from '@/app/components/workflow/types' @@ -28,6 +28,7 @@ const Panel: FC> = ({ inputs, handleVarListChange, handleAddVariable, + handleAddEmptyVariable, handleCodeChange, filterVar, // single run @@ -49,7 +50,7 @@ const Panel: FC> = ({ : undefined + !readOnly ? : undefined } > > = ({ { const { nodesReadOnly: readOnly } = useNodesReadOnly() const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type] - const { inputs, setInputs } = useNodeCrud(id, payload) - const { handleVarListChange, handleAddVariable } = useVarList({ + const { inputs, setInputs: doSetInputs } = useNodeCrud(id, payload) + const inputsRef = useRef(inputs) + const setInputs = useCallback((newPayload: TemplateTransformNodeType) => { + doSetInputs(newPayload) + inputsRef.current = newPayload + }, [doSetInputs]) + + const { handleVarListChange, handleAddVariable: handleAddEmptyVariable } = useVarList({ inputs, setInputs, }) + const handleAddVariable = useCallback((payload: Variable) => { + const newInputs = produce(inputsRef.current, (draft: any) => { + draft.variables.push(payload) + }) + setInputs(newInputs) + }, [setInputs]) + useEffect(() => { if (inputs.template) return @@ -36,11 +49,11 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { }, [defaultConfig]) const handleCodeChange = useCallback((template: string) => { - const newInputs = produce(inputs, (draft: any) => { + const newInputs = produce(inputsRef.current, (draft: any) => { draft.template = template }) setInputs(newInputs) - }, [inputs, setInputs]) + }, [setInputs]) // single run const { @@ -82,6 +95,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { inputs, handleVarListChange, handleAddVariable, + handleAddEmptyVariable, handleCodeChange, filterVar, // single run diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index d6997164aa..6ea3ccd734 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -49,6 +49,7 @@ const translation = { processData: 'Process Data', input: 'Input', output: 'Output', + jinjaEditorPlaceholder: 'Type \'/\' or \'{\' to insert variable', viewOnly: 'View Only', showRunHistory: 'Show Run History', }, diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 4bb75bf3bd..6b6c408a62 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -49,6 +49,7 @@ const translation = { processData: '数据处理', input: '输入', output: '输出', + jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量', viewOnly: '只读', showRunHistory: '显示运行历史', },