diff --git a/web/app/components/workflow/skill/file-tree/artifacts-section.tsx b/web/app/components/workflow/skill/file-tree/artifacts-section.tsx index faf4c2f692..6fc2448e0d 100644 --- a/web/app/components/workflow/skill/file-tree/artifacts-section.tsx +++ b/web/app/components/workflow/skill/file-tree/artifacts-section.tsx @@ -1,66 +1,109 @@ 'use client' import type { FC } from 'react' -import { RiArrowRightSLine } from '@remixicon/react' +import type { SandboxFileTreeNode } from '@/types/sandbox-file' +import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import * as React from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import FolderSpark from '@/app/components/base/icons/src/vender/workflow/FolderSpark' +import { useAppContext } from '@/context/app-context' +import { useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file' import { cn } from '@/utils/classnames' +import ArtifactsTree from './artifacts-tree' type ArtifactsSectionProps = { className?: string } -/** - * Artifacts section component for file tree. - * Shows the artifacts folder with badge and navigation arrow. - * Clicking expands to show artifact files from test runs. - * Placeholder implementation - functionality to be added later. - */ const ArtifactsSection: FC = ({ className }) => { const { t } = useTranslation('workflow') - // TODO: Replace with actual data - const badgeText = 'Test Run#3' - const hasNewFiles = true + const { userProfile } = useAppContext() + const sandboxId = userProfile?.id + + const [isExpanded, setIsExpanded] = useState(false) + + const { data: treeData, hasFiles, isLoading } = useSandboxFilesTree(sandboxId, { + enabled: isExpanded, + }) + + const downloadMutation = useDownloadSandboxFile(sandboxId) + + const handleToggle = useCallback(() => { + setIsExpanded(prev => !prev) + }, []) + + const handleDownload = useCallback(async (node: SandboxFileTreeNode) => { + try { + const ticket = await downloadMutation.mutateAsync(node.path) + window.open(ticket.download_url, '_blank') + } + catch (error) { + console.error('Download failed:', error) + } + }, [downloadMutation]) + + const showBlueDot = !isExpanded && hasFiles return (
+ + {isExpanded && ( +
+ {isLoading + ? ( +
+
+
+ ) + : hasFiles + ? ( + + ) + : ( +
+
+

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

+
+
+ )} +
+ )}
) } diff --git a/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx b/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx new file mode 100644 index 0000000000..8fb03b0efe --- /dev/null +++ b/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx @@ -0,0 +1,130 @@ +'use client' + +import type { FC } from 'react' +import type { SandboxFileTreeNode } from '@/types/sandbox-file' +import { RiDownloadLine, RiFile3Fill, RiFolderLine } from '@remixicon/react' +import * as React from 'react' +import { useCallback, useState } from 'react' +import { cn } from '@/utils/classnames' + +type ArtifactsTreeProps = { + data: SandboxFileTreeNode[] | undefined + onDownload: (node: SandboxFileTreeNode) => void + isDownloading?: boolean +} + +type ArtifactsTreeNodeProps = { + node: SandboxFileTreeNode + depth: number + onDownload: (node: SandboxFileTreeNode) => void + isDownloading?: boolean +} + +const ArtifactsTreeNode: FC = ({ + node, + depth, + onDownload, + isDownloading, +}) => { + const [isExpanded, setIsExpanded] = useState(false) + const isFolder = node.node_type === 'folder' + const hasChildren = isFolder && node.children.length > 0 + + const handleToggle = useCallback(() => { + if (isFolder) + setIsExpanded(prev => !prev) + }, [isFolder]) + + const handleDownload = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + onDownload(node) + }, [node, onDownload]) + + return ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') + handleToggle() + } + : undefined} + className={cn( + 'group flex items-center gap-0 rounded-md py-0.5 pr-1.5', + isFolder && 'cursor-pointer hover:bg-state-base-hover', + !isFolder && 'hover:bg-state-base-hover', + )} + style={{ paddingLeft: `${8 + depth * 20}px` }} + > +
+ {isFolder + ? + : } +
+ + + {node.name} + + + {!isFolder && ( + + )} +
+ + {isFolder && isExpanded && hasChildren && ( +
+ {node.children.map(child => ( + + ))} +
+ )} +
+ ) +} + +const ArtifactsTree: FC = ({ + data, + onDownload, + isDownloading, +}) => { + if (!data || data.length === 0) + return null + + return ( +
+ {data.map(node => ( + + ))} +
+ ) +} + +export default React.memo(ArtifactsTree) diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 20fda0b89c..9cee8be6dc 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -1069,6 +1069,7 @@ "skillEditor.unsupportedPreview": "This file type is not supported for preview", "skillSidebar.addFile": "Upload File", "skillSidebar.addFolder": "New Folder", + "skillSidebar.artifacts.emptyState": "Files generated by the agent during test runs. They may be automatically cleared later.", "skillSidebar.artifacts.openArtifacts": "Open artifacts", "skillSidebar.artifacts.title": "Artifacts", "skillSidebar.dragAction.moveTo": "Move to ", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 0f1ca368c1..e922562826 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -1061,6 +1061,7 @@ "skillEditor.unsupportedPreview": "该文件类型不支持预览", "skillSidebar.addFile": "上传文件", "skillSidebar.addFolder": "新建文件夹", + "skillSidebar.artifacts.emptyState": "代理在测试运行期间生成的文件。这些文件可能会在稍后自动清除。", "skillSidebar.artifacts.openArtifacts": "打开产物文件夹", "skillSidebar.artifacts.title": "产物", "skillSidebar.dragAction.moveTo": "移动到 ",