diff --git a/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx b/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx index b2c2b68216..7cf5fdb889 100644 --- a/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx @@ -17,17 +17,33 @@ const { mockEmit, mockFetchInspectVarValue, mockHandleNodeSelect, + mockDownloadUrlOptions, + mockTreeOptions, + mockUseQuery, mockResetConversationVar, mockResetToLastRunVar, mockSetInputs, + flatData, + isLoading, } = vi.hoisted(() => ({ mockEditInspectVarValue: vi.fn(), mockEmit: vi.fn(), mockFetchInspectVarValue: vi.fn(), mockHandleNodeSelect: vi.fn(), + mockDownloadUrlOptions: vi.fn().mockReturnValue({ + queryKey: ['sandboxFile', 'downloadFile'], + queryFn: vi.fn(), + }), + mockTreeOptions: vi.fn().mockReturnValue({ + queryKey: ['sandboxFile', 'listFiles'], + queryFn: vi.fn(), + }), + mockUseQuery: vi.fn(), mockResetConversationVar: vi.fn(), mockResetToLastRunVar: vi.fn(), mockSetInputs: vi.fn(), + flatData: [] as unknown[], + isLoading: false, })) let inspectVarsState: InspectVarsState @@ -89,6 +105,21 @@ vi.mock('../../hooks-store', () => ({ }), })) +vi.mock('@tanstack/react-query', async importOriginal => ({ + ...await importOriginal(), + useQuery: (options: { queryKey?: unknown }) => mockUseQuery(options), +})) + +vi.mock('@/service/use-sandbox-file', async importOriginal => ({ + ...(await importOriginal()), + sandboxFileDownloadUrlOptions: (...args: unknown[]) => mockDownloadUrlOptions(...args), + sandboxFilesTreeOptions: (...args: unknown[]) => mockTreeOptions(...args), + useDownloadSandboxFile: () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), +})) + vi.mock('@/context/event-emitter', () => ({ useEventEmitterContextContext: () => ({ eventEmitter: { @@ -97,6 +128,17 @@ vi.mock('@/context/event-emitter', () => ({ }), })) +vi.mock('@/app/components/base/features/hooks', async importOriginal => ({ + ...(await importOriginal()), + useFeatures: (selector: (state: { features: { sandbox: { enabled: boolean } } }) => unknown) => selector({ + features: { + sandbox: { + enabled: true, + }, + }, + }), +})) + const createEnvironmentVariable = (overrides: Partial = {}): EnvironmentVariable => ({ id: 'env-1', name: 'API_KEY', @@ -124,6 +166,20 @@ const renderPanel = (initialStoreState: Record = {}) => { describe('VariableInspect Panel', () => { beforeEach(() => { vi.clearAllMocks() + mockUseQuery.mockImplementation((options: { queryKey?: unknown }) => { + const treeKey = mockTreeOptions.mock.results.at(-1)?.value?.queryKey + if (treeKey && options.queryKey === treeKey) { + return { + data: flatData, + isLoading, + } + } + + return { + data: undefined, + isLoading: false, + } + }) inspectVarsState = { conversationVars: [], systemVars: [], diff --git a/web/app/components/workflow/variable-inspect/artifacts-empty-state.tsx b/web/app/components/workflow/variable-inspect/artifacts-empty-state.tsx new file mode 100644 index 0000000000..f304094458 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/artifacts-empty-state.tsx @@ -0,0 +1,40 @@ +import type { DocPathWithoutLang } from '@/types/doc-paths' +import { useTranslation } from 'react-i18next' +import SearchLinesSparkle from '@/app/components/base/icons/src/vender/knowledge/SearchLinesSparkle' +import { useDocLink } from '@/context/i18n' + +const fileSystemArtifactsLocalizedPathMap = { + 'zh-Hans': '/use-dify/build/file-system#产物' as DocPathWithoutLang, + 'zh_Hans': '/use-dify/build/file-system#产物' as DocPathWithoutLang, + 'ja-JP': '/use-dify/build/file-system#アーティファクト' as DocPathWithoutLang, + 'ja_JP': '/use-dify/build/file-system#アーティファクト' as DocPathWithoutLang, +} + +type Props = { + description: string +} + +export default function ArtifactsEmptyState({ description }: Props) { + const { t } = useTranslation('workflow') + const docLink = useDocLink() + + return ( +
+
+
+
+
{t('debug.variableInspect.tabArtifacts.emptyTitle')}
+
{description}
+ + {t('debug.variableInspect.tabArtifacts.emptyLink')} + +
+
+ ) +} diff --git a/web/app/components/workflow/variable-inspect/artifacts-left-pane.tsx b/web/app/components/workflow/variable-inspect/artifacts-left-pane.tsx new file mode 100644 index 0000000000..08baf6cf29 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/artifacts-left-pane.tsx @@ -0,0 +1,27 @@ +import type { ArtifactsInspectView } from './hooks/use-artifacts-inspect-state' +import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts/artifacts-tree' + +type Props = Pick< + ArtifactsInspectView, + 'handleFileSelect' | 'handleTreeDownload' | 'isDownloading' | 'selectedFilePath' | 'treeData' +> + +export default function ArtifactsLeftPane({ + treeData, + handleTreeDownload, + handleFileSelect, + selectedFilePath, + isDownloading, +}: Props) { + return ( +
+ +
+ ) +} diff --git a/web/app/components/workflow/variable-inspect/artifacts-right-pane.tsx b/web/app/components/workflow/variable-inspect/artifacts-right-pane.tsx new file mode 100644 index 0000000000..2ca302a1c8 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/artifacts-right-pane.tsx @@ -0,0 +1,118 @@ +import type { ArtifactsInspectView } from './hooks/use-artifacts-inspect-state' +import { useTranslation } from 'react-i18next' +import ActionButton from '@/app/components/base/action-button' +import Loading from '@/app/components/base/loading' +import ReadOnlyFilePreview from '@/app/components/workflow/skill/viewer/read-only-file-preview' +import { cn } from '@/utils/classnames' +import ArtifactsEmptyState from './artifacts-empty-state' +import useInspectShell from './hooks/use-inspect-shell' + +type Props = Pick< + ArtifactsInspectView, + 'downloadUrlData' | 'handleSelectedFileDownload' | 'isDownloadUrlLoading' | 'pathSegments' | 'selectedFile' | 'selectedFilePath' +> + +function 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]}` +} + +export default function ArtifactsRightPane({ + downloadUrlData, + handleSelectedFileDownload, + isDownloadUrlLoading, + pathSegments, + selectedFile, + selectedFilePath, +}: Props) { + const { t } = useTranslation('workflow') + const { isNarrow, onClose, openLeftPane } = useInspectShell() + const file = selectedFilePath ? selectedFile : null + + return ( + <> +
+
+ {isNarrow + ? ( + + + ) + : null} + {file + ? ( + <> +
+
+ {pathSegments.map(seg => ( + + {!seg.isFirst ? / : null} + + {seg.part} + + + ))} +
+ + {formatFileSize(file.size)} + +
+
+ + +
+ + ) + : null} +
+ + +
+
+ {file + ? ( +
+ {isDownloadUrlLoading + ?
+ : downloadUrlData?.download_url + ? ( + + ) + : ( +
+

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

+
+ )} +
+ ) + : ( +
+ +
+ )} +
+ + ) +} diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx index 404f418ab4..fd3c345e4a 100644 --- a/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx +++ b/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx @@ -1,7 +1,6 @@ import type { SandboxFileNode } from '@/types/sandbox-file' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import ArtifactsTab from './artifacts-tab' -import { InspectTab } from './types' +import { act, renderHook, waitFor } from '@testing-library/react' +import { useArtifactsInspectView } from './hooks/use-artifacts-inspect-state' type MockStoreState = { appId: string | undefined @@ -11,7 +10,6 @@ type MockStoreState = { } } isResponding: boolean - bottomPanelWidth: number } const mocks = vi.hoisted(() => ({ @@ -19,7 +17,6 @@ const mocks = vi.hoisted(() => ({ appId: 'app-1', workflowRunningData: undefined, isResponding: false, - bottomPanelWidth: 640, } as MockStoreState, flatData: [] as SandboxFileNode[], isLoading: false, @@ -41,7 +38,7 @@ vi.mock('../store', () => ({ vi.mock('@tanstack/react-query', async importOriginal => ({ ...await importOriginal(), - useQuery: (options: unknown) => mocks.mockUseQuery(options), + useQuery: (options: { queryKey?: unknown }) => mocks.mockUseQuery(options), })) vi.mock('@/service/use-sandbox-file', async importOriginal => ({ @@ -54,20 +51,6 @@ vi.mock('@/service/use-sandbox-file', async importOriginal => ({ }), })) -vi.mock('@/context/i18n', () => ({ - useDocLink: () => (path: string) => path, -})) - -vi.mock('@/app/components/base/features/hooks', () => ({ - useFeatures: (selector: (state: { features: { sandbox: { enabled: boolean } } }) => unknown) => selector({ - features: { - sandbox: { - enabled: true, - }, - }, - }), -})) - vi.mock('@/utils/download', () => ({ downloadUrl: vi.fn(), })) @@ -81,13 +64,12 @@ const createFlatFileNode = (overrides: Partial = {}): SandboxFi ...overrides, }) -describe('ArtifactsTab', () => { +describe('useArtifactsInspectState', () => { beforeEach(() => { vi.clearAllMocks() mocks.storeState.appId = 'app-1' mocks.storeState.workflowRunningData = undefined mocks.storeState.isResponding = false - mocks.storeState.bottomPanelWidth = 640 mocks.flatData = [createFlatFileNode()] mocks.isLoading = false @@ -99,6 +81,7 @@ describe('ArtifactsTab', () => { isLoading: mocks.isLoading, } } + return { data: undefined, isLoading: false, @@ -107,23 +90,24 @@ describe('ArtifactsTab', () => { }) it('should stop using stale file path for download url query after files are cleared', async () => { - const headerProps = { - activeTab: InspectTab.Artifacts, - onTabChange: vi.fn(), - onClose: vi.fn(), - } + const { result, rerender } = renderHook(() => useArtifactsInspectView()) - const { rerender } = render() - - fireEvent.click(screen.getByRole('button', { name: 'a.txt' })) + act(() => { + result.current.handleFileSelect({ + name: 'a.txt', + path: 'a.txt', + node_type: 'file', + size: 128, + extension: 'txt', + } as never) + }) await waitFor(() => { expect(mocks.mockDownloadUrlOptions).toHaveBeenCalledWith('app-1', 'a.txt') }) mocks.flatData = [] - - rerender() + rerender() await waitFor(() => { const lastCall = mocks.mockDownloadUrlOptions.mock.calls.at(-1) diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx deleted file mode 100644 index 0ee451fc9a..0000000000 --- a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import type { InspectHeaderProps } from './inspect-layout' -import type { DocPathWithoutLang } from '@/types/doc-paths' -import type { SandboxFileTreeNode } from '@/types/sandbox-file' -import { useQuery } from '@tanstack/react-query' -import { useCallback, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import ActionButton from '@/app/components/base/action-button' -import SearchLinesSparkle from '@/app/components/base/icons/src/vender/knowledge/SearchLinesSparkle' -import { FileDownload01 } from '@/app/components/base/icons/src/vender/line/files' -import Loading from '@/app/components/base/loading' -import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts/artifacts-tree' -import ReadOnlyFilePreview from '@/app/components/workflow/skill/viewer/read-only-file-preview' -import { useDocLink } from '@/context/i18n' -import { sandboxFileDownloadUrlOptions, sandboxFilesTreeOptions, useDownloadSandboxFile } from '@/service/use-sandbox-file' -import { cn } from '@/utils/classnames' -import { downloadUrl } from '@/utils/download' -import { buildTreeFromFlatList } from '../skill/file-tree/artifacts/utils' -import { useStore } from '../store' -import { WorkflowRunningStatus } from '../types' -import InspectLayout from './inspect-layout' -import SplitPanel from './split-panel' - -const fileSystemArtifactsLocalizedPathMap = { - 'zh-Hans': '/use-dify/build/file-system#产物' as DocPathWithoutLang, - 'zh_Hans': '/use-dify/build/file-system#产物' as DocPathWithoutLang, - 'ja-JP': '/use-dify/build/file-system#アーティファクト' as DocPathWithoutLang, - 'ja_JP': '/use-dify/build/file-system#アーティファクト' as DocPathWithoutLang, -} - -const ArtifactsEmpty = ({ description }: { description: string }) => { - const { t } = useTranslation('workflow') - const docLink = useDocLink() - - return ( -
-
-
-
-
{t('debug.variableInspect.tabArtifacts.emptyTitle')}
-
{description}
- - {t('debug.variableInspect.tabArtifacts.emptyLink')} - -
-
- ) -} - -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]}` -} - -const ArtifactsTab = (headerProps: InspectHeaderProps) => { - const { t } = useTranslation('workflow') - const appId = useStore(s => s.appId) - const isWorkflowRunning = useStore( - s => s.workflowRunningData?.result?.status === WorkflowRunningStatus.Running, - ) - const isResponding = useStore(s => s.isResponding) - - const { data: flatData, isLoading } = useQuery({ - ...sandboxFilesTreeOptions(appId), - refetchInterval: (isWorkflowRunning || isResponding) ? 5000 : false, - }) - const treeData = useMemo(() => flatData ? buildTreeFromFlatList(flatData) : undefined, [flatData]) - const hasFiles = (flatData?.length ?? 0) > 0 - const { mutateAsync: fetchDownloadUrl, isPending: isDownloading } = useDownloadSandboxFile(appId) - const [selectedFile, setSelectedFile] = useState(null) - const selectedFilePath = useMemo(() => { - if (!selectedFile) - return undefined - - const selectedExists = flatData?.some( - node => !node.is_dir && node.path === selectedFile.path, - ) ?? false - - return selectedExists ? selectedFile.path : undefined - }, [flatData, selectedFile]) - - const { data: downloadUrlData, isLoading: isDownloadUrlLoading } = useQuery({ - ...sandboxFileDownloadUrlOptions(appId, selectedFilePath), - retry: false, - }) - - const handleFileSelect = useCallback((node: SandboxFileTreeNode) => { - if (node.node_type === 'file') - setSelectedFile(node) - }, []) - - const handleTreeDownload = useCallback(async (node: SandboxFileTreeNode) => { - try { - const ticket = await fetchDownloadUrl(node.path) - downloadUrl({ url: ticket.download_url, fileName: node.name }) - } - catch (error) { - console.error('Download failed:', error) - } - }, [fetchDownloadUrl]) - - const handleSelectedFileDownload = useCallback(() => { - if (downloadUrlData?.download_url && selectedFile) - downloadUrl({ url: downloadUrlData.download_url, fileName: selectedFile.name }) - }, [downloadUrlData, selectedFile]) - - if (isLoading) { - return ( - -
- -
-
- ) - } - - if (!hasFiles) { - return ( - -
- -
-
- ) - } - - const file = selectedFilePath ? selectedFile : null - const parts = file?.path.split('/') ?? [] - let cumPath = '' - const pathSegments = parts.map((part, i) => { - cumPath += (cumPath ? '/' : '') + part - return { part, key: cumPath, isFirst: i === 0, isLast: i === parts.length - 1 } - }) - - return ( - - - - )} - > - {({ isNarrow, onOpenMenu, onClose: handleClose }) => ( - <> -
-
- {isNarrow && ( - - - )} - {file && ( - <> -
-
- {pathSegments!.map(seg => ( - - {!seg.isFirst && /} - - {seg.part} - - - ))} -
- - {formatFileSize(file.size)} - -
-
- - - -
- - )} -
- - -
-
- {file - ? ( -
- {isDownloadUrlLoading - ?
- : downloadUrlData?.download_url - ? ( - - ) - : ( -
-

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

-
- )} -
- ) - : ( -
- -
- )} -
- - )} -
- ) -} - -export default ArtifactsTab diff --git a/web/app/components/workflow/variable-inspect/group.tsx b/web/app/components/workflow/variable-inspect/group.tsx index 07f986f417..aeec4f20a8 100644 --- a/web/app/components/workflow/variable-inspect/group.tsx +++ b/web/app/components/workflow/variable-inspect/group.tsx @@ -1,18 +1,9 @@ -import type { currentVarType } from './variables-tab' +import type { CurrentVarInInspect } from './types' import type { NodeWithVar, VarInInspect } from '@/types/workflow' -import { - RiArrowRightSLine, - RiDeleteBinLine, - RiFileList3Line, - RiLoader2Line, - // RiErrorWarningFill, -} from '@remixicon/react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -// import Button from '@/app/components/base/button' import ActionButton from '@/app/components/base/action-button' -import { AtSign } from '@/app/components/base/icons/src/vender/workflow' -import Tooltip from '@/app/components/base/tooltip' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' import { VarInInspectType } from '@/types/workflow' @@ -22,15 +13,15 @@ import { formatVarTypeLabel } from './utils' type Props = { nodeData?: NodeWithVar - currentVar?: currentVarType + currentVar?: CurrentVarInInspect varType: VarInInspectType varList: VarInInspect[] - handleSelect: (state: currentVarType) => void + handleSelect: (state: CurrentVarInInspect) => void handleView?: () => void handleClear?: () => void } -const Group = ({ +export default function Group({ nodeData, currentVar, varType, @@ -38,7 +29,7 @@ const Group = ({ handleSelect, handleView, handleClear, -}: Props) => { +}: Props) { const { t } = useTranslation() const [isCollapsed, setIsCollapsed] = useState(false) @@ -104,10 +95,10 @@ const Group = ({
{nodeData?.isSingRunRunning && ( - +
setIsCollapsed(!isCollapsed)}> @@ -132,15 +123,29 @@ const Group = ({
{nodeData && !nodeData.isSingRunRunning && (
- - - - + + + + + + + )} + /> + {t('debug.variableInspect.view', { ns: 'workflow' })} - - - - + + + + + + + )} + /> + {t('debug.variableInspect.clearNode', { ns: 'workflow' })}
)} @@ -162,7 +167,7 @@ const Group = ({ onClick={() => handleSelectVar(varItem, varType)} > {isAgentAliasVar - ? + ? @ : ( ) } - -export default Group diff --git a/web/app/components/workflow/variable-inspect/hooks/use-artifacts-inspect-state.ts b/web/app/components/workflow/variable-inspect/hooks/use-artifacts-inspect-state.ts new file mode 100644 index 0000000000..8b2336f1b9 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/hooks/use-artifacts-inspect-state.ts @@ -0,0 +1,141 @@ +import type { SandboxFileTreeNode } from '@/types/sandbox-file' +import { useQuery } from '@tanstack/react-query' +import { useCallback, useMemo, useState } from 'react' +import { sandboxFileDownloadUrlOptions, sandboxFilesTreeOptions, useDownloadSandboxFile } from '@/service/use-sandbox-file' +import { downloadUrl } from '@/utils/download' +import { buildTreeFromFlatList } from '../../skill/file-tree/artifacts/utils' +import { useStore } from '../../store' +import { WorkflowRunningStatus } from '../../types' + +type PathSegment = { + part: string + key: string + isFirst: boolean + isLast: boolean +} + +export type ArtifactsInspectStatus = 'loading' | 'empty' | 'split' + +export type ArtifactsInspectView = { + downloadUrlData?: { download_url?: string } + handleFileSelect: (node: SandboxFileTreeNode) => void + handleSelectedFileDownload: () => void + handleTreeDownload: (node: SandboxFileTreeNode) => Promise + isDownloadUrlLoading: boolean + isDownloading: boolean + pathSegments: PathSegment[] + selectedFile: SandboxFileTreeNode | null + selectedFilePath?: string + status: ArtifactsInspectStatus + treeData?: SandboxFileTreeNode[] +} + +export const useArtifactsInspectView = (): ArtifactsInspectView => { + const appId = useStore(s => s.appId) + const isWorkflowRunning = useStore( + s => s.workflowRunningData?.result?.status === WorkflowRunningStatus.Running, + ) + const isResponding = useStore(s => s.isResponding) + + const { data: flatData, isLoading } = useQuery({ + ...sandboxFilesTreeOptions(appId), + refetchInterval: (isWorkflowRunning || isResponding) ? 5000 : false, + }) + const treeData = useMemo(() => flatData ? buildTreeFromFlatList(flatData) : undefined, [flatData]) + const hasFiles = (flatData?.length ?? 0) > 0 + const { mutateAsync: fetchDownloadUrl, isPending: isDownloading } = useDownloadSandboxFile(appId) + const [selectedFile, setSelectedFile] = useState(null) + + const selectedFilePath = useMemo(() => { + if (!selectedFile) + return undefined + + const selectedExists = flatData?.some(node => !node.is_dir && node.path === selectedFile.path) ?? false + return selectedExists ? selectedFile.path : undefined + }, [flatData, selectedFile]) + + const { data: downloadUrlData, isLoading: isDownloadUrlLoading } = useQuery({ + ...sandboxFileDownloadUrlOptions(appId, selectedFilePath), + retry: false, + }) + + const handleFileSelect = useCallback((node: SandboxFileTreeNode) => { + if (node.node_type === 'file') + setSelectedFile(node) + }, []) + + const handleTreeDownload = useCallback(async (node: SandboxFileTreeNode) => { + try { + const ticket = await fetchDownloadUrl(node.path) + downloadUrl({ url: ticket.download_url, fileName: node.name }) + } + catch (error) { + console.error('Download failed:', error) + } + }, [fetchDownloadUrl]) + + const handleSelectedFileDownload = useCallback(() => { + if (downloadUrlData?.download_url && selectedFile) + downloadUrl({ url: downloadUrlData.download_url, fileName: selectedFile.name }) + }, [downloadUrlData, selectedFile]) + + const pathSegments = useMemo(() => { + const parts = selectedFilePath ? selectedFilePath.split('/') : [] + let cumPath = '' + return parts.map((part, index) => { + cumPath += `${cumPath ? '/' : ''}${part}` + return { + part, + key: cumPath, + isFirst: index === 0, + isLast: index === parts.length - 1, + } + }) + }, [selectedFilePath]) + + if (isLoading) { + return { + downloadUrlData, + handleFileSelect, + handleSelectedFileDownload, + handleTreeDownload, + isDownloadUrlLoading, + isDownloading, + pathSegments, + selectedFile, + selectedFilePath, + status: 'loading', + treeData, + } + } + + if (!hasFiles) { + return { + downloadUrlData, + handleFileSelect, + handleSelectedFileDownload, + handleTreeDownload, + isDownloadUrlLoading, + isDownloading, + pathSegments, + selectedFile, + selectedFilePath, + status: 'empty', + treeData, + } + } + + return { + downloadUrlData, + handleFileSelect, + handleSelectedFileDownload, + handleTreeDownload, + isDownloadUrlLoading, + isDownloading, + pathSegments, + selectedFile, + selectedFilePath, + status: 'split', + treeData, + } +} diff --git a/web/app/components/workflow/variable-inspect/hooks/use-inspect-shell.ts b/web/app/components/workflow/variable-inspect/hooks/use-inspect-shell.ts new file mode 100644 index 0000000000..8f8093b976 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/hooks/use-inspect-shell.ts @@ -0,0 +1,19 @@ +import { createContext, use } from 'react' + +type InspectShellContextValue = { + closeLeftPane: () => void + isNarrow: boolean + onClose: () => void + openLeftPane: () => void +} + +export const InspectShellContext = createContext(null) + +export default function useInspectShell() { + const context = use(InspectShellContext) + + if (!context) + throw new Error('useInspectShell must be used within InspectShell') + + return context +} diff --git a/web/app/components/workflow/variable-inspect/variables-tab.tsx b/web/app/components/workflow/variable-inspect/hooks/use-variables-inspect-state.ts similarity index 58% rename from web/app/components/workflow/variable-inspect/variables-tab.tsx rename to web/app/components/workflow/variable-inspect/hooks/use-variables-inspect-state.ts index 74792e41c8..febc7c671a 100644 --- a/web/app/components/workflow/variable-inspect/variables-tab.tsx +++ b/web/app/components/workflow/variable-inspect/hooks/use-variables-inspect-state.ts @@ -1,32 +1,25 @@ -import type { FC } from 'react' -import type { NodeProps } from '../types' -import type { InspectHeaderProps } from './inspect-layout' -import type { VarInInspect } from '@/types/workflow' +import type { CurrentVarInInspect } from '../types' import { useCallback, useEffect, useMemo, useState } from 'react' import { useEventEmitterContextContext } from '@/context/event-emitter' import { VarInInspectType } from '@/types/workflow' -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' +import useCurrentVars from '../../hooks/use-inspect-vars-crud' +import useMatchSchemaType from '../../nodes/_base/components/variable/use-match-schema-type' +import { useStore } from '../../store' +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'] +export type VariablesInspectStatus = 'listening' | 'empty' | 'split' + +export type VariablesInspectView = { + currentFocusNodeId: string | null + currentNodeVar?: CurrentVarInInspect + isValueFetching: boolean + onSelectVar: (node: CurrentVarInInspect) => void + onStopListening: () => void + status: VariablesInspectStatus } -const VariablesTab: FC = (headerProps) => { +export const useVariablesInspectView = (): VariablesInspectView => { const isListening = useStore(s => s.isListening) const environmentVariables = useStore(s => s.environmentVariables) const currentFocusNodeId = useStore(s => s.currentFocusNodeId) @@ -44,9 +37,10 @@ const VariablesTab: FC = (headerProps) => { return [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars].length === 0 }, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) - const currentNodeInfo = useMemo(() => { + const currentNodeVar = useMemo(() => { if (!currentFocusNodeId) - return + return undefined + if (currentFocusNodeId === VarInInspectType.environment) { const currentVar = environmentVariables.find(v => v.id === currentVarId) return { @@ -56,6 +50,7 @@ const VariablesTab: FC = (headerProps) => { var: currentVar ? toEnvVarInInspect(currentVar) : undefined, } } + if (currentFocusNodeId === VarInInspectType.conversation) { const currentVar = conversationVars.find(v => v.id === currentVarId) return { @@ -70,6 +65,7 @@ const VariablesTab: FC = (headerProps) => { : undefined, } } + if (currentFocusNodeId === VarInInspectType.system) { const currentVar = systemVars.find(v => v.id === currentVarId) return { @@ -84,44 +80,46 @@ const VariablesTab: FC = (headerProps) => { : undefined, } } + const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) if (!targetNode) - return + return undefined + 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]) + }, [conversationVars, currentFocusNodeId, currentVarId, environmentVariables, nodesWithInspectVars, systemVars]) 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(() => { + const isValueFetching = useMemo(() => { if (!fetchNodeId) return false + const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) - if (!targetNode) - return false - return !targetNode.isValueFetched + return targetNode ? !targetNode.isValueFetched : false }, [fetchNodeId, nodesWithInspectVars]) - const handleNodeVarSelect = useCallback((node: currentVarType) => { + const onSelectVar = useCallback((node: CurrentVarInInspect) => { setCurrentFocusNodeId(node.nodeId) if (node.var) setCurrentVarId(node.var.id) - }, [setCurrentFocusNodeId, setCurrentVarId]) + }, [setCurrentFocusNodeId]) const { isLoading, schemaTypeDefinitions } = useMatchSchemaType() const { eventEmitter } = useEventEmitterContextContext() @@ -131,49 +129,42 @@ const VariablesTab: FC = (headerProps) => { }, [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 (!currentFocusNodeId || !currentVarId || isLoading || !fetchNodeId) + return + + const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) + if (targetNode && !targetNode.isValueFetched) + fetchInspectVarValue([fetchNodeId], schemaTypeDefinitions!) + }, [currentFocusNodeId, currentVarId, fetchInspectVarValue, fetchNodeId, isLoading, nodesWithInspectVars, schemaTypeDefinitions]) if (isListening) { - return ( - -
-
- ) + return { + currentFocusNodeId, + currentNodeVar, + isValueFetching, + onSelectVar, + onStopListening, + status: 'listening', + } } if (isEmpty) { - return ( - -
-
- ) + return { + currentFocusNodeId, + currentNodeVar, + isValueFetching, + onSelectVar, + onStopListening, + status: 'empty', + } } - return ( - - )} - > - {rightProps => ( - - )} - - ) + return { + currentFocusNodeId, + currentNodeVar, + isValueFetching, + onSelectVar, + onStopListening, + status: 'split', + } } - -export default VariablesTab diff --git a/web/app/components/workflow/variable-inspect/index.tsx b/web/app/components/workflow/variable-inspect/index.tsx index 775c761eca..ea5d463feb 100644 --- a/web/app/components/workflow/variable-inspect/index.tsx +++ b/web/app/components/workflow/variable-inspect/index.tsx @@ -1,4 +1,3 @@ -import type { FC } from 'react' import { debounce } from 'es-toolkit/compat' import { useCallback, @@ -9,7 +8,7 @@ import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel' import { useStore } from '../store' import Panel from './panel' -const VariableInspectPanel: FC = () => { +export default function VariableInspectPanel() { const showVariableInspectPanel = useStore(s => s.showVariableInspectPanel) const workflowCanvasHeight = useStore(s => s.workflowCanvasHeight) const variableInspectPanelHeight = useStore(s => s.variableInspectPanelHeight) @@ -58,5 +57,3 @@ const VariableInspectPanel: FC = () => {
) } - -export default VariableInspectPanel diff --git a/web/app/components/workflow/variable-inspect/inspect-layout.tsx b/web/app/components/workflow/variable-inspect/inspect-layout.tsx deleted file mode 100644 index 15560db083..0000000000 --- a/web/app/components/workflow/variable-inspect/inspect-layout.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { FC, ReactNode } from 'react' -import type { InspectTab } from './types' -import { RiCloseLine } from '@remixicon/react' -import ActionButton from '@/app/components/base/action-button' -import TabHeader from './tab-header' - -export type InspectHeaderProps = { - activeTab: InspectTab - onTabChange: (tab: InspectTab) => void - onClose: () => void - headerActions?: ReactNode -} - -type InspectLayoutProps = InspectHeaderProps & { - children: ReactNode -} - -const InspectLayout: FC = ({ - activeTab, - onTabChange, - onClose, - headerActions, - children, -}) => { - return ( -
-
-
- - {headerActions} - -
-
- - - -
-
-
- {children} -
-
- ) -} - -export default InspectLayout diff --git a/web/app/components/workflow/variable-inspect/inspect-scroll-area.tsx b/web/app/components/workflow/variable-inspect/inspect-scroll-area.tsx new file mode 100644 index 0000000000..5ef2a8e5ad --- /dev/null +++ b/web/app/components/workflow/variable-inspect/inspect-scroll-area.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from 'react' +import { ScrollArea } from '@/app/components/base/ui/scroll-area' +import { cn } from '@/utils/classnames' + +type InspectScrollAreaProps = { + children: ReactNode + className?: string + contentClassName?: string + label?: string +} + +export default function InspectScrollArea({ + children, + className, + contentClassName, + label, +}: InspectScrollAreaProps) { + return ( + + {children} + + ) +} diff --git a/web/app/components/workflow/variable-inspect/inspect-shell.tsx b/web/app/components/workflow/variable-inspect/inspect-shell.tsx new file mode 100644 index 0000000000..1914cb9a68 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/inspect-shell.tsx @@ -0,0 +1,102 @@ +import type { ReactNode } from 'react' +import type { InspectHeaderProps } from './types' +import { useMemo, useState } from 'react' +import ActionButton from '@/app/components/base/action-button' +import { cn } from '@/utils/classnames' +import { useStore } from '../store' +import useInspectShell, { InspectShellContext } from './hooks/use-inspect-shell' +import TabHeader from './tab-header' + +type InspectShellProps = InspectHeaderProps & { + children: ReactNode + left?: ReactNode +} + +function SinglePaneCloseButton() { + const { onClose } = useInspectShell() + + return ( +
+ + +
+ ) +} + +export default function InspectShell({ + activeTab, + onTabChange, + onClose, + headerActions, + left, + children, +}: InspectShellProps) { + const bottomPanelWidth = useStore(s => s.bottomPanelWidth) + const isNarrow = bottomPanelWidth < 488 + const [showLeftPane, setShowLeftPane] = useState(true) + const hasLeftPane = !!left + + const contextValue = useMemo(() => ({ + closeLeftPane: () => setShowLeftPane(false), + isNarrow: hasLeftPane + ? isNarrow + : false, + onClose, + openLeftPane: () => setShowLeftPane(true), + }), [hasLeftPane, isNarrow, onClose]) + + return ( + + {hasLeftPane + ? ( +
+
+
+ + {headerActions} + +
+ {isNarrow && showLeftPane && ( +
setShowLeftPane(false)} + /> + )} +
+ {left} +
+
+
+ {children} +
+
+ ) + : ( +
+
+
+ + {headerActions} + +
+ +
+
+ {children} +
+
+ )} + + ) +} diff --git a/web/app/components/workflow/variable-inspect/left.tsx b/web/app/components/workflow/variable-inspect/left.tsx index fc25601d72..74df6d20dc 100644 --- a/web/app/components/workflow/variable-inspect/left.tsx +++ b/web/app/components/workflow/variable-inspect/left.tsx @@ -1,18 +1,14 @@ -import type { currentVarType } from './variables-tab' - +import type { CurrentVarInInspect } from './types' import type { VarInInspect } from '@/types/workflow' import { VarInInspectType } from '@/types/workflow' -import { cn } from '@/utils/classnames' import useCurrentVars from '../hooks/use-inspect-vars-crud' import { useNodesInteractions } from '../hooks/use-nodes-interactions' import { useStore } from '../store' -// import ActionButton from '@/app/components/base/action-button' -// import Tooltip from '@/app/components/base/tooltip' import Group from './group' type Props = { - currentNodeVar?: currentVarType - handleVarSelect: (state: currentVarType) => void + currentNodeVar?: CurrentVarInInspect + handleVarSelect: (state: CurrentVarInInspect) => void } const Left = ({ @@ -40,55 +36,48 @@ const Left = ({ } return ( -
-
- {/* group ENV */} - {environmentVariables.length > 0 && ( - - )} - {/* group CHAT VAR */} - {conversationVars.length > 0 && ( - - )} - {/* group SYSTEM VAR */} - {systemVars.length > 0 && ( - - )} - {/* divider */} - {showDivider && ( -
-
-
- )} - {/* group nodes */} - {visibleNodesWithInspectVars.length > 0 && visibleNodesWithInspectVars.map(group => ( - handleNodeSelect(group.nodeId, false, true)} - handleClear={() => handleClearNode(group.nodeId)} - /> - ))} -
+
+ {environmentVariables.length > 0 && ( + + )} + {conversationVars.length > 0 && ( + + )} + {systemVars.length > 0 && ( + + )} + {showDivider && ( +
+
+
+ )} + {visibleNodesWithInspectVars.length > 0 && visibleNodesWithInspectVars.map(group => ( + handleNodeSelect(group.nodeId, false, true)} + handleClear={() => handleClearNode(group.nodeId)} + /> + ))}
) } diff --git a/web/app/components/workflow/variable-inspect/listening.tsx b/web/app/components/workflow/variable-inspect/listening.tsx index 130936147b..69421cf303 100644 --- a/web/app/components/workflow/variable-inspect/listening.tsx +++ b/web/app/components/workflow/variable-inspect/listening.tsx @@ -1,5 +1,4 @@ import type { TFunction } from 'i18next' -import type { FC } from 'react' import type { Node } from 'reactflow' import type { ScheduleTriggerNodeType } from '@/app/components/workflow/nodes/trigger-schedule/types' import type { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types' @@ -8,8 +7,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' import Button from '@/app/components/base/button' -import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' -import Tooltip from '@/app/components/base/tooltip' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon' import { getNextExecutionTime } from '@/app/components/workflow/nodes/trigger-schedule/utils/execution-time-calculator' @@ -75,10 +73,10 @@ export type ListeningProps = { message?: string } -const Listening: FC = ({ +export default function Listening({ onStop, message, -}) => { +}: ListeningProps) { const { t } = useTranslation() const store = useStoreApi() @@ -179,28 +177,33 @@ const Listening: FC = ({
{t('nodes.triggerWebhook.debugUrlTitle', { ns: 'workflow' })}
- - + )} + /> + - - {webhookDebugUrl} - - + {debugUrlCopied + ? t('nodes.triggerWebhook.debugUrlCopied', { ns: 'workflow' }) + : t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' })} +
)} @@ -211,12 +214,10 @@ const Listening: FC = ({ variant="primary" onClick={onStop} > - +
) } - -export default Listening diff --git a/web/app/components/workflow/variable-inspect/panel.tsx b/web/app/components/workflow/variable-inspect/panel.tsx index 32803e5a13..5a2533a7af 100644 --- a/web/app/components/workflow/variable-inspect/panel.tsx +++ b/web/app/components/workflow/variable-inspect/panel.tsx @@ -1,71 +1,164 @@ -import type { FC } from 'react' -import { useQuery } from '@tanstack/react-query' +import type { ReactNode } from 'react' +import type { InspectTab as InspectTabType } from './types' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { useFeatures } from '@/app/components/base/features/hooks' -import { sandboxFilesTreeOptions } from '@/service/use-sandbox-file' +import Loading from '@/app/components/base/loading' import useCurrentVars from '../hooks/use-inspect-vars-crud' import { useStore } from '../store' -import ArtifactsTab from './artifacts-tab' +import ArtifactsEmptyState from './artifacts-empty-state' +import ArtifactsLeftPane from './artifacts-left-pane' +import ArtifactsRightPane from './artifacts-right-pane' +import Empty from './empty' +import { + useArtifactsInspectView, +} from './hooks/use-artifacts-inspect-state' +import { + useVariablesInspectView, +} from './hooks/use-variables-inspect-state' +import InspectScrollArea from './inspect-scroll-area' +import InspectShell from './inspect-shell' +import Left from './left' +import Listening from './listening' +import Right from './right' import { InspectTab } from './types' -import VariablesTab from './variables-tab' -const VariablesPanel: FC<{ onClose: () => void }> = ({ onClose }) => { +export default function Panel() { const { t } = useTranslation('workflow') const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) - const appId = useStore(s => s.appId) + const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) + const environmentVariables = useStore(s => s.environmentVariables) const sandboxEnabled = useFeatures(s => s.features.sandbox?.enabled) ?? false - const [activeTab, setActiveTab] = useState(InspectTab.Variables) + const [activeTab, setActiveTab] = useState(InspectTab.Variables) + + const { + conversationVars, + systemVars, + nodesWithInspectVars, + deleteAllInspectorVars, + } = useCurrentVars() + + const variablesState = useVariablesInspectView() + const artifactsState = useArtifactsInspectView() const resolvedTab = (!sandboxEnabled && activeTab === InspectTab.Artifacts) ? InspectTab.Variables : activeTab - const environmentVariables = useStore(s => s.environmentVariables) - const { conversationVars, systemVars, nodesWithInspectVars, deleteAllInspectorVars } = useCurrentVars() - const isVariablesEmpty = useMemo(() => { return [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars].length === 0 - }, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + }, [conversationVars, environmentVariables, nodesWithInspectVars, systemVars]) - const { data: sandboxFiles } = useQuery(sandboxFilesTreeOptions(sandboxEnabled ? appId : undefined)) - const hasArtifacts = (sandboxFiles?.length ?? 0) > 0 + const hasArtifacts = artifactsState.status === 'split' + const hasData = !isVariablesEmpty || hasArtifacts const handleClear = useCallback(() => { deleteAllInspectorVars() setCurrentFocusNodeId('') }, [deleteAllInspectorVars, setCurrentFocusNodeId]) - const hasData = !isVariablesEmpty || hasArtifacts - const headerActions = hasData - ? ( - - ) - : undefined - - const headerProps = { - activeTab: resolvedTab, - onTabChange: setActiveTab, - onClose, - headerActions, - } - - return resolvedTab === InspectTab.Variables - ? - : -} - -const Panel: FC = () => { - const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) - const handleClose = useCallback(() => { setShowVariableInspectPanel(false) }, [setShowVariableInspectPanel]) - return -} + const headerActions = ( + + ) -export default Panel + const headerProps = { + activeTab: resolvedTab, + headerActions, + onClose: handleClose, + onTabChange: setActiveTab, + } + + let leftPane: ReactNode | undefined + let body: ReactNode + + if (resolvedTab === InspectTab.Variables) { + if (variablesState.status === 'listening') { + body = ( +
+ +
+ ) + } + else if (variablesState.status === 'empty') { + body = ( +
+ +
+ ) + } + else { + leftPane = ( + + + + ) + body = ( + + ) + } + } + else if (artifactsState.status === 'loading') { + body = ( +
+ +
+ ) + } + else if (artifactsState.status === 'empty') { + body = ( +
+ +
+ ) + } + else { + leftPane = ( + + + + ) + body = ( + + ) + } + + return ( + + {body} + + ) +} diff --git a/web/app/components/workflow/variable-inspect/right.tsx b/web/app/components/workflow/variable-inspect/right.tsx index 7aab66f41b..74b9cb27a6 100644 --- a/web/app/components/workflow/variable-inspect/right.tsx +++ b/web/app/components/workflow/variable-inspect/right.tsx @@ -1,15 +1,6 @@ -import type { FC } from 'react' -import type { SplitRightProps } from './split-panel' +import type { CurrentVarInInspect } from './types' import type { VarInspectValue } from './value-types' -import type { currentVarType } from './variables-tab' import type { GenRes } from '@/service/debug' -import { - RiArrowGoBackLine, - RiCloseLine, - RiFileDownloadFill, - RiMenuLine, - RiSparklingFill, -} from '@remixicon/react' import { useBoolean } from 'ahooks' import { produce } from 'immer' import { useCallback, useMemo } from 'react' @@ -19,7 +10,7 @@ import ActionButton from '@/app/components/base/action-button' import Badge from '@/app/components/base/badge' import CopyFeedback from '@/app/components/base/copy-feedback' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' import { useEventEmitterContextContext } from '@/context/event-emitter' @@ -35,24 +26,23 @@ import useNodeInfo from '../nodes/_base/hooks/use-node-info' import { CodeLanguage } from '../nodes/code/types' import { BlockEnum } from '../types' import Empty from './empty' +import useInspectShell from './hooks/use-inspect-shell' import { formatVarTypeLabel } from './utils' import ValueContent from './value-content' -type Props = SplitRightProps & { +type Props = { nodeId: string - currentNodeVar?: currentVarType + currentNodeVar?: CurrentVarInInspect isValueFetching?: boolean } -const Right: FC = ({ +export default function Right({ nodeId, currentNodeVar, isValueFetching, - isNarrow, - onOpenMenu, - onClose, -}) => { +}: Props) { const { t } = useTranslation() + const { isNarrow, onClose, openLeftPane } = useInspectShell() const toolIcon = useToolIcon(currentNodeVar?.nodeData) const currentVar = currentNodeVar?.var const currentNodeType = currentNodeVar?.nodeType @@ -111,7 +101,7 @@ const Right: FC = ({ return node?.data?.prompt_template?.text || node?.data?.prompt_template?.[0].text if (blockType === BlockEnum.Code) return node?.data?.code - }, [canShowPromptGenerator]) + }, [blockType, canShowPromptGenerator, node?.data?.code, node?.data?.prompt_template]) const [isShowPromptGenerator, { setTrue: doShowPromptGenerator, @@ -145,7 +135,7 @@ const Right: FC = ({ payload: res.modified, }) handleHidePromptGenerator() - }, [setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator]) + }, [eventEmitter, setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator]) const schemaType = currentVar?.schemaType const valueType = currentVar?.value_type @@ -159,11 +149,13 @@ const Right: FC = ({ <>
- {isNarrow && ( - - - - )} + {isNarrow + ? ( + + + ) + : null}
{currentVar && ( <> @@ -212,44 +204,70 @@ const Right: FC = ({ {currentVar && ( <> {canShowPromptGenerator && ( - - + + + )} {isTruncated && ( - - window.open(fullContent?.download_url, '_blank')} - aria-label={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })} - > - - + + + window.open(fullContent?.download_url, '_blank')} + aria-label={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })} + > + + + )} + /> + {t('debug.variableInspect.exportToolTip', { ns: 'workflow' })} )} {!isTruncated && currentVar.edited && ( - {t('debug.variableInspect.edited', { ns: 'workflow' })} + {t('debug.variableInspect.edited', { ns: 'workflow' })} )} {!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && ( - - - - + + + + + + )} + /> + {t('debug.variableInspect.reset', { ns: 'workflow' })} )} {!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && ( - - - - + + + + + + )} + /> + {t('debug.variableInspect.resetConversationVar', { ns: 'workflow' })} )} {currentVar.value_type !== 'secret' && ( @@ -260,7 +278,7 @@ const Right: FC = ({
- +
@@ -310,5 +328,3 @@ const Right: FC = ({ ) } - -export default Right diff --git a/web/app/components/workflow/variable-inspect/split-panel.tsx b/web/app/components/workflow/variable-inspect/split-panel.tsx deleted file mode 100644 index bc2ec164c4..0000000000 --- a/web/app/components/workflow/variable-inspect/split-panel.tsx +++ /dev/null @@ -1,62 +0,0 @@ -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/tab-header.tsx b/web/app/components/workflow/variable-inspect/tab-header.tsx index 98c0e501dd..6ca73c4097 100644 --- a/web/app/components/workflow/variable-inspect/tab-header.tsx +++ b/web/app/components/workflow/variable-inspect/tab-header.tsx @@ -1,4 +1,4 @@ -import type { FC, ReactNode } from 'react' +import type { ReactNode } from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useFeatures } from '@/app/components/base/features/hooks' @@ -16,11 +16,11 @@ type TabHeaderProps = { children?: ReactNode } -const TabHeader: FC = ({ +export default function TabHeader({ activeTab, onTabChange, children, -}) => { +}: TabHeaderProps) { const { t } = useTranslation('workflow') const sandboxEnabled = useFeatures(s => s.features.sandbox?.enabled) ?? false @@ -46,9 +46,7 @@ const TabHeader: FC = ({ {t(tab.labelKey)} ))} -
{children}
+
{children}
) } - -export default TabHeader diff --git a/web/app/components/workflow/variable-inspect/trigger.tsx b/web/app/components/workflow/variable-inspect/trigger.tsx index 30e0b459a2..3a0050c6a9 100644 --- a/web/app/components/workflow/variable-inspect/trigger.tsx +++ b/web/app/components/workflow/variable-inspect/trigger.tsx @@ -1,10 +1,8 @@ -import type { FC } from 'react' import type { CommonNodeType } from '@/app/components/workflow/types' -import { RiLoader2Line, RiStopCircleFill } from '@remixicon/react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useNodes } from 'reactflow' -import Tooltip from '@/app/components/base/tooltip' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types' import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' import { useEventEmitterContextContext } from '@/context/event-emitter' @@ -13,7 +11,7 @@ import useCurrentVars from '../hooks/use-inspect-vars-crud' import { useNodesReadOnly } from '../hooks/use-workflow' import { useStore } from '../store' -const VariableInspectTrigger: FC = () => { +export default function VariableInspectTrigger() { const { t } = useTranslation() const { eventEmitter } = useEventEmitterContextContext() @@ -104,19 +102,22 @@ const VariableInspectTrigger: FC = () => { className="flex h-6 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-accent shadow-lg backdrop-blur-sm system-xs-medium hover:bg-components-actionbar-bg-accent" onClick={() => setShowVariableInspectPanel(true)} > - +
{isPreviewRunning && ( - -
- -
+ + + )} @@ -124,5 +125,3 @@ const VariableInspectTrigger: FC = () => { ) } - -export default VariableInspectTrigger diff --git a/web/app/components/workflow/variable-inspect/types.ts b/web/app/components/workflow/variable-inspect/types.ts index 3119d7fd27..cab821aef0 100644 --- a/web/app/components/workflow/variable-inspect/types.ts +++ b/web/app/components/workflow/variable-inspect/types.ts @@ -1,3 +1,7 @@ +import type { ReactNode } from 'react' +import type { NodeProps } from '../types' +import type { VarInInspect } from '@/types/workflow' + /* eslint-disable ts/no-redeclare -- const + type share names (erasable enum replacement) */ export const EVENT_WORKFLOW_STOP = 'WORKFLOW_STOP' @@ -20,3 +24,19 @@ export const InspectTab = { Artifacts: 'artifacts', } as const export type InspectTab = typeof InspectTab[keyof typeof InspectTab] + +export type InspectHeaderProps = { + activeTab: InspectTab + headerActions?: ReactNode + onClose: () => void + onTabChange: (tab: InspectTab) => void +} + +export type CurrentVarInInspect = { + nodeId: string + nodeType: string + title: string + isValueFetched?: boolean + var?: VarInInspect + nodeData?: NodeProps['data'] +} diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 58aa711c22..8baf1a0b5a 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -6059,32 +6059,16 @@ "count": 1 } }, - "app/components/workflow/variable-inspect/group.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/workflow/variable-inspect/listening.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } }, "app/components/workflow/variable-inspect/right.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } }, - "app/components/workflow/variable-inspect/trigger.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/workflow/variable-inspect/value-content.tsx": { "react/set-state-in-effect": { "count": 5 @@ -6568,14 +6552,42 @@ "count": 1 } }, - "utils/clipboard.ts": { + "utils/__tests__/completion-params.spec.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "utils/__tests__/get-icon.spec.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "utils/__tests__/index.spec.ts": { + "test/no-identical-title": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 8 + } + }, + "utils/__tests__/model-config.spec.ts": { + "ts/no-explicit-any": { + "count": 13 + } + }, + "utils/__tests__/navigation.spec.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "utils/__tests__/tool-call.spec.ts": { "ts/no-explicit-any": { "count": 1 } }, - "utils/__tests__/completion-params.spec.ts": { + "utils/clipboard.ts": { "ts/no-explicit-any": { - "count": 3 + "count": 1 } }, "utils/completion-params.ts": { @@ -6596,24 +6608,11 @@ "count": 1 } }, - "utils/__tests__/get-icon.spec.ts": { - "ts/no-explicit-any": { - "count": 2 - } - }, "utils/gtag.ts": { "ts/no-explicit-any": { "count": 2 } }, - "utils/__tests__/index.spec.ts": { - "test/no-identical-title": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 8 - } - }, "utils/index.ts": { "ts/no-explicit-any": { "count": 3 @@ -6624,29 +6623,14 @@ "count": 1 } }, - "utils/__tests__/model-config.spec.ts": { - "ts/no-explicit-any": { - "count": 13 - } - }, "utils/model-config.ts": { "ts/no-explicit-any": { "count": 6 } }, - "utils/__tests__/navigation.spec.ts": { - "ts/no-explicit-any": { - "count": 4 - } - }, - "utils/__tests__/tool-call.spec.ts": { - "ts/no-explicit-any": { - "count": 1 - } - }, "utils/validators.ts": { "ts/no-explicit-any": { "count": 2 } } -} +} \ No newline at end of file