mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
feat: split different filetypes
This commit is contained in:
@ -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<CodeFileEditorProps> = ({ language, theme, value, onChange, onMount }) => {
|
||||
return (
|
||||
<Editor
|
||||
language={language}
|
||||
theme={theme}
|
||||
value={value}
|
||||
loading={<Loading type="area" />}
|
||||
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)
|
||||
@ -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<MarkdownFileEditorProps> = ({ title, value, onChange }) => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<PromptEditor
|
||||
title={title}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="h-full"
|
||||
editorContainerClassName="h-full"
|
||||
containerBackgroundClassName="bg-components-panel-bg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(MarkdownFileEditor)
|
||||
@ -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<MediaFilePreviewProps> = ({ type, src }) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
|
||||
if (!src) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center text-text-tertiary">
|
||||
<span className="system-sm-regular">
|
||||
{t('skillEditor.previewUnavailable')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center p-6">
|
||||
{type === 'image' && (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
className="max-h-full max-w-full object-contain"
|
||||
/>
|
||||
)}
|
||||
{type === 'video' && (
|
||||
<video
|
||||
src={src}
|
||||
controls
|
||||
className="max-h-full max-w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(MediaFilePreview)
|
||||
@ -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 (
|
||||
<div className="flex h-full w-full items-center justify-center text-text-tertiary">
|
||||
<span className="system-sm-regular">
|
||||
{t('skillEditor.officePlaceholder')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(OfficeFilePlaceholder)
|
||||
@ -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<UnsupportedFileDownloadProps> = ({ 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 (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex w-full max-w-[360px] flex-col items-center gap-3 pb-0 pr-2 pt-1">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<FileTypeIcon type={FileAppearanceTypeEnum.custom} size="xl" className="size-16 text-text-tertiary" />
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<p className="system-md-medium text-text-secondary">{name}</p>
|
||||
{fileSize && (
|
||||
<p className="system-xs-regular text-text-tertiary">{fileSize}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-px w-64 bg-components-panel-border-subtle" />
|
||||
<p className="system-sm-regular text-center text-text-tertiary">
|
||||
{t('skillEditor.unsupportedPreview')}
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
onClick={handleDownload}
|
||||
disabled={!downloadUrl}
|
||||
>
|
||||
{t('operation.download', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(UnsupportedFileDownload)
|
||||
@ -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 (
|
||||
<div className="h-full w-full overflow-hidden bg-components-panel-bg">
|
||||
<Editor
|
||||
language={language}
|
||||
theme={isMounted ? theme : 'default-theme'}
|
||||
value={currentContent}
|
||||
loading={<Loading type="area" />}
|
||||
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 && (
|
||||
<MarkdownFileEditor
|
||||
title={fileName}
|
||||
value={currentContent}
|
||||
onChange={handleEditorChange}
|
||||
/>
|
||||
)}
|
||||
{isCodeOrText && (
|
||||
<CodeFileEditor
|
||||
language={language}
|
||||
theme={isMounted ? theme : 'default-theme'}
|
||||
value={currentContent}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
)}
|
||||
{(isImage || isVideo) && (
|
||||
<MediaFilePreview
|
||||
type={isImage ? 'image' : 'video'}
|
||||
src={previewUrl}
|
||||
/>
|
||||
)}
|
||||
{isOffice && (
|
||||
<OfficeFilePlaceholder />
|
||||
)}
|
||||
{!isMarkdown && !isCodeOrText && !isImage && !isVideo && !isOffice && (
|
||||
<UnsupportedFileDownload
|
||||
name={fileName}
|
||||
size={fileSize}
|
||||
downloadUrl={previewUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -989,6 +989,9 @@
|
||||
"singleRun.testRun": "测试运行",
|
||||
"singleRun.testRunIteration": "测试运行迭代",
|
||||
"singleRun.testRunLoop": "测试运行循环",
|
||||
"skillEditor.officePlaceholder": "预览功能将在后续版本支持",
|
||||
"skillEditor.previewUnavailable": "无法预览",
|
||||
"skillEditor.unsupportedPreview": "该文件类型不支持预览",
|
||||
"skillSidebar.addFile": "上传文件",
|
||||
"skillSidebar.addFolder": "新建文件夹",
|
||||
"skillSidebar.dropTip": "拖放文件到此处上传",
|
||||
|
||||
Reference in New Issue
Block a user