diff --git a/web/app/components/workflow/skill/editor/code-file-editor.tsx b/web/app/components/workflow/skill/editor/code-file-editor.tsx index 9b6aa12c3b..216576e1d4 100644 --- a/web/app/components/workflow/skill/editor/code-file-editor.tsx +++ b/web/app/components/workflow/skill/editor/code-file-editor.tsx @@ -12,6 +12,7 @@ type CodeFileEditorProps = { onMount: OnMount fileId?: string | null collaborationEnabled?: boolean + readOnly?: boolean } const CodeFileEditor = ({ @@ -22,12 +23,13 @@ const CodeFileEditor = ({ onMount, fileId, collaborationEnabled, + readOnly, }: CodeFileEditorProps) => { const [editorInstance, setEditorInstance] = React.useState[0] | null>(null) const { overlay } = useSkillCodeCursors({ editor: editorInstance, fileId: fileId ?? null, - enabled: Boolean(collaborationEnabled && fileId), + enabled: Boolean(collaborationEnabled && fileId && !readOnly), }) const handleMount = React.useCallback((editor, monaco) => { setEditorInstance(editor) @@ -53,6 +55,7 @@ const CodeFileEditor = ({ fontSize: 13, lineHeight: 20, padding: { top: 12, bottom: 12 }, + readOnly, }} onMount={handleMount} /> diff --git a/web/app/components/workflow/skill/editor/markdown-file-editor.tsx b/web/app/components/workflow/skill/editor/markdown-file-editor.tsx index df3d9045eb..a61f139123 100644 --- a/web/app/components/workflow/skill/editor/markdown-file-editor.tsx +++ b/web/app/components/workflow/skill/editor/markdown-file-editor.tsx @@ -7,6 +7,7 @@ type MarkdownFileEditorProps = { value: string onChange: (value: string) => void collaborationEnabled?: boolean + readOnly?: boolean } const MarkdownFileEditor = ({ @@ -14,6 +15,7 @@ const MarkdownFileEditor = ({ value, onChange, collaborationEnabled, + readOnly, }: MarkdownFileEditorProps) => { const { t } = useTranslation() const handleChange = React.useCallback((val: string) => { @@ -28,23 +30,26 @@ const MarkdownFileEditor = ({ instanceId={instanceId} value={value} onChange={handleChange} - collaborationEnabled={collaborationEnabled} + editable={!readOnly} + collaborationEnabled={readOnly ? false : collaborationEnabled} showLineNumbers className="h-full" wrapperClassName="h-full" - placeholder={( - - {t('promptEditor.skillMarkdown.placeholderPrefix', { ns: 'common' })} - / - - {t('promptEditor.skillMarkdown.placeholderReferenceFiles', { ns: 'common' })} - - @ - - {t('promptEditor.skillMarkdown.placeholderUseTools', { ns: 'common' })} - - - )} + placeholder={readOnly + ? undefined + : ( + + {t('promptEditor.skillMarkdown.placeholderPrefix', { ns: 'common' })} + / + + {t('promptEditor.skillMarkdown.placeholderReferenceFiles', { ns: 'common' })} + + @ + + {t('promptEditor.skillMarkdown.placeholderUseTools', { ns: 'common' })} + + + )} /> ) diff --git a/web/app/components/workflow/skill/hooks/use-fetch-text-content.ts b/web/app/components/workflow/skill/hooks/use-fetch-text-content.ts new file mode 100644 index 0000000000..9b950fd1eb --- /dev/null +++ b/web/app/components/workflow/skill/hooks/use-fetch-text-content.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query' + +export function useFetchTextContent(downloadUrl: string | undefined) { + return useQuery({ + queryKey: ['fileTextContent', downloadUrl], + queryFn: () => fetch(downloadUrl!).then(r => r.text()), + enabled: !!downloadUrl, + staleTime: Infinity, + }) +} diff --git a/web/app/components/workflow/skill/hooks/use-file-type-info.ts b/web/app/components/workflow/skill/hooks/use-file-type-info.ts index 2e888f79c8..a867950c44 100644 --- a/web/app/components/workflow/skill/hooks/use-file-type-info.ts +++ b/web/app/components/workflow/skill/hooks/use-file-type-info.ts @@ -1,4 +1,3 @@ -import type { AppAssetTreeView } from '@/types/app-asset' import { useMemo } from 'react' import { getFileExtension, @@ -19,9 +18,9 @@ export type FileTypeInfo = { isMediaFile: boolean } -export function useFileTypeInfo(fileNode: AppAssetTreeView | undefined): FileTypeInfo { +export function useFileTypeInfo(fileNode: { name: string, extension?: string | null } | undefined): FileTypeInfo { return useMemo(() => { - const ext = getFileExtension(fileNode?.name, fileNode?.extension) + const ext = getFileExtension(fileNode?.name, fileNode?.extension ?? undefined) const markdown = isMarkdownFile(ext) const image = isImageFile(ext) const video = isVideoFile(ext) diff --git a/web/app/components/workflow/skill/viewer/read-only-code-preview.tsx b/web/app/components/workflow/skill/viewer/read-only-code-preview.tsx new file mode 100644 index 0000000000..54ca616da7 --- /dev/null +++ b/web/app/components/workflow/skill/viewer/read-only-code-preview.tsx @@ -0,0 +1,47 @@ +'use client' + +import type { OnMount } from '@monaco-editor/react' +import { loader } from '@monaco-editor/react' +import * as React from 'react' +import { useCallback, useRef, useState } from 'react' +import useTheme from '@/hooks/use-theme' +import { Theme } from '@/types/app' +import { basePath } from '@/utils/var' +import CodeFileEditor from '../editor/code-file-editor' + +if (typeof window !== 'undefined') + loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } }) + +type ReadOnlyCodePreviewProps = { + value: string + language: string +} + +const ReadOnlyCodePreview = ({ value, language }: ReadOnlyCodePreviewProps) => { + const { theme: appTheme } = useTheme() + const [isMounted, setIsMounted] = useState(false) + const editorRef = useRef[0] | null>(null) + + const theme = appTheme === Theme.light ? 'light' : 'vs-dark' + + const handleMount: OnMount = useCallback((editor, monaco) => { + editorRef.current = editor + monaco.editor.setTheme(appTheme === Theme.light ? 'light' : 'vs-dark') + setIsMounted(true) + }, [appTheme]) + + const noop = useCallback(() => {}, []) + + return ( + + ) +} + +export default React.memo(ReadOnlyCodePreview) diff --git a/web/app/components/workflow/skill/viewer/read-only-file-preview.tsx b/web/app/components/workflow/skill/viewer/read-only-file-preview.tsx new file mode 100644 index 0000000000..deb5c9a060 --- /dev/null +++ b/web/app/components/workflow/skill/viewer/read-only-file-preview.tsx @@ -0,0 +1,80 @@ +'use client' + +import dynamic from 'next/dynamic' +import * as React from 'react' +import Loading from '@/app/components/base/loading' +import { useFetchTextContent } from '../hooks/use-fetch-text-content' +import { useFileTypeInfo } from '../hooks/use-file-type-info' +import { getFileLanguage } from '../utils/file-utils' +import MediaFilePreview from './media-file-preview' +import UnsupportedFileDownload from './unsupported-file-download' + +const ReadOnlyCodePreview = dynamic( + () => import('./read-only-code-preview'), + { ssr: false, loading: () => }, +) + +const ReadOnlyMarkdownPreview = dynamic( + () => import('./read-only-markdown-preview'), + { ssr: false, loading: () => }, +) + +const SQLiteFilePreview = dynamic( + () => import('./sqlite-file-preview'), + { ssr: false, loading: () => }, +) + +type ReadOnlyFilePreviewProps = { + downloadUrl: string + fileName: string + extension?: string | null + fileSize?: number | null +} + +const ReadOnlyFilePreview = ({ + downloadUrl, + fileName, + extension, + fileSize, +}: ReadOnlyFilePreviewProps) => { + const fileNode = React.useMemo( + () => ({ name: fileName, extension }), + [fileName, extension], + ) + const { isMarkdown, isCodeOrText, isImage, isVideo, isSQLite } = useFileTypeInfo(fileNode) + const isTextFile = isMarkdown || isCodeOrText + const { data: textContent, isLoading: isTextLoading } = useFetchTextContent( + isTextFile ? downloadUrl : undefined, + ) + + if (isTextFile && isTextLoading) + return + + if (isMarkdown) + return + + if (isCodeOrText) { + return ( + + ) + } + + if (isImage || isVideo) + return + + if (isSQLite) + return + + return ( + + ) +} + +export default React.memo(ReadOnlyFilePreview) diff --git a/web/app/components/workflow/skill/viewer/read-only-markdown-preview.tsx b/web/app/components/workflow/skill/viewer/read-only-markdown-preview.tsx new file mode 100644 index 0000000000..8c6aaa4970 --- /dev/null +++ b/web/app/components/workflow/skill/viewer/read-only-markdown-preview.tsx @@ -0,0 +1,22 @@ +'use client' + +import * as React from 'react' +import MarkdownFileEditor from '../editor/markdown-file-editor' + +type ReadOnlyMarkdownPreviewProps = { + value: string +} + +const noop = () => {} + +const ReadOnlyMarkdownPreview = ({ value }: ReadOnlyMarkdownPreviewProps) => { + return ( + + ) +} + +export default React.memo(ReadOnlyMarkdownPreview) diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx index 41235d1137..bfec17b113 100644 --- a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx +++ b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx @@ -11,9 +11,10 @@ import SearchLinesSparkle from '@/app/components/base/icons/src/vender/knowledge 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-tree' +import ReadOnlyFilePreview from '@/app/components/workflow/skill/viewer/read-only-file-preview' import { useAppContext } from '@/context/app-context' import { useDocLink } from '@/context/i18n' -import { useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file' +import { useDownloadSandboxFile, useSandboxFileDownloadUrl, useSandboxFilesTree } from '@/service/use-sandbox-file' import { cn } from '@/utils/classnames' import InspectLayout from './inspect-layout' import SplitPanel from './split-panel' @@ -60,8 +61,11 @@ const ArtifactsTab = (headerProps: InspectHeaderProps) => { enabled: !!sandboxId, }) const downloadMutation = useDownloadSandboxFile(sandboxId) - const [selectedFile, setSelectedFile] = useState(null) + const { data: downloadUrlData, isLoading: isDownloadUrlLoading } = useSandboxFileDownloadUrl( + sandboxId, + selectedFile?.path, + ) const handleFileSelect = useCallback((node: SandboxFileTreeNode) => { if (node.node_type === 'file') @@ -172,12 +176,25 @@ const ArtifactsTab = (headerProps: InspectHeaderProps) => {
{file ? ( -
-
-

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

-
+
+ {isDownloadUrlLoading + ? + : downloadUrlData?.download_url + ? ( + + ) + : ( +
+

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

+
+ )}
) : ( diff --git a/web/service/use-sandbox-file.ts b/web/service/use-sandbox-file.ts index 983cccd05b..9603f8d20c 100644 --- a/web/service/use-sandbox-file.ts +++ b/web/service/use-sandbox-file.ts @@ -53,6 +53,20 @@ export function useDownloadSandboxFile(sandboxId: string | undefined) { }) } +export function useSandboxFileDownloadUrl( + sandboxId: string | undefined, + path: string | undefined, +) { + return useQuery({ + queryKey: ['sandboxFileDownloadUrl', sandboxId, path], + queryFn: () => consoleClient.sandboxFile.downloadFile({ + params: { sandboxId: sandboxId! }, + body: { path: path! }, + }), + enabled: !!sandboxId && !!path, + }) +} + function buildTreeFromFlatList(nodes: SandboxFileNode[]): SandboxFileTreeNode[] { const nodeMap = new Map() const roots: SandboxFileTreeNode[] = []