diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index a8f310a962..ed7fe31561 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -292,6 +292,29 @@ export const downloadFileBlob = async ( return { blob, filename }; }; +export const downloadFilePreview = async ( + baseUrl: string, + apiKey: string, + path: string, + sessionId?: string +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/view?path=${encodeURIComponent(path)}&preview=true`; + const headers: Record = bearerHeaders(apiKey); + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { headers }).catch(() => null); + + if (!res) return null; + if (!res.ok) return null; + + const contentType = res.headers.get('content-type') ?? ''; + const filename = path.split('/').pop() ?? 'file'; + if (!contentType.includes('application/pdf')) return null; + + const blob = await res.blob().catch(() => null); + if (!blob) return null; + return { blob, filename }; +}; + export const archiveFromTerminal = async ( baseUrl: string, apiKey: string, diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 4f64ef7ea1..23f9392592 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -23,6 +23,7 @@ listFiles, readFile, downloadFileBlob, + downloadFilePreview, archiveFromTerminal, uploadToTerminal, createDirectory, @@ -865,19 +866,38 @@ ); if (result) fileSqliteData = await result.blob.arrayBuffer(); } else if (isOffice(filePath)) { - const result = await downloadFileBlob( - terminal.url, - terminal.key, - filePath, - chatId ?? undefined - ); - if (result) { - const ext = getFileExt(filePath); - const arrayBuffer = await result.blob.arrayBuffer(); - try { - if (ext === 'docx') { + const ext = getFileExt(filePath); + try { + if (ext === 'docx') { + const preview = await downloadFilePreview( + terminal.url, + terminal.key, + filePath, + chatId ?? undefined + ); + if (preview) { + filePdfData = await preview.blob.arrayBuffer(); + } else { + const result = await downloadFileBlob( + terminal.url, + terminal.key, + filePath, + chatId ?? undefined + ); + if (!result) throw new Error('Preview failed'); + const arrayBuffer = await result.blob.arrayBuffer(); fileDocxData = arrayBuffer; - } else if (ext === 'xlsx') { + toast.info($i18n.t('Preview may differ from download.')); + } + } else if (ext === 'xlsx') { + const result = await downloadFileBlob( + terminal.url, + terminal.key, + filePath, + chatId ?? undefined + ); + if (result) { + const arrayBuffer = await result.blob.arrayBuffer(); const XLSX = await import('xlsx'); const wb = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' }); excelWorkbook = wb; @@ -889,16 +909,35 @@ const DOMPurify = (await import('dompurify')).default; fileOfficeHtml = DOMPurify.sanitize(result.html); } - } else if (ext === 'pptx') { - const { pptxToImages } = await import('$lib/utils/pptxToHtml'); - const result = await pptxToImages(arrayBuffer); - fileOfficeSlides = result.images; - currentSlide = 0; } - } catch (e) { - console.error('Failed to render Office file:', e); - fileContent = `Error previewing file: ${e instanceof Error ? e.message : 'Unknown error'}`; + } else if (ext === 'pptx') { + const preview = await downloadFilePreview( + terminal.url, + terminal.key, + filePath, + chatId ?? undefined + ); + if (preview) { + filePdfData = await preview.blob.arrayBuffer(); + } else { + const result = await downloadFileBlob( + terminal.url, + terminal.key, + filePath, + chatId ?? undefined + ); + if (!result) throw new Error('Preview failed'); + const arrayBuffer = await result.blob.arrayBuffer(); + const { pptxToImages } = await import('$lib/utils/pptxToHtml'); + const fallback = await pptxToImages(arrayBuffer); + fileOfficeSlides = fallback.images; + currentSlide = 0; + toast.info($i18n.t('Preview may differ from download.')); + } } + } catch (e) { + console.error('Failed to render Office file:', e); + fileContent = `Error previewing file: ${e instanceof Error ? e.message : 'Unknown error'}`; } } else { fileContent = await readFile(terminal.url, terminal.key, filePath, chatId ?? undefined); diff --git a/src/lib/components/chat/FileNav/FilePreview.svelte b/src/lib/components/chat/FileNav/FilePreview.svelte index b5ea5507bb..73ce9d1a29 100644 --- a/src/lib/components/chat/FileNav/FilePreview.svelte +++ b/src/lib/components/chat/FileNav/FilePreview.svelte @@ -8,6 +8,7 @@ import { initMermaid, renderMermaidDiagram } from '$lib/utils'; import Spinner from '../../common/Spinner.svelte'; import PDFViewer from '../../common/PDFViewer.svelte'; + import PptxPdfPreview from '../../common/PptxPdfPreview.svelte'; import PanzoomContainer from '../../common/PanzoomContainer.svelte'; import DocxPreview from '../../common/DocxPreview.svelte'; import PptxPreview from '../../common/PptxPreview.svelte'; @@ -17,6 +18,7 @@ import FileCodeEditor from './FileCodeEditor.svelte'; let pdfViewerRef: PDFViewer; + let pptxPdfPreviewRef: PptxPdfPreview; let fileCodeEditorRef: FileCodeEditor; const i18n = getContext('i18n'); @@ -116,6 +118,7 @@ $: isNotebook = getExt(selectedFile) === 'ipynb'; $: isCode = isCodeFile(selectedFile); $: csvDelimiter = getExt(selectedFile) === 'tsv' ? '\t' : ','; + $: isPptx = getExt(selectedFile) === 'pptx'; // For HTML files on system terminals (proxy URL), use path-based serving // so the iframe can resolve relative CSS/JS/image references via cookie auth. @@ -279,6 +282,7 @@ }; export const resetPdfView = () => { + pptxPdfPreviewRef?.resetView(); pdfViewerRef?.resetView(); }; @@ -286,6 +290,7 @@
{:else if filePdfData !== null} - + {#if isPptx} + + {:else} + + {/if} {:else if fileSqliteData !== null} {:else if fileDocxData !== null} diff --git a/src/lib/components/chat/Messages/TerminalOutputFile.svelte b/src/lib/components/chat/Messages/TerminalOutputFile.svelte index 655320af9f..f8c7a13619 100644 --- a/src/lib/components/chat/Messages/TerminalOutputFile.svelte +++ b/src/lib/components/chat/Messages/TerminalOutputFile.svelte @@ -10,7 +10,7 @@ showFileNavPath, terminalServers } from '$lib/stores'; - import { downloadFileBlob, readFile } from '$lib/apis/terminal'; + import { downloadFileBlob, downloadFilePreview, readFile } from '$lib/apis/terminal'; import FilePreview from '$lib/components/chat/FileNav/FilePreview.svelte'; import Icon from '$lib/components/chat/FileNav/Icon.svelte'; import { fileIconName } from '$lib/components/chat/FileNav/fileIcon'; @@ -143,26 +143,61 @@ else if (isVideo(path)) fileVideoUrl = url; else fileAudioUrl = url; } else if (isPdf(path) || isSqlite(path) || isOffice(path)) { - const result = await blobForPreview(); - if (!result) throw new Error(t('Preview failed')); - const arrayBuffer = await result.blob.arrayBuffer(); const ext = getExt(path); if (isPdf(path)) { + const result = await blobForPreview(); + if (!result) throw new Error(t('Preview failed')); + const arrayBuffer = await result.blob.arrayBuffer(); filePdfData = arrayBuffer; } else if (isSqlite(path)) { + const result = await blobForPreview(); + if (!result) throw new Error(t('Preview failed')); + const arrayBuffer = await result.blob.arrayBuffer(); fileSqliteData = arrayBuffer; } else if (ext === 'docx') { - fileDocxData = arrayBuffer; + const preview = await downloadFilePreview( + terminal.url, + terminal.key, + path, + chatId || undefined + ); + if (preview) { + filePdfData = await preview.blob.arrayBuffer(); + } else { + const result = await blobForPreview(); + if (!result) throw new Error(t('Preview failed')); + fileDocxData = await result.blob.arrayBuffer(); + toast.info(t('Preview may differ from download.')); + } } else if (ext === 'xlsx' || ext === 'xls') { + const result = await blobForPreview(); + if (!result) throw new Error(t('Preview failed')); + const arrayBuffer = await result.blob.arrayBuffer(); const XLSX = await import('xlsx'); excelWorkbook = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' }); excelSheetNames = excelWorkbook.SheetNames; - if (excelSheetNames.length > 0) await loadExcelSheet(excelSheetNames[0]); + if (excelSheetNames.length > 0) { + await loadExcelSheet(excelSheetNames[0]); + } } else if (ext === 'pptx') { - const { pptxToImages } = await import('$lib/utils/pptxToHtml'); - const result = await pptxToImages(arrayBuffer); - fileOfficeSlides = result.images; + const preview = await downloadFilePreview( + terminal.url, + terminal.key, + path, + chatId || undefined + ); + if (preview) { + filePdfData = await preview.blob.arrayBuffer(); + } else { + const result = await blobForPreview(); + if (!result) throw new Error(t('Preview failed')); + const arrayBuffer = await result.blob.arrayBuffer(); + const { pptxToImages } = await import('$lib/utils/pptxToHtml'); + const resultImages = await pptxToImages(arrayBuffer); + fileOfficeSlides = resultImages.images; + toast.info(t('Preview may differ from download.')); + } } } else if (terminal) { fileContent = await readFile(terminal.url, terminal.key, path, chatId || undefined); diff --git a/src/lib/components/common/PptxPdfPreview.svelte b/src/lib/components/common/PptxPdfPreview.svelte new file mode 100644 index 0000000000..edf6d17e97 --- /dev/null +++ b/src/lib/components/common/PptxPdfPreview.svelte @@ -0,0 +1,84 @@ + + +{#if loading} +
+ +
+{:else if error} +
+ {error} +
+{:else} + +{/if}