diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 5b236b28..ed66f23b 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -42,7 +42,7 @@ import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { useFileSearchStore } from '@/stores/useFileSearchStore'; import { useDeviceInfo } from '@/lib/device'; import { cn, getModifierLabel, getRevealLabelKey, hasModifier } from '@/lib/utils'; -import { getLanguageFromExtension, getImageMimeType, isDrawioFile, isImageFile } from '@/lib/toolHelpers'; +import { getLanguageFromExtension, getImageMimeType, isDrawioFile, isImageFile, isPdfFile } from '@/lib/toolHelpers'; import { getRuntimeUrlResolver } from '@/lib/runtime-url'; import { refreshRuntimeUrlAuthToken } from '@/lib/runtime-auth'; import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; @@ -823,9 +823,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const [fileContent, setFileContent] = React.useState(''); const { isPlaying: isTTSPlaying, play: playTTS, stop: stopTTS } = useMessageTTS(); const [fileLoading, setFileLoading] = React.useState(false); - const [fileError, setFileError] = React.useState(null); - const [desktopImageSrc, setDesktopImageSrc] = React.useState(''); - const [imageAssetAuthReadyKey, setImageAssetAuthReadyKey] = React.useState(''); + const [fileError, setFileError] = React.useState(null); + const [desktopImageSrc, setDesktopImageSrc] = React.useState(''); + const [imageAssetAuthReadyKey, setImageAssetAuthReadyKey] = React.useState(''); + const [pdfAssetAuthReadyKey, setPdfAssetAuthReadyKey] = React.useState(''); const [loadedFilePath, setLoadedFilePath] = React.useState(null); @@ -1693,8 +1694,9 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { setDesktopImageSrc(''); setLoadedFilePath(null); - const selectedIsImage = isImageFile(node.path); - const isSvg = node.path.toLowerCase().endsWith('.svg'); + const selectedIsImage = isImageFile(node.path); + const isSvg = node.path.toLowerCase().endsWith('.svg'); + const selectedIsPdf = isPdfFile(node.path); if (isMobile) { setShowMobilePageContent(true); @@ -1709,13 +1711,21 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { } // Web: binary images should not be read as utf8. - if (!runtime.isDesktop && selectedIsImage && !isSvg) { - setFileContent(''); - setDraftContent(''); - setLoadedFilePath(node.path); - setFileLoading(false); - return; - } + if (!runtime.isDesktop && selectedIsImage && !isSvg) { + setFileContent(''); + setDraftContent(''); + setLoadedFilePath(node.path); + setFileLoading(false); + return; + } + + if (selectedIsPdf) { + setFileContent(''); + setDraftContent(''); + setLoadedFilePath(node.path); + setFileLoading(false); + return; + } setFileLoading(true); @@ -2189,8 +2199,9 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }); } - const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path)); - const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg')); + const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path)); + const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg')); + const isSelectedPdf = Boolean(selectedFile?.path && isPdfFile(selectedFile.path)); const pendingNavigationTargetPath = React.useMemo( () => normalizePath(pendingFileNavigation?.path ?? ''), [pendingFileNavigation?.path], @@ -2200,23 +2211,24 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { && pendingNavigationTargetPath && selectedFilePath && selectedFilePath === pendingNavigationTargetPath - && !fileLoading - && !fileError - && !isSelectedImage, - ); + && !fileLoading + && !fileError + && !isSelectedImage + && !isSelectedPdf, + ); const displaySelectedPath = React.useMemo(() => { return getDisplayPath(root, selectedFilePath); }, [selectedFilePath, root]); - const canCopy = Boolean(selectedFile && (!isSelectedImage || isSelectedSvg) && fileContent.length > 0); - const canCopyPath = Boolean(selectedFile && displaySelectedPath.length > 0); - const canEdit = Boolean(selectedFile && !selectedFileIsOutsideWorkspace && !isSelectedImage && files.writeFile && fileContent.length <= MAX_VIEW_CHARS); + const canCopy = Boolean(selectedFile && (!isSelectedImage || isSelectedSvg) && !isSelectedPdf && fileContent.length > 0); + const canCopyPath = Boolean(selectedFile && displaySelectedPath.length > 0); + const canEdit = Boolean(selectedFile && !selectedFileIsOutsideWorkspace && !isSelectedImage && !isSelectedPdf && files.writeFile && fileContent.length <= MAX_VIEW_CHARS); const isMarkdown = Boolean(selectedFile?.path && isMarkdownFile(selectedFile.path)); const isJson = Boolean(selectedFile?.path && isJsonFile(selectedFile.path)); const isHtml = Boolean(selectedFile?.path && isHtmlFile(selectedFile.path)); const isDrawio = Boolean(selectedFile?.path && isDrawioFile(selectedFile.path)); - const isTextFile = Boolean(selectedFile && !isSelectedImage); + const isTextFile = Boolean(selectedFile && !isSelectedImage && !isSelectedPdf); const canUseShikiFileView = isTextFile && !isMarkdown && !isDrawio && !(isHtml && htmlViewMode === 'preview'); const staticLanguageExtension = React.useMemo( () => (selectedFilePath ? languageByExtension(selectedFilePath) : null), @@ -2554,7 +2566,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return; } - if (fileError || isSelectedImage) { + if (fileError || isSelectedImage || isSelectedPdf) { setPendingFileNavigation(null); pendingNavigationCycleRef.current = { key: '', attempts: 0 }; return; @@ -2623,9 +2635,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { draftContent, editorViewReadyNonce, fileError, - fileLoading, - isSelectedImage, - loadedFilePath, + fileLoading, + isSelectedImage, + isSelectedPdf, + loadedFilePath, handleSelectFile, pendingFileNavigation, root, @@ -2654,7 +2667,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return; } - if (fileLoading || loadedFilePath !== targetPath || fileError || isSelectedImage) { + if (fileLoading || loadedFilePath !== targetPath || fileError || isSelectedImage || isSelectedPdf) { return; } @@ -2672,9 +2685,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { confirmDiscardOpen, fileError, fileLoading, - handleSelectFile, - isSelectedImage, - loadedFilePath, + handleSelectFile, + isSelectedImage, + isSelectedPdf, + loadedFilePath, pendingFileFocusPath, root, selectedFile?.path, @@ -2813,6 +2827,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const imageAssetAuthKey = selectedFile?.path && isSelectedImage && !runtime.isDesktop && !isSelectedSvg ? `${selectedFile.path}|${selectedFileReadOptions.allowOutsideWorkspace ? 'outside' : 'workspace'}|${selectedFileReadOptions.outsideFileGrant ?? ''}` : ''; + + const pdfAssetAuthKey = selectedFile?.path && isSelectedPdf + ? `${selectedFile.path}|${selectedFileReadOptions.allowOutsideWorkspace ? 'outside' : 'workspace'}|${selectedFileReadOptions.outsideFileGrant ?? ''}` + : ''; React.useEffect(() => { if (!imageAssetAuthKey) { @@ -2833,9 +2851,35 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }; }, [imageAssetAuthKey]); - const isImageAssetAuthLoading = Boolean(imageAssetAuthKey && imageAssetAuthReadyKey !== imageAssetAuthKey); + const isImageAssetAuthLoading = Boolean(imageAssetAuthKey && imageAssetAuthReadyKey !== imageAssetAuthKey); + + React.useEffect(() => { + if (!pdfAssetAuthKey) { + setPdfAssetAuthReadyKey(''); + return; + } + + let cancelled = false; + setPdfAssetAuthReadyKey(''); + void refreshRuntimeUrlAuthToken(getRuntimeApiBaseUrl()) + .then((token) => { + if (!cancelled && token) setPdfAssetAuthReadyKey(pdfAssetAuthKey); + }) + .catch((error) => { + if (!cancelled) { + setFileError(error instanceof Error ? error.message : t('filesView.error.readFileFailed')); + setPdfAssetAuthReadyKey(pdfAssetAuthKey); + } + }); + + return () => { + cancelled = true; + }; + }, [pdfAssetAuthKey, t]); + + const isPdfAssetAuthLoading = Boolean(pdfAssetAuthKey && pdfAssetAuthReadyKey !== pdfAssetAuthKey); - const imageSrc = selectedFile?.path && isSelectedImage + const imageSrc = selectedFile?.path && isSelectedImage ? (runtime.isDesktop ? (isSelectedSvg ? `data:${getImageMimeType(selectedFile.path)};utf8,${encodeURIComponent(fileContent)}` @@ -2847,7 +2891,25 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined, outsideFileGrant: selectedFileReadOptions.outsideFileGrant, }) : '')) - : ''; + : ''; + + const pdfSrc = selectedFile?.path && isSelectedPdf && pdfAssetAuthReadyKey === pdfAssetAuthKey + ? getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', { + path: selectedFile.path, + allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined, + outsideFileGrant: selectedFileReadOptions.outsideFileGrant, + }) + : ''; + + const renderPdfPreview = React.useCallback((file: FileNode) => ( +
+