From ff7467b4c593f1e775311c088c63340d0cc1e4a2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 10 Aug 2026 01:14:53 -0600 Subject: [PATCH] refac --- package-lock.json | 10 + package.json | 1 + src/lib/components/chat/FileNav.svelte | 11 +- .../chat/FileNav/FilePreview.svelte | 8 +- src/lib/components/common/DocxPreview.svelte | 318 ++++++++++++++++++ .../components/common/FileItemModal.svelte | 19 +- src/lib/utils/docxToHtml.ts | 30 ++ 7 files changed, 377 insertions(+), 20 deletions(-) create mode 100644 src/lib/components/common/DocxPreview.svelte create mode 100644 src/lib/utils/docxToHtml.ts diff --git a/package-lock.json b/package-lock.json index 994e0fa266..8e7a3197dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "codemirror-lang-hcl": "^0.1.0", "crc-32": "^1.2.2", "dayjs": "^1.11.10", + "docx-preview": "^0.4.0", "dompurify": "^3.2.6", "eventsource-parser": "^1.1.2", "fast-deep-equal": "^3.1.3", @@ -7640,6 +7641,15 @@ "node": ">=6.0.0" } }, + "node_modules/docx-preview": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.4.0.tgz", + "integrity": "sha512-OdKtE/uj3M4RfGarLkGjahUzRg8/kBp0Sraj1r1NAY1tp/sTpHOBqDrzVf9onMBt9vxP6SdQ6bpLCUCsFwjgcA==", + "license": "Apache-2.0", + "dependencies": { + "jszip": ">=3.0.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", diff --git a/package.json b/package.json index 8864d55895..8cdcd7333d 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "codemirror-lang-hcl": "^0.1.0", "crc-32": "^1.2.2", "dayjs": "^1.11.10", + "docx-preview": "^0.4.0", "dompurify": "^3.2.6", "eventsource-parser": "^1.1.2", "fast-deep-equal": "^3.1.3", diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index f3cd3828b2..fe726defd8 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -184,6 +184,7 @@ let fileAudioUrl: string | null = null; let filePdfData: ArrayBuffer | null = null; let fileSqliteData: ArrayBuffer | null = null; + let fileDocxData: ArrayBuffer | null = null; let fileLoading = false; let filePreviewRef: FilePreview; @@ -247,8 +248,7 @@ const chatScoped = !!systemTerminal && chatConfig?.context_id === 'chat_id'; terminalChatContextHidden = !!systemTerminal && (chatConfig === false || (chatScoped && isTemporaryChatId(chatId))); - terminalChatContextPending = - chatScoped && !terminalChatContextHidden && !isSavedChatId(chatId); + terminalChatContextPending = chatScoped && !terminalChatContextHidden && !isSavedChatId(chatId); if (terminalChatContextHidden || terminalChatContextPending) return null; const userTerminal = ($settings?.terminalServers ?? []).find( @@ -395,6 +395,7 @@ } filePdfData = null; fileSqliteData = null; + fileDocxData = null; fileOfficeHtml = null; fileOfficeSlides = null; currentSlide = 0; @@ -505,10 +506,7 @@ const arrayBuffer = await result.blob.arrayBuffer(); try { if (ext === 'docx') { - const mammoth = await import('mammoth'); - const res = await mammoth.convertToHtml({ arrayBuffer }); - const DOMPurify = (await import('dompurify')).default; - fileOfficeHtml = DOMPurify.sanitize(res.value); + fileDocxData = arrayBuffer; } else if (ext === 'xlsx') { const XLSX = await import('xlsx'); const wb = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' }); @@ -1396,6 +1394,7 @@ {fileAudioUrl} {filePdfData} {fileSqliteData} + {fileDocxData} {fileContent} {fileOfficeHtml} {fileOfficeSlides} diff --git a/src/lib/components/chat/FileNav/FilePreview.svelte b/src/lib/components/chat/FileNav/FilePreview.svelte index ada8862ff3..37489d008a 100644 --- a/src/lib/components/chat/FileNav/FilePreview.svelte +++ b/src/lib/components/chat/FileNav/FilePreview.svelte @@ -9,6 +9,7 @@ import Spinner from '../../common/Spinner.svelte'; import PDFViewer from '../../common/PDFViewer.svelte'; import PanzoomContainer from '../../common/PanzoomContainer.svelte'; + import DocxPreview from '../../common/DocxPreview.svelte'; import JsonTreeView from './JsonTreeView.svelte'; import NotebookView from './NotebookView.svelte'; import SqliteView from './SqliteView.svelte'; @@ -26,6 +27,7 @@ export let fileAudioUrl: string | null = null; export let filePdfData: ArrayBuffer | null = null; export let fileSqliteData: ArrayBuffer | null = null; + export let fileDocxData: ArrayBuffer | null = null; export let fileContent: string | null = null; // Terminal connection for notebook execution @@ -271,7 +273,9 @@
0) ? 'overflow-hidden' : 'overflow-y-auto'} min-h-0 min-w-0 relative h-full" > @@ -308,6 +312,8 @@ {:else if fileSqliteData !== null} + {:else if fileDocxData !== null} + {:else if fileOfficeHtml !== null}
diff --git a/src/lib/components/common/DocxPreview.svelte b/src/lib/components/common/DocxPreview.svelte new file mode 100644 index 0000000000..be73c0ae46 --- /dev/null +++ b/src/lib/components/common/DocxPreview.svelte @@ -0,0 +1,318 @@ + + +
+
+ + {#if loading} +
+ +
+ {/if} + + {#if error} +
{error}
+ {:else if fallbackHtml} +
+
+ + {@html fallbackHtml} +
+
+ {:else} +
+
+
+ {/if} + + {#if !loading && !error && data && !fallbackHtml} +
+ + + +
+ {/if} +
+ + diff --git a/src/lib/components/common/FileItemModal.svelte b/src/lib/components/common/FileItemModal.svelte index a41fbdbb67..fae8337e92 100644 --- a/src/lib/components/common/FileItemModal.svelte +++ b/src/lib/components/common/FileItemModal.svelte @@ -26,6 +26,7 @@ import Spinner from './Spinner.svelte'; import PDFViewer from './PDFViewer.svelte'; import PanzoomContainer from './PanzoomContainer.svelte'; + import DocxPreview from './DocxPreview.svelte'; import Reset from '../icons/Reset.svelte'; export let item; @@ -51,7 +52,7 @@ let rowCount = 0; // DOCX state - let docxHtml = ''; + let docxData: ArrayBuffer | null = null; let docxError = ''; // PPTX state @@ -172,12 +173,7 @@ const loadDocxContent = async () => { try { docxError = ''; - const [arrayBuffer, mammoth] = await Promise.all([ - getFileContentById(item.id), - import('mammoth') - ]); - const result = await mammoth.convertToHtml({ arrayBuffer }); - docxHtml = DOMPurify.sanitize(result.value); + docxData = await getFileContentById(item.id); } catch (error) { console.error('Error loading DOCX file:', error); docxError = $i18n.t('Failed to load DOCX file. Please try downloading it instead.'); @@ -203,6 +199,7 @@ const loadContent = async () => { selectedTab = ''; expandedContent = false; + docxData = null; if (item?.type === 'collection') { loading = true; @@ -570,12 +567,8 @@ {:else if isDocx} {#if docxError}
{docxError}
- {:else if docxHtml} -
- {@html docxHtml} -
+ {:else if docxData} + {:else}
No content available
{/if} diff --git a/src/lib/utils/docxToHtml.ts b/src/lib/utils/docxToHtml.ts new file mode 100644 index 0000000000..8305c39087 --- /dev/null +++ b/src/lib/utils/docxToHtml.ts @@ -0,0 +1,30 @@ +const DOCX_STYLE_MAP = [ + "p[style-name='Title'] => h1.docx-title:fresh", + "p[style-name='Subtitle'] => p.docx-subtitle:fresh", + "p[style-name='Caption'] => p.docx-caption:fresh", + "p[style-name='Quote'] => blockquote:fresh", + "p[style-name='Intense Quote'] => blockquote:fresh", + 'r[style-name="Strong"] => strong', + 'r[style-name="Emphasis"] => em', + 'u => u', + 'strike => s' +]; + +export async function docxToHtml(arrayBuffer: ArrayBuffer): Promise { + const mammoth = await import('mammoth'); + const convertImage = mammoth.images.imgElement(async (image) => ({ + src: `data:${image.contentType};base64,${await image.readAsBase64String()}` + })); + + const result = await mammoth.convertToHtml( + { arrayBuffer }, + { + convertImage, + ignoreEmptyParagraphs: false, + includeDefaultStyleMap: true, + styleMap: DOCX_STYLE_MAP + } + ); + + return result.value; +}