'use client' import type { OnMount } from '@monaco-editor/react' import type { FC } from 'react' import { loader } from '@monaco-editor/react' import dynamic from 'next/dynamic' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' import { START_TAB_ID } from './constants' import CodeFileEditor from './editor/code-file-editor' import MarkdownFileEditor from './editor/markdown-file-editor' import { useFileTypeInfo } from './hooks/use-file-type-info' import { useSkillAssetNodeMap } from './hooks/use-skill-asset-tree' import { useSkillFileData } from './hooks/use-skill-file-data' import { useSkillSaveManager } from './hooks/use-skill-save-manager' import StartTabContent from './start-tab' import { getFileLanguage } from './utils/file-utils' import MediaFilePreview from './viewer/media-file-preview' import UnsupportedFileDownload from './viewer/unsupported-file-download' const SQLiteFilePreview = dynamic( () => import('./viewer/sqlite-file-preview'), { ssr: false, loading: () => }, ) if (typeof window !== 'undefined') loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } }) const FileContentPanel: FC = () => { const { t } = useTranslation('workflow') const { theme: appTheme } = useTheme() const [isMounted, setIsMounted] = useState(false) const editorRef = useRef[0] | null>(null) const appDetail = useAppStore(s => s.appDetail) const appId = appDetail?.id || '' const activeTabId = useStore(s => s.activeTabId) const storeApi = useWorkflowStore() const { data: nodeMap } = useSkillAssetNodeMap() const isStartTab = activeTabId === START_TAB_ID const fileTabId = isStartTab ? null : activeTabId const draftContent = useStore(s => fileTabId ? s.dirtyContents.get(fileTabId) : undefined) const currentMetadata = useStore(s => fileTabId ? s.fileMetadata.get(fileTabId) : undefined) const isMetadataDirty = useStore(s => fileTabId ? s.dirtyMetadataIds.has(fileTabId) : false) const currentFileNode = fileTabId ? nodeMap?.get(fileTabId) : undefined const { isMarkdown, isCodeOrText, isImage, isVideo, isSQLite, isEditable } = useFileTypeInfo(currentFileNode) const { fileContent, downloadUrlData, isLoading, error } = useSkillFileData(appId, fileTabId, isEditable) const originalContent = fileContent?.content ?? '' const currentContent = draftContent !== undefined ? draftContent : originalContent useEffect(() => { if (!fileTabId || !fileContent) return if (isMetadataDirty) return let nextMetadata: Record = {} if (fileContent.metadata) { if (typeof fileContent.metadata === 'string') { try { nextMetadata = JSON.parse(fileContent.metadata) } catch { nextMetadata = {} } } else { nextMetadata = fileContent.metadata } } const { setFileMetadata, clearDraftMetadata } = storeApi.getState() setFileMetadata(fileTabId, nextMetadata) clearDraftMetadata(fileTabId) }, [fileTabId, isMetadataDirty, fileContent, storeApi]) const handleEditorChange = useCallback((value: string | undefined) => { if (!fileTabId || !isEditable) return const newValue = value ?? '' if (newValue === originalContent) storeApi.getState().clearDraftContent(fileTabId) else storeApi.getState().setDraftContent(fileTabId, newValue) storeApi.getState().pinTab(fileTabId) }, [fileTabId, isEditable, originalContent, storeApi]) const { saveFile, registerFallback, unregisterFallback } = useSkillSaveManager() const fallbackRef = useRef({ content: originalContent, metadata: currentMetadata }) useEffect(() => { if (!fileTabId || fileContent?.content === undefined) return const fallback = { content: originalContent, metadata: currentMetadata } fallbackRef.current = fallback registerFallback(fileTabId, fallback) return () => { unregisterFallback(fileTabId) } }, [fileTabId, fileContent?.content, originalContent, currentMetadata, registerFallback, unregisterFallback]) useEffect(() => { if (!fileTabId || !isEditable) return return () => { const { content: fallbackContent, metadata: fallbackMetadata } = fallbackRef.current void saveFile(fileTabId, { fallbackContent, fallbackMetadata, }) } }, [fileTabId, isEditable, saveFile]) const handleEditorDidMount: OnMount = useCallback((editor, monaco) => { editorRef.current = editor monaco.editor.setTheme(appTheme === Theme.light ? 'light' : 'vs-dark') setIsMounted(true) }, [appTheme]) const language = currentFileNode ? getFileLanguage(currentFileNode.name) : 'plaintext' const theme = appTheme === Theme.light ? 'light' : 'vs-dark' if (isStartTab) return if (!fileTabId) { return (
{t('skillSidebar.empty')}
) } if (isLoading) { return (
) } if (error) { return (
{t('skillSidebar.loadError')}
) } // For non-editable files (media, sqlite, unsupported), use download URL const downloadUrl = downloadUrlData?.download_url || '' const fileName = currentFileNode?.name || '' const fileSize = currentFileNode?.size const isUnsupportedFile = !isMarkdown && !isCodeOrText && !isImage && !isVideo && !isSQLite return (
{isMarkdown ? ( ) : null} {isCodeOrText ? ( ) : null} {isImage || isVideo ? ( ) : null} {isSQLite ? ( ) : null} {isUnsupportedFile ? ( ) : null}
) } export default React.memo(FileContentPanel)