From b1977eb920a6ae0ebe8fff3f1be05aa0947ca479 Mon Sep 17 00:00:00 2001 From: jZonG Date: Fri, 18 Apr 2025 10:46:26 +0800 Subject: [PATCH] right panel resize --- .../components/workflow/hooks/use-workflow.ts | 5 - web/app/components/workflow/index.tsx | 33 ++++ .../_base/components/workflow-panel/index.tsx | 67 +++++--- .../workflow/operator/add-block.tsx | 2 +- .../components/workflow/operator/control.tsx | 6 +- .../components/workflow/operator/index.tsx | 72 ++++++--- .../panel/debug-and-preview/index.tsx | 151 +++++++++--------- web/app/components/workflow/panel/index.tsx | 121 +++++++++----- .../workflow/panel/workflow-preview.tsx | 2 +- .../workflow/store/workflow/index.ts | 6 +- .../workflow/store/workflow/layout-slice.ts | 36 +++++ 11 files changed, 337 insertions(+), 164 deletions(-) create mode 100644 web/app/components/workflow/store/workflow/layout-slice.ts diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 7a15afa2e4..5e8c21f8d7 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -76,10 +76,6 @@ export const useWorkflow = () => { const appId = useStore(s => s.appId) const nodesExtraData = useNodesExtraData() const { data: workflowConfig } = useWorkflowConfig(appId) - const setPanelWidth = useCallback((width: number) => { - localStorage.setItem('workflow-node-panel-width', `${width}`) - workflowStore.setState({ panelWidth: width }) - }, [workflowStore]) const getTreeLeafNodes = useCallback((nodeId: string) => { const { @@ -419,7 +415,6 @@ export const useWorkflow = () => { }, [store]) return { - setPanelWidth, getTreeLeafNodes, getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent, diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 4c48afb56c..4114793435 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -64,6 +64,7 @@ import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants' import CustomSimpleNode from './simple-node' import { CUSTOM_SIMPLE_NODE } from './simple-node/constants' import Operator from './operator' +import Control from './operator/control' import CustomEdge from './custom-edge' import CustomConnectionLine from './custom-connection-line' import Panel from './panel' @@ -135,6 +136,32 @@ const Workflow: FC = memo(({ const nodeAnimation = useStore(s => s.nodeAnimation) const showConfirm = useStore(s => s.showConfirm) const showImportDSLModal = useStore(s => s.showImportDSLModal) + const workflowCanvasHeight = useStore(s => s.workflowCanvasHeight) + const bottomPanelHeight = useStore(s => s.bottomPanelHeight) + const setWorkflowCanvasWidth = useStore(s => s.setWorkflowCanvasWidth) + const setWorkflowCanvasHeight = useStore(s => s.setWorkflowCanvasHeight) + const controlHeight = useMemo(() => { + if (!workflowCanvasHeight) + return '100%' + return workflowCanvasHeight - bottomPanelHeight + }, [workflowCanvasHeight, bottomPanelHeight]) + + // update workflow Canvas width and height + useEffect(() => { + if (workflowContainerRef.current) { + const resizeContainerObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize, blockSize } = entry.borderBoxSize[0] + setWorkflowCanvasWidth(inlineSize) + setWorkflowCanvasHeight(blockSize) + } + }) + resizeContainerObserver.observe(workflowContainerRef.current) + return () => { + resizeContainerObserver.disconnect() + } + } + }, [setWorkflowCanvasHeight, setWorkflowCanvasWidth]) const { setShowConfirm, @@ -299,6 +326,12 @@ const Workflow: FC = memo(({
+
+ +
{ showFeaturesPanel && diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index f6f431a46f..683566d5cc 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -8,6 +8,8 @@ import { useCallback, useRef, useState, + useEffect, + useMemo, } from 'react' import { RiCloseLine, @@ -36,7 +38,6 @@ import { useNodesReadOnly, useNodesSyncDraft, useToolIcon, - useWorkflow, useWorkflowHistory, } from '@/app/components/workflow/hooks' import { @@ -54,6 +55,7 @@ import useOneStepRun from '../../hooks/use-one-step-run' import type { PanelExposedType } from '@/types/workflow' import BeforeRunForm from '../before-run-form' import { sleep } from '@/utils' +import { debounce } from 'lodash-es' type BasePanelProps = { children: ReactNode @@ -69,19 +71,30 @@ const BasePanel: FC = ({ showMessageLogModal: state.showMessageLogModal, }))) const showSingleRunPanel = useStore(s => s.showSingleRunPanel) - const panelWidth = localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420 - const { - setPanelWidth, - } = useWorkflow() - const { handleNodeSelect } = useNodesInteractions() - const { handleSyncWorkflowDraft } = useNodesSyncDraft() - const { nodesReadOnly } = useNodesReadOnly() - const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop) - const toolIcon = useToolIcon(data) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const nodePanelWidth = useStore(s => s.nodePanelWidth) + const otherPanelWidth = useStore(s => s.otherPanelWidth) + const setNodePanelWidth = useStore(s => s.setNodePanelWidth) + + const maxNodePanelWidth = useMemo(() => { + if (!workflowCanvasWidth) + return 720 + if (!otherPanelWidth) + return workflowCanvasWidth - 400 + + return workflowCanvasWidth - otherPanelWidth - 400 + }, [workflowCanvasWidth, otherPanelWidth]) + + const updateNodePanelWidth = useCallback((width: number) => { + // Ensure the width is within the min and max range + const newValue = Math.min(Math.max(width, 400), maxNodePanelWidth) + localStorage.setItem('workflow-node-panel-width', `${newValue}`) + setNodePanelWidth(newValue) + }, [maxNodePanelWidth, setNodePanelWidth]) const handleResize = useCallback((width: number) => { - setPanelWidth(width) - }, [setPanelWidth]) + updateNodePanelWidth(width) + }, [updateNodePanelWidth]) const { triggerRef, @@ -89,15 +102,28 @@ const BasePanel: FC = ({ } = useResizePanel({ direction: 'horizontal', triggerDirection: 'left', - minWidth: 420, - maxWidth: 720, - onResize: handleResize, + minWidth: 400, + maxWidth: maxNodePanelWidth, + onResize: debounce(handleResize), }) + const debounceUpdate = debounce(updateNodePanelWidth) + useEffect(() => { + if (!workflowCanvasWidth) + return + if (workflowCanvasWidth - 400 <= nodePanelWidth + otherPanelWidth) + debounceUpdate(workflowCanvasWidth - 400 - otherPanelWidth) + }, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, updateNodePanelWidth]) + + const { handleNodeSelect } = useNodesInteractions() + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const { nodesReadOnly } = useNodesReadOnly() + const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop) + const toolIcon = useToolIcon(data) + const { saveStateToHistory } = useWorkflowHistory() const { - handleNodeDataUpdate, handleNodeDataUpdateWithSyncDraft, } = useNodeDataUpdate() @@ -117,7 +143,6 @@ const BasePanel: FC = ({ isShowSingleRun, showSingleRun, hideSingleRun, - toVarInputs, runningStatus, handleRun, handleStop, @@ -144,19 +169,19 @@ const BasePanel: FC = ({ return (
-
+ className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'> +
diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index d35a5be8b4..5bc541a45a 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -96,7 +96,7 @@ const AddBlock = ({ onOpenChange={handleOpenChange} disabled={nodesReadOnly} onSelect={handleSelect} - placement='top-start' + placement='right-start' offset={offset ?? { mainAxis: 4, crossAxis: -8, diff --git a/web/app/components/workflow/operator/control.tsx b/web/app/components/workflow/operator/control.tsx index 5f7d19a17f..81ed7a07fa 100644 --- a/web/app/components/workflow/operator/control.tsx +++ b/web/app/components/workflow/operator/control.tsx @@ -45,7 +45,7 @@ const Control = () => { } return ( -
+
{
- +
{
- +
void @@ -10,25 +10,61 @@ export type OperatorProps = { } const Operator = ({ handleUndo, handleRedo }: OperatorProps) => { + const bottomPanelRef = useRef(null) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const rightPanelWidth = useStore(s => s.rightPanelWidth) + const setBottomPanelHeight = useStore(s => s.setBottomPanelHeight) + + const bottomPanelWidth = useMemo(() => { + if (!workflowCanvasWidth || !rightPanelWidth) + return 'auto' + return Math.max((workflowCanvasWidth - rightPanelWidth), 400) + }, [workflowCanvasWidth, rightPanelWidth]) + + // update bottom panel height + useEffect(() => { + if (bottomPanelRef.current) { + const resizeContainerObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { blockSize } = entry.borderBoxSize[0] + setBottomPanelHeight(blockSize) + } + }) + resizeContainerObserver.observe(bottomPanelRef.current) + return () => { + resizeContainerObserver.disconnect() + } + } + }, [setBottomPanelHeight]) + return ( - <> - -
- +
+
- +
+ + +
- +
) } diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index 53c91299a2..9ee784858e 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, - useEffect, + useMemo, useRef, useState, } from 'react' @@ -16,6 +16,7 @@ import { } from '../../hooks' import { BlockEnum } from '../../types' import type { StartNodeType } from '../../nodes/start/types' +import { useResizePanel } from '../../nodes/_base/hooks/use-resize-panel' import ChatWrapper from './chat-wrapper' import cn from '@/utils/classnames' import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' @@ -23,7 +24,7 @@ import { BubbleX } from '@/app/components/base/icons/src/vender/line/others' import Tooltip from '@/app/components/base/tooltip' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' import { useStore } from '@/app/components/workflow/store' -import { noop } from 'lodash-es' +import { debounce, noop } from 'lodash-es' export type ChatWrapperRefType = { handleRestart: () => void @@ -37,6 +38,7 @@ const DebugAndPreview = () => { const varList = useStore(s => s.conversationVariables) const [expanded, setExpanded] = useState(true) const nodes = useNodes() + const selectedNode = nodes.find(node => node.data.selected) const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const variables = startNode?.data.variables || [] @@ -54,94 +56,95 @@ const DebugAndPreview = () => { exactMatch: true, }) - const [panelWidth, setPanelWidth] = useState(420) - const [isResizing, setIsResizing] = useState(false) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const nodePanelWidth = useStore(s => s.nodePanelWidth) + const [panelWidth, setPanelWidth] = useState(400) + const handleResize = useCallback((width: number) => { + setPanelWidth(width) + }, [setPanelWidth]) + const maxPanelWidth = useMemo(() => { + if (!workflowCanvasWidth) + return 720 - const startResizing = useCallback((e: React.MouseEvent) => { - e.preventDefault() - setIsResizing(true) - }, []) + if (!selectedNode) + return workflowCanvasWidth - 400 - const stopResizing = useCallback(() => { - setIsResizing(false) - }, []) - - const resize = useCallback((e: MouseEvent) => { - if (isResizing) { - const newWidth = window.innerWidth - e.clientX - if (newWidth > 420 && newWidth < 1024) - setPanelWidth(newWidth) - } - }, [isResizing]) - - useEffect(() => { - window.addEventListener('mousemove', resize) - window.addEventListener('mouseup', stopResizing) - return () => { - window.removeEventListener('mousemove', resize) - window.removeEventListener('mouseup', stopResizing) - } - }, [resize, stopResizing]) + return workflowCanvasWidth - 400 - 400 + }, [workflowCanvasWidth, selectedNode, nodePanelWidth]) + const { + triggerRef, + containerRef, + } = useResizePanel({ + direction: 'horizontal', + triggerDirection: 'left', + minWidth: 400, + maxWidth: maxPanelWidth, + onResize: debounce(handleResize), + }) return ( -
+
-
-
{t('workflow.common.debugAndPreview').toLocaleUpperCase()}
-
- - handleRestartChat()}> - - - - {varList.length > 0 && ( + ref={triggerRef} + className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'> +
+
+
+
+
{t('workflow.common.debugAndPreview').toLocaleUpperCase()}
+
- setShowConversationVariableModal(true)}> - + handleRestartChat()}> + - )} - {variables.length > 0 && ( -
+ {varList.length > 0 && ( - setExpanded(!expanded)}> - + setShowConversationVariableModal(true)}> + - {expanded &&
} + )} + {variables.length > 0 && ( +
+ + setExpanded(!expanded)}> + + + + {expanded &&
} +
+ )} +
+
+
- )} -
-
-
-
-
- setShowConversationVariableModal(false)} - showInputsFieldsPanel={expanded} - onHide={() => setExpanded(false)} - /> +
+ setShowConversationVariableModal(false)} + showInputsFieldsPanel={expanded} + onHide={() => setExpanded(false)} + /> +
) diff --git a/web/app/components/workflow/panel/index.tsx b/web/app/components/workflow/panel/index.tsx index 40920ab256..b5bbe16bf1 100644 --- a/web/app/components/workflow/panel/index.tsx +++ b/web/app/components/workflow/panel/index.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { memo } from 'react' +import { memo, useEffect, useRef } from 'react' import { useNodes } from 'reactflow' import { useShallow } from 'zustand/react/shallow' import type { CommonNodeType } from '../types' @@ -39,8 +39,46 @@ const Panel: FC = () => { currentLogModalActiveTab: state.currentLogModalActiveTab, }))) + const rightPanelRef = useRef(null) + const setRightPanelWidth = useStore(s => s.setRightPanelWidth) + + // get right panel width + useEffect(() => { + if (rightPanelRef.current) { + const resizeRightPanelObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize } = entry.borderBoxSize[0] + setRightPanelWidth(inlineSize) + } + }) + resizeRightPanelObserver.observe(rightPanelRef.current) + return () => { + resizeRightPanelObserver.disconnect() + } + } + }, [setRightPanelWidth]) + + const otherPanelRef = useRef(null) + const setOtherPanelWidth = useStore(s => s.setOtherPanelWidth) + + // get other panel width + useEffect(() => { + if (otherPanelRef.current) { + const resizeOtherPanelObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize } = entry.borderBoxSize[0] + setOtherPanelWidth(inlineSize) + } + }) + resizeOtherPanelObserver.observe(otherPanelRef.current) + return () => { + resizeOtherPanelObserver.disconnect() + } + } + }, [setOtherPanelWidth]) return (
{ ) } - { - historyWorkflowData && !isChatMode && ( - - ) - } - { - historyWorkflowData && isChatMode && ( - - ) - } - { - showDebugAndPreviewPanel && isChatMode && ( +
+ {showDebugAndPreviewPanel && isChatMode && ( - ) - } - { - showDebugAndPreviewPanel && !isChatMode && ( - - ) - } - { - showEnvPanel && ( - - ) - } - { - showChatVariablePanel && ( - - ) - } - { - showGlobalVariablePanel && ( - - ) - } - { - showWorkflowVersionHistoryPanel && ( - - ) - } + )} + { + historyWorkflowData && !isChatMode && ( + + ) + } + { + historyWorkflowData && isChatMode && ( + + ) + } + { + showDebugAndPreviewPanel && !isChatMode && ( + + ) + } + { + showEnvPanel && ( + + ) + } + { + showChatVariablePanel && ( + + ) + } + { + showGlobalVariablePanel && ( + + ) + } + { + showWorkflowVersionHistoryPanel && ( + + ) + } +
) } diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index 228a376535..3478581310 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -49,7 +49,7 @@ const WorkflowPreview = () => { return (
{`Test Run${!workflowRunningData?.result.sequence_number ? '' : `#${workflowRunningData?.result.sequence_number}`}`} diff --git a/web/app/components/workflow/store/workflow/index.ts b/web/app/components/workflow/store/workflow/index.ts index 38eeb67129..f5b87a838c 100644 --- a/web/app/components/workflow/store/workflow/index.ts +++ b/web/app/components/workflow/store/workflow/index.ts @@ -31,6 +31,8 @@ import type { CurrentVarsSliceShape } from './current-vars-slice' import { createCurrentVarsSlice } from './current-vars-slice' import { WorkflowContext } from '@/app/components/workflow/context' +import type { LayoutSliceShape } from './layout-slice' +import { createLayoutSlice } from './layout-slice' export type Shape = ChatVariableSliceShape & @@ -45,7 +47,8 @@ export type Shape = WorkflowDraftSliceShape & WorkflowSliceShape & LastRunSliceShape & - CurrentVarsSliceShape + CurrentVarsSliceShape & + LayoutSliceShape export const createWorkflowStore = () => { return createStore((...args) => ({ @@ -62,6 +65,7 @@ export const createWorkflowStore = () => { ...createWorkflowSlice(...args), ...createLastRunSlice(...args), ...createCurrentVarsSlice(...args), + ...createLayoutSlice(...args), })) } diff --git a/web/app/components/workflow/store/workflow/layout-slice.ts b/web/app/components/workflow/store/workflow/layout-slice.ts new file mode 100644 index 0000000000..ea691670ae --- /dev/null +++ b/web/app/components/workflow/store/workflow/layout-slice.ts @@ -0,0 +1,36 @@ +import type { StateCreator } from 'zustand' + +export type LayoutSliceShape = { + workflowCanvasWidth?: number + workflowCanvasHeight?: number + setWorkflowCanvasWidth: (width: number) => void + setWorkflowCanvasHeight: (height: number) => void + // rightPanelWidth - otherPanelWidth = nodePanelWidth + rightPanelWidth?: number + setRightPanelWidth: (width: number) => void + nodePanelWidth: number + setNodePanelWidth: (width: number) => void + otherPanelWidth: number + setOtherPanelWidth: (width: number) => void + bottomPanelWidth: number // min-width = 400px; default-width = auto || 480px; + setBottomPanelWidth: (width: number) => void + bottomPanelHeight: number // min-height = 120px; max-height = 480px; default-height = 320px; + setBottomPanelHeight: (height: number) => void +} + +export const createLayoutSlice: StateCreator = set => ({ + workflowCanvasWidth: undefined, + workflowCanvasHeight: undefined, + setWorkflowCanvasWidth: width => set(() => ({ workflowCanvasWidth: width })), + setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })), + rightPanelWidth: undefined, + setRightPanelWidth: width => set(() => ({ rightPanelWidth: width })), + nodePanelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 400, + setNodePanelWidth: width => set(() => ({ nodePanelWidth: width })), + otherPanelWidth: 400, + setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })), + bottomPanelWidth: 480, + setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })), + bottomPanelHeight: 320, + setBottomPanelHeight: height => set(() => ({ bottomPanelHeight: height })), +})