From 3663f8ffbcf8edffea15ed3c9ed00e643c8306f4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:46:27 +0000 Subject: [PATCH 1/4] Implement PDF report generation with HTML styling - Added jspdf and html2canvas for PDF generation from HTML - Created ReportTemplate component for styled report layout - Implemented DownloadReportButton with document icon in SettingsView - Added report-generator utility using html2canvas for capturing React components - Integrated conversation history, map images, and drawn features into the report Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- bun.lock | 46 ++++++++- components/download-report-button.tsx | 107 +++++++++++++++++++ components/report-template.tsx | 143 ++++++++++++++++++++++++++ components/settings/settings-view.tsx | 12 ++- lib/utils/report-generator.ts | 51 +++++++++ package.json | 2 + 6 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 components/download-report-button.tsx create mode 100644 components/report-template.tsx create mode 100644 lib/utils/report-generator.ts diff --git a/bun.lock b/bun.lock index f101e5d7..38f68cec 100644 --- a/bun.lock +++ b/bun.lock @@ -56,6 +56,8 @@ "framer-motion": "^12.23.24", "geotiff": "^2.1.4-beta.1", "glassmorphic": "^0.0.3", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", "katex": "^0.16.22", "lodash": "^4.17.21", "lottie-react": "^2.4.1", @@ -199,7 +201,7 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], @@ -959,12 +961,16 @@ "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], + "@types/pbf": ["@types/pbf@3.0.5", "", {}, "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="], "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], "@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], + "@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="], + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -977,6 +983,8 @@ "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/tz-lookup": ["@types/tz-lookup@6.1.2", "", {}, "sha512-9y31Xf/8FHXrCHjvVjGZLcsayAa6ABNc8bZlk6MPOQLLlr41tICSqW3TRPRIx2nodbzdKs5N7ipHWBrUsWUiAA=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -1131,6 +1139,8 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -1169,6 +1179,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], + "canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1237,6 +1249,8 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], @@ -1245,6 +1259,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], + "csscolorparser": ["csscolorparser@1.0.3", "", {}, "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -1309,6 +1325,8 @@ "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + "dompurify": ["dompurify@3.4.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], @@ -1433,6 +1451,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], @@ -1585,6 +1605,8 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], @@ -1617,6 +1639,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-absolute-url": ["is-absolute-url@4.0.1", "", {}, "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A=="], @@ -1739,6 +1763,8 @@ "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "jspdf": ["jspdf@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ=="], + "jsts": ["jsts@2.7.1", "", {}, "sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg=="], "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], @@ -2021,6 +2047,8 @@ "pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="], + "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], @@ -2113,6 +2141,8 @@ "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], + "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], + "ramda": ["ramda@0.29.1", "", {}, "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -2153,6 +2183,8 @@ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "rehype-external-links": ["rehype-external-links@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-is-element": "^3.0.0", "is-absolute-url": "^4.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw=="], @@ -2189,6 +2221,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "robust-predicates": ["robust-predicates@2.0.4", "", {}, "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="], @@ -2275,6 +2309,8 @@ "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -2335,6 +2371,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="], + "sweepline-intersections": ["sweepline-intersections@1.5.0", "", { "dependencies": { "tinyqueue": "^2.0.0" } }, "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ=="], "swr": ["swr@2.3.8", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w=="], @@ -2349,6 +2387,8 @@ "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -2465,6 +2505,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], + "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -2745,6 +2787,8 @@ "react-reconciler/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "react-textarea-autosize/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/components/download-report-button.tsx b/components/download-report-button.tsx new file mode 100644 index 00000000..7485f05c --- /dev/null +++ b/components/download-report-button.tsx @@ -0,0 +1,107 @@ +'use client' + +import React, { useState } from 'react' +import { Button } from '@/components/ui/button' +import { FileText, Loader2 } from 'lucide-react' +import { useAIState } from 'ai/rsc' +import { useMap } from './map/map-context' +import { useMapData } from './map/map-data-context' +import { generatePDFReport } from '@/lib/utils/report-generator' +import { AI } from '@/app/actions' +import { toast } from 'sonner' +import { ReportTemplate } from './report-template' +import { createPortal } from 'react-dom' + +export const DownloadReportButton = () => { + const [aiState] = useAIState() + const { map } = useMap() + const { mapData } = useMapData() + const [isGenerating, setIsGenerating] = useState(false) + const [showTemplate, setShowTemplate] = useState(false) + const [mapSnapshot, setMapSnapshot] = useState() + + const handleDownload = async () => { + if (!aiState || aiState.messages.length === 0) { + toast.error('No conversation to export') + return + } + + setIsGenerating(true) + try { + let snapshot: string | undefined + if (map) { + snapshot = map.getCanvas().toDataURL('image/png') + setMapSnapshot(snapshot) + } + + setShowTemplate(true) + + // Wait for React to render the template + await new Promise(resolve => setTimeout(resolve, 500)) + + let chatTitle = 'Untitled Chat' + if (aiState.messages.length > 0) { + const firstMessage = aiState.messages[0] + if (typeof firstMessage.content === 'string') { + try { + const parsed = JSON.parse(firstMessage.content) + chatTitle = parsed.input || firstMessage.content + } catch (e) { + chatTitle = firstMessage.content + } + } + } + const finalTitle = chatTitle.substring(0, 50) + + await generatePDFReport('report-template', finalTitle) + + toast.success('Report generated successfully') + } catch (error) { + console.error('Failed to generate report:', error) + toast.error('Failed to generate report') + } finally { + setIsGenerating(false) + setShowTemplate(false) + } + } + + return ( + <> + + + {showTemplate && createPortal( +
+ +
, + document.body + )} + + ) +} diff --git a/components/report-template.tsx b/components/report-template.tsx new file mode 100644 index 00000000..8b23eb5a --- /dev/null +++ b/components/report-template.tsx @@ -0,0 +1,143 @@ +import React from 'react' +import { AIMessage } from '@/lib/types' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' + +export interface ReportTemplateProps { + messages: AIMessage[] + drawnFeatures?: Array<{ + id: string + type: 'Polygon' | 'LineString' + measurement: string + geometry: any + }> + mapSnapshot?: string + chatTitle: string +} + +export const ReportTemplate: React.FC = ({ + messages, + drawnFeatures, + mapSnapshot, + chatTitle +}) => { + const filteredMessages = messages.filter(m => + m.type === 'input' || + m.type === 'input_related' || + m.type === 'response' || + m.type === 'resolution_search_result' + ) + + return ( +
+
+

{chatTitle}

+

Generated on: {new Date().toLocaleString()}

+
+ + {mapSnapshot && ( +
+

Live Map View

+
+ Map Snapshot +
+
+ )} + +
+

Conversation History

+
+ {filteredMessages.map((message, index) => { + if (message.type === 'input' || message.type === 'input_related') { + let content = '' + try { + const json = JSON.parse(message.content as string) + content = message.type === 'input' ? json.input : json.related_query + } catch (e) { + content = message.content as string + } + return ( +
+

User Question

+

{content}

+
+ ) + } else if (message.type === 'response') { + return ( +
+

AI Response

+ + {message.content as string} + +
+ ) + } else if (message.type === 'resolution_search_result') { + try { + const result = JSON.parse(message.content as string) + return ( +
+

Analysis Result

+ {result.summary && ( +
+ {result.summary} +
+ )} +
+ {result.mapboxImage && ( +
+

Mapbox View

+ Mapbox View +
+ )} + {result.googleImage && ( +
+

Google Satellite

+ Google Satellite +
+ )} +
+
+ ) + } catch (e) { + return null + } + } + return null + })} +
+
+ + {drawnFeatures && drawnFeatures.length > 0 && ( +
+

Appendix: Drawn Features & Measurements

+
+ + + + + + + + + + {drawnFeatures.map((feature, i) => ( + + + + + + ))} + +
TypeMeasurementGeometry
{feature.type}{feature.measurement} + {JSON.stringify(feature.geometry.coordinates).substring(0, 100)}... +
+
+
+ )} + +
+

© {new Date().getFullYear()} QCX - Planet Computer Analysis Report

+
+
+ ) +} diff --git a/components/settings/settings-view.tsx b/components/settings/settings-view.tsx index 5f9e16ff..cddce42f 100644 --- a/components/settings/settings-view.tsx +++ b/components/settings/settings-view.tsx @@ -4,6 +4,7 @@ import { SettingsSkeleton } from "@/components/settings/components/settings-skel import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle-context" import { Button } from "@/components/ui/button" import { Minus } from "lucide-react" +import { DownloadReportButton } from "@/components/download-report-button" export default function SettingsView() { const { toggleProfileSection, activeView } = useProfileToggle(); @@ -22,10 +23,13 @@ export default function SettingsView() {

Settings

Manage your planetary copilot preferences and user access

- +
+ + +
}> diff --git a/lib/utils/report-generator.ts b/lib/utils/report-generator.ts new file mode 100644 index 00000000..c93d2849 --- /dev/null +++ b/lib/utils/report-generator.ts @@ -0,0 +1,51 @@ +import { jsPDF } from 'jspdf' +import html2canvas from 'html2canvas' + +export const generatePDFReport = async (elementId: string, fileName: string) => { + const element = document.getElementById(elementId) + if (!element) { + throw new Error(`Element with id ${elementId} not found`) + } + + try { + const canvas = await html2canvas(element, { + scale: 2, // Higher scale for better quality + useCORS: true, // Allow loading images from different domains + logging: false, + allowTaint: true + }) + + const imgData = canvas.toDataURL('image/png') + const pdf = new jsPDF({ + orientation: 'portrait', + unit: 'px', + format: 'a4' + }) + + const imgProps = pdf.getImageProperties(imgData) + const pdfWidth = pdf.internal.pageSize.getWidth() + const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width + + // Handle multi-page if necessary (though html2canvas captures the whole element) + // For simplicity in this implementation, we'll scale it to one page or use multiple pages if it's very long + + let heightLeft = pdfHeight + let position = 0 + const pageHeight = pdf.internal.pageSize.getHeight() + + pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pdfHeight) + heightLeft -= pageHeight + + while (heightLeft >= 0) { + position = heightLeft - pdfHeight + pdf.addPage() + pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pdfHeight) + heightLeft -= pageHeight + } + + pdf.save(`${fileName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_report.pdf`) + } catch (error) { + console.error('Error generating PDF:', error) + throw error + } +} diff --git a/package.json b/package.json index 87066925..a587a11b 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,8 @@ "framer-motion": "^12.23.24", "geotiff": "^2.1.4-beta.1", "glassmorphic": "^0.0.3", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", "katex": "^0.16.22", "lodash": "^4.17.21", "lottie-react": "^2.4.1", From b1d2e0ebba768f2b321e2842427106b664c18ecf Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:09:31 +0000 Subject: [PATCH 2/4] Fix report generation crash and refine UI - Fixed 'Objects are not valid as a React child' error by properly rendering complex message content - Improved PDF generation reliability by waiting for image loads and increasing render delay - Moved Download Report button to Settings view with a document icon as requested - Cleaned up previous header and portal-based integration Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> From fae859e2b1951708764f9e7350e96ec76c486a70 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:58:35 +0000 Subject: [PATCH 3/4] Fix report generation performance and restore resolution search compatibility - Reverted accidental dependency upgrades to ensure resolution search remains functional - Optimized PDF generation with dynamic imports for jspdf and html2canvas - Improved performance by using JPEG compression and reduced scale for report capture - Added a 5s timeout to image loading in the report generator to prevent hanging - Ensured the 'Download Report' button is safely integrated into the Settings view Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/download-report-button.tsx | 58 +++++++++++++++++--------- lib/utils/report-generator.ts | 59 +++++++++++++++++++-------- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/components/download-report-button.tsx b/components/download-report-button.tsx index 7485f05c..b938c871 100644 --- a/components/download-report-button.tsx +++ b/components/download-report-button.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Button } from '@/components/ui/button' import { FileText, Loader2 } from 'lucide-react' import { useAIState } from 'ai/rsc' @@ -19,6 +19,12 @@ export const DownloadReportButton = () => { const [isGenerating, setIsGenerating] = useState(false) const [showTemplate, setShowTemplate] = useState(false) const [mapSnapshot, setMapSnapshot] = useState() + const [reportTitle, setReportTitle] = useState('QCX Analysis Report') + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + setIsMounted(true) + }, []) const handleDownload = async () => { if (!aiState || aiState.messages.length === 0) { @@ -27,44 +33,56 @@ export const DownloadReportButton = () => { } setIsGenerating(true) + const toastId = toast.loading('Generating report...') + try { let snapshot: string | undefined if (map) { - snapshot = map.getCanvas().toDataURL('image/png') + // Use a smaller snapshot if possible, or just the current canvas + snapshot = map.getCanvas().toDataURL('image/jpeg', 0.5) setMapSnapshot(snapshot) } - setShowTemplate(true) - - // Wait for React to render the template - await new Promise(resolve => setTimeout(resolve, 500)) - + // Extract title let chatTitle = 'Untitled Chat' if (aiState.messages.length > 0) { const firstMessage = aiState.messages[0] - if (typeof firstMessage.content === 'string') { - try { - const parsed = JSON.parse(firstMessage.content) - chatTitle = parsed.input || firstMessage.content - } catch (e) { - chatTitle = firstMessage.content - } + const content = typeof firstMessage.content === 'string' + ? firstMessage.content + : Array.isArray(firstMessage.content) + ? (firstMessage.content as any[]).map(p => p.type === 'text' ? p.text : '').join(' ') + : '' + + try { + const parsed = JSON.parse(content) + chatTitle = parsed.input || content + } catch (e) { + chatTitle = content } } - const finalTitle = chatTitle.substring(0, 50) + const finalTitle = (chatTitle || 'QCX Analysis Report').substring(0, 50) + setReportTitle(finalTitle) + + setShowTemplate(true) + + // Short delay for React portal to mount + await new Promise(resolve => setTimeout(resolve, 800)) await generatePDFReport('report-template', finalTitle) - toast.success('Report generated successfully') + toast.success('Report generated successfully', { id: toastId }) } catch (error) { console.error('Failed to generate report:', error) - toast.error('Failed to generate report') + toast.error('Failed to generate report', { id: toastId }) } finally { setIsGenerating(false) setShowTemplate(false) + setMapSnapshot(undefined) // Clear snapshot memory } } + if (!isMounted) return null + return ( <> + {/* Hidden container for PDF rendering - ensure it's in the DOM with appropriate styles */} {showTemplate && createPortal(
= ({ m.type === 'resolution_search_result' ) + const renderMessageContent = (content: any): string => { + if (typeof content === 'string') { + return content + } + if (Array.isArray(content)) { + return content + .map(part => { + if (typeof part === 'string') return part + if (part && typeof part === 'object' && part.type === 'text') return part.text + return '' + }) + .join('\n') + } + return '' + } + return (
-
-

{chatTitle}

+
+

{chatTitle}

Generated on: {new Date().toLocaleString()}

{mapSnapshot && (
-

Live Map View

-
- Map Snapshot +

Live Map View

+
+ Map Snapshot
)}
-

Conversation History

+

Conversation History

{filteredMessages.map((message, index) => { + const contentString = renderMessageContent(message.content) + if (message.type === 'input' || message.type === 'input_related') { let content = '' try { - const json = JSON.parse(message.content as string) - content = message.type === 'input' ? json.input : json.related_query + const json = JSON.parse(contentString) + content = message.type === 'input' ? (json.input || contentString) : (json.related_query || contentString) } catch (e) { - content = message.content as string + content = contentString } return ( -
+

User Question

{content}

) } else if (message.type === 'response') { return ( -
+

AI Response

- - {message.content as string} - +
+ + {contentString} + +
) } else if (message.type === 'resolution_search_result') { try { - const result = JSON.parse(message.content as string) + const result = JSON.parse(contentString) return ( -
+

Analysis Result

{result.summary && ( -
+
{result.summary}
)}
{result.mapboxImage && (
-

Mapbox View

- Mapbox View +

Mapbox View

+ Mapbox View
)} {result.googleImage && (
-

Google Satellite

- Google Satellite +

Google Satellite

+ Google Satellite
)}
@@ -120,7 +140,7 @@ export const ReportTemplate: React.FC = ({ - {drawnFeatures.map((feature, i) => ( + {drawnFeatures.map((feature) => ( {feature.type} {feature.measurement} diff --git a/lib/utils/report-generator.ts b/lib/utils/report-generator.ts index 99fd9f22..07ac7ba6 100644 --- a/lib/utils/report-generator.ts +++ b/lib/utils/report-generator.ts @@ -1,8 +1,8 @@ export const generatePDFReport = async (elementId: string, fileName: string) => { const element = document.getElementById(elementId) if (!element) { - console.error(`Element with id ${elementId} not found in the DOM`) - throw new Error(`Element with id ${elementId} not found`) + console.error(`Element with id ${elementId} not found in the DOM. Full document structure:`, document.body.innerHTML.substring(0, 500)) + throw new Error(`Element with id ${elementId} not found. Please try again.`) } try { @@ -12,17 +12,18 @@ export const generatePDFReport = async (elementId: string, fileName: string) => ]) const images = Array.from(element.getElementsByTagName('img')) - const imageLoadTimeout = 3000 // 3 seconds timeout + const imageLoadTimeout = 5000 // 5 seconds timeout + // Wait for images to load, but don't block forever await Promise.race([ Promise.all( images.map(img => { if (img.complete) return Promise.resolve() return new Promise((resolve) => { img.onload = resolve - img.onerror = resolve - // Fallback for data URLs which might already be loaded - if (img.src.startsWith('data:')) setTimeout(resolve, 100) + img.onerror = resolve // Continue even if one image fails + // Force a check after a small delay for data URLs + if (img.src.startsWith('data:')) setTimeout(resolve, 500) }) }) ), @@ -30,16 +31,24 @@ export const generatePDFReport = async (elementId: string, fileName: string) => ]) const canvas = await html2canvas(element, { - scale: 1, // Reduced scale for speed + scale: 1, // Keep scale low for performance on large reports useCORS: true, logging: false, allowTaint: true, backgroundColor: '#ffffff', - imageTimeout: 5000, - removeContainer: true + imageTimeout: 10000, + onclone: (clonedDoc) => { + // Ensure the cloned element is visible for html2canvas + const clonedElement = clonedDoc.getElementById(elementId) + if (clonedElement) { + clonedElement.style.position = 'static' + clonedElement.style.left = '0' + clonedElement.style.visibility = 'visible' + } + } }) - const imgData = canvas.toDataURL('image/jpeg', 0.7) + const imgData = canvas.toDataURL('image/jpeg', 0.6) const pdf = new jsPDF({ orientation: 'portrait', unit: 'px', @@ -56,9 +65,11 @@ export const generatePDFReport = async (elementId: string, fileName: string) => let heightLeft = scaledHeight let position = 0 + // Add first page pdf.addImage(imgData, 'JPEG', 0, position, pdfWidth, scaledHeight) heightLeft -= pdfHeight + // Add subsequent pages if content overflows while (heightLeft >= 0) { position = heightLeft - scaledHeight pdf.addPage()