From 890949abe6b01d201355a86c50317e20da07dd34 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 4 Mar 2026 15:50:37 -0600 Subject: [PATCH] feat: add DOCX/XLSX/PPTX file preview - DOCX: mammoth converts to semantic HTML (prose preview) - XLSX: xlsx library extended to FileNav with sheet tabs at bottom - PPTX: custom canvas renderer produces PNG images per slide with panzoom zoom/pan and slide navigation Changes: - New: src/lib/utils/pptxToHtml.ts (canvas-based PPTX renderer) - FileNav.svelte: office format detection, blob download, conversion - FilePreview.svelte: office rendering branches, sheet tabs, slide viewer - FileItemModal.svelte: DOCX/PPTX preview tabs - package.json: added mammoth dependency --- package-lock.json | 209 +++++++------- package.json | 1 + src/lib/components/chat/FileNav.svelte | 81 +++++- .../chat/FileNav/FilePreview.svelte | 128 ++++++++- .../components/common/FileItemModal.svelte | 110 +++++++- src/lib/utils/pptxToHtml.ts | 259 ++++++++++++++++++ 6 files changed, 683 insertions(+), 105 deletions(-) create mode 100644 src/lib/utils/pptxToHtml.ts diff --git a/package-lock.json b/package-lock.json index b18ff000dc..75b3f3e20c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "jspdf": "^4.0.0", + "jszip": "^3.10.1", "katex": "^0.16.22", "kokoro-js": "^1.1.1", "leaflet": "^1.9.4", @@ -223,7 +224,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz", "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==", - "devOptional": true, + "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@chevrotain/cst-dts-gen": { @@ -1850,6 +1851,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1860,6 +1862,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1870,6 +1873,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -1884,6 +1888,7 @@ "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2304,6 +2309,7 @@ "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, "license": "MIT" }, "node_modules/@popperjs/core": { @@ -2702,6 +2708,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -2747,6 +2754,7 @@ "version": "2.22.4", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.4.tgz", "integrity": "sha512-BXK9hTbP8AeQIfoz6+P3uoyVYStVHc5CIKqoTSF7hXm3Q5P9BwFMdEus4jsQuhaYmXGHzukcGlxe2QrsE8BJfQ==", + "dev": true, "license": "MIT", "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", @@ -2784,6 +2792,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", + "dev": true, "license": "MIT", "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", @@ -2805,6 +2814,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.7" @@ -2822,6 +2832,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2839,12 +2850,14 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/@sveltejs/vite-plugin-svelte/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2862,6 +2875,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/@swc/helpers": { @@ -3265,23 +3279,6 @@ "lowlight": "^2 || ^3" } }, - "node_modules/@tiptap/extension-collaboration": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.20.0.tgz", - "integrity": "sha512-JItmI4U0i4kqorO114u24hM9k945IdaQ6Uc2DEtPBFFuS8cepJf2zw+ulAT1kAx6ZRiNvNpT9M7w+J0mWRn+Sg==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.20.0", - "@tiptap/pm": "^3.20.0", - "@tiptap/y-tiptap": "^3.0.2", - "yjs": "^13" - } - }, "node_modules/@tiptap/extension-document": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.0.7.tgz", @@ -3524,21 +3521,6 @@ "@tiptap/suggestion": "^3.0.9" } }, - "node_modules/@tiptap/extension-node-range": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.20.0.tgz", - "integrity": "sha512-XeKKTV88VuJ4Mh0Rxvc/PPzG76cb44sE+rB4u0J/ms63R/WFTm6yJQlCgUVGnGeHleSlrWuZY8gGSuoljmQzqg==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.20.0", - "@tiptap/pm": "^3.20.0" - } - }, "node_modules/@tiptap/extension-ordered-list": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.0.7.tgz", @@ -3605,20 +3587,6 @@ "@tiptap/core": "^3.0.7" } }, - "node_modules/@tiptap/extension-text-style": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.0.tgz", - "integrity": "sha512-zyWW1a6W+kaXAn3wv2svJ1XuVMapujftvH7Xn2Q3QmKKiDkO+NiFkrGe8BhMopu8Im51nO3NylIgVA0X1mS1rQ==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.20.0" - } - }, "node_modules/@tiptap/extension-typography": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-3.0.7.tgz", @@ -3752,31 +3720,11 @@ "@tiptap/pm": "^3.4.2" } }, - "node_modules/@tiptap/y-tiptap": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@tiptap/y-tiptap/-/y-tiptap-3.0.2.tgz", - "integrity": "sha512-flMn/YW6zTbc6cvDaUPh/NfLRTXDIqgpBUkYzM74KA1snqQwhOMjnRcnpu4hDFrTnPO6QGzr99vRyXEA7M44WA==", - "license": "MIT", - "peer": true, - "dependencies": { - "lib0": "^0.2.100" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "peerDependencies": { - "prosemirror-model": "^1.7.1", - "prosemirror-state": "^1.2.3", - "prosemirror-view": "^1.9.10", - "y-protocols": "^1.0.1", - "yjs": "^13.5.38" - } - }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true }, "node_modules/@types/d3": { "version": "7.4.3", @@ -4713,6 +4661,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -4794,6 +4743,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -5074,7 +5024,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", - "devOptional": true, + "dev": true, "license": "MIT/X11" }, "node_modules/buffer-crc32": { @@ -5620,6 +5570,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5737,7 +5688,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/colors": { @@ -5806,6 +5757,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6675,7 +6627,8 @@ "node_modules/devalue": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true }, "node_modules/devlop": { "version": "1.1.0", @@ -7229,6 +7182,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, "license": "MIT" }, "node_modules/espree": { @@ -7264,6 +7218,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -8011,7 +7966,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">=8" } @@ -8341,7 +8296,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/import-fresh": { @@ -8800,6 +8755,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, "engines": { "node": ">=6" } @@ -8928,7 +8884,7 @@ "version": "1.29.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", - "devOptional": true, + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^1.0.3" @@ -8960,6 +8916,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8980,6 +8937,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9000,6 +8958,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9020,6 +8979,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9040,6 +9000,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9060,6 +9021,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9080,6 +9042,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9100,6 +9063,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9120,6 +9084,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9140,6 +9105,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9157,7 +9123,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "detect-libc": "bin/detect-libc.js" @@ -9276,7 +9242,8 @@ "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true }, "node_modules/locate-path": { "version": "6.0.0", @@ -9840,6 +9807,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, "engines": { "node": ">=4" } @@ -9848,6 +9816,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10379,6 +10348,7 @@ "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10510,6 +10480,7 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, "funding": [ { "type": "github", @@ -11340,7 +11311,7 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "devOptional": true, + "dev": true, "dependencies": { "tslib": "^2.1.0" } @@ -11349,6 +11320,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, "dependencies": { "mri": "^1.1.0" }, @@ -11370,7 +11342,7 @@ "version": "1.81.0", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.81.0.tgz", "integrity": "sha512-uZQ2Faxb1oWBHpeSSzjxnhClbMb3QadN0ql0ZFNuqWOLUxwaVhrMlMhPq6TDPbbfDUjihuwrMCuy695Bgna5RA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", @@ -11418,6 +11390,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11434,6 +11407,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11450,6 +11424,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11466,6 +11441,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11482,6 +11458,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11498,6 +11475,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11514,6 +11492,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11530,6 +11509,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11546,6 +11526,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11562,6 +11543,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11578,6 +11560,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11594,6 +11577,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11610,6 +11594,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11626,6 +11611,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11642,6 +11628,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11658,6 +11645,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11674,6 +11662,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11690,6 +11679,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11706,6 +11696,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11722,6 +11713,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11735,7 +11727,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -11761,7 +11753,8 @@ "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true }, "node_modules/set-function-length": { "version": "1.2.2", @@ -11890,6 +11883,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "dev": true, "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -11965,6 +11959,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -12223,6 +12218,7 @@ "version": "5.42.2", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.2.tgz", "integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -12302,21 +12298,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-check/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -12380,12 +12361,14 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/svelte/node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -12411,7 +12394,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "sync-message-port": "^1.0.0" @@ -12424,7 +12407,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=16.0.0" @@ -12633,6 +12616,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12893,7 +12877,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/vega": { @@ -13402,6 +13386,7 @@ "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -13505,6 +13490,7 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "aix" @@ -13520,6 +13506,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" @@ -13535,6 +13522,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" @@ -13550,6 +13538,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "android" @@ -13565,6 +13554,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -13580,6 +13570,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -13595,6 +13586,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -13610,6 +13602,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -13625,6 +13618,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13640,6 +13634,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13655,6 +13650,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13670,6 +13666,7 @@ "cpu": [ "loong64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13685,6 +13682,7 @@ "cpu": [ "mips64el" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13700,6 +13698,7 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13715,6 +13714,7 @@ "cpu": [ "riscv64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13730,6 +13730,7 @@ "cpu": [ "s390x" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13745,6 +13746,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -13760,6 +13762,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "netbsd" @@ -13775,6 +13778,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "openbsd" @@ -13790,6 +13794,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "sunos" @@ -13805,6 +13810,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -13820,6 +13826,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" @@ -13835,6 +13842,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -13847,6 +13855,7 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -13884,6 +13893,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, "license": "MIT", "workspaces": [ "tests/deps/*", @@ -14607,6 +14617,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, "license": "MIT" } } diff --git a/package.json b/package.json index 703f1bc553..0774ca5d2a 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "jspdf": "^4.0.0", + "jszip": "^3.10.1", "katex": "^0.16.22", "kokoro-js": "^1.1.1", "leaflet": "^1.9.4", diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index b6ba861e6a..9daf03592a 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -37,6 +37,7 @@ import FileNavToolbar from './FileNav/FileNavToolbar.svelte'; import FilePreview from './FileNav/FilePreview.svelte'; import FileEntryRow from './FileNav/FileEntryRow.svelte'; + import PortList from './FileNav/PortList.svelte'; import XTerminal from './XTerminal.svelte'; const i18n = getContext('i18n'); @@ -93,6 +94,14 @@ let fileLoading = false; let filePreviewRef: FilePreview; + // ── Office preview state ──────────────────────────────────────────── + let fileOfficeHtml: string | null = null; + let fileOfficeSlides: string[] | null = null; + let currentSlide = 0; + let excelSheetNames: string[] = []; + let selectedExcelSheet = ''; + let excelWorkbook: import('xlsx').WorkBook | null = null; + // ── File preview toolbar state (bound from FilePreview) ───────────── let editing = false; let showRaw = false; @@ -101,12 +110,15 @@ const MD_EXTS = new Set(['md', 'markdown', 'mdx']); const CSV_EXTS = new Set(['csv', 'tsv']); const HTML_EXTS = new Set(['html', 'htm']); + const OFFICE_EXTS = new Set(['docx', 'xlsx', 'pptx']); const getFileExt = (path: string | null) => path?.split('.').pop()?.toLowerCase() ?? ''; $: isMarkdown = MD_EXTS.has(getFileExt(selectedFile)); $: isCsv = CSV_EXTS.has(getFileExt(selectedFile)); $: isHtml = HTML_EXTS.has(getFileExt(selectedFile)); - $: isTextFile = fileContent !== null && fileImageUrl === null && filePdfData === null; + $: isOfficeFile = OFFICE_EXTS.has(getFileExt(selectedFile)); + $: isTextFile = + fileContent !== null && fileImageUrl === null && filePdfData === null && !isOfficeFile; // ── Upload / folder creation ───────────────────────────────────────── let isDragOver = false; @@ -169,6 +181,7 @@ const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']); const isImage = (path: string) => IMAGE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? ''); const isPdf = (path: string) => path.split('.').pop()?.toLowerCase() === 'pdf'; + const isOffice = (path: string) => OFFICE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? ''); const buildBreadcrumbs = (path: string) => path @@ -192,6 +205,12 @@ fileImageUrl = null; } filePdfData = null; + fileOfficeHtml = null; + fileOfficeSlides = null; + currentSlide = 0; + excelSheetNames = []; + selectedExcelSheet = ''; + excelWorkbook = null; }; // ── Directory operations ───────────────────────────────────────────── @@ -244,6 +263,41 @@ } else if (isPdf(filePath)) { const result = await downloadFileBlob(terminal.url, terminal.key, filePath); if (result) filePdfData = await result.blob.arrayBuffer(); + } else if (isOffice(filePath)) { + const result = await downloadFileBlob(terminal.url, terminal.key, filePath); + if (result) { + const ext = getFileExt(filePath); + 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); + } else if (ext === 'xlsx') { + const XLSX = await import('xlsx'); + const wb = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' }); + excelWorkbook = wb; + excelSheetNames = wb.SheetNames; + if (excelSheetNames.length > 0) { + selectedExcelSheet = excelSheetNames[0]; + const ws = wb.Sheets[selectedExcelSheet]; + const DOMPurify = (await import('dompurify')).default; + fileOfficeHtml = DOMPurify.sanitize( + XLSX.utils.sheet_to_html(ws, { id: 'excel-table', editable: false, header: '' }) + ); + } + } 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 { fileContent = await readFile(terminal.url, terminal.key, filePath); } @@ -536,7 +590,7 @@ onUploadFiles={handleUploadFiles} onMove={handleMove} > - {#if fileImageUrl !== null} + {#if fileImageUrl !== null || (fileOfficeSlides !== null && fileOfficeSlides.length > 0)} + {/each} + + {/if} + + {:else if fileOfficeSlides !== null && fileOfficeSlides.length > 0} +
+
+ Slide {currentSlide + 1} +
+ {#if fileOfficeSlides.length > 1} +
+ + {currentSlide + 1} / {fileOfficeSlides.length} + +
+ {/if} +
{:else if fileContent !== null} {#if isHtml && !showRaw} {#if overlay} @@ -226,7 +290,7 @@ class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed p-3">{fileContent} {/if} {:else} -
+
{$i18n.t('Could not read file.')}
{/if} @@ -284,4 +348,62 @@ :global(.dark) .csv-row-num { color: #6b7280; } + /* ── Office preview styles ──────────────────────────────────────── */ + :global(.office-preview) { + font-size: 0.875rem; + line-height: 1.6; + color: #1f2937; + background: #fff; + border-radius: 4px; + } + :global(.dark .office-preview) { + color: #e5e7eb; + background: #1a1a2e; + } + :global(.office-preview table) { + border-collapse: collapse; + font-size: 0.8rem; + line-height: 1.25rem; + } + :global(.office-preview table td), + :global(.office-preview table th) { + border: 1px solid rgba(128, 128, 128, 0.25); + padding: 0.5rem 0.75rem; + text-align: left; + white-space: nowrap; + } + :global(.dark .office-preview table td), + :global(.dark .office-preview table th) { + border-color: rgba(128, 128, 128, 0.35); + } + :global(.office-preview table th) { + background: rgba(243, 244, 246, 0.95); + font-weight: 600; + position: sticky; + top: 0; + z-index: 1; + } + :global(.dark .office-preview table th) { + background: rgba(31, 41, 55, 0.95); + } + :global(.office-preview table tr:nth-child(even)) { + background: rgba(128, 128, 128, 0.04); + } + :global(.office-preview table tr:hover) { + background: rgba(59, 130, 246, 0.06); + } + :global(.dark .office-preview table tr:hover) { + background: rgba(59, 130, 246, 0.1); + } + :global(.office-preview img) { + max-width: 100%; + height: auto; + } + :global(.office-preview h1) { font-size: 1.5rem; font-weight: 700; margin: 0.75em 0 0.5em; } + :global(.office-preview h2) { font-size: 1.25rem; font-weight: 600; margin: 0.75em 0 0.5em; } + :global(.office-preview h3) { font-size: 1.1rem; font-weight: 600; margin: 0.5em 0 0.25em; } + :global(.office-preview p) { margin: 0.25em 0; } + :global(.office-preview ul), + :global(.office-preview ol) { padding-left: 1.5em; margin: 0.5em 0; } + diff --git a/src/lib/components/common/FileItemModal.svelte b/src/lib/components/common/FileItemModal.svelte index 9d617f3604..0058cf637a 100644 --- a/src/lib/components/common/FileItemModal.svelte +++ b/src/lib/components/common/FileItemModal.svelte @@ -40,6 +40,8 @@ let isAudio = false; let isImage = false; let isExcel = false; + let isDocx = false; + let isPptx = false; let selectedTab = ''; let excelWorkbook: WorkBook | null = null; @@ -49,6 +51,15 @@ let excelError = ''; let rowCount = 0; + // DOCX state + let docxHtml = ''; + let docxError = ''; + + // PPTX state + let pptxSlides: string[] = []; + let pptxCurrentSlide = 0; + let pptxError = ''; + let pzInstance: PanZoom | null = null; const initImagePanzoom = (node: HTMLElement) => { @@ -128,6 +139,16 @@ item.name.toLowerCase().endsWith('.xlsx') || item.name.toLowerCase().endsWith('.csv'))); + $: isDocx = + item?.meta?.content_type === + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + (item?.name && item.name.toLowerCase().endsWith('.docx')); + + $: isPptx = + item?.meta?.content_type === + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || + (item?.name && item.name.toLowerCase().endsWith('.pptx')); + const loadExcelContent = async () => { try { excelError = ''; @@ -170,6 +191,37 @@ renderExcelSheet(); } + 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); + } catch (error) { + console.error('Error loading DOCX file:', error); + docxError = $i18n.t('Failed to load DOCX file. Please try downloading it instead.'); + } + }; + + const loadPptxContent = async () => { + try { + pptxError = ''; + const [arrayBuffer, { pptxToImages }] = await Promise.all([ + getFileContentById(item.id), + import('$lib/utils/pptxToHtml') + ]); + const result = await pptxToImages(arrayBuffer); + pptxSlides = result.images; + pptxCurrentSlide = 0; + } catch (error) { + console.error('Error loading PPTX file:', error); + pptxError = $i18n.t('Failed to load PPTX file. Please try downloading it instead.'); + } + }; + const loadContent = async () => { selectedTab = ''; expandedContent = false; @@ -201,6 +253,12 @@ if (isExcel) { await loadExcelContent(); } + if (isDocx) { + await loadDocxContent(); + } + if (isPptx) { + await loadPptxContent(); + } loading = false; } @@ -358,7 +416,7 @@
{/if} - {#if isAudio || isPDF || isExcel || isCode || isMarkdown} + {#if isAudio || isPDF || isExcel || isCode || isMarkdown || isDocx || isPptx}
@@ -534,6 +592,56 @@ >
+ {:else if isDocx} + {#if docxError} +
{docxError}
+ {:else if docxHtml} +
+ {@html docxHtml} +
+ {:else} +
No content available
+ {/if} + {:else if isPptx} + {#if pptxError} +
{pptxError}
+ {:else if pptxSlides.length > 0} +
+
+ Slide {pptxCurrentSlide + 1} +
+ {#if pptxSlides.length > 1} +
+ + {pptxCurrentSlide + 1} / {pptxSlides.length} + +
+ {/if} +
+ {:else} +
No content available
+ {/if} {:else}
{(item?.file?.data?.content ?? '').trim() || 'No content'} diff --git a/src/lib/utils/pptxToHtml.ts b/src/lib/utils/pptxToHtml.ts new file mode 100644 index 0000000000..193469ddc5 --- /dev/null +++ b/src/lib/utils/pptxToHtml.ts @@ -0,0 +1,259 @@ +/** + * Lightweight PPTX → Image renderer. + * + * Extracts text and images from each slide and renders them + * directly to canvas, returning PNG data URLs. + * + * Uses jszip (dynamically imported) and the browser Canvas 2D API. + * No theme resolution, charts, SmartArt, or animations — preview only. + */ + +const EMU_PER_PX = 9525; +const emuToPx = (emu: number) => Math.round(emu / EMU_PER_PX); + +const parseEmu = (val: string | null | undefined): number => + val ? parseInt(val, 10) || 0 : 0; + +/** Load a data URI into an Image element and wait for it. */ +const loadImage = (src: string): Promise => + new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error('Failed to load image')); + img.src = src; + }); + +/** + * Convert PPTX ArrayBuffer → array of PNG data URL strings, one per slide. + */ +export async function pptxToImages( + buffer: ArrayBuffer +): Promise<{ images: string[]; width: number; height: number }> { + const JSZip = (await import('jszip')).default; + const zip = await JSZip.loadAsync(buffer); + + // ── Read slide dimensions from presentation.xml ────────────────── + let slideW = 960; + let slideH = 540; + const presXml = zip.file('ppt/presentation.xml'); + if (presXml) { + const presText = await presXml.async('text'); + const presDoc = new DOMParser().parseFromString(presText, 'application/xml'); + const sldSz = presDoc.getElementsByTagName('p:sldSz')[0]; + if (sldSz) { + slideW = emuToPx(parseEmu(sldSz.getAttribute('cx'))); + slideH = emuToPx(parseEmu(sldSz.getAttribute('cy'))); + } + } + + // ── Collect media files (images) as base64 data URIs ───────────── + const media: Record = {}; + const mediaFiles = Object.keys(zip.files).filter((f) => f.startsWith('ppt/media/')); + await Promise.all( + mediaFiles.map(async (path) => { + const file = zip.file(path); + if (!file) return; + const base64 = await file.async('base64'); + const ext = path.split('.').pop()?.toLowerCase() ?? ''; + const mime = + ext === 'png' + ? 'image/png' + : ext === 'gif' + ? 'image/gif' + : ext === 'svg' + ? 'image/svg+xml' + : ext === 'emf' || ext === 'wmf' + ? 'image/x-emf' + : 'image/jpeg'; + media[path] = `data:${mime};base64,${base64}`; + }) + ); + + // ── Discover slide files ───────────────────────────────────────── + const slideFiles = Object.keys(zip.files) + .filter((f) => /^ppt\/slides\/slide\d+\.xml$/.test(f)) + .sort((a, b) => { + const na = parseInt(a.match(/slide(\d+)/)?.[1] ?? '0'); + const nb = parseInt(b.match(/slide(\d+)/)?.[1] ?? '0'); + return na - nb; + }); + + const images: string[] = []; + + for (const slidePath of slideFiles) { + const slideText = await zip.file(slidePath)!.async('text'); + const slideDoc = new DOMParser().parseFromString(slideText, 'application/xml'); + + // Load relationship file for this slide to resolve image references + const slideNum = slidePath.match(/slide(\d+)/)?.[1]; + const relsPath = `ppt/slides/_rels/slide${slideNum}.xml.rels`; + const rels: Record = {}; + const relsFile = zip.file(relsPath); + if (relsFile) { + const relsText = await relsFile.async('text'); + const relsDoc = new DOMParser().parseFromString(relsText, 'application/xml'); + const relEls = relsDoc.getElementsByTagName('Relationship'); + for (let i = 0; i < relEls.length; i++) { + const rel = relEls[i]; + const id = rel.getAttribute('Id') ?? ''; + const target = rel.getAttribute('Target') ?? ''; + if (target.startsWith('../')) { + rels[id] = 'ppt/' + target.replace('../', ''); + } else { + rels[id] = target; + } + } + } + + // ── Create canvas and render slide ─────────────────────────── + const canvas = document.createElement('canvas'); + canvas.width = slideW; + canvas.height = slideH; + const ctx = canvas.getContext('2d')!; + + // White background + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, slideW, slideH); + + const spTree = slideDoc.getElementsByTagName('p:spTree')[0]; + if (!spTree) { + images.push(canvas.toDataURL('image/png')); + continue; + } + + const shapes = [ + ...Array.from(spTree.getElementsByTagName('p:sp')), + ...Array.from(spTree.getElementsByTagName('p:pic')) + ]; + + for (const shape of shapes) { + const xfrm = + shape.getElementsByTagName('a:xfrm')[0] ?? + shape.getElementsByTagName('p:xfrm')[0]; + if (!xfrm) continue; + + const off = xfrm.getElementsByTagName('a:off')[0]; + const ext = xfrm.getElementsByTagName('a:ext')[0]; + if (!off || !ext) continue; + + const x = emuToPx(parseEmu(off.getAttribute('x'))); + const y = emuToPx(parseEmu(off.getAttribute('y'))); + const w = emuToPx(parseEmu(ext.getAttribute('cx'))); + const h = emuToPx(parseEmu(ext.getAttribute('cy'))); + + if (w === 0 && h === 0) continue; + + // ── Picture ────────────────────────────────────────────── + const blipFill = shape.getElementsByTagName('p:blipFill')[0]; + if (blipFill) { + const blip = blipFill.getElementsByTagName('a:blip')[0]; + if (blip) { + const rEmbed = blip.getAttribute('r:embed') ?? ''; + const mediaPath = rels[rEmbed]; + const dataUri = mediaPath ? media[mediaPath] : ''; + if (dataUri && !dataUri.includes('image/x-emf')) { + try { + const img = await loadImage(dataUri); + ctx.drawImage(img, x, y, w, h); + } catch { + // Skip images that fail to load + } + } + } + continue; + } + + // ── Text shape ─────────────────────────────────────────── + const txBody = shape.getElementsByTagName('p:txBody')[0]; + if (!txBody) continue; + + ctx.save(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const paragraphs = txBody.getElementsByTagName('a:p'); + let cursorY = y; + const defaultFontSize = 12; + + for (let pi = 0; pi < paragraphs.length; pi++) { + const para = paragraphs[pi]; + const runs = para.getElementsByTagName('a:r'); + + if (runs.length === 0) { + cursorY += defaultFontSize * 1.5; + continue; + } + + // Calculate max font size in this paragraph for line height + let maxFontPt = defaultFontSize; + for (let ri = 0; ri < runs.length; ri++) { + const rPr = runs[ri].getElementsByTagName('a:rPr')[0]; + if (rPr) { + const sz = rPr.getAttribute('sz'); + if (sz) { + const pt = parseInt(sz, 10) / 100; + if (pt > maxFontPt) maxFontPt = pt; + } + } + } + + const lineHeight = maxFontPt * 1.4; + cursorY += maxFontPt; // baseline offset + + let cursorX = x + 4; // small left padding + + for (let ri = 0; ri < runs.length; ri++) { + const run = runs[ri]; + const rPr = run.getElementsByTagName('a:rPr')[0]; + const text = run.getElementsByTagName('a:t')[0]?.textContent ?? ''; + if (!text) continue; + + let fontPt = defaultFontSize; + let bold = false; + let italic = false; + let color = '#000000'; + + if (rPr) { + if (rPr.getAttribute('b') === '1') bold = true; + if (rPr.getAttribute('i') === '1') italic = true; + const sz = rPr.getAttribute('sz'); + if (sz) fontPt = parseInt(sz, 10) / 100; + const solidFill = rPr.getElementsByTagName('a:solidFill')[0]; + if (solidFill) { + const srgb = solidFill.getElementsByTagName('a:srgbClr')[0]; + if (srgb) { + const val = srgb.getAttribute('val'); + if (val) color = `#${val}`; + } + } + } + + ctx.font = `${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${fontPt}pt Calibri, Arial, sans-serif`; + ctx.fillStyle = color; + ctx.textBaseline = 'alphabetic'; + + // Simple word-wrap within the shape bounds + const words = text.split(/(\s+)/); + for (const word of words) { + const metrics = ctx.measureText(word); + if (cursorX + metrics.width > x + w && cursorX > x + 4) { + cursorX = x + 4; + cursorY += lineHeight; + } + if (cursorY > y + h) break; + ctx.fillText(word, cursorX, cursorY); + cursorX += metrics.width; + } + } + + cursorY += lineHeight * 0.4; // paragraph spacing + } + + ctx.restore(); + } + + images.push(canvas.toDataURL('image/png')); + } + + return { images, width: slideW, height: slideH }; +}