From d098e72c1305303cc167f7ef47ab17d69e927e0a Mon Sep 17 00:00:00 2001 From: yyh Date: Tue, 27 Jan 2026 15:05:11 +0800 Subject: [PATCH] feat(variable-inspect): add Artifacts tab with sandbox file tree browser Refactor the variable inspect panel into a tabbed layout with Variables and Artifacts tabs. Extract variable logic into VariablesTab, add new ArtifactsTab with sandbox file tree selection and preview pane, and improve accessibility across tree nodes and interactive elements. --- .../skill/file-tree/artifacts-tree.tsx | 49 ++-- .../variable-inspect/artifacts-tab.tsx | 192 ++++++++++++++ .../workflow/variable-inspect/group.tsx | 11 +- .../workflow/variable-inspect/left.tsx | 19 +- .../workflow/variable-inspect/panel.tsx | 250 ++++-------------- .../workflow/variable-inspect/right.tsx | 32 +-- .../workflow/variable-inspect/types.ts | 5 + .../variable-inspect/variables-tab.tsx | 190 +++++++++++++ web/eslint-suppressions.json | 5 - web/i18n/en-US/workflow.json | 4 + web/i18n/zh-Hans/workflow.json | 4 + 11 files changed, 503 insertions(+), 258 deletions(-) create mode 100644 web/app/components/workflow/variable-inspect/artifacts-tab.tsx create mode 100644 web/app/components/workflow/variable-inspect/variables-tab.tsx diff --git a/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx b/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx index 3b8aa7f892..dad85c0814 100644 --- a/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx +++ b/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx @@ -16,6 +16,8 @@ const INDENT_SIZE = 20 type ArtifactsTreeProps = { data: SandboxFileTreeNode[] | undefined onDownload: (node: SandboxFileTreeNode) => void + onSelect?: (node: SandboxFileTreeNode) => void + selectedPath?: string isDownloading?: boolean } @@ -23,6 +25,8 @@ type ArtifactsTreeNodeProps = { node: SandboxFileTreeNode depth: number onDownload: (node: SandboxFileTreeNode) => void + onSelect?: (node: SandboxFileTreeNode) => void + selectedPath?: string isDownloading?: boolean } @@ -30,16 +34,24 @@ const ArtifactsTreeNode: FC = ({ node, depth, onDownload, + onSelect, + selectedPath, isDownloading, }) => { const [isExpanded, setIsExpanded] = useState(false) const isFolder = node.node_type === 'folder' const hasChildren = isFolder && node.children.length > 0 - const handleToggle = useCallback(() => { - if (isFolder) + const isSelected = !isFolder && selectedPath === node.path + + const handleClick = useCallback(() => { + if (isFolder) { setIsExpanded(prev => !prev) - }, [isFolder]) + } + else { + onSelect?.(node) + } + }, [isFolder, node, onSelect]) const handleDownload = useCallback((e: React.MouseEvent) => { e.stopPropagation() @@ -51,21 +63,20 @@ const ArtifactsTreeNode: FC = ({ return (
{ - if (e.key === 'Enter' || e.key === ' ') - handleToggle() - } - : undefined} + aria-selected={isSelected} + onClick={handleClick} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') + handleClick() + }} className={cn( - 'group relative flex h-6 items-center rounded-md px-2', - isFolder && 'cursor-pointer hover:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-components-input-border-active', - !isFolder && 'hover:bg-state-base-hover', + 'group relative flex h-6 cursor-pointer items-center rounded-md px-2', + 'hover:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-components-input-border-active', + isSelected && 'bg-state-base-hover', )} style={{ paddingLeft: `${8 + depth * INDENT_SIZE}px` }} > @@ -110,6 +121,8 @@ const ArtifactsTreeNode: FC = ({ node={child} depth={depth + 1} onDownload={onDownload} + onSelect={onSelect} + selectedPath={selectedPath} isDownloading={isDownloading} /> ))} @@ -122,6 +135,8 @@ const ArtifactsTreeNode: FC = ({ const ArtifactsTree: FC = ({ data, onDownload, + onSelect, + selectedPath, isDownloading, }) => { if (!data || data.length === 0) @@ -135,6 +150,8 @@ const ArtifactsTree: FC = ({ node={node} depth={0} onDownload={onDownload} + onSelect={onSelect} + selectedPath={selectedPath} isDownloading={isDownloading} /> ))} diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx new file mode 100644 index 0000000000..b60becf1c5 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx @@ -0,0 +1,192 @@ +import type { FC } from 'react' +import type { SandboxFileTreeNode } from '@/types/sandbox-file' +import { + RiDownloadLine, + RiMenuLine, +} from '@remixicon/react' +import { memo, 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' +import Loading from '@/app/components/base/loading' +import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts-tree' +import { useAppContext } from '@/context/app-context' +import { useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file' +import { cn } from '@/utils/classnames' +import { useStore } from '../store' + +const formatFileSize = (bytes: number | null): string => { + if (bytes === null || bytes === 0) + return '0 B' + const units = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + 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 { 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, + }) + const downloadMutation = useDownloadSandboxFile(sandboxId) + + const [selectedFile, setSelectedFile] = useState(null) + const [showLeftPanel, setShowLeftPanel] = useState(true) + + const handleFileSelect = useCallback((node: SandboxFileTreeNode) => { + if (node.node_type === 'file') + setSelectedFile(node) + }, []) + + const { mutateAsync: downloadFile } = downloadMutation + const handleDownload = useCallback(async (node: SandboxFileTreeNode) => { + try { + const ticket = await downloadFile(node.path) + window.open(ticket.download_url, '_blank') + } + catch (error) { + console.error('Download failed:', error) + } + }, [downloadFile]) + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (!hasFiles) { + return ( +
+
+

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

+
+
+ ) + } + + return ( +
+ {bottomPanelWidth < 488 && showLeftPanel && ( +
setShowLeftPanel(false)} /> + )} +
+
+
+ +
+
+
+
+ setShowLeftPanel(true)} + /> +
+
+ ) +} + +export default ArtifactsTab diff --git a/web/app/components/workflow/variable-inspect/group.tsx b/web/app/components/workflow/variable-inspect/group.tsx index 23693ac1d8..d02f20aa39 100644 --- a/web/app/components/workflow/variable-inspect/group.tsx +++ b/web/app/components/workflow/variable-inspect/group.tsx @@ -1,4 +1,4 @@ -import type { currentVarType } from './panel' +import type { currentVarType } from './variables-tab' import type { NodeWithVar, VarInInspect } from '@/types/workflow' import { RiArrowRightSLine, @@ -107,7 +107,7 @@ const Group = ({ )} {(!nodeData || !nodeData.isSingRunRunning) && visibleVarList.length > 0 && ( - setIsCollapsed(!isCollapsed)} /> +
setIsCollapsed(!isCollapsed)}> @@ -152,10 +152,11 @@ const Group = ({ const isAgentAliasVar = typeof varItem.name === 'string' && varItem.name.startsWith('@') const displayName = isAgentAliasVar ? varItem.name.slice(1) : varItem.name return ( -
handleSelectVar(varItem, varType)} @@ -171,7 +172,7 @@ const Group = ({ )}
{displayName}
{formatVarTypeLabel(varItem.value_type)}
-
+ ) })}
diff --git a/web/app/components/workflow/variable-inspect/left.tsx b/web/app/components/workflow/variable-inspect/left.tsx index b5f49ffcca..e7bd7d16b7 100644 --- a/web/app/components/workflow/variable-inspect/left.tsx +++ b/web/app/components/workflow/variable-inspect/left.tsx @@ -1,9 +1,6 @@ -import type { currentVarType } from './panel' +import type { currentVarType } from './variables-tab' import type { VarInInspect } from '@/types/workflow' -// import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import Button from '@/app/components/base/button' import { VarInInspectType } from '@/types/workflow' import { cn } from '@/utils/classnames' import useCurrentVars from '../hooks/use-inspect-vars-crud' @@ -22,8 +19,6 @@ const Left = ({ currentNodeVar, handleVarSelect, }: Props) => { - const { t } = useTranslation() - const environmentVariables = useStore(s => s.environmentVariables) const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) @@ -31,7 +26,6 @@ const Left = ({ conversationVars, systemVars, nodesWithInspectVars, - deleteAllInspectorVars, deleteNodeInspectorVars, } = useCurrentVars() const { handleNodeSelect } = useNodesInteractions() @@ -40,11 +34,6 @@ const Left = ({ const showDivider = environmentVariables.length > 0 || conversationVars.length > 0 || systemVars.length > 0 - const handleClearAll = () => { - deleteAllInspectorVars() - setCurrentFocusNodeId('') - } - const handleClearNode = (nodeId: string) => { deleteNodeInspectorVars(nodeId) setCurrentFocusNodeId('') @@ -52,12 +41,6 @@ const Left = ({ return (
- {/* header */} -
-
{t('debug.variableInspect.title', { ns: 'workflow' })}
- -
- {/* content */}
{/* group ENV */} {environmentVariables.length > 0 && ( diff --git a/web/app/components/workflow/variable-inspect/panel.tsx b/web/app/components/workflow/variable-inspect/panel.tsx index f3b67b1ccd..b2c3aafd87 100644 --- a/web/app/components/workflow/variable-inspect/panel.tsx +++ b/web/app/components/workflow/variable-inspect/panel.tsx @@ -1,217 +1,83 @@ import type { FC } from 'react' -import type { NodeProps } from '../types' -import type { VarInInspect } from '@/types/workflow' import { RiCloseLine, } from '@remixicon/react' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { lazy, Suspense, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' -import { useEventEmitterContextContext } from '@/context/event-emitter' -import { VarInInspectType } from '@/types/workflow' +import Button from '@/app/components/base/button' +import Loading from '@/app/components/base/loading' 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 Left from './left' -import Listening from './listening' -import Right from './right' -import { toEnvVarInInspect } from './utils' +import { InspectTab } from './types' +import VariablesTab from './variables-tab' -export type currentVarType = { - nodeId: string - nodeType: string - title: string - isValueFetched?: boolean - var?: VarInInspect - nodeData?: NodeProps['data'] -} +const ArtifactsTab = lazy(() => import('./artifacts-tab')) + +const TAB_ITEMS = [ + { value: InspectTab.Variables, labelKey: 'debug.variableInspect.tab.variables' }, + { value: InspectTab.Artifacts, labelKey: 'debug.variableInspect.tab.artifacts' }, +] as const const Panel: FC = () => { - const { t } = useTranslation() - - const bottomPanelWidth = useStore(s => s.bottomPanelWidth) + const { t } = useTranslation('workflow') const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) - const [showLeftPanel, setShowLeftPanel] = useState(true) - const isListening = useStore(s => s.isListening) + const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) + const [activeTab, setActiveTab] = useState(InspectTab.Variables) const environmentVariables = useStore(s => s.environmentVariables) - const currentFocusNodeId = useStore(s => s.currentFocusNodeId) - const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) - const [currentVarId, setCurrentVarId] = useState('') + const { conversationVars, systemVars, nodesWithInspectVars, deleteAllInspectorVars } = useCurrentVars() - const { - conversationVars, - systemVars, - nodesWithInspectVars, - fetchInspectVarValue, - } = useCurrentVars() - - const isEmpty = useMemo(() => { - const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars] - return allVars.length === 0 + const isVariablesEmpty = useMemo(() => { + return [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars].length === 0 }, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) - const currentNodeInfo = useMemo(() => { - if (!currentFocusNodeId) - return - if (currentFocusNodeId === VarInInspectType.environment) { - const currentVar = environmentVariables.find(v => v.id === currentVarId) - return { - nodeId: VarInInspectType.environment, - title: VarInInspectType.environment, - nodeType: VarInInspectType.environment, - var: currentVar ? toEnvVarInInspect(currentVar) : undefined, - } - } - if (currentFocusNodeId === VarInInspectType.conversation) { - const currentVar = conversationVars.find(v => v.id === currentVarId) - const res = { - nodeId: VarInInspectType.conversation, - title: VarInInspectType.conversation, - nodeType: VarInInspectType.conversation, - var: currentVar - ? { - ...currentVar, - type: VarInInspectType.conversation, - } - : undefined, - } - return res - } - if (currentFocusNodeId === VarInInspectType.system) { - const currentVar = systemVars.find(v => v.id === currentVarId) - const res = { - nodeId: VarInInspectType.system, - title: VarInInspectType.system, - nodeType: VarInInspectType.system, - var: currentVar - ? { - ...currentVar, - type: VarInInspectType.system, - } - : undefined, - } - return res - } - const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) - if (!targetNode) - return - const currentVar = targetNode.vars.find(v => v.id === currentVarId) - return { - nodeId: targetNode.nodeId, - nodeType: targetNode.nodeType, - title: targetNode.title, - isSingRunRunning: targetNode.isSingRunRunning, - isValueFetched: targetNode.isValueFetched, - nodeData: targetNode.nodePayload, - var: currentVar, - } - }, [currentFocusNodeId, currentVarId, environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + const handleClear = useCallback(() => { + deleteAllInspectorVars() + setCurrentFocusNodeId('') + }, [deleteAllInspectorVars, setCurrentFocusNodeId]) - const currentAliasMeta = useMemo(() => { - if (!currentFocusNodeId || !currentVarId) - return undefined - const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) - const targetVar = targetNode?.vars.find(v => v.id === currentVarId) - return targetVar?.aliasMeta - }, [currentFocusNodeId, currentVarId, nodesWithInspectVars]) - const fetchNodeId = currentAliasMeta?.extractorNodeId || currentFocusNodeId - - const isCurrentNodeVarValueFetching = useMemo(() => { - if (!fetchNodeId) - return false - const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) - if (!targetNode) - return false - return !targetNode.isValueFetched - }, [fetchNodeId, nodesWithInspectVars]) - - const handleNodeVarSelect = useCallback((node: currentVarType) => { - setCurrentFocusNodeId(node.nodeId) - if (node.var) - setCurrentVarId(node.var.id) - }, [setCurrentFocusNodeId, setCurrentVarId]) - - const { isLoading, schemaTypeDefinitions } = useMatchSchemaType() - const { eventEmitter } = useEventEmitterContextContext() - - const handleStopListening = useCallback(() => { - eventEmitter?.emit({ type: EVENT_WORKFLOW_STOP } as any) - }, [eventEmitter]) - - useEffect(() => { - if (currentFocusNodeId && currentVarId && !isLoading && fetchNodeId) { - const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) - if (targetNode && !targetNode.isValueFetched) - fetchInspectVarValue([fetchNodeId], schemaTypeDefinitions!) - } - }, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading, fetchNodeId]) - - if (isListening) { - return ( -
-
-
{t('debug.variableInspect.title', { ns: 'workflow' })}
- setShowVariableInspectPanel(false)}> - - -
-
- -
-
- ) - } - - if (isEmpty) { - return ( -
-
-
{t('debug.variableInspect.title', { ns: 'workflow' })}
- setShowVariableInspectPanel(false)}> - - -
-
- -
-
- ) - } + const handleClose = useCallback(() => { + setShowVariableInspectPanel(false) + }, [setShowVariableInspectPanel]) return ( -
- {/* left */} - {bottomPanelWidth < 488 && showLeftPanel &&
setShowLeftPanel(false)}>
} -
- +
+
+
+ {TAB_ITEMS.map(tab => ( + + ))} + {activeTab === InspectTab.Variables && !isVariablesEmpty && ( + + )} +
+ + +
- {/* right */} -
- setShowLeftPanel(true)} - /> +
+ {activeTab === InspectTab.Variables && } + {activeTab === InspectTab.Artifacts && ( +
}> + + + )}
) diff --git a/web/app/components/workflow/variable-inspect/right.tsx b/web/app/components/workflow/variable-inspect/right.tsx index b20349ade2..4ff3e67c11 100644 --- a/web/app/components/workflow/variable-inspect/right.tsx +++ b/web/app/components/workflow/variable-inspect/right.tsx @@ -1,8 +1,7 @@ -import type { currentVarType } from './panel' +import type { currentVarType } from './variables-tab' import type { GenRes } from '@/service/debug' import { RiArrowGoBackLine, - RiCloseLine, RiFileDownloadFill, RiMenuLine, RiSparklingFill, @@ -52,8 +51,6 @@ const Right = ({ }: Props) => { const { t } = useTranslation() const bottomPanelWidth = useStore(s => s.bottomPanelWidth) - const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) - const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) const toolIcon = useToolIcon(currentNodeVar?.nodeData) const currentVar = currentNodeVar?.var const currentNodeType = currentNodeVar?.nodeType @@ -82,11 +79,6 @@ const Right = ({ resetToLastRunVar(currentNodeVar.nodeId, currentVar.id) } - const handleClose = () => { - setShowVariableInspectPanel(false) - setCurrentFocusNodeId('') - } - const handleClear = () => { if (!currentNodeVar || !currentVar) return @@ -179,7 +171,7 @@ const Right = ({ {/* header */}
{bottomPanelWidth < 488 && ( - + )} @@ -233,23 +225,22 @@ const Right = ({ <> {canShowPromptGenerator && ( -
-
+
)} {isTruncated && ( - - - - + window.open(fullContent?.download_url, '_blank')} + aria-label={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })} + > + )} @@ -278,9 +269,6 @@ const Right = ({ )} )} - - -
{/* content */} diff --git a/web/app/components/workflow/variable-inspect/types.ts b/web/app/components/workflow/variable-inspect/types.ts index 4172f51d1e..f9eb952c71 100644 --- a/web/app/components/workflow/variable-inspect/types.ts +++ b/web/app/components/workflow/variable-inspect/types.ts @@ -11,3 +11,8 @@ export enum PreviewType { Markdown = 'markdown', Chunks = 'chunks', } + +export enum InspectTab { + Variables = 'variables', + Artifacts = 'artifacts', +} diff --git a/web/app/components/workflow/variable-inspect/variables-tab.tsx b/web/app/components/workflow/variable-inspect/variables-tab.tsx new file mode 100644 index 0000000000..d3863eaff5 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/variables-tab.tsx @@ -0,0 +1,190 @@ +import type { FC } from 'react' +import type { NodeProps } from '../types' +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 Left from './left' +import Listening from './listening' +import Right from './right' +import { EVENT_WORKFLOW_STOP } from './types' +import { toEnvVarInInspect } from './utils' + +export type currentVarType = { + nodeId: string + nodeType: string + title: string + isValueFetched?: boolean + var?: VarInInspect + nodeData?: NodeProps['data'] +} + +const VariablesTab: FC = () => { + const bottomPanelWidth = useStore(s => s.bottomPanelWidth) + const [showLeftPanel, setShowLeftPanel] = useState(true) + const isListening = useStore(s => s.isListening) + + const environmentVariables = useStore(s => s.environmentVariables) + const currentFocusNodeId = useStore(s => s.currentFocusNodeId) + const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) + const [currentVarId, setCurrentVarId] = useState('') + + const { + conversationVars, + systemVars, + nodesWithInspectVars, + fetchInspectVarValue, + } = useCurrentVars() + + const isEmpty = useMemo(() => { + const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars] + return allVars.length === 0 + }, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + + const currentNodeInfo = useMemo(() => { + if (!currentFocusNodeId) + return + if (currentFocusNodeId === VarInInspectType.environment) { + const currentVar = environmentVariables.find(v => v.id === currentVarId) + return { + nodeId: VarInInspectType.environment, + title: VarInInspectType.environment, + nodeType: VarInInspectType.environment, + var: currentVar ? toEnvVarInInspect(currentVar) : undefined, + } + } + if (currentFocusNodeId === VarInInspectType.conversation) { + const currentVar = conversationVars.find(v => v.id === currentVarId) + return { + nodeId: VarInInspectType.conversation, + title: VarInInspectType.conversation, + nodeType: VarInInspectType.conversation, + var: currentVar + ? { + ...currentVar, + type: VarInInspectType.conversation, + } + : undefined, + } + } + if (currentFocusNodeId === VarInInspectType.system) { + const currentVar = systemVars.find(v => v.id === currentVarId) + return { + nodeId: VarInInspectType.system, + title: VarInInspectType.system, + nodeType: VarInInspectType.system, + var: currentVar + ? { + ...currentVar, + type: VarInInspectType.system, + } + : undefined, + } + } + const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) + if (!targetNode) + return + const currentVar = targetNode.vars.find(v => v.id === currentVarId) + return { + nodeId: targetNode.nodeId, + nodeType: targetNode.nodeType, + title: targetNode.title, + isSingRunRunning: targetNode.isSingRunRunning, + isValueFetched: targetNode.isValueFetched, + nodeData: targetNode.nodePayload, + var: currentVar, + } + }, [currentFocusNodeId, currentVarId, environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + + const currentAliasMeta = useMemo(() => { + if (!currentFocusNodeId || !currentVarId) + return undefined + const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) + const targetVar = targetNode?.vars.find(v => v.id === currentVarId) + return targetVar?.aliasMeta + }, [currentFocusNodeId, currentVarId, nodesWithInspectVars]) + const fetchNodeId = currentAliasMeta?.extractorNodeId || currentFocusNodeId + + const isCurrentNodeVarValueFetching = useMemo(() => { + if (!fetchNodeId) + return false + const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) + if (!targetNode) + return false + return !targetNode.isValueFetched + }, [fetchNodeId, nodesWithInspectVars]) + + const handleNodeVarSelect = useCallback((node: currentVarType) => { + setCurrentFocusNodeId(node.nodeId) + if (node.var) + setCurrentVarId(node.var.id) + }, [setCurrentFocusNodeId, setCurrentVarId]) + + const { isLoading, schemaTypeDefinitions } = useMatchSchemaType() + const { eventEmitter } = useEventEmitterContextContext() + + const handleStopListening = 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]) + + useEffect(() => { + if (currentFocusNodeId && currentVarId && !isLoading && fetchNodeId) { + const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) + if (targetNode && !targetNode.isValueFetched) + fetchInspectVarValue([fetchNodeId], schemaTypeDefinitions!) + } + }, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading, fetchNodeId]) + + if (isListening) { + return ( +
+ +
+ ) + } + + if (isEmpty) { + return ( +
+ +
+ ) + } + + return ( +
+ {bottomPanelWidth < 488 && showLeftPanel &&
setShowLeftPanel(false)}>
} +
+ +
+
+ setShowLeftPanel(true)} + /> +
+
+ ) +} + +export default VariablesTab diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 356f701a6c..bd62546167 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -3958,11 +3958,6 @@ "count": 2 } }, - "app/components/workflow/variable-inspect/panel.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/workflow/variable-inspect/right.tsx": { "ts/no-explicit-any": { "count": 3 diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 9cee8be6dc..5c1ccbb266 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -290,6 +290,10 @@ "debug.variableInspect.reset": "Reset to last run value", "debug.variableInspect.resetConversationVar": "Reset conversation variable to default value", "debug.variableInspect.systemNode": "System", + "debug.variableInspect.tab.artifacts": "Artifacts", + "debug.variableInspect.tab.variables": "Variables", + "debug.variableInspect.tabArtifacts.previewNotAvailable": "Preview not available. Click download to view this file.", + "debug.variableInspect.tabArtifacts.selectFile": "Select a file to preview", "debug.variableInspect.title": "Variable Inspect", "debug.variableInspect.trigger.cached": "View cached variables", "debug.variableInspect.trigger.clear": "Clear", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index e922562826..c89f945481 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -288,6 +288,10 @@ "debug.variableInspect.reset": "还原至上一次运行", "debug.variableInspect.resetConversationVar": "重置会话变量为默认值", "debug.variableInspect.systemNode": "系统变量", + "debug.variableInspect.tab.artifacts": "产物", + "debug.variableInspect.tab.variables": "变量", + "debug.variableInspect.tabArtifacts.previewNotAvailable": "暂不支持预览,请点击下载查看此文件。", + "debug.variableInspect.tabArtifacts.selectFile": "选择文件进行预览", "debug.variableInspect.title": "变量检查", "debug.variableInspect.trigger.cached": "查看缓存", "debug.variableInspect.trigger.clear": "清除",