PDF Report Generation with HTML Styling#638
Conversation
- 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>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
WalkthroughAdds a PDF report export: DownloadReportButton in Settings mounts ReportTemplate off-screen, captures an optional map snapshot, and calls generatePDFReport which renders the template to PDF using html2canvas and jsPDF. ChangesPDF Report Export Feature
Sequence DiagramsequenceDiagram
participant User
participant DownloadReportButton
participant MapInstance
participant ReportTemplate
participant generatePDFReport
participant jsPDF
User->>DownloadReportButton: clicks download
DownloadReportButton->>MapInstance: optional capture toDataURL()
DownloadReportButton->>ReportTemplate: mount off-screen portal with data
DownloadReportButton->>generatePDFReport: invoke with element id and title
generatePDFReport->>ReportTemplate: html2canvas(render DOM -> canvas)
generatePDFReport->>jsPDF: add canvas image(s), paginate, save PDF
jsPDF-->>User: browser download saved
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review Summary by QodoAdd PDF report generation with styled templates
WalkthroughsDescription• Implements PDF report generation from conversation history • Captures map snapshots and drawn features into reports • Adds download button with document icon in SettingsView • Integrates html2canvas and jsPDF dependencies Diagramflowchart LR
A["DownloadReportButton"] -->|triggers| B["generatePDFReport"]
A -->|renders| C["ReportTemplate"]
C -->|includes| D["Conversation History"]
C -->|includes| E["Map Snapshot"]
C -->|includes| F["Drawn Features"]
B -->|converts to| G["PDF File"]
H["SettingsView"] -->|displays| A
File Changes1. lib/utils/report-generator.ts
|
Code Review by Qodo
1. Image message report crash
|
| 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 ( | ||
| <div key={index} className="bg-gray-50 p-4 rounded-lg border-l-4 border-blue-500"> | ||
| <p className="text-sm font-bold text-blue-600 mb-1">User Question</p> | ||
| <p className="text-gray-800 italic">{content}</p> | ||
| </div> |
There was a problem hiding this comment.
1. Image message report crash 🐞 Bug ≡ Correctness
ReportTemplate assumes message.content is a string for input/input_related messages; when it is actually an array of content parts (text/image), the catch-path assigns the array to content and then renders it, which throws at runtime and prevents PDF generation. This is triggered by resolution search and any chat input that includes an image attachment, since those messages are stored with array content.
Agent Prompt
### Issue description
`ReportTemplate` assumes `AIMessage.content` is always a string for user input messages. In this app, user messages (including resolution search) can store `content` as an array of `{type:'text'|'image', ...}` parts. The current code falls back to rendering that array directly, which causes React to throw `Objects are not valid as a React child`, breaking PDF generation.
### Issue Context
- `app/actions.tsx` stores user messages with `content: CoreMessage['content']`, which can be a string **or** an array of parts (especially for image uploads and resolution search).
- `ReportTemplate` should render a safe textual representation (and optionally images) for array content.
### Fix Focus Areas
- components/report-template.tsx[50-73]
- app/actions.tsx[355-418]
### Implementation notes
- Add a helper like `renderMessageContent(content: CoreMessage['content'])`:
- If `typeof content === 'string'`: return the string.
- If `Array.isArray(content)`:
- Extract and join text parts (`part.type==='text'`).
- Optionally render image parts (`part.type==='image'`) as `<img src={part.image} ... />` when `part.image` is a data URL.
- Never pass raw objects/arrays directly into JSX.
- Apply this for `input`/`input_related` messages (and consider other message types too, for consistency).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/download-report-button.tsx`:
- Around line 42-54: The derived chat title (finalTitle) is computed inside
handleDownload but never passed to the rendered ReportTemplate, so the PDF
header remains static; move finalTitle into component state (e.g., add a state
variable like chatTitleState and set it inside handleDownload before calling
setShowTemplate(true)) and then pass that state into ReportTemplate instead of
the hardcoded "QCX Analysis Report", and also use the same state when calling
generatePDFReport to keep filename and rendered header consistent; update
references to finalTitle, generatePDFReport, ReportTemplate, and handleDownload
accordingly.
- Around line 37-40: Replace the fragile fixed 500ms sleep after
setShowTemplate(true) with a reliable "wait for paint and assets" strategy:
after calling setShowTemplate(true) (in the DownloadReportButton component),
wait for the next animation frame (use requestAnimationFrame/double-rAF) to
ensure React has painted, and additionally await decoding/loading of any
embedded images or snapshot elements (call HTMLImageElement.decode() or listen
for load events for images used in the template) before proceeding to PDF
generation; ensure this logic targets the DOM nodes rendered by the template so
PDF capture only begins once paint and image decode have completed.
In `@components/report-template.tsx`:
- Around line 123-129: The table cell rendering inside the drawnFeatures.map (in
the JSX mapping that produces <tr key={feature.id}>) can crash if
feature.geometry or feature.geometry.coordinates is missing; update the cell
that currently does {JSON.stringify(feature.geometry.coordinates).substring(0,
100)}... to safely compute a string first (e.g. use optional chaining and a
fallback like const coordsStr = JSON.stringify(feature?.geometry?.coordinates)
?? '' or String(JSON.stringify(...)) and then call .substring on that ensured
string) so the render never calls .substring on undefined and instead shows a
safe placeholder when coordinates are absent.
In `@lib/utils/report-generator.ts`:
- Around line 39-44: The loop that adds pages in report-generator.ts uses while
(heightLeft >= 0) which causes a trailing blank page when pdfHeight is an exact
multiple of pageHeight; change the loop condition to a strict check (e.g., while
(heightLeft > 0) or while (heightLeft > Number.EPSILON)) to avoid the extra
iteration and ensure pdf.addPage()/pdf.addImage() only run when there is
remaining content to render (referencing variables heightLeft, pdfHeight,
pageHeight and calls pdf.addPage() / pdf.addImage()).
- Around line 11-18: Remove the allowTaint option from the html2canvas call
(delete allowTaint: true) so canvas.toDataURL() won’t rely on tainted canvases,
and fix the pagination loop by changing the off-by-one condition that creates an
extra page: where you iterate using pdfHeight, pageHeight, heightLeft and
position (the loop currently using while (heightLeft >= 0)), change it to while
(heightLeft > 0) (and ensure heightLeft is decremented by pageHeight each
iteration) so an exact multiple of pageHeight does not produce a blank extra PDF
page.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b13cf943-ebe5-4ce7-b845-1309a866de3c
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
components/download-report-button.tsxcomponents/report-template.tsxcomponents/settings/settings-view.tsxlib/utils/report-generator.tspackage.json
📜 Review details
🔇 Additional comments (3)
package.json (1)
71-72: LGTM!components/settings/settings-view.tsx (1)
7-7: LGTM!Also applies to: 26-32
components/download-report-button.tsx (1)
32-34: ⚡ Quick winPreserveDrawingBuffer is already enabled; this specific blank-snapshot cause is likely not applicable.
components/map/mapbox-map.tsxinitializes the Mapbox map withpreserveDrawingBuffer: true, somap.getCanvas().toDataURL('image/png')being blank shouldn’t be due to missing drawing-buffer preservation. If snapshots are still empty, focus on other factors like map render readiness/timing incomponents/download-report-button.tsx(e.g., the fixed delay) and double-check how the parsed title (finalTitle) is used versus any hardcodedchatTitlein the rendered report.
| setShowTemplate(true) | ||
|
|
||
| // Wait for React to render the template | ||
| await new Promise(resolve => setTimeout(resolve, 500)) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Fixed 500 ms delay to await render is fragile.
A hardcoded setTimeout is a timing assumption that can race on slow renders, large conversations, or image-heavy reports (snapshot/satellite tiles not yet decoded), producing partial/blank PDFs. Consider deferring generation until after paint (e.g., requestAnimationFrame/double-rAF, or awaiting image decode()/load of the snapshot images) rather than a flat timeout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/download-report-button.tsx` around lines 37 - 40, Replace the
fragile fixed 500ms sleep after setShowTemplate(true) with a reliable "wait for
paint and assets" strategy: after calling setShowTemplate(true) (in the
DownloadReportButton component), wait for the next animation frame (use
requestAnimationFrame/double-rAF) to ensure React has painted, and additionally
await decoding/loading of any embedded images or snapshot elements (call
HTMLImageElement.decode() or listen for load events for images used in the
template) before proceeding to PDF generation; ensure this logic targets the DOM
nodes rendered by the template so PDF capture only begins once paint and image
decode have completed.
| {drawnFeatures.map((feature, i) => ( | ||
| <tr key={feature.id}> | ||
| <td className="px-4 py-2 whitespace-nowrap text-gray-900">{feature.type}</td> | ||
| <td className="px-4 py-2 whitespace-nowrap text-gray-900">{feature.measurement}</td> | ||
| <td className="px-4 py-2 text-gray-500 break-all font-mono text-[10px]"> | ||
| {JSON.stringify(feature.geometry.coordinates).substring(0, 100)}... | ||
| </td> |
There was a problem hiding this comment.
Guard against missing geometry.coordinates to avoid a render-time crash.
geometry is typed any. If a feature lacks coordinates (or geometry is null), JSON.stringify(undefined) returns undefined (not a string), and the subsequent .substring(...) throws a TypeError, which crashes the entire off-screen template render and fails the export.
🛡️ Proposed fix
<td className="px-4 py-2 text-gray-500 break-all font-mono text-[10px]">
- {JSON.stringify(feature.geometry.coordinates).substring(0, 100)}...
+ {JSON.stringify(feature.geometry?.coordinates ?? []).substring(0, 100)}...
</td>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {drawnFeatures.map((feature, i) => ( | |
| <tr key={feature.id}> | |
| <td className="px-4 py-2 whitespace-nowrap text-gray-900">{feature.type}</td> | |
| <td className="px-4 py-2 whitespace-nowrap text-gray-900">{feature.measurement}</td> | |
| <td className="px-4 py-2 text-gray-500 break-all font-mono text-[10px]"> | |
| {JSON.stringify(feature.geometry.coordinates).substring(0, 100)}... | |
| </td> | |
| {drawnFeatures.map((feature, i) => ( | |
| <tr key={feature.id}> | |
| <td className="px-4 py-2 whitespace-nowrap text-gray-900">{feature.type}</td> | |
| <td className="px-4 py-2 whitespace-nowrap text-gray-900">{feature.measurement}</td> | |
| <td className="px-4 py-2 text-gray-500 break-all font-mono text-[10px]"> | |
| {JSON.stringify(feature.geometry?.coordinates ?? []).substring(0, 100)}... | |
| </td> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/report-template.tsx` around lines 123 - 129, The table cell
rendering inside the drawnFeatures.map (in the JSX mapping that produces <tr
key={feature.id}>) can crash if feature.geometry or feature.geometry.coordinates
is missing; update the cell that currently does
{JSON.stringify(feature.geometry.coordinates).substring(0, 100)}... to safely
compute a string first (e.g. use optional chaining and a fallback like const
coordsStr = JSON.stringify(feature?.geometry?.coordinates) ?? '' or
String(JSON.stringify(...)) and then call .substring on that ensured string) so
the render never calls .substring on undefined and instead shows a safe
placeholder when coordinates are absent.
| 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') |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does html2canvas allowTaint true cause canvas.toDataURL to throw SecurityError on tainted canvas?
💡 Result:
No, setting allowTaint: true in html2canvas does not prevent a SecurityError when calling canvas.toDataURL on a tainted canvas; rather, it explicitly allows the canvas to become tainted [1][2]. When you set allowTaint: true, you are instructing html2canvas to permit cross-origin images to be drawn onto the canvas, even though this operation inherently taints the canvas according to browser security policies [3][1]. Once a canvas is tainted, the browser's security model prevents any attempt to export its pixel data via methods like canvas.toDataURL or canvas.getImageData [3][2]. Consequently, if you then attempt to call canvas.toDataURL on that tainted canvas, the browser will throw a SecurityError [4][3][2]. To resolve this issue and enable toDataURL to work, you should instead focus on preventing the canvas from becoming tainted in the first place [3][2]. The standard approach is to ensure that all images loaded into the canvas originate from the same origin or are served with proper CORS (Cross-Origin Resource Sharing) headers [3][5][6]. Recommended steps to resolve the issue: 1. Set useCORS: true in your html2canvas configuration [5][7][2]. This instructs the library to attempt to load images using CORS, which, if the server is configured correctly, allows the browser to consider the images "clean" [6][8]. 2. Ensure the servers hosting your cross-origin images are configured to return the appropriate Access-Control-Allow-Origin headers [3][5]. 3. If using elements directly, you may also need to set the crossorigin attribute on the image tag to "anonymous" to ensure the browser requests the image with CORS headers [7][9]. 4. Avoid setting allowTaint: true, as it is often counter-productive for this use case [2]. In summary, allowTaint: true is intended for situations where you want to render cross-origin content despite the tainting, but it will inevitably break any subsequent efforts to export that canvas as an image file [1][2].
Citations:
- 1: http://html2canvas.hertzen.com/configuration/
- 2: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported. niklasvh/html2canvas#1614
- 3: https://eraoftech.medium.com/canvas-cors-error-why-todataurl-fails-how-to-fix-it-complete-guide-f5b54d83fa2f
- 4: Unhandled Promise Rejection: SecurityError: The operation is insecure. niklasvh/html2canvas#3184
- 5: https://stackoverflow.com/questions/16371316/html2canvas-securityerror-the-operation-is-insecure
- 6: html2canvas does not render images located in the element niklasvh/html2canvas#722
- 7: https://stackoverflow.com/questions/22710627/tainted-canvases-may-not-be-exported
- 8: https://dev.to/bibekkakati/take-screenshot-of-html-element-using-javascript-13b7
- 9: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported. niklasvh/html2canvas#1466
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file and print relevant sections with line numbers
ls -la lib/utils/report-generator.ts
nl -ba lib/utils/report-generator.ts | sed -n '1,120p'
# Focus on the pagination/export loop if present
rg -n "while\s*\(heightLeft\s*>=\s*0\)|heightLeft|pageHeight|pdfHeight|addPage|blank" lib/utils/report-generator.ts || trueRepository: QueueLab/QCX
Length of output: 196
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="lib/utils/report-generator.ts"
echo "File: $FILE"
awk '{printf "%5d %s\n", NR, $0}' "$FILE" | sed -n '1,140p'
echo "---- rg pagination/export related patterns ----"
rg -n "while\s*\(|heightLeft|pageHeight|pdfHeight|addPage|blank" "$FILE" || trueRepository: QueueLab/QCX
Length of output: 2593
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "resolution_search_result" . || true
rg -n "mapbox|google.*map|google.*satellite|gstatic|googleapis" . || true
rg -n "generatePDFReport\(|report-generator\.ts" . || trueRepository: QueueLab/QCX
Length of output: 23189
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# show relevant sections with line numbers via awk
FILE1="components/report-template.tsx"
FILE2="app/actions.tsx"
echo "---- $FILE1 (around mapboxImage/googleImage rendering) ----"
awk '{printf "%5d %s\n", NR, $0}' "$FILE1" | sed -n '1,200p'
echo "---- $FILE2 (around resolution_search_result handling) ----"
awk '{printf "%5d %s\n", NR, $0}' "$FILE2" | sed -n '150,280p'Repository: QueueLab/QCX
Length of output: 12450
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# show how mapboxDataUrl/googleDataUrl are produced
FILE="app/actions.tsx"
awk '{printf "%5d %s\n", NR, $0}' "$FILE" | sed -n '1,260p'
awk '{printf "%5d %s\n", NR, $0}' "$FILE" | sed -n '720,840p'
# show where generatePDFReport is called + which element id is passed
FILE2="components/download-report-button.tsx"
awk '{printf "%5d %s\n", NR, $0}' "$FILE2" | sed -n '1,120p'Repository: QueueLab/QCX
Length of output: 20410
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "file_mapbox|file_google" components app lib tests || trueRepository: QueueLab/QCX
Length of output: 1067
Remove allowTaint: true (and fix pagination off-by-one) in lib/utils/report-generator.ts.
allowTaint: true+canvas.toDataURL()can still throw aSecurityErrorif html2canvas ends up rendering any non-CORS cross-origin content; for this report flow the rendered images are alreadydata:URLs produced from captured/uploaded content, soallowTaintis unnecessary—dropping it makes the export safer. (Lines 11-18)- PDF pagination:
while (heightLeft >= 0)adds an extra page whenpdfHeightis an exact multiple ofpageHeight. (Lines 39-44)
🛡️ Proposed fix
const canvas = await html2canvas(element, {
scale: 2, // Higher scale for better quality
useCORS: true, // Allow loading images from different domains
logging: false,
- allowTaint: true
+ logging: false
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 canvas = await html2canvas(element, { | |
| scale: 2, // Higher scale for better quality | |
| useCORS: true, // Allow loading images from different domains | |
| logging: false | |
| }) | |
| const imgData = canvas.toDataURL('image/png') |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/utils/report-generator.ts` around lines 11 - 18, Remove the allowTaint
option from the html2canvas call (delete allowTaint: true) so canvas.toDataURL()
won’t rely on tainted canvases, and fix the pagination loop by changing the
off-by-one condition that creates an extra page: where you iterate using
pdfHeight, pageHeight, heightLeft and position (the loop currently using while
(heightLeft >= 0)), change it to while (heightLeft > 0) (and ensure heightLeft
is decremented by pageHeight each iteration) so an exact multiple of pageHeight
does not produce a blank extra PDF page.
| while (heightLeft >= 0) { | ||
| position = heightLeft - pdfHeight | ||
| pdf.addPage() | ||
| pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pdfHeight) | ||
| heightLeft -= pageHeight | ||
| } |
There was a problem hiding this comment.
Off-by-one produces a trailing blank page.
When pdfHeight is an exact multiple of pageHeight, heightLeft reaches exactly 0 and the heightLeft >= 0 condition runs one extra iteration, adding a blank page. Use a strict > 0 comparison (with a small epsilon if needed) to avoid it.
🐛 Proposed fix
- while (heightLeft >= 0) {
+ while (heightLeft > 0) {
position = heightLeft - pdfHeight
pdf.addPage()
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pdfHeight)
heightLeft -= pageHeight
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/utils/report-generator.ts` around lines 39 - 44, The loop that adds pages
in report-generator.ts uses while (heightLeft >= 0) which causes a trailing
blank page when pdfHeight is an exact multiple of pageHeight; change the loop
condition to a strict check (e.g., while (heightLeft > 0) or while (heightLeft >
Number.EPSILON)) to avoid the extra iteration and ensure
pdf.addPage()/pdf.addImage() only run when there is remaining content to render
(referencing variables heightLeft, pdfHeight, pageHeight and calls pdf.addPage()
/ pdf.addImage()).
|
@coderabbitai upon clicking the document icon the application crashed and this error was shown report: Error: Element with id report-template not found as well as this error Objects are not valid as a React child (found: object with keys {type, text}). If you meant to render a collection of children, use an array instead. can you debug this and make sure that the styled report is generated when the document icon is clicked for the chats and maps report to be generated as a pdf document. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
cat components/download-report-button.tsx
echo "---"
cat components/report-template.tsx
echo "---"
cat lib/utils/report-generator.tsLength of output: 11019
Bug 1 —
|
| File | Change |
|---|---|
components/report-template.tsx |
Added getContentString() helper; replaced all message.content as string with rawContent derived from the helper — fixes the React child crash |
components/download-report-button.tsx |
Replaced setTimeout(500) with useEffect + double requestAnimationFrame — guarantees the portal DOM element exists before generatePDFReport is called |
(╯°□°)╯ The two bugs were acting together — the render crash from Bug 2 prevented the element from ever appearing, which then triggered Bug 1.
- Fix
components/report-template.tsx: addgetContentStringnormaliser and replace all directmessage.content as stringusages - Fix
components/download-report-button.tsx: replacesetTimeout(500)withuseEffect+ doublerequestAnimationFramepattern; normalise first-message content the same way
The requested changes have been implemented and a pull request has been created: View PR
|
These errors were not resolved Objects are not valid as a React child (found: object with keys {type, text}). If you meant to render a collection of children, use an array instead. and report: Error: Element with id report-template not found |
I have debugged and fixed the issues you mentioned:
|
- 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>
…tibility - 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>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/download-report-button.tsx`:
- Around line 50-54: The code uses an unsafe cast "(firstMessage.content as
any[])" which discards type info; change it to a type-safe check by adding a
narrow type guard or explicit interface for message content and use it in the
conditional that computes "content" (the block referencing
firstMessage.content). Ensure you verify Array.isArray(firstMessage.content) and
that each item satisfies the shape (e.g., has "type" and "text" properties)
before mapping, or create a typed predicate function (e.g., isTextSegment(item))
and use it to filter/map so you avoid "any" casts and runtime surprises if the
SDK shape changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 217d58ab-7d7d-41cf-a248-09cd7cb610d6
📒 Files selected for processing (2)
components/download-report-button.tsxlib/utils/report-generator.ts
📜 Review details
🔇 Additional comments (9)
lib/utils/report-generator.ts (5)
32-42:allowTaint: truecan causetoDataURL()to throw aSecurityError.This concern was previously raised. The combination of
allowTaint: true(line 36) withcanvas.toDataURL()(line 42) will throw aSecurityErrorif any cross-origin content taints the canvas. Since the rendered images should be data URLs or same-origin, droppingallowTaintis safer.
62-67: Off-by-one loop adds a trailing blank page.This concern was previously raised. When
scaledHeightis an exact multiple ofpdfHeight,heightLeftreaches exactly0and the>= 0condition executes one extra iteration, adding a blank page.
9-12: LGTM!
14-30: LGTM!
43-60: LGTM!Also applies to: 69-73
components/download-report-button.tsx (4)
68-69: Hardcoded 800ms delay for portal mounting remains fragile.This concern was previously raised (when the delay was 500ms). A fixed timeout can race on slow renders or waste time on fast devices. Consider using double
requestAnimationFrameor polling for element existence instead.
25-27: LGTM!Also applies to: 84-84
29-45: LGTM!Also applies to: 71-81
86-127: LGTM!
| const content = typeof firstMessage.content === 'string' | ||
| ? firstMessage.content | ||
| : Array.isArray(firstMessage.content) | ||
| ? (firstMessage.content as any[]).map(p => p.type === 'text' ? p.text : '').join(' ') | ||
| : '' |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Unsafe as any[] cast; consider narrowing the type.
The cast discards type information and could mask runtime issues if the AI SDK changes the content structure. A type guard or explicit interface would improve safety.
♻️ Proposed type-safe alternative
+type ContentPart = { type: string; text?: string };
+
const content = typeof firstMessage.content === 'string'
? firstMessage.content
: Array.isArray(firstMessage.content)
- ? (firstMessage.content as any[]).map(p => p.type === 'text' ? p.text : '').join(' ')
+ ? (firstMessage.content as ContentPart[])
+ .filter((p): p is ContentPart & { text: string } => p.type === 'text' && typeof p.text === 'string')
+ .map(p => p.text)
+ .join(' ')
: ''📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const content = typeof firstMessage.content === 'string' | |
| ? firstMessage.content | |
| : Array.isArray(firstMessage.content) | |
| ? (firstMessage.content as any[]).map(p => p.type === 'text' ? p.text : '').join(' ') | |
| : '' | |
| type ContentPart = { type: string; text?: string }; | |
| const content = typeof firstMessage.content === 'string' | |
| ? firstMessage.content | |
| : Array.isArray(firstMessage.content) | |
| ? (firstMessage.content as ContentPart[]) | |
| .filter((p): p is ContentPart & { text: string } => p.type === 'text' && typeof p.text === 'string') | |
| .map(p => p.text) | |
| .join(' ') | |
| : '' |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/download-report-button.tsx` around lines 50 - 54, The code uses an
unsafe cast "(firstMessage.content as any[])" which discards type info; change
it to a type-safe check by adding a narrow type guard or explicit interface for
message content and use it in the conditional that computes "content" (the block
referencing firstMessage.content). Ensure you verify
Array.isArray(firstMessage.content) and that each item satisfies the shape
(e.g., has "type" and "text" properties) before mapping, or create a typed
predicate function (e.g., isTextSegment(item)) and use it to filter/map so you
avoid "any" casts and runtime surprises if the SDK shape changes.
|
@coderabbitai Failed to load resource: the server responded with a status of 403 () |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== components/download-report-button.tsx ==="
cat components/download-report-button.tsx
echo ""
echo "=== components/report-template.tsx ==="
cat components/report-template.tsx
echo ""
echo "=== lib/utils/report-generator.ts ==="
cat lib/utils/report-generator.tsLength of output: 12654
Root cause confirmed
Summary of what to change
The resolution search data will appear correctly once Bug 1 is fixed — the silent (づ。◕‿‿◕。)づ Hang tight — these are the two precise changes needed and I've queued them for implementation above. 🧠 Learnings usedThe requested changes have been implemented and a pull request has been created: View PR |
I have addressed the issues with report generation and resolution search data:
|
…sion - Resolved 'Objects are not valid as a React child' crash by adding robust content rendering in the report template - Fixed 'Element not found' error by increasing render timeout and ensuring portal visibility during capture - Specifically parsed and included resolution search data (summary and images) in the generated report - Added progress feedback via toast messages during the PDF generation process Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/download-report-button.tsx`:
- Around line 70-80: The fixed 1500ms setTimeout is unreliable; replace the
artificial delay before calling generatePDFReport('report-template', finalTitle)
with a DOM paint synchronization using requestAnimationFrame twice (or a small
loop of rAFs) after setShowTemplate(true), and additionally await any
image/resource readiness inside the template (e.g., wait for all images within
the 'report-template' container to fire their load/error events) before calling
toast.loading and generatePDFReport; keep toast.loading(toastId) calls but move
them to just before the final capture step so the sequence is:
setShowTemplate(true) -> await double rAF (and images loaded) ->
toast.loading('Capturing styled report...', { id: toastId }) ->
generatePDFReport('report-template', finalTitle).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f341d486-fcce-4d10-bc11-e0445a792f1f
📒 Files selected for processing (3)
components/download-report-button.tsxcomponents/report-template.tsxlib/utils/report-generator.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-04-25T08:10:37.673Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-04-25T08:10:37.673Z
Learning: In the QCX repository (feat/ai-sdk-ui-migration branch and going forward), there is no token generation/counting tracking and no multimodal LLM features. Base64 image data sent via `append()` body fields in components like `header-search-button.tsx` and `resolution-carousel.tsx` is for server-side `resolution_search` processing only — it is NOT passed as multimodal message content parts to a vision model. Avoid flagging token tracking or multimodal LLM handling gaps as review concerns in this codebase.
Applied to files:
components/report-template.tsxcomponents/download-report-button.tsx
📚 Learning: 2026-02-08T16:50:33.791Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-02-08T16:50:33.791Z
Learning: The PR branch implements RAG (Retrieval-Augmented Generation) via `lib/actions/rag.ts` which uses Supabase RPC functions `generate_embedding()` and `hybrid_search()` to retrieve relevant context from past conversations. However, there's a bug on line 433 of `app/actions.tsx` where `currentSystemPrompt` is passed to the researcher instead of `augmentedSystemPrompt`, preventing the retrieved context from being used.
Applied to files:
components/report-template.tsx
🔇 Additional comments (11)
lib/utils/report-generator.ts (3)
33-49:allowTaint: truecan still causeSecurityErroroncanvas.toDataURL().This issue was previously identified. When
allowTaintis enabled and any cross-origin image without CORS headers is rendered, the canvas becomes tainted andtoDataURL()will throw. Since the images in this flow are already data URLs or CORS-enabled,allowTaintis unnecessary and risky.
72-78: Off-by-one pagination loop produces a trailing blank page.This issue was previously identified. When
scaledHeightis an exact multiple ofpdfHeight,heightLeftreaches exactly0and the>= 0condition runs one extra iteration, adding a blank page.
1-6: LGTM!components/report-template.tsx (4)
147-149: Missing null guard ongeometry.coordinatescan crash the render.This issue was previously identified. If
feature.geometryorfeature.geometry.coordinatesis null/undefined,JSON.stringify(undefined)returnsundefined(not a string), and.substring()throws aTypeError, crashing the entire template render.
31-45: LGTM!
66-126: LGTM!
54-61: LGTM!Also applies to: 106-117
components/download-report-button.tsx (4)
54-58: Unsafeas any[]cast discards type safety.This issue was previously identified. The cast could mask runtime issues if the AI SDK changes the content structure. A type guard or explicit interface would improve safety.
1-27: LGTM!
38-48: LGTM!
113-137: LGTM!
| // Step 1: Render template in portal | ||
| setShowTemplate(true) | ||
|
|
||
| // Step 2: Wait for DOM and React to synchronize | ||
| // Using a longer timeout to ensure large images and complex content are rendered | ||
| toast.loading('Rendering report elements...', { id: toastId }) | ||
| await new Promise(resolve => setTimeout(resolve, 1500)) | ||
|
|
||
| // Step 3: Capture and Generate | ||
| toast.loading('Capturing styled report...', { id: toastId }) | ||
| await generatePDFReport('report-template', finalTitle) |
There was a problem hiding this comment.
Fixed 1500ms timeout remains fragile for ensuring DOM readiness.
While increased from the previous 500ms, using setTimeout to await React rendering and paint completion is inherently unreliable. On slow devices, large conversations, or image-heavy reports, the template may not be fully painted before generatePDFReport captures the DOM, resulting in partial or blank PDFs.
Consider using a double requestAnimationFrame pattern to ensure the browser has committed and painted the DOM:
🛡️ Proposed fix using double rAF
// Step 1: Render template in portal
setShowTemplate(true)
- // Step 2: Wait for DOM and React to synchronize
- // Using a longer timeout to ensure large images and complex content are rendered
- toast.loading('Rendering report elements...', { id: toastId })
- await new Promise(resolve => setTimeout(resolve, 1500))
+ // Step 2: Wait for React commit + browser paint
+ toast.loading('Rendering report elements...', { id: toastId })
+ await new Promise<void>(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => resolve())
+ })
+ })
+
+ // Step 2b: Wait for images in the template to decode
+ const templateEl = document.getElementById('report-template')
+ if (templateEl) {
+ const imgs = Array.from(templateEl.getElementsByTagName('img'))
+ await Promise.all(
+ imgs.map(img => img.decode?.().catch(() => {}))
+ )
+ }
// Step 3: Capture and Generate🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/download-report-button.tsx` around lines 70 - 80, The fixed 1500ms
setTimeout is unreliable; replace the artificial delay before calling
generatePDFReport('report-template', finalTitle) with a DOM paint
synchronization using requestAnimationFrame twice (or a small loop of rAFs)
after setShowTemplate(true), and additionally await any image/resource readiness
inside the template (e.g., wait for all images within the 'report-template'
container to fire their load/error events) before calling toast.loading and
generatePDFReport; keep toast.loading(toastId) calls but move them to just
before the final capture step so the sequence is: setShowTemplate(true) -> await
double rAF (and images loaded) -> toast.loading('Capturing styled report...', {
id: toastId }) -> generatePDFReport('report-template', finalTitle).
This PR implements the requested PDF report generation feature. It uses a React-based template styled with Tailwind CSS, which is then converted to a PDF using html2canvas and jsPDF. The download button is placed in the SettingsView as a document icon. The report includes the full conversation history, map snapshots from the live view and resolution searches, and an appendix for drawn features and measurements.
PR created automatically by Jules for task 18302854165168169137 started by @ngoiyaeric
Summary by CodeRabbit
New Features
User Experience