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": "清除",