From a4f64abd1f442835caedeee93bb591b0ae3dd0c4 Mon Sep 17 00:00:00 2001 From: Jiahe Wu <117764480+blueberrycongee@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:06:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20add=20multi-format=20report=20expo?= =?UTF-8?q?rt=20(Markdown,=20HTML,=20PDF,=20Word,=E2=80=A6=20(#756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(web): add multi-format report export (Markdown, HTML, PDF, Word, Image) * fix: correct import order for docx (lint error) * fix(web): address Copilot review comments for multi-format export - Add i18n support for dropdown menu items (en/zh) - Add DOMPurify for HTML sanitization (XSS protection) - Fix async handling for canvas.toBlob with Promise wrapper - Add toast notifications for export errors - Fix Tooltip + DropdownMenuTrigger nesting (accessibility) - Ensure container cleanup in finally block * fix(web): enhance markdown parsing for PDF and Word export - Add list support (bullet and numbered) for PDF export - Add parseInlineMarkdown helper for Word export to handle bold, italic, code, links - Add list support for Word export (bullet and numbered) - Address Copilot review comments from PR #756 * fix(web): address PR review feedback for multi-format export - Extract PDF formatting magic numbers into PDF_CONSTANTS - Add Tooltip wrapper for download dropdown button - Reduce triggerDownload cleanup timeout from 1000ms to 100ms - Use marked.Lexer.lexInline for robust markdown parsing - Add console.warn for image export cleanup errors - Add numbering config for Word document ordered lists - Fix CSS class typo: px-5pb-20 -> px-5 pb-20 - Remove unreachable dead code in parseInlineMarkdown --------- Co-authored-by: Willem Jiang --- web/messages/en.json | 8 +- web/messages/zh.json | 8 +- web/package.json | 8 + web/pnpm-lock.yaml | 525 ++++++++++++-- .../app/chat/components/research-block.tsx | 647 ++++++++++++++++-- 5 files changed, 1083 insertions(+), 113 deletions(-) diff --git a/web/messages/en.json b/web/messages/en.json index 14c52b6..7eb1a2c 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -130,7 +130,13 @@ "generatePodcast": "Generate podcast", "edit": "Edit", "copy": "Copy", - "downloadReport": "Download report as markdown", + "downloadReport": "Download report", + "downloadMarkdown": "Markdown (.md)", + "downloadHTML": "HTML (.html)", + "downloadPDF": "PDF (.pdf)", + "downloadWord": "Word (.docx)", + "downloadImage": "Image (.png)", + "exportFailed": "Export failed, please try again", "searchingFor": "Searching for", "reading": "Reading", "runningPythonCode": "Running Python code", diff --git a/web/messages/zh.json b/web/messages/zh.json index 7407894..d74b11f 100644 --- a/web/messages/zh.json +++ b/web/messages/zh.json @@ -130,7 +130,13 @@ "generatePodcast": "生成播客", "edit": "编辑", "copy": "复制", - "downloadReport": "下载报告为 Markdown", + "downloadReport": "下载报告", + "downloadMarkdown": "Markdown (.md)", + "downloadHTML": "HTML (.html)", + "downloadPDF": "PDF (.pdf)", + "downloadWord": "Word (.docx)", + "downloadImage": "图片 (.png)", + "exportFailed": "导出失败,请重试", "searchingFor": "搜索", "reading": "阅读中", "runningPythonCode": "运行 Python 代码", diff --git a/web/package.json b/web/package.json index 34ef8e8..fc816ff 100644 --- a/web/package.json +++ b/web/package.json @@ -49,21 +49,28 @@ "@tiptap/extension-table-row": "^2.11.7", "@tiptap/extension-text": "^2.12.0", "@tiptap/react": "^2.11.7", + "@types/dompurify": "^3.2.0", "@types/js-cookie": "^3.0.6", "@xyflow/react": "^12.6.0", "best-effort-json-parser": "^1.1.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "docx": "^9.5.1", + "dompurify": "^3.3.1", + "file-saver": "^2.0.5", "framer-motion": "^12.6.5", "hast": "^1.0.0", "highlight.js": "^11.11.1", + "html2canvas": "^1.4.1", "immer": "^10.1.1", "js-cookie": "^3.0.5", + "jspdf": "^3.0.4", "katex": "^0.16.21", "lowlight": "^3.3.0", "lru-cache": "^11.1.0", "lucide-react": "^0.487.0", + "marked": "^17.0.1", "motion": "^12.7.4", "nanoid": "^5.1.5", "next": "^15.4.10", @@ -94,6 +101,7 @@ "@tailwindcss/postcss": "^4.0.15", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", + "@types/file-saver": "^2.0.7", "@types/hast": "^3.0.4", "@types/jest": "^30.0.0", "@types/node": "^20.14.10", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 990efea..c72f849 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: '@tiptap/react': specifier: ^2.11.7 version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -113,6 +116,15 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.1.1(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + docx: + specifier: ^9.5.1 + version: 9.5.1 + dompurify: + specifier: ^3.3.1 + version: 3.3.1 + file-saver: + specifier: ^2.0.5 + version: 2.0.5 framer-motion: specifier: ^12.6.5 version: 12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -122,12 +134,18 @@ importers: highlight.js: specifier: ^11.11.1 version: 11.11.1 + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 immer: specifier: ^10.1.1 version: 10.1.1 js-cookie: specifier: ^3.0.5 version: 3.0.5 + jspdf: + specifier: ^3.0.4 + version: 3.0.4 katex: specifier: ^0.16.21 version: 0.16.21 @@ -140,6 +158,9 @@ importers: lucide-react: specifier: ^0.487.0 version: 0.487.0(react@19.1.0) + marked: + specifier: ^17.0.1 + version: 17.0.1 motion: specifier: ^12.7.4 version: 12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -148,10 +169,10 @@ importers: version: 5.1.5 next: specifier: ^15.4.10 - version: 15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-intl: specifier: ^4.3.1 - version: 4.3.1(next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + version: 4.3.1(next@15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -225,6 +246,9 @@ importers: '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.1(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 '@types/hast': specifier: ^3.0.4 version: 3.0.4 @@ -466,6 +490,10 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -672,89 +700,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -911,56 +955,60 @@ packages: '@napi-rs/wasm-runtime@0.2.9': resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} - '@next/env@15.4.10': - resolution: {integrity: sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg==} + '@next/env@15.5.9': + resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} '@next/eslint-plugin-next@15.3.0': resolution: {integrity: sha512-511UUcpWw5GWTyKfzW58U2F/bYJyjLE9e3SlnGK/zSXq7RqLlqFO8B9bitJjumLpj317fycC96KZ2RZsjGNfBw==} - '@next/swc-darwin-arm64@15.4.8': - resolution: {integrity: sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==} + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.8': - resolution: {integrity: sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw==} + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.8': - resolution: {integrity: sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw==} + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] - '@next/swc-linux-arm64-musl@15.4.8': - resolution: {integrity: sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==} + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] - '@next/swc-linux-x64-gnu@15.4.8': - resolution: {integrity: sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==} + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] - '@next/swc-linux-x64-musl@15.4.8': - resolution: {integrity: sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==} + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] - '@next/swc-win32-arm64-msvc@15.4.8': - resolution: {integrity: sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==} + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.8': - resolution: {integrity: sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg==} + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1738,24 +1786,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.4': resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.4': resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.4': resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.4': resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==} @@ -2083,6 +2135,10 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -2098,6 +2154,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} @@ -2161,12 +2220,21 @@ packages: '@types/node@20.19.13': resolution: {integrity: sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==} - '@types/node@20.19.26': - resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==} + '@types/node@20.19.25': + resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} '@types/node@22.15.2': resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} + '@types/node@24.10.3': + resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} + + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/react-dom@19.1.1': resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==} peerDependencies: @@ -2184,6 +2252,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2313,76 +2384,91 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-gnu@1.5.0': resolution: {integrity: sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-arm64-musl@1.5.0': resolution: {integrity: sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-ppc64-gnu@1.5.0': resolution: {integrity: sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.5.0': resolution: {integrity: sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-s390x-gnu@1.5.0': resolution: {integrity: sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.5.0': resolution: {integrity: sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-x64-musl@1.5.0': resolution: {integrity: sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -2654,8 +2740,12 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.9.6: - resolution: {integrity: sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + + baseline-browser-mapping@2.9.2: + resolution: {integrity: sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw==} hasBin: true best-effort-json-parser@1.1.3: @@ -2718,8 +2808,12 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001760: - resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2823,6 +2917,12 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -2830,6 +2930,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-styled@1.0.8: resolution: {integrity: sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g==} @@ -2973,9 +3076,16 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + docx@9.5.1: + resolution: {integrity: sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==} + engines: {node: '>=10'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dotenv-cli@8.0.0: resolution: {integrity: sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==} hasBin: true @@ -2998,8 +3108,8 @@ packages: electron-to-chromium@1.5.214: resolution: {integrity: sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + electron-to-chromium@1.5.265: + resolution: {integrity: sha512-B7IkLR1/AE+9jR2LtVF/1/6PFhY5TlnEHnlrKmGk7PvkJibg5jr+mLXLLzq3QYl6PA1T/vLDthQPqIPAlS/PPA==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -3019,8 +3129,8 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -3249,6 +3359,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -3269,10 +3382,16 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -3445,6 +3564,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3509,6 +3631,10 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3533,6 +3659,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.1: resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} @@ -3566,6 +3695,9 @@ packages: intl-messageformat@10.7.16: resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -3711,6 +3843,9 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3954,10 +4089,16 @@ packages: engines: {node: '>=6'} hasBin: true + jspdf@3.0.4: + resolution: {integrity: sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + katex@0.16.21: resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==} hasBin: true @@ -3986,6 +4127,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-darwin-arm64@1.29.2: resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} engines: {node: '>= 12.0.0'} @@ -4015,24 +4159,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.29.2: resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} @@ -4139,6 +4287,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@17.0.1: + resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4304,6 +4457,9 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4382,8 +4538,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.4.10: - resolution: {integrity: sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ==} + next@15.5.9: + resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -4504,6 +4660,12 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4540,6 +4702,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4655,6 +4820,9 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4736,6 +4904,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -4849,6 +5020,9 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -4856,6 +5030,9 @@ packages: refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -4920,6 +5097,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} @@ -4933,6 +5114,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -4947,6 +5131,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -4989,6 +5176,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -5064,6 +5254,10 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -5099,6 +5293,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -5157,6 +5354,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + swr@2.3.3: resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} peerDependencies: @@ -5183,8 +5384,8 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -5208,6 +5409,9 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} @@ -5358,6 +5562,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -5447,6 +5654,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -5562,10 +5772,17 @@ packages: utf-8-validate: optional: true + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -5828,6 +6045,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -6378,34 +6597,34 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.4.10': {} + '@next/env@15.5.9': {} '@next/eslint-plugin-next@15.3.0': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.4.8': + '@next/swc-darwin-arm64@15.5.7': optional: true - '@next/swc-darwin-x64@15.4.8': + '@next/swc-darwin-x64@15.5.7': optional: true - '@next/swc-linux-arm64-gnu@15.4.8': + '@next/swc-linux-arm64-gnu@15.5.7': optional: true - '@next/swc-linux-arm64-musl@15.4.8': + '@next/swc-linux-arm64-musl@15.5.7': optional: true - '@next/swc-linux-x64-gnu@15.4.8': + '@next/swc-linux-x64-gnu@15.5.7': optional: true - '@next/swc-linux-x64-musl@15.4.8': + '@next/swc-linux-x64-musl@15.5.7': optional: true - '@next/swc-win32-arm64-msvc@15.4.8': + '@next/swc-win32-arm64-msvc@15.5.7': optional: true - '@next/swc-win32-x64-msvc@15.4.8': + '@next/swc-win32-x64-msvc@15.5.7': optional: true '@nodelib/fs.scandir@2.1.5': @@ -7552,6 +7771,10 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.1 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -7570,6 +7793,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/file-saver@2.0.7': {} + '@types/hast@2.3.10': dependencies: '@types/unist': 2.0.11 @@ -7639,7 +7864,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@20.19.26': + '@types/node@20.19.25': dependencies: undici-types: 6.21.0 @@ -7647,6 +7872,15 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.10.3': + dependencies: + undici-types: 7.16.0 + + '@types/pako@2.0.4': {} + + '@types/raf@3.4.3': + optional: true + '@types/react-dom@19.1.1(@types/react@19.1.2)': dependencies: '@types/react': 19.1.2 @@ -7663,6 +7897,9 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8171,7 +8408,9 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.9.6: {} + base64-arraybuffer@1.0.2: {} + + baseline-browser-mapping@2.9.2: {} best-effort-json-parser@1.1.3: {} @@ -8192,16 +8431,16 @@ snapshots: browserslist@4.25.4: dependencies: - caniuse-lite: 1.0.30001760 + caniuse-lite: 1.0.30001759 electron-to-chromium: 1.5.214 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.4) browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.6 - caniuse-lite: 1.0.30001760 - electron-to-chromium: 1.5.267 + baseline-browser-mapping: 2.9.2 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.265 node-releases: 2.0.27 update-browserslist-db: 1.2.2(browserslist@4.28.1) @@ -8238,7 +8477,19 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001760: {} + caniuse-lite@1.0.30001759: {} + + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.4 + '@types/raf': 3.4.3 + core-js: 3.47.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true ccount@2.0.1: {} @@ -8321,6 +8572,11 @@ snapshots: convert-source-map@2.0.0: {} + core-js@3.47.0: + optional: true + + core-util-is@1.0.3: {} + crelt@1.0.6: {} cross-spawn@7.0.6: @@ -8329,6 +8585,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-styled@1.0.8: dependencies: '@daybrush/utils': 1.13.0 @@ -8456,8 +8716,21 @@ snapshots: dependencies: esutils: 2.0.3 + docx@9.5.1: + dependencies: + '@types/node': 24.10.3 + hash.js: 1.1.7 + jszip: 3.10.1 + nanoid: 5.1.5 + xml: 1.0.1 + xml-js: 1.6.11 + dom-accessibility-api@0.5.16: {} + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dotenv-cli@8.0.0: dependencies: cross-spawn: 7.0.6 @@ -8479,7 +8752,7 @@ snapshots: electron-to-chromium@1.5.214: {} - electron-to-chromium@1.5.267: {} + electron-to-chromium@1.5.265: {} emittery@0.13.1: {} @@ -8494,7 +8767,7 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 - enhanced-resolve@5.18.4: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -8870,6 +9143,12 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + fast-uri@3.1.0: {} fastq@1.19.1: @@ -8888,10 +9167,14 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 + file-saver@2.0.5: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -9066,6 +9349,11 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -9176,6 +9464,11 @@ snapshots: html-url-attributes@3.0.1: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -9202,6 +9495,8 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + immer@10.1.1: {} import-fresh@3.3.1: @@ -9238,6 +9533,8 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.2 tslib: 2.8.1 + iobuffer@5.4.0: {} + is-alphabetical@1.0.4: {} is-alphabetical@2.0.1: {} @@ -9381,6 +9678,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -9767,7 +10066,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.26 + '@types/node': 20.19.25 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -9857,6 +10156,17 @@ snapshots: json5@2.2.3: {} + jspdf@3.0.4: + dependencies: + '@babel/runtime': 7.28.4 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.47.0 + dompurify: 3.3.1 + html2canvas: 1.4.1 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -9864,6 +10174,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + katex@0.16.21: dependencies: commander: 8.3.0 @@ -9894,6 +10211,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-darwin-arm64@1.29.2: optional: true @@ -10025,6 +10346,8 @@ snapshots: markdown-table@3.0.4: {} + marked@17.0.1: {} + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -10412,6 +10735,8 @@ snapshots: mimic-fn@2.1.0: {} + minimalistic-assert@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -10452,11 +10777,11 @@ snapshots: neo-async@2.6.2: {} - next-intl@4.3.1(next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + next-intl@4.3.1(next@15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3): dependencies: '@formatjs/intl-localematcher': 0.5.10 negotiator: 1.0.0 - next: 15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 use-intl: 4.3.1(react@19.1.0) optionalDependencies: @@ -10467,24 +10792,24 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.4.10 + '@next/env': 15.5.9 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001760 + caniuse-lite: 1.0.30001759 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.8 - '@next/swc-darwin-x64': 15.4.8 - '@next/swc-linux-arm64-gnu': 15.4.8 - '@next/swc-linux-arm64-musl': 15.4.8 - '@next/swc-linux-x64-gnu': 15.4.8 - '@next/swc-linux-x64-musl': 15.4.8 - '@next/swc-win32-arm64-msvc': 15.4.8 - '@next/swc-win32-x64-msvc': 15.4.8 + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: @@ -10638,6 +10963,10 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -10685,6 +11014,9 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -10740,6 +11072,8 @@ snapshots: prismjs@1.30.0: {} + process-nextick-args@2.0.1: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10863,6 +11197,11 @@ snapshots: queue-microtask@1.2.3: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -11021,6 +11360,16 @@ snapshots: react@19.1.0: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -11038,6 +11387,9 @@ snapshots: parse-entities: 2.0.0 prismjs: 1.27.0 + regenerator-runtime@0.13.11: + optional: true + regenerator-runtime@0.14.1: {} regexp.prototype.flags@1.5.4: @@ -11132,6 +11484,9 @@ snapshots: reusify@1.1.0: {} + rgbcolor@1.0.1: + optional: true + rope-sequence@1.3.4: {} rrweb-cssom@0.8.0: {} @@ -11148,6 +11503,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -11163,6 +11520,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.4.3: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -11225,6 +11584,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -11328,6 +11689,9 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackblur-canvas@2.7.0: + optional: true + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -11395,6 +11759,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -11441,6 +11809,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + swr@2.3.3(react@19.1.0): dependencies: dequal: 2.0.3 @@ -11461,7 +11832,7 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.16(webpack@5.99.6): + terser-webpack-plugin@5.3.14(webpack@5.99.6): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 @@ -11483,6 +11854,10 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) @@ -11640,6 +12015,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.16.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -11778,6 +12155,10 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.30 @@ -11830,7 +12211,7 @@ snapshots: acorn: 8.15.0 browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -11842,7 +12223,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.99.6) + terser-webpack-plugin: 5.3.14(webpack@5.99.6) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -11931,8 +12312,14 @@ snapshots: ws@8.18.3: {} + xml-js@1.6.11: + dependencies: + sax: 1.4.3 + xml-name-validator@5.0.0: {} + xml@1.0.1: {} + xmlchars@2.2.0: {} xtend@4.0.2: {} diff --git a/web/src/app/chat/components/research-block.tsx b/web/src/app/chat/components/research-block.tsx index 8cebffc..2880351 100644 --- a/web/src/app/chat/components/research-block.tsx +++ b/web/src/app/chat/components/research-block.tsx @@ -1,14 +1,46 @@ // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT -import { Check, Copy, Headphones, Pencil, Undo2, X, Download } from "lucide-react"; +import { + Document, + Packer, + Paragraph, + TextRun, + HeadingLevel, + ExternalHyperlink, +} from "docx"; +import DOMPurify from "dompurify"; +import { saveAs } from "file-saver"; +import html2canvas from "html2canvas"; +import { jsPDF } from "jspdf"; +import { + Check, + Copy, + Headphones, + Pencil, + Undo2, + X, + Download, + FileText, + FileCode, + FileImage, + FileType, +} from "lucide-react"; +import { marked } from "marked"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; import { ScrollContainer } from "~/components/deer-flow/scroll-container"; import { Tooltip } from "~/components/deer-flow/tooltip"; import { Button } from "~/components/ui/button"; import { Card } from "~/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { useReplay } from "~/core/replay"; import { closeResearch, listenToPodcast, useStore } from "~/core/store"; @@ -50,6 +82,7 @@ export function ResearchBlock({ }, [researchId]); const [editing, setEditing] = useState(false); + const [isDownloading, setIsDownloading] = useState(false); const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { if (!reportId) { @@ -66,38 +99,534 @@ export function ResearchBlock({ }, 1000); }, [reportId]); - // Download report as markdown - const handleDownload = useCallback(() => { - if (!reportId) { - return; - } - const report = useStore.getState().messages.get(reportId); - if (!report) { - return; - } + // Helper function to generate timestamp for filenames + const getTimestamp = useCallback(() => { const now = new Date(); - const pad = (n: number) => n.toString().padStart(2, '0'); - const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`; - const filename = `research-report-${timestamp}.md`; - const blob = new Blob([report.content], { type: 'text/markdown' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - setTimeout(() => { - try { - if (a.parentNode) { - a.parentNode.removeChild(a); - } - } finally { - URL.revokeObjectURL(url); - } - }, 0); - }, [reportId]); + const pad = (n: number) => n.toString().padStart(2, "0"); + return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`; + }, []); + + // Helper function to trigger file download + const triggerDownload = useCallback( + (content: string, filename: string, mimeType: string) => { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + try { + if (a.parentNode) { + a.parentNode.removeChild(a); + } + } finally { + URL.revokeObjectURL(url); + } + }, 100); + }, + [], + ); + + // Download report as Markdown + const handleDownloadMarkdown = useCallback(() => { + if (!reportId) return; + const report = useStore.getState().messages.get(reportId); + if (!report) return; + triggerDownload( + report.content, + `research-report-${getTimestamp()}.md`, + "text/markdown", + ); + }, [reportId, getTimestamp, triggerDownload]); + + // Download report as HTML + const handleDownloadHTML = useCallback(() => { + if (!reportId) return; + const report = useStore.getState().messages.get(reportId); + if (!report) return; + const rawHtml = marked(report.content) as string; + const htmlContent = DOMPurify.sanitize(rawHtml); + const fullHTML = ` + + + + + Research Report + + + +${htmlContent} + +`; + triggerDownload( + fullHTML, + `research-report-${getTimestamp()}.html`, + "text/html", + ); + }, [reportId, getTimestamp, triggerDownload]); + + // Download report as PDF (text-based, no html2canvas) + const handleDownloadPDF = useCallback(async () => { + if (!reportId || isDownloading) return; + const report = useStore.getState().messages.get(reportId); + if (!report) return; + + setIsDownloading(true); + try { + const pdf = new jsPDF("p", "mm", "a4"); + const pageWidth = 210; + const pageHeight = 297; + const margin = 20; + const maxWidth = pageWidth - 2 * margin; + let y = margin; + + // PDF formatting constants for maintainability + const PDF_CONSTANTS = { + headings: { + h1: { fontSize: 20, lineHeight: 9, spacing: 6 }, + h2: { fontSize: 16, lineHeight: 7, spacing: 5 }, + h3: { fontSize: 14, lineHeight: 6, spacing: 4 }, + }, + text: { fontSize: 11, normalHeight: 5, paragraphSpacing: 2 }, + list: { bullet: "• ", indentLevel: 2 }, + emptyLine: { height: 4 }, + }; + + const lines = report.content.split("\n"); + + for (const line of lines) { + // Handle headings + if (line.startsWith("### ")) { + const h3 = PDF_CONSTANTS.headings.h3; + pdf.setFontSize(h3.fontSize); + pdf.setFont("helvetica", "bold"); + const text = line.substring(4); + const splitText = pdf.splitTextToSize(text, maxWidth); + if (y + 10 > pageHeight - margin) { + pdf.addPage(); + y = margin; + } + pdf.text(splitText, margin, y); + y += splitText.length * h3.lineHeight + h3.spacing; + } else if (line.startsWith("## ")) { + const h2 = PDF_CONSTANTS.headings.h2; + pdf.setFontSize(h2.fontSize); + pdf.setFont("helvetica", "bold"); + const text = line.substring(3); + const splitText = pdf.splitTextToSize(text, maxWidth); + if (y + 12 > pageHeight - margin) { + pdf.addPage(); + y = margin; + } + pdf.text(splitText, margin, y); + y += splitText.length * h2.lineHeight + h2.spacing; + } else if (line.startsWith("# ")) { + const h1 = PDF_CONSTANTS.headings.h1; + pdf.setFontSize(h1.fontSize); + pdf.setFont("helvetica", "bold"); + const text = line.substring(2); + const splitText = pdf.splitTextToSize(text, maxWidth); + if (y + 14 > pageHeight - margin) { + pdf.addPage(); + y = margin; + } + pdf.text(splitText, margin, y); + y += splitText.length * h1.lineHeight + h1.spacing; + } else if (line.startsWith("- ") || line.startsWith("* ")) { + // Unordered list item + const textConfig = PDF_CONSTANTS.text; + pdf.setFontSize(textConfig.fontSize); + pdf.setFont("helvetica", "normal"); + const cleanText = line + .substring(2) + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/`(.*?)`/g, "$1") + .replace(/\[(.*?)\]\(.*?\)/g, "$1"); + const bulletText = `• ${cleanText}`; + const splitText = pdf.splitTextToSize(bulletText, maxWidth - 5); + + if ( + y + splitText.length * textConfig.normalHeight > + pageHeight - margin + ) { + pdf.addPage(); + y = margin; + } + pdf.text(splitText, margin + PDF_CONSTANTS.list.indentLevel, y); + y += + splitText.length * textConfig.normalHeight + + PDF_CONSTANTS.text.paragraphSpacing; + } else if (/^\d+\.\s/.test(line)) { + // Ordered list item + const textConfig = PDF_CONSTANTS.text; + pdf.setFontSize(textConfig.fontSize); + pdf.setFont("helvetica", "normal"); + const match = /^(\d+)\.\s(.*)$/.exec(line); + if (match?.[1] && match[2]) { + const cleanText = match[2] + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/`(.*?)`/g, "$1") + .replace(/\[(.*?)\]\(.*?\)/g, "$1"); + const numberedText = `${match[1]}. ${cleanText}`; + const splitText = pdf.splitTextToSize(numberedText, maxWidth - 5); + + if ( + y + splitText.length * textConfig.normalHeight > + pageHeight - margin + ) { + pdf.addPage(); + y = margin; + } + pdf.text(splitText, margin + PDF_CONSTANTS.list.indentLevel, y); + y += + splitText.length * textConfig.normalHeight + + PDF_CONSTANTS.text.paragraphSpacing; + } + } else if (line.trim()) { + // Normal text + const textConfig = PDF_CONSTANTS.text; + pdf.setFontSize(textConfig.fontSize); + pdf.setFont("helvetica", "normal"); + // Remove markdown formatting + const cleanText = line + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/`(.*?)`/g, "$1") + .replace(/\[(.*?)\]\(.*?\)/g, "$1"); + const splitText = pdf.splitTextToSize(cleanText, maxWidth); + + if ( + y + splitText.length * textConfig.normalHeight > + pageHeight - margin + ) { + pdf.addPage(); + y = margin; + } + pdf.text(splitText, margin, y); + y += + splitText.length * textConfig.normalHeight + + PDF_CONSTANTS.text.paragraphSpacing; + } else { + // Empty line + y += PDF_CONSTANTS.emptyLine.height; + } + + // Check page overflow + if (y > pageHeight - margin) { + pdf.addPage(); + y = margin; + } + } + + pdf.save(`research-report-${getTimestamp()}.pdf`); + } catch (error) { + console.error("Failed to generate PDF:", error); + toast.error(t("exportFailed")); + } finally { + setIsDownloading(false); + } + }, [reportId, getTimestamp, isDownloading, t]); + + // Helper function to parse inline markdown formatting for Word export + const parseInlineMarkdown = useCallback( + (text: string): (TextRun | ExternalHyperlink)[] => { + // Process text recursively using marked's inline lexer + const runs: (TextRun | ExternalHyperlink)[] = []; + + try { + // Use marked's Lexer to safely parse inline markdown + const tokens = marked.Lexer.lexInline(text); + + interface MarkedToken { + type: string; + text?: string; + tokens?: MarkedToken[]; + href?: string; + } + + const processTokens = (tokens: MarkedToken[]): void => { + for (const token of tokens) { + if (token.type === "text") { + // Regular text + if (token.text) { + runs.push(new TextRun(token.text)); + } + } else if (token.type === "strong") { + // Bold text - may contain nested tokens + if (token.tokens && token.tokens.length > 0) { + // Process nested tokens and mark them as bold + const nestedRuns: TextRun[] = []; + for (const nestedToken of token.tokens) { + if (nestedToken.type === "text") { + nestedRuns.push( + new TextRun({ text: nestedToken.text, bold: true }), + ); + } else if (nestedToken.type === "em") { + // Bold + italic nested tokens + nestedRuns.push( + new TextRun({ + text: nestedToken.text, + bold: true, + italics: true, + }), + ); + } + } + runs.push(...nestedRuns); + } else { + runs.push(new TextRun({ text: token.text, bold: true })); + } + } else if (token.type === "em") { + // Italic text + runs.push( + new TextRun({ + text: token.text ?? token.tokens?.[0]?.text, + italics: true, + }), + ); + } else if (token.type === "codespan") { + // Inline code + if (token.text) { + runs.push( + new TextRun({ text: token.text, font: "Courier New" }), + ); + } + } else if (token.type === "link") { + // Link - use the link text or fallback to URL + const linkText = token.text ?? token.href ?? ""; + const linkUrl = token.href ?? ""; + if (linkUrl) { + runs.push( + new ExternalHyperlink({ + children: [ + new TextRun({ text: linkText, style: "Hyperlink" }), + ], + link: linkUrl, + }), + ); + } else { + // Fallback to plain text if no URL + runs.push(new TextRun(linkText)); + } + } else if (token.type === "space") { + // Preserve spaces + runs.push(new TextRun(" ")); + } + } + }; + + processTokens(tokens); + } catch (error) { + // Fallback to simple text parsing if marked fails + console.warn("Marked parsing failed, using fallback:", error); + // Pattern to match: bold (**text**), italic (*text*), inline code (`text`), links [text](url) + const pattern = + /(\*\*(.+?)\*\*)|(\*(.+?)\*)|(`(.+?)`)|\[(.+?)\]\((.+?)\)/g; + let lastIndex = 0; + let match; + + while ((match = pattern.exec(text)) !== null) { + // Add plain text before the match + if (match.index > lastIndex) { + runs.push(new TextRun(text.slice(lastIndex, match.index))); + } + + if (match[1]) { + // Bold: **text** + const boldText = match[2] ?? ""; + runs.push(new TextRun({ text: boldText, bold: true })); + } else if (match[3]) { + // Italic: *text* + const italicText = match[4] ?? ""; + runs.push(new TextRun({ text: italicText, italics: true })); + } else if (match[5]) { + // Inline code: `text` + const codeText = match[6] ?? ""; + runs.push(new TextRun({ text: codeText, font: "Courier New" })); + } else if (match[7] && match[8]) { + // Link: [text](url) + runs.push( + new ExternalHyperlink({ + children: [ + new TextRun({ text: match[7] ?? "", style: "Hyperlink" }), + ], + link: match[8], + }), + ); + } + + lastIndex = pattern.lastIndex; + } + + // Add remaining plain text + if (lastIndex < text.length) { + runs.push(new TextRun(text.slice(lastIndex))); + } + } + + return runs.length > 0 ? runs : [new TextRun(text)]; + }, + [], + ); + + // Download report as Word document + const handleDownloadWord = useCallback(async () => { + if (!reportId || isDownloading) return; + const report = useStore.getState().messages.get(reportId); + if (!report) return; + + setIsDownloading(true); + try { + // Parse markdown content into paragraphs + const lines = report.content.split("\n"); + const children: Paragraph[] = []; + + for (const line of lines) { + if (line.startsWith("# ")) { + children.push( + new Paragraph({ + children: parseInlineMarkdown(line.substring(2)), + heading: HeadingLevel.HEADING_1, + }), + ); + } else if (line.startsWith("## ")) { + children.push( + new Paragraph({ + children: parseInlineMarkdown(line.substring(3)), + heading: HeadingLevel.HEADING_2, + }), + ); + } else if (line.startsWith("### ")) { + children.push( + new Paragraph({ + children: parseInlineMarkdown(line.substring(4)), + heading: HeadingLevel.HEADING_3, + }), + ); + } else if (line.startsWith("- ") || line.startsWith("* ")) { + // Unordered list item + children.push( + new Paragraph({ + children: parseInlineMarkdown(line.substring(2)), + bullet: { level: 0 }, + }), + ); + } else if (/^\d+\.\s/.test(line)) { + // Ordered list item + const text = line.replace(/^\d+\.\s/, ""); + children.push( + new Paragraph({ + children: parseInlineMarkdown(text), + numbering: { reference: "default-numbering", level: 0 }, + }), + ); + } else if (line.trim()) { + children.push( + new Paragraph({ + children: parseInlineMarkdown(line), + }), + ); + } else { + children.push(new Paragraph({ text: "" })); + } + } + + const doc = new Document({ + numbering: { + config: [ + { + reference: "default-numbering", + levels: [ + { + level: 0, + format: "decimal", + text: "%1.", + alignment: "start", + }, + ], + }, + ], + }, + sections: [{ children }], + }); + + const blob = await Packer.toBlob(doc); + saveAs(blob, `research-report-${getTimestamp()}.docx`); + } catch (error) { + console.error("Failed to generate Word document:", error); + toast.error(t("exportFailed")); + } finally { + setIsDownloading(false); + } + }, [reportId, getTimestamp, isDownloading, t, parseInlineMarkdown]); + + // Download report as Image + const handleDownloadImage = useCallback(async () => { + if (!reportId || isDownloading) return; + const report = useStore.getState().messages.get(reportId); + if (!report) return; + + setIsDownloading(true); + let container: HTMLDivElement | null = null; + try { + // Create a temporary container with simple styles to avoid color parsing issues + container = document.createElement("div"); + container.style.cssText = + "position: absolute; left: -9999px; top: 0; width: 800px; padding: 40px; font-family: Arial, sans-serif; line-height: 1.6; background-color: #ffffff; color: #000000;"; + const styleTag = + ""; + const rawHtml = marked(report.content) as string; + const sanitizedHtml = DOMPurify.sanitize(rawHtml); + container.innerHTML = styleTag + sanitizedHtml; + document.body.appendChild(container); + + const canvas = await html2canvas(container, { + scale: 2, + useCORS: true, + logging: false, + backgroundColor: "#ffffff", + }); + + // Promisify toBlob for proper async handling + const blob = await new Promise((resolve) => { + canvas.toBlob((b) => resolve(b), "image/png"); + }); + + if (blob) { + saveAs(blob, `research-report-${getTimestamp()}.png`); + } + } catch (error) { + console.error("Failed to generate image:", error); + toast.error(t("exportFailed")); + } finally { + // Ensure container is always removed + try { + container?.parentNode?.removeChild(container); + } catch (error) { + // Log cleanup errors for better debugging (not just in development) + console.warn( + "Failed to remove temporary container during image export cleanup:", + error, + ); + // Don't throw - cleanup failures are expected and harmless + } + setIsDownloading(false); + } + }, [reportId, getTimestamp, isDownloading, t]); - const handleEdit = useCallback(() => { setEditing((editing) => !editing); }, []); @@ -147,16 +676,50 @@ export function ResearchBlock({ {copied ? : } - - - + + + + + + + + + + {t("downloadMarkdown")} + + + + {t("downloadHTML")} + + + + {t("downloadPDF")} + + + + {t("downloadWord")} + + + + {t("downloadImage")} + + + )} @@ -198,7 +761,7 @@ export function ResearchBlock({ hidden={activeTab !== "report"} >