From 2fb888391800255725b7ac861c66980a0226fea0 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 15 Jan 2026 14:52:42 +0800 Subject: [PATCH] feat: split different filetypes --- .../skill/editor/code-file-editor.tsx | 39 +++++++++ .../skill/editor/markdown-file-editor.tsx | 26 ++++++ .../skill/editor/media-file-preview.tsx | 43 ++++++++++ .../skill/editor/office-file-placeholder.tsx | 17 ++++ .../editor/unsupported-file-download.tsx | 55 +++++++++++++ .../workflow/skill/skill-doc-editor.tsx | 82 +++++++++++++------ web/app/components/workflow/skill/utils.ts | 25 +++++- web/i18n/en-US/workflow.json | 3 + web/i18n/zh-Hans/workflow.json | 3 + 9 files changed, 265 insertions(+), 28 deletions(-) create mode 100644 web/app/components/workflow/skill/editor/code-file-editor.tsx create mode 100644 web/app/components/workflow/skill/editor/markdown-file-editor.tsx create mode 100644 web/app/components/workflow/skill/editor/media-file-preview.tsx create mode 100644 web/app/components/workflow/skill/editor/office-file-placeholder.tsx create mode 100644 web/app/components/workflow/skill/editor/unsupported-file-download.tsx diff --git a/web/app/components/workflow/skill/editor/code-file-editor.tsx b/web/app/components/workflow/skill/editor/code-file-editor.tsx new file mode 100644 index 0000000000..650d6d13f9 --- /dev/null +++ b/web/app/components/workflow/skill/editor/code-file-editor.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react' +import Editor from '@monaco-editor/react' +import * as React from 'react' +import Loading from '@/app/components/base/loading' + +type CodeFileEditorProps = { + language: string + theme: string + value: string + onChange: (value: string | undefined) => void + onMount: (editor: any, monaco: any) => void +} + +const CodeFileEditor: FC = ({ language, theme, value, onChange, onMount }) => { + return ( + } + onChange={onChange} + options={{ + minimap: { enabled: false }, + lineNumbersMinChars: 3, + wordWrap: 'on', + unicodeHighlight: { + ambiguousCharacters: false, + }, + stickyScroll: { enabled: false }, + fontSize: 13, + lineHeight: 20, + padding: { top: 12, bottom: 12 }, + }} + onMount={onMount} + /> + ) +} + +export default React.memo(CodeFileEditor) diff --git a/web/app/components/workflow/skill/editor/markdown-file-editor.tsx b/web/app/components/workflow/skill/editor/markdown-file-editor.tsx new file mode 100644 index 0000000000..7744c21584 --- /dev/null +++ b/web/app/components/workflow/skill/editor/markdown-file-editor.tsx @@ -0,0 +1,26 @@ +import type { FC } from 'react' +import * as React from 'react' +import PromptEditor from '@/app/components/workflow/nodes/_base/components/prompt/editor' + +type MarkdownFileEditorProps = { + title: string + value: string + onChange: (value: string) => void +} + +const MarkdownFileEditor: FC = ({ title, value, onChange }) => { + return ( +
+ +
+ ) +} + +export default React.memo(MarkdownFileEditor) diff --git a/web/app/components/workflow/skill/editor/media-file-preview.tsx b/web/app/components/workflow/skill/editor/media-file-preview.tsx new file mode 100644 index 0000000000..13aa675b5f --- /dev/null +++ b/web/app/components/workflow/skill/editor/media-file-preview.tsx @@ -0,0 +1,43 @@ +import type { FC } from 'react' +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +type MediaFilePreviewProps = { + type: 'image' | 'video' + src: string +} + +const MediaFilePreview: FC = ({ type, src }) => { + const { t } = useTranslation('workflow') + + if (!src) { + return ( +
+ + {t('skillEditor.previewUnavailable')} + +
+ ) + } + + return ( +
+ {type === 'image' && ( + + )} + {type === 'video' && ( +
+ ) +} + +export default React.memo(MediaFilePreview) diff --git a/web/app/components/workflow/skill/editor/office-file-placeholder.tsx b/web/app/components/workflow/skill/editor/office-file-placeholder.tsx new file mode 100644 index 0000000000..d9ea429f57 --- /dev/null +++ b/web/app/components/workflow/skill/editor/office-file-placeholder.tsx @@ -0,0 +1,17 @@ +import type { FC } from 'react' +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +const OfficeFilePlaceholder: FC = () => { + const { t } = useTranslation('workflow') + + return ( +
+ + {t('skillEditor.officePlaceholder')} + +
+ ) +} + +export default React.memo(OfficeFilePlaceholder) diff --git a/web/app/components/workflow/skill/editor/unsupported-file-download.tsx b/web/app/components/workflow/skill/editor/unsupported-file-download.tsx new file mode 100644 index 0000000000..fcbb5a4f6e --- /dev/null +++ b/web/app/components/workflow/skill/editor/unsupported-file-download.tsx @@ -0,0 +1,55 @@ +import type { FC } from 'react' +import * as React from 'react' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon' +import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' +import { formatFileSize } from '@/utils/format' + +type UnsupportedFileDownloadProps = { + name: string + size?: number + downloadUrl?: string +} + +const UnsupportedFileDownload: FC = ({ name, size, downloadUrl }) => { + const { t } = useTranslation('workflow') + const fileSize = size ? formatFileSize(size) : '' + + const handleDownload = useCallback(() => { + if (!downloadUrl || typeof window === 'undefined') + return + window.open(downloadUrl, '_blank', 'noopener,noreferrer') + }, [downloadUrl]) + + return ( +
+
+
+ +
+

{name}

+ {fileSize && ( +

{fileSize}

+ )} +
+
+
+

+ {t('skillEditor.unsupportedPreview')} +

+ +
+
+ ) +} + +export default React.memo(UnsupportedFileDownload) diff --git a/web/app/components/workflow/skill/skill-doc-editor.tsx b/web/app/components/workflow/skill/skill-doc-editor.tsx index f2dab8a5d2..fa645dc096 100644 --- a/web/app/components/workflow/skill/skill-doc-editor.tsx +++ b/web/app/components/workflow/skill/skill-doc-editor.tsx @@ -3,7 +3,7 @@ import type { OnMount } from '@monaco-editor/react' import type { FC } from 'react' import type { AppAssetTreeView } from './type' -import Editor, { loader } from '@monaco-editor/react' +import { loader } from '@monaco-editor/react' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -14,9 +14,14 @@ import useTheme from '@/hooks/use-theme' import { useGetAppAssetFileContent, useGetAppAssetTree, useUpdateAppAssetFileContent } from '@/service/use-app-asset' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' +import CodeFileEditor from './editor/code-file-editor' +import MarkdownFileEditor from './editor/markdown-file-editor' +import MediaFilePreview from './editor/media-file-preview' +import OfficeFilePlaceholder from './editor/office-file-placeholder' +import UnsupportedFileDownload from './editor/unsupported-file-download' import { useSkillEditorStore, useSkillEditorStoreApi } from './store' import { buildNodeMap } from './type' -import { getFileLanguage } from './utils' +import { getFileExtension, getFileLanguage, isCodeOrTextFile, isImageFile, isMarkdownFile, isOfficeFile, isVideoFile } from './utils' // load file from local instead of cdn if (typeof window !== 'undefined') @@ -64,6 +69,15 @@ const SkillDocEditor: FC = () => { // Get current file node const currentFileNode = activeTabId ? nodeMap.get(activeTabId) : undefined + const fileExtension = useMemo(() => { + return getFileExtension(currentFileNode?.name, currentFileNode?.extension) + }, [currentFileNode?.extension, currentFileNode?.name]) + const isMarkdown = useMemo(() => isMarkdownFile(fileExtension), [fileExtension]) + const isCodeOrText = useMemo(() => isCodeOrTextFile(fileExtension), [fileExtension]) + const isImage = useMemo(() => isImageFile(fileExtension), [fileExtension]) + const isVideo = useMemo(() => isVideoFile(fileExtension), [fileExtension]) + const isOffice = useMemo(() => isOfficeFile(fileExtension), [fileExtension]) + const isEditable = isMarkdown || isCodeOrText // Fetch file content from API const { @@ -89,15 +103,15 @@ const SkillDocEditor: FC = () => { // Handle editor content change const handleEditorChange = useCallback((value: string | undefined) => { - if (!activeTabId) + if (!activeTabId || !isEditable) return // Set draft content in store storeApi.getState().setDraftContent(activeTabId, value ?? '') - }, [activeTabId, storeApi]) + }, [activeTabId, isEditable, storeApi]) // Handle save const handleSave = useCallback(async () => { - if (!activeTabId || !appId) + if (!activeTabId || !appId || !isEditable) return const content = dirtyContents.get(activeTabId) @@ -123,7 +137,7 @@ const SkillDocEditor: FC = () => { message: String(error), }) } - }, [activeTabId, appId, dirtyContents, storeApi, t, updateContent]) + }, [activeTabId, appId, dirtyContents, isEditable, storeApi, t, updateContent]) // Handle keyboard shortcuts useEffect(() => { @@ -189,28 +203,44 @@ const SkillDocEditor: FC = () => { ) } + const previewUrl = fileContent?.content || '' + const fileName = currentFileNode?.name || '' + const fileSize = currentFileNode?.size + return (
- } - onChange={handleEditorChange} - options={{ - minimap: { enabled: false }, - lineNumbersMinChars: 3, - wordWrap: 'on', - unicodeHighlight: { - ambiguousCharacters: false, - }, - stickyScroll: { enabled: false }, - fontSize: 13, - lineHeight: 20, - padding: { top: 12, bottom: 12 }, - }} - onMount={handleEditorDidMount} - /> + {isMarkdown && ( + + )} + {isCodeOrText && ( + + )} + {(isImage || isVideo) && ( + + )} + {isOffice && ( + + )} + {!isMarkdown && !isCodeOrText && !isImage && !isVideo && !isOffice && ( + + )}
) } diff --git a/web/app/components/workflow/skill/utils.ts b/web/app/components/workflow/skill/utils.ts index 0e4497fbf8..bfcaf2d445 100644 --- a/web/app/components/workflow/skill/utils.ts +++ b/web/app/components/workflow/skill/utils.ts @@ -1,17 +1,38 @@ import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' +const MARKDOWN_EXTENSIONS = ['md', 'markdown', 'mdx'] +const CODE_EXTENSIONS = ['json', 'yaml', 'yml', 'toml', 'js', 'jsx', 'ts', 'tsx', 'py', 'schema'] +const TEXT_EXTENSIONS = ['txt', 'log', 'ini', 'env'] +const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico'] +const VIDEO_EXTENSIONS = ['mp4', 'mov', 'webm', 'mpeg', 'mpg', 'm4v', 'avi'] +const OFFICE_EXTENSIONS = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'] + +export const getFileExtension = (name?: string, extension?: string) => { + if (extension) + return extension.toLowerCase() + if (!name) + return '' + return name.split('.').pop()?.toLowerCase() ?? '' +} + export const getFileIconType = (name: string) => { const extension = name.split('.').pop()?.toLowerCase() ?? '' - if (['md', 'markdown', 'mdx'].includes(extension)) + if (MARKDOWN_EXTENSIONS.includes(extension)) return FileAppearanceTypeEnum.markdown - if (['json', 'yaml', 'yml', 'toml', 'js', 'jsx', 'ts', 'tsx', 'py', 'schema'].includes(extension)) + if (CODE_EXTENSIONS.includes(extension)) return FileAppearanceTypeEnum.code return FileAppearanceTypeEnum.document } +export const isMarkdownFile = (extension: string) => MARKDOWN_EXTENSIONS.includes(extension) +export const isCodeOrTextFile = (extension: string) => CODE_EXTENSIONS.includes(extension) || TEXT_EXTENSIONS.includes(extension) +export const isImageFile = (extension: string) => IMAGE_EXTENSIONS.includes(extension) +export const isVideoFile = (extension: string) => VIDEO_EXTENSIONS.includes(extension) +export const isOfficeFile = (extension: string) => OFFICE_EXTENSIONS.includes(extension) + /** * Get Monaco editor language from file name extension */ diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 4c2bad1ddc..a0c0fe9bbd 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -995,6 +995,9 @@ "singleRun.testRun": "Test Run", "singleRun.testRunIteration": "Test Run Iteration", "singleRun.testRunLoop": "Test Run Loop", + "skillEditor.officePlaceholder": "Preview will be supported in a future update", + "skillEditor.previewUnavailable": "Preview unavailable", + "skillEditor.unsupportedPreview": "This file type is not supported for preview", "skillSidebar.addFile": "Upload File", "skillSidebar.addFolder": "New Folder", "skillSidebar.dropTip": "Drop files here to upload", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 63ed8689c9..6bbb60dd14 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -989,6 +989,9 @@ "singleRun.testRun": "测试运行", "singleRun.testRunIteration": "测试运行迭代", "singleRun.testRunLoop": "测试运行循环", + "skillEditor.officePlaceholder": "预览功能将在后续版本支持", + "skillEditor.previewUnavailable": "无法预览", + "skillEditor.unsupportedPreview": "该文件类型不支持预览", "skillSidebar.addFile": "上传文件", "skillSidebar.addFolder": "新建文件夹", "skillSidebar.dropTip": "拖放文件到此处上传",