From 4cc9c2f3f104671c207c0a0130d0438b52d1ee88 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 10:15:47 -0700 Subject: [PATCH] FN-6972: add browser-native file previews Files modal now renders common media and PDF files with browser-native previews while preserving editor flows for text and unknown binaries. - Add shared extension-based preview classification for images, videos, audio, and PDFs. - Render preview-only files from workspace-safe download URLs and skip binary editor loading/saving actions for those selections. - Update preview styling, localized labels, documentation, tests, and release notes. Files changed: .changeset/fn-6972-browser-file-previews.md | 7 + docs/dashboard-guide.md | 1 + packages/dashboard/app/components/FileBrowser.css | 39 ++- .../dashboard/app/components/FileBrowserModal.tsx | 107 +++++--- .../components/__tests__/FileBrowserModal.test.tsx | 302 ++++++++++----------- .../app/utils/__tests__/file-preview-kind.test.ts | 40 +++ packages/dashboard/app/utils/file-preview-kind.ts | 70 +++++ packages/i18n/locales/en/app.json | 4 +- packages/i18n/locales/es/app.json | 4 +- packages/i18n/locales/fr/app.json | 4 +- packages/i18n/locales/ko/app.json | 4 +- packages/i18n/locales/zh-CN/app.json | 4 +- packages/i18n/locales/zh-TW/app.json | 4 +- 13 files changed, 381 insertions(+), 209 deletions(-) Fusion-Task-Id: FN-6972 Fusion-Task-Lineage: 04b07def-e490-43f7-8b92-39ca704b20b6 --- .changeset/fn-6972-browser-file-previews.md | 7 + docs/dashboard-guide.md | 1 + .../dashboard/app/components/FileBrowser.css | 39 ++- .../app/components/FileBrowserModal.tsx | 107 ++++-- .../__tests__/FileBrowserModal.test.tsx | 324 +++++++++--------- .../utils/__tests__/file-preview-kind.test.ts | 40 +++ .../dashboard/app/utils/file-preview-kind.ts | 70 ++++ packages/i18n/locales/en/app.json | 4 +- packages/i18n/locales/es/app.json | 4 +- packages/i18n/locales/fr/app.json | 4 +- packages/i18n/locales/ko/app.json | 4 +- packages/i18n/locales/zh-CN/app.json | 4 +- packages/i18n/locales/zh-TW/app.json | 4 +- 13 files changed, 392 insertions(+), 220 deletions(-) create mode 100644 .changeset/fn-6972-browser-file-previews.md create mode 100644 packages/dashboard/app/utils/__tests__/file-preview-kind.test.ts create mode 100644 packages/dashboard/app/utils/file-preview-kind.ts diff --git a/.changeset/fn-6972-browser-file-previews.md b/.changeset/fn-6972-browser-file-previews.md new file mode 100644 index 0000000000..5487db7324 --- /dev/null +++ b/.changeset/fn-6972-browser-file-previews.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Preview images, videos, audio, and PDFs directly in the Files modal. +category: feature +dev: Adds browser-native previews backed by workspace-safe file download URLs. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 1b0d4f73a6..cf75b63f5c 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -670,6 +670,7 @@ The Files modal provides a workspace-aware file browser and editor. - Use **New File** or **New Folder** in the browser header to create entries in the current folder; new files open in the editor after creation - Source/text editing supports a **Line #** header toggle to show or hide line numbers in the editor gutter - The line-number preference is saved per project and restored automatically when you switch projects +- Known image, video/movie, audio, and PDF files render browser-native read-only previews from the selected project or task workspace download URL; text files remain editable, and unknown binary files keep the read-only editor fallback - In editable files and markdown preview mode, highlighted text exposes **Add comment** so you can send the file path, selected snippet, best-effort line range, and your note to the **New Task** dialog without copy/paste ## Memory View diff --git a/packages/dashboard/app/components/FileBrowser.css b/packages/dashboard/app/components/FileBrowser.css index 4ca535d7a3..ca644fe78e 100644 --- a/packages/dashboard/app/components/FileBrowser.css +++ b/packages/dashboard/app/components/FileBrowser.css @@ -326,7 +326,8 @@ color: var(--text-muted); } -.file-browser-binary-indicator { +.file-browser-binary-indicator, +.file-browser-preview-indicator { display: inline-flex; align-items: center; gap: var(--space-xs); @@ -365,7 +366,7 @@ flex-direction: column; } -.file-browser-image-preview { +.file-browser-preview { flex: 1; overflow: auto; background: var(--bg); @@ -377,14 +378,32 @@ padding: var(--space-lg); } -.file-browser-image { +.file-browser-preview-media { max-width: 100%; max-height: 100%; + min-width: 0; + border: none; +} + +.file-browser-preview-media--image, +.file-browser-preview-media--video { object-fit: contain; border-radius: var(--radius-md); box-shadow: var(--shadow-md); } +.file-browser-preview-media--audio { + width: min(100%, calc(var(--space-xl) * 18)); +} + +.file-browser-preview-media--pdf { + width: 100%; + height: 100%; + flex: 1; + border-radius: var(--radius-md); + background: var(--surface); +} + .file-browser-footer { display: flex; align-items: center; @@ -537,11 +556,12 @@ Narrow Files windows use the same single-pane list/editor behavior as mobile eve box-shadow: var(--focus-ring); } -.file-browser-modal--narrow .file-browser-image-preview { +.file-browser-modal--narrow .file-browser-preview { padding: var(--space-md); } -.file-browser-modal--narrow .file-browser-image { +.file-browser-modal--narrow .file-browser-preview-media--image, +.file-browser-modal--narrow .file-browser-preview-media--video { max-width: 100%; max-height: calc(100dvh - var(--space-2xl) * 6.25); } @@ -731,14 +751,19 @@ Narrow Files windows use the same single-pane list/editor behavior as mobile eve box-shadow: var(--focus-ring); } - .file-browser-image-preview { + .file-browser-preview { padding: var(--space-md); } - .file-browser-image { + .file-browser-preview-media--image, + .file-browser-preview-media--video { max-width: 100%; max-height: calc(100dvh - var(--space-2xl) * 6.25); } + + .file-browser-preview-media--pdf { + min-height: calc(var(--space-xl) * 12); + } } /* File Browser Context Menu */ diff --git a/packages/dashboard/app/components/FileBrowserModal.tsx b/packages/dashboard/app/components/FileBrowserModal.tsx index 211cd0a1ca..fa25d8704b 100644 --- a/packages/dashboard/app/components/FileBrowserModal.tsx +++ b/packages/dashboard/app/components/FileBrowserModal.tsx @@ -10,6 +10,7 @@ import { FileBrowser } from "./FileBrowser"; import { FileEditor } from "./FileEditor"; import { FloatingWindow } from "./FloatingWindow"; import { WorkspaceSelector } from "./WorkspaceSelector"; +import { getFilePreviewKind, IMAGE_PREVIEW_EXTENSIONS, VIDEO_PREVIEW_EXTENSIONS, AUDIO_PREVIEW_EXTENSIONS, PDF_PREVIEW_EXTENSIONS } from "../utils/file-preview-kind"; import { getScopedItem, setScopedItem } from "../utils/projectStorage"; const MOBILE_BREAKPOINT = 768; @@ -20,33 +21,24 @@ const SIDEBAR_STORAGE_KEY = "fusion:file-browser-sidebar-width"; const FILES_LINE_NUMBERS_STORAGE_KEY = "kb-files-line-numbers"; /** - * Image file extensions that should be rendered as image previews. - */ -const IMAGE_EXTENSIONS = new Set([ - ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp", ".svgz", -]); - -/** - * Binary file extensions that should be displayed as read-only. + * Binary file extensions that should be displayed as read-only when the browser cannot preview them natively. */ const BINARY_EXTENSIONS = new Set([ - ...IMAGE_EXTENSIONS, + ...IMAGE_PREVIEW_EXTENSIONS, + ...VIDEO_PREVIEW_EXTENSIONS, + ...AUDIO_PREVIEW_EXTENSIONS, + ...PDF_PREVIEW_EXTENSIONS, ".exe", ".dll", ".so", ".dylib", ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar", - ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", - ".mp3", ".mp4", ".avi", ".mov", ".webm", ".mkv", ".flv", + ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", + ".avi", ".mkv", ".flv", ".woff", ".woff2", ".ttf", ".otf", ".eot", ".wasm", ".bin", ]); function isBinaryFile(filename: string): boolean { const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase(); - return BINARY_EXTENSIONS.has(ext); -} - -function isImageFile(filename: string): boolean { - const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase(); - return IMAGE_EXTENSIONS.has(ext); + return Boolean(getFilePreviewKind(filename)) || BINARY_EXTENSIONS.has(ext); } function getParentDirectory(path: string): string { @@ -99,6 +91,9 @@ export function FileBrowserModal({ refresh, } = useWorkspaceFileBrowser(currentWorkspace, true, projectId); + const selectedPreviewKind = useMemo(() => getFilePreviewKind(selectedFile), [selectedFile]); + const isPreviewOnlyFile = selectedPreviewKind !== null; + const { content, setContent, @@ -109,7 +104,7 @@ export function FileBrowserModal({ save, hasChanges, mtime, - } = useWorkspaceFileEditor(currentWorkspace, selectedFile, true, projectId); + } = useWorkspaceFileEditor(currentWorkspace, selectedFile, !isPreviewOnlyFile, projectId); useEffect(() => { setCurrentWorkspace(initialWorkspace); @@ -208,7 +203,7 @@ export function FileBrowserModal({ } if ((e.metaKey || e.ctrlKey) && e.key === "s") { e.preventDefault(); - if (hasChanges && !saving) { + if (!isPreviewOnlyFile && hasChanges && !saving) { void save(); } } @@ -216,7 +211,7 @@ export function FileBrowserModal({ document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [onClose, hasChanges, saving, save]); + }, [onClose, hasChanges, saving, save, isPreviewOnlyFile]); const handleSelectFile = useCallback((path: string) => { setSelectedFile(path); @@ -326,11 +321,21 @@ export function FileBrowserModal({ const modalTitle = t("fileBrowser.modalTitle", "Files — {{workspace}}", { workspace: workspaceLabel }); const isNarrowEditorView = Boolean(isMobile && selectedFile && mobileView === "editor" && !isBinaryFile(selectedFile)); - // Compute image source URL when an image file is selected - const imageSrc = useMemo(() => { - if (!selectedFile || !isImageFile(selectedFile)) return null; + /* + FNXC:FileBrowser 2026-06-25-00:00: + Known image, video, audio, and PDF files must use browser-native previews from the workspace-safe download route without fetching binary content into CodeMirror. Editable text and unknown binary files keep the existing editor/read-only paths so save/discard, selection comments, and binary indicators remain scoped to editor-backed files. + */ + const previewUrl = useMemo(() => { + if (!selectedFile || !selectedPreviewKind) return null; return downloadFileUrl(currentWorkspace, selectedFile, projectId); - }, [selectedFile, currentWorkspace, projectId]); + }, [currentWorkspace, projectId, selectedFile, selectedPreviewKind]); + + const selectedPreviewLabel = selectedFile + ? t("fileBrowser.previewOnly", "Preview only") + : null; + const selectedPreviewTitle = selectedFile + ? t("fileBrowser.previewTitle", "Preview for {{file}}", { file: selectedFile }) + : ""; const formatFileSize = (value: string): string => { const bytes = new Blob([value]).size; @@ -440,12 +445,17 @@ export function FileBrowserModal({ )} {selectedFile} - {isBinaryFile(selectedFile) && ( + {selectedPreviewKind && selectedPreviewLabel ? ( + + + {selectedPreviewLabel} + + ) : isBinaryFile(selectedFile) ? ( {t("fileBrowser.binaryReadOnly", "Binary file — read only")} - )} + ) : null} {mtime && ( {t("fileBrowser.modified", "Modified: {{date}}", { date: new Date(mtime).toLocaleString() })} @@ -456,7 +466,7 @@ export function FileBrowserModal({ )}
- {!imageSrc && hasChanges && ( + {!previewUrl && hasChanges && ( <>
- {editorError && !imageSrc && ( + {editorError && !previewUrl && (
{editorError}
)} - {imageSrc ? ( -
- {selectedFile + {previewUrl && selectedPreviewKind ? ( +
+ {selectedPreviewKind === "image" && ( + {selectedFile + )} + {selectedPreviewKind === "video" && ( +