mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
Improve docx preview (#15907)
This commit is contained in:
197
web/package-lock.json
generated
197
web/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@antv/g2": "^5.2.10",
|
||||
"@antv/g6": "^5.1.0",
|
||||
"@extend-ai/react-docx": "^0.6.7",
|
||||
"@floating-ui/react": "^0.27.19",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@js-preview/excel": "^1.7.14",
|
||||
@@ -73,7 +74,6 @@
|
||||
"lexical": "^0.23.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^1.7.0",
|
||||
"mammoth": "^1.7.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"openai-speech-stream-player": "^1.0.8",
|
||||
"papaparse": "^5.5.3",
|
||||
@@ -1275,6 +1275,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@chenglou/pretext": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@chenglou/pretext/-/pretext-0.0.5.tgz",
|
||||
"integrity": "sha512-A8GZN10REdFGsyuiUgLV8jjPDDFMg5GmgxGWV0I3igxBOnzj+jgz2VMmVD7g+SFyoctfeqHFxbNatKSzVRWtRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -2014,6 +2020,22 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@extend-ai/react-docx": {
|
||||
"version": "0.6.7",
|
||||
"resolved": "https://registry.npmmirror.com/@extend-ai/react-docx/-/react-docx-0.6.7.tgz",
|
||||
"integrity": "sha512-4z95OFWNYKOEzIVKxoe78Eg6tS+Cu3gA3HnF7C0DEozK+ThECh8U1xSmiqZwp4bjC2OSxvMUtgNNhkEkl6YjAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chenglou/pretext": "^0.0.5",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"fast-png": "^8.0.0",
|
||||
"utif": "^3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffmpeg/ffmpeg": {
|
||||
"version": "0.11.6",
|
||||
"resolved": "https://registry.npmmirror.com/@ffmpeg/ffmpeg/-/ffmpeg-0.11.6.tgz",
|
||||
@@ -7685,6 +7707,23 @@
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmmirror.com/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz",
|
||||
"integrity": "sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.17.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmmirror.com/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||
@@ -7698,6 +7737,16 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmmirror.com/@tanstack/virtual-core/-/virtual-core-3.17.0.tgz",
|
||||
"integrity": "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
@@ -9247,15 +9296,6 @@
|
||||
"react": "^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.11",
|
||||
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
|
||||
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
@@ -10075,26 +10115,6 @@
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
@@ -10140,12 +10160,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz",
|
||||
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -12010,12 +12024,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dingbat-to-unicode": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
|
||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -12224,15 +12232,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/duck": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz",
|
||||
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -13419,6 +13418,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/fast-png/-/fast-png-8.0.0.tgz",
|
||||
"integrity": "sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fflate": "^0.8.2",
|
||||
"iobuffer": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
@@ -13490,6 +13499,12 @@
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz",
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@@ -15373,6 +15388,12 @@
|
||||
"deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/iobuffer/-/iobuffer-6.0.1.tgz",
|
||||
"integrity": "sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
||||
@@ -17990,17 +18011,6 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lop": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/lop/-/lop-0.4.2.tgz",
|
||||
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"duck": "^0.1.12",
|
||||
"option": "~0.2.1",
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz",
|
||||
@@ -18114,39 +18124,6 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/mammoth": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.11.0.tgz",
|
||||
"integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.6",
|
||||
"argparse": "~1.0.3",
|
||||
"base64-js": "^1.5.1",
|
||||
"bluebird": "~3.4.0",
|
||||
"dingbat-to-unicode": "^1.0.1",
|
||||
"jszip": "^3.7.1",
|
||||
"lop": "^0.4.2",
|
||||
"path-is-absolute": "^1.0.0",
|
||||
"underscore": "^1.13.1",
|
||||
"xmlbuilder": "^10.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"mammoth": "bin/mammoth"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mammoth/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-extensions": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
|
||||
@@ -19847,12 +19824,6 @@
|
||||
"integrity": "sha512-4pYnxvvOpL1PVgyuXly2GXG7IyGZErKgoaRUZKTi84Sd2sRLYtu5YfcpVip1nbH6awvxDYaIRyQrKP4Jm1zzRA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/option": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
|
||||
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -25911,12 +25882,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.7.tgz",
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
|
||||
@@ -26220,6 +26185,15 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utif": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/utif/-/utif-3.1.0.tgz",
|
||||
"integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -27825,15 +27799,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@antv/g2": "^5.2.10",
|
||||
"@antv/g6": "^5.1.0",
|
||||
"@extend-ai/react-docx": "^0.6.7",
|
||||
"@floating-ui/react": "^0.27.19",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@js-preview/excel": "^1.7.14",
|
||||
@@ -95,7 +96,6 @@
|
||||
"lexical": "^0.23.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^1.7.0",
|
||||
"mammoth": "^1.7.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"openai-speech-stream-player": "^1.0.8",
|
||||
"papaparse": "^5.5.3",
|
||||
|
||||
@@ -1,134 +1,242 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import request from '@/utils/request';
|
||||
import { DocxEditorViewer, useDocxEditor } from '@extend-ai/react-docx';
|
||||
import classNames from 'classnames';
|
||||
import mammoth from 'mammoth';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ZoomIn, ZoomOut } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface DocPreviewerProps {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Word document preview component. Behavior:
|
||||
// 1) Fetches the document as a Blob.
|
||||
// 2) Detects .docx input via a ZIP header probe.
|
||||
// 3) Renders .docx using Mammoth; presents a controlled "unsupported" notice for non-ZIP payloads.
|
||||
// ZIP file header bytes "PK"
|
||||
const ZIP_HEADER_0 = 0x50;
|
||||
const ZIP_HEADER_1 = 0x4b;
|
||||
|
||||
const isZipLikeBlob = async (blob: Blob): Promise<boolean> => {
|
||||
try {
|
||||
const headerSlice = blob.slice(0, 4);
|
||||
const buf = await headerSlice.arrayBuffer();
|
||||
const bytes = new Uint8Array(buf);
|
||||
return (
|
||||
bytes.length >= 2 &&
|
||||
bytes[0] === ZIP_HEADER_0 &&
|
||||
bytes[1] === ZIP_HEADER_1
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to inspect blob header', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ZOOM_STEPS = [25, 50, 75, 100, 125, 150, 175, 200] as const;
|
||||
|
||||
const clampZoom = (scale: number, direction: 1 | -1): number => {
|
||||
let idx = ZOOM_STEPS.indexOf(scale as (typeof ZOOM_STEPS)[number]);
|
||||
if (idx < 0) {
|
||||
if (direction > 0) {
|
||||
idx = ZOOM_STEPS.findIndex((v) => v > scale);
|
||||
} else {
|
||||
for (let i = ZOOM_STEPS.length - 1; i >= 0; i--) {
|
||||
if (ZOOM_STEPS[i] < scale) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
idx = Math.max(
|
||||
0,
|
||||
Math.min(ZOOM_STEPS.length - 1, idx < 0 ? 0 : idx + direction),
|
||||
);
|
||||
return ZOOM_STEPS[idx] ?? scale;
|
||||
};
|
||||
|
||||
// Word document preview component.
|
||||
// Uses @extend-ai/react-docx for canvas-based page-level rendering.
|
||||
// Falls back to an unsupported notice for legacy .doc (non-ZIP) payloads.
|
||||
export const DocPreviewer: React.FC<DocPreviewerProps> = ({
|
||||
className,
|
||||
url,
|
||||
}) => {
|
||||
const [htmlContent, setHtmlContent] = useState<string>('');
|
||||
const editor = useDocxEditor({ initialFileName: 'document.docx' });
|
||||
const { importDocxFile, status, totalPages } = editor;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [zoomScale, setZoomScale] = useState(100);
|
||||
const cancelledRef = useRef(false);
|
||||
|
||||
// Determines whether the Blob represents a .docx document by checking for the ZIP
|
||||
// file signature ("PK") in the initial bytes. A valid .docx file is a ZIP container
|
||||
// and always begins with:
|
||||
// 50 4B 03 04 ("PK..")
|
||||
//
|
||||
// Legacy .doc files use the CFBF binary format, commonly starting with:
|
||||
// D0 CF 11 E0 A1 B1 1A E1
|
||||
//
|
||||
// Note that some files distributed with a “.doc” extension may internally be .docx
|
||||
// documents (e.g., renamed files or files produced by systems that export .docx
|
||||
// content under a .doc filename). These files will still present the ZIP signature
|
||||
// and are therefore treated as supported .docx payloads. The header inspection
|
||||
// ensures correct routing regardless of filename or reported extension.
|
||||
const isZipLikeBlob = async (blob: Blob): Promise<boolean> => {
|
||||
try {
|
||||
const headerSlice = blob.slice(0, 4);
|
||||
const buf = await headerSlice.arrayBuffer();
|
||||
const bytes = new Uint8Array(buf);
|
||||
|
||||
// ZIP files start with "PK" (0x50, 0x4B)
|
||||
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
||||
} catch (e) {
|
||||
console.error('Failed to inspect blob header', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDocument = async () => {
|
||||
// Fetch the document blob and load it into the editor
|
||||
const fetchDocument = useCallback(async () => {
|
||||
if (!url) return;
|
||||
|
||||
cancelledRef.current = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
onError: () => {
|
||||
message.error('Document parsing failed');
|
||||
console.error('Error loading document:', url);
|
||||
},
|
||||
});
|
||||
let res;
|
||||
try {
|
||||
res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
onError: () => {
|
||||
if (!cancelledRef.current) {
|
||||
message.error('Document parsing failed');
|
||||
console.error('Error loading document:', url);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
if (!cancelledRef.current) {
|
||||
setError('Failed to fetch document.');
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
try {
|
||||
const blob: Blob = res.data;
|
||||
const contentType: string =
|
||||
blob.type || (res as any).headers?.['content-type'] || '';
|
||||
|
||||
// Execution path selection: ZIP-like payloads are treated as .docx and rendered via Mammoth;
|
||||
// non-ZIP payloads receive an explicit unsupported notice.
|
||||
const looksLikeZip = await isZipLikeBlob(blob);
|
||||
|
||||
if (!looksLikeZip) {
|
||||
// Non-ZIP payload (likely legacy .doc or another format): skip Mammoth processing.
|
||||
setHtmlContent(`
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<div class="border border-dashed border-border-normal rounded-xl p-8 max-w-2xl text-center">
|
||||
<p class="text-2xl font-bold mb-4">
|
||||
Preview is not available for this Word document
|
||||
</p>
|
||||
<p class="italic text-sm text-muted-foreground leading-relaxed">
|
||||
Mammoth supports modern <code>.docx</code> files only.<br/>
|
||||
The file header does not indicate a <code>.docx</code> ZIP archive.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
setError(
|
||||
'This file header does not indicate a .docx ZIP archive. Only .docx files are supported.',
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// ZIP-like payload: parse as .docx with Mammoth
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const result = await mammoth.convertToHtml(
|
||||
{ arrayBuffer },
|
||||
{ includeDefaultStyleMap: true },
|
||||
);
|
||||
const file = new File([blob], 'document.docx', {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
});
|
||||
|
||||
const styledContent = result.value
|
||||
.replace(/<p>/g, '<p class="mb-2">')
|
||||
.replace(/<h(\d)>/g, '<h$1 class="font-semibold mt-4 mb-2">');
|
||||
await importDocxFile(file);
|
||||
|
||||
setHtmlContent(styledContent);
|
||||
if (!cancelledRef.current) {
|
||||
setZoomScale(100);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('Failed to parse document.');
|
||||
console.error('Error parsing document:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!cancelledRef.current) {
|
||||
message.error('Failed to parse document.');
|
||||
console.error('Error parsing document:', err);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [url, importDocxFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
fetchDocument();
|
||||
fetchDocument();
|
||||
return () => {
|
||||
cancelledRef.current = true;
|
||||
};
|
||||
}, [fetchDocument]);
|
||||
|
||||
// Monitor editor status for library-level errors
|
||||
useEffect(() => {
|
||||
if (status === 'Only .docx files are supported') {
|
||||
setError(status);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [url]);
|
||||
}, [status]);
|
||||
|
||||
const handleZoomIn = useCallback(() => {
|
||||
setZoomScale((s) => clampZoom(s, 1));
|
||||
}, []);
|
||||
|
||||
const handleZoomOut = useCallback(() => {
|
||||
setZoomScale((s) => clampZoom(s, -1));
|
||||
}, []);
|
||||
|
||||
const showContent = !loading && !error;
|
||||
const pageCount = showContent && totalPages > 0 ? totalPages : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md overflow-auto',
|
||||
'relative w-full h-full flex flex-col bg-background-paper border border-border-normal rounded-md overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between shrink-0 px-4 py-2 border-b border-border-normal bg-background-paper">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{loading ? 'Loading...' : error ? '' : `Page ${pageCount || '-'}`}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading || !!error || zoomScale <= ZOOM_STEPS[0]}
|
||||
className="p-1 rounded hover:bg-gray-100 disabled:opacity-30 transition-opacity"
|
||||
onClick={handleZoomOut}
|
||||
aria-label="Zoom out"
|
||||
>
|
||||
<ZoomOut className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-sm w-12 text-center tabular-nums select-none">
|
||||
{zoomScale}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
loading ||
|
||||
!!error ||
|
||||
zoomScale >= ZOOM_STEPS[ZOOM_STEPS.length - 1]
|
||||
}
|
||||
className="p-1 rounded hover:bg-gray-100 disabled:opacity-30 transition-opacity"
|
||||
onClick={handleZoomIn}
|
||||
aria-label="Zoom in"
|
||||
>
|
||||
<ZoomIn className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!loading && <div dangerouslySetInnerHTML={{ __html: htmlContent }} />}
|
||||
{/* Viewer / Error area */}
|
||||
<div className="relative flex-1 overflow-auto bg-background-paper">
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div className="flex items-center justify-center h-full p-8">
|
||||
<div className="border border-dashed border-border-normal rounded-xl p-8 max-w-2xl text-center">
|
||||
<p className="text-2xl font-bold mb-4">
|
||||
Preview is not available for this Word document
|
||||
</p>
|
||||
<p className="italic text-sm text-muted-foreground leading-relaxed">
|
||||
@extend-ai/react-docx supports modern <code>.docx</code> files
|
||||
only.
|
||||
<br />
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showContent && (
|
||||
<div className="flex justify-center p-4">
|
||||
<div style={{ zoom: zoomScale / 100 }}>
|
||||
<DocxEditorViewer
|
||||
editor={editor}
|
||||
mode="read-only"
|
||||
loadingState={
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Spin />
|
||||
</div>
|
||||
}
|
||||
pageGapBackgroundColor="#f5f5f5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getAuthorization } from '@/utils/authorization-util';
|
||||
import jsPreviewExcel from '@js-preview/excel';
|
||||
import { useSize } from 'ahooks';
|
||||
import axios from 'axios';
|
||||
import mammoth from 'mammoth';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
export const useDocumentResizeObserver = () => {
|
||||
@@ -127,45 +126,6 @@ export const useFetchExcel = (filePath: string) => {
|
||||
return { status, containerRef, error };
|
||||
};
|
||||
|
||||
export const useFetchDocx = (filePath: string) => {
|
||||
const [succeed, setSucceed] = useState(true);
|
||||
const [error, setError] = useState<string>();
|
||||
const { fetchDocument } = useFetchDocument();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchDocumentAsync = useCallback(async () => {
|
||||
try {
|
||||
const jsonFile = await fetchDocument(filePath);
|
||||
mammoth
|
||||
.convertToHtml(
|
||||
{ arrayBuffer: jsonFile.data },
|
||||
{ includeDefaultStyleMap: true },
|
||||
)
|
||||
.then((result) => {
|
||||
setSucceed(true);
|
||||
const docEl = document.createElement('div');
|
||||
docEl.className = 'document-container';
|
||||
docEl.innerHTML = result.value;
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
container.innerHTML = docEl.outerHTML;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setSucceed(false);
|
||||
});
|
||||
} catch (error: any) {
|
||||
setError(error.toString());
|
||||
}
|
||||
}, [filePath, fetchDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocumentAsync();
|
||||
}, [fetchDocumentAsync]);
|
||||
|
||||
return { succeed, containerRef, error };
|
||||
};
|
||||
|
||||
export const useCatchDocumentError = (url: string) => {
|
||||
const httpHeaders = useMemo(() => {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user