From 0b6522df42951d61d6211723290d7e4bdeef0a0a Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 28 Jan 2026 14:03:58 +0800 Subject: [PATCH] refactor(web): extract split layout for variable inspect - add SplitPanel to share left/right shell and narrow menu handling - reuse InspectHeaderProps for tab header + actions across tabs - refactor variables/artifacts tabs to plug into shared split layout - align right-side header/close behavior and consolidate empty/loading flows --- .../variable-inspect/artifacts-tab.tsx | 213 ++++++------ .../variable-inspect/inspect-layout.tsx | 7 +- .../workflow/variable-inspect/panel.tsx | 49 ++- .../workflow/variable-inspect/right.tsx | 310 +++++++++--------- .../workflow/variable-inspect/split-panel.tsx | 62 ++++ .../variable-inspect/variables-tab.tsx | 68 ++-- 6 files changed, 365 insertions(+), 344 deletions(-) create mode 100644 web/app/components/workflow/variable-inspect/split-panel.tsx diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx index 8274ec3079..cc639f2bc1 100644 --- a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx +++ b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx @@ -1,10 +1,12 @@ import type { FC } from 'react' +import type { InspectHeaderProps } from './inspect-layout' import type { SandboxFileTreeNode } from '@/types/sandbox-file' import { + RiCloseLine, RiDownloadLine, RiMenuLine, } from '@remixicon/react' -import { memo, useCallback, useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import CopyFeedback from '@/app/components/base/copy-feedback' @@ -13,7 +15,8 @@ import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts-t import { useAppContext } from '@/context/app-context' import { useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file' import { cn } from '@/utils/classnames' -import { useStore } from '../store' +import InspectLayout from './inspect-layout' +import SplitPanel from './split-panel' const formatFileSize = (bytes: number | null): string => { if (bytes === null || bytes === 0) @@ -23,88 +26,10 @@ const formatFileSize = (bytes: number | null): string => { return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}` } -type ArtifactsPreviewPaneProps = { - file: SandboxFileTreeNode | null - onDownload: (node: SandboxFileTreeNode) => void - isDownloading: boolean - onOpenMenu: () => void -} - -const ArtifactsPreviewPane = memo(({ - file, - onDownload, - isDownloading, - onOpenMenu, -}) => { +const ArtifactsTab: FC = (headerProps) => { const { t } = useTranslation('workflow') - const bottomPanelWidth = useStore(s => s.bottomPanelWidth) - - if (!file) { - return ( -
-

- {t('debug.variableInspect.tabArtifacts.selectFile')} -

-
- ) - } - - const pathParts = file.path.split('/') - - return ( -
-
- {bottomPanelWidth < 488 && ( - - - - )} -
-
- {pathParts.map((part, i) => ( - - {i > 0 && /} - - {part} - - - ))} -
- - {formatFileSize(file.size)} - -
-
- - onDownload(file)} - disabled={isDownloading} - aria-label={`Download ${file.name}`} - > - - -
-
-
-
-

- {t('debug.variableInspect.tabArtifacts.previewNotAvailable')} -

-
-
-
- ) -}) - -const ArtifactsTab: FC = () => { - const { t } = useTranslation() const { userProfile } = useAppContext() const sandboxId = userProfile?.id - const bottomPanelWidth = useStore(s => s.bottomPanelWidth) const { data: treeData, hasFiles, isLoading } = useSandboxFilesTree(sandboxId, { enabled: !!sandboxId, @@ -112,7 +37,6 @@ const ArtifactsTab: FC = () => { const downloadMutation = useDownloadSandboxFile(sandboxId) const [selectedFile, setSelectedFile] = useState(null) - const [showLeftPanel, setShowLeftPanel] = useState(true) const handleFileSelect = useCallback((node: SandboxFileTreeNode) => { if (node.node_type === 'file') @@ -132,40 +56,35 @@ const ArtifactsTab: FC = () => { if (isLoading) { return ( -
- -
+ +
+ +
+
) } if (!hasFiles) { return ( -
-
-

- {t('skillSidebar.artifacts.emptyState', { ns: 'workflow' })} -

+ +
+
+

+ {t('skillSidebar.artifacts.emptyState')} +

+
-
+ ) } + const file = selectedFile + return ( -
- {bottomPanelWidth < 488 && showLeftPanel && ( -
setShowLeftPanel(false)} /> - )} -
-
+ { isDownloading={downloadMutation.isPending} />
-
-
- setShowLeftPanel(true)} - /> -
-
+ )} + > + {({ isNarrow, onOpenMenu, onClose: handleClose }) => ( + <> +
+
+ {isNarrow && ( + + + + )} + {file && ( + <> +
+
+ {file.path.split('/').map((part, i, arr) => ( + + {i > 0 && /} + + {part} + + + ))} +
+ + {formatFileSize(file.size)} + +
+
+ + handleDownload(file)} + disabled={downloadMutation.isPending} + aria-label={`Download ${file.name}`} + > + + +
+ + )} +
+ + + +
+
+ {file + ? ( +
+
+

+ {t('debug.variableInspect.tabArtifacts.previewNotAvailable')} +

+
+
+ ) + : ( +
+

+ {t('debug.variableInspect.tabArtifacts.selectFile')} +

+
+ )} +
+ + )} + ) } diff --git a/web/app/components/workflow/variable-inspect/inspect-layout.tsx b/web/app/components/workflow/variable-inspect/inspect-layout.tsx index 08200ab16d..36b95ec6b6 100644 --- a/web/app/components/workflow/variable-inspect/inspect-layout.tsx +++ b/web/app/components/workflow/variable-inspect/inspect-layout.tsx @@ -4,11 +4,14 @@ import { RiCloseLine } from '@remixicon/react' import ActionButton from '@/app/components/base/action-button' import TabHeader from './tab-header' -type InspectLayoutProps = { +export type InspectHeaderProps = { activeTab: InspectTab onTabChange: (tab: InspectTab) => void onClose: () => void headerActions?: ReactNode +} + +type InspectLayoutProps = InspectHeaderProps & { children: ReactNode } @@ -31,7 +34,7 @@ const InspectLayout: FC = ({
-
+
{children}
diff --git a/web/app/components/workflow/variable-inspect/panel.tsx b/web/app/components/workflow/variable-inspect/panel.tsx index a8878c684a..be596a1770 100644 --- a/web/app/components/workflow/variable-inspect/panel.tsx +++ b/web/app/components/workflow/variable-inspect/panel.tsx @@ -1,19 +1,15 @@ import type { FC } from 'react' -import { lazy, Suspense, useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' -import Loading from '@/app/components/base/loading' import useCurrentVars from '../hooks/use-inspect-vars-crud' import { useStore } from '../store' -import InspectLayout from './inspect-layout' +import ArtifactsTab from './artifacts-tab' import { InspectTab } from './types' import VariablesTab from './variables-tab' -const ArtifactsTab = lazy(() => import('./artifacts-tab')) - -const Panel: FC = () => { +const VariablesPanel: FC<{ onClose: () => void }> = ({ onClose }) => { const { t } = useTranslation('workflow') - const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) const [activeTab, setActiveTab] = useState(InspectTab.Variables) @@ -29,10 +25,6 @@ const Panel: FC = () => { setCurrentFocusNodeId('') }, [deleteAllInspectorVars, setCurrentFocusNodeId]) - const handleClose = useCallback(() => { - setShowVariableInspectPanel(false) - }, [setShowVariableInspectPanel]) - const headerActions = activeTab === InspectTab.Variables && !isVariablesEmpty ? ( - - )} - {isTruncated && ( - - window.open(fullContent?.download_url, '_blank')} - aria-label={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })} - > - - - - )} - {!isTruncated && currentVar.edited && ( - - - {t('debug.variableInspect.edited', { ns: 'workflow' })} - - )} - {!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && ( - - - - - - )} - {!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && ( - - - - - - )} - {currentVar.value_type !== 'secret' && ( - - )} - - )} - - - {/* content */} -
- {!currentVar && } - {isValueFetching && ( -
- + {currentNodeType + && currentNodeType !== VarInInspectType.environment + && currentNodeType !== VarInInspectType.conversation + && currentNodeType !== VarInInspectType.system + && ( + <> + +
{currentNodeTitle}
+
/
+ + )} +
{displayVarName}
+
+ {`${valueTypeLabel}${displaySchemaType}`} + {isTruncated && ( + <> + · + + {((fullContent?.size_bytes || 0) / 1024 / 1024).toFixed(1)} + MB + + + )} +
+ + )}
- )} - {currentVar && currentNodeId && !isValueFetching && ( - +
+ {currentVar && ( + <> + {canShowPromptGenerator && ( + + + + )} + {isTruncated && ( + + window.open(fullContent?.download_url, '_blank')} + aria-label={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })} + > + + + + )} + {!isTruncated && currentVar.edited && ( + + + {t('debug.variableInspect.edited', { ns: 'workflow' })} + + )} + {!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && ( + + + + + + )} + {!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && ( + + + + + + )} + {currentVar.value_type !== 'secret' && ( + + )} + + )} +
+
+ + + + +
+
+ {!currentVar && } + {isValueFetching && ( +
+ +
+ )} + {currentVar && currentNodeId && !isValueFetching && ( + + )} +
+ {isShowPromptGenerator && ( + isCodeBlock + ? ( + + ) + : ( + + ) )}
- {isShowPromptGenerator && ( - isCodeBlock - ? ( - - ) - : ( - - ) - )} - + ) } diff --git a/web/app/components/workflow/variable-inspect/split-panel.tsx b/web/app/components/workflow/variable-inspect/split-panel.tsx new file mode 100644 index 0000000000..bc2ec164c4 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/split-panel.tsx @@ -0,0 +1,62 @@ +import type { FC, ReactNode } from 'react' +import type { InspectHeaderProps } from './inspect-layout' +import { useState } from 'react' +import { cn } from '@/utils/classnames' +import { useStore } from '../store' +import TabHeader from './tab-header' + +export type SplitRightProps = { + isNarrow: boolean + onOpenMenu: () => void + onClose: () => void +} + +type SplitPanelProps = InspectHeaderProps & { + left: ReactNode + children: (rightProps: SplitRightProps) => ReactNode +} + +const SplitPanel: FC = ({ + activeTab, + onTabChange, + onClose, + headerActions, + left, + children, +}) => { + const bottomPanelWidth = useStore(s => s.bottomPanelWidth) + const isNarrow = bottomPanelWidth < 488 + const [showLeftPanel, setShowLeftPanel] = useState(true) + + return ( +
+
+
+ + {headerActions} + +
+ {isNarrow && showLeftPanel && ( +
setShowLeftPanel(false)} /> + )} +
+ {left} +
+
+
+ {children({ isNarrow, onOpenMenu: () => setShowLeftPanel(true), onClose })} +
+
+ ) +} + +export default SplitPanel diff --git a/web/app/components/workflow/variable-inspect/variables-tab.tsx b/web/app/components/workflow/variable-inspect/variables-tab.tsx index 80e5c7f0ec..f71c379af1 100644 --- a/web/app/components/workflow/variable-inspect/variables-tab.tsx +++ b/web/app/components/workflow/variable-inspect/variables-tab.tsx @@ -1,17 +1,19 @@ import type { FC } from 'react' import type { NodeProps } from '../types' +import type { InspectHeaderProps } from './inspect-layout' import type { VarInInspect } from '@/types/workflow' import { useCallback, useEffect, useMemo, useState } from 'react' import { useEventEmitterContextContext } from '@/context/event-emitter' import { VarInInspectType } from '@/types/workflow' -import { cn } from '@/utils/classnames' import useCurrentVars from '../hooks/use-inspect-vars-crud' import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type' import { useStore } from '../store' import Empty from './empty' +import InspectLayout from './inspect-layout' import Left from './left' import Listening from './listening' import Right from './right' +import SplitPanel from './split-panel' import { EVENT_WORKFLOW_STOP } from './types' import { toEnvVarInInspect } from './utils' @@ -24,11 +26,8 @@ export type currentVarType = { nodeData?: NodeProps['data'] } -const VariablesTab: FC = () => { - const bottomPanelWidth = useStore(s => s.bottomPanelWidth) - const [showLeftPanel, setShowLeftPanel] = useState(true) +const VariablesTab: FC = (headerProps) => { const isListening = useStore(s => s.isListening) - const environmentVariables = useStore(s => s.environmentVariables) const currentFocusNodeId = useStore(s => s.currentFocusNodeId) const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) @@ -42,8 +41,7 @@ const VariablesTab: FC = () => { } = useCurrentVars() const isEmpty = useMemo(() => { - const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars] - return allVars.length === 0 + return [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars].length === 0 }, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) const currentNodeInfo = useMemo(() => { @@ -128,7 +126,7 @@ const VariablesTab: FC = () => { const { isLoading, schemaTypeDefinitions } = useMatchSchemaType() const { eventEmitter } = useEventEmitterContextContext() - const handleStopListening = useCallback(() => { + const onStopListening = useCallback(() => { // eslint-disable-next-line ts/no-explicit-any -- EventEmitter is typed as string but project-wide convention passes { type } objects eventEmitter?.emit({ type: EVENT_WORKFLOW_STOP } as any) }, [eventEmitter]) @@ -143,49 +141,39 @@ const VariablesTab: FC = () => { if (isListening) { return ( -
- -
+ +
+
) } if (isEmpty) { return ( -
- -
+ +
+
) } return ( -
- {bottomPanelWidth < 488 && showLeftPanel &&
setShowLeftPanel(false)}>
} -
-
- -
-
-
- setShowLeftPanel(true)} + handleVarSelect={handleNodeVarSelect} /> -
-
+ )} + > + {rightProps => ( + + )} + ) }