Skip to content

Implement PDF Report Generation#602

Open
ngoiyaeric wants to merge 4 commits into
mainfrom
feature/pdf-report-generation-10179943642823521584
Open

Implement PDF Report Generation#602
ngoiyaeric wants to merge 4 commits into
mainfrom
feature/pdf-report-generation-10179943642823521584

Conversation

@ngoiyaeric
Copy link
Copy Markdown
Collaborator

@ngoiyaeric ngoiyaeric commented May 12, 2026

This PR implements a PDF report generation feature for QCX.

Key features:

  • Comprehensive Reports: Includes chat history, map screenshots, and a detailed appendix of drawn features with measurements and coordinates.
  • Improved Rendering: AI responses are converted from markdown to plain text for better readability in the PDF.
  • Robust Pagination: Added automatic page breaks for long conversation turns to prevent content truncation.
  • UI Integration: A new "Download Report" button is added to the header, providing easy access to the feature.
  • Reliable Data Fetching: Utilizes useAIState, useMapData, and useMap to gather all necessary context for the report.

PR created automatically by Jules for task 10179943642823521584 started by @ngoiyaeric

Summary by CodeRabbit

  • New Features
    • Added a report download button in headers and chat UI (mobile & desktop) to generate multi-page PDF reports.
    • PDFs include a cover page with title/timestamp, conversation history, embedded map snapshot(s), and drawings & measurements with basic summaries.
    • New "Reports" tab in Settings to access report generation and related map provider options.

Review Change Stack

- Added `jspdf` and `markdown-to-txt` dependencies.
- Created `lib/utils/report-generator.ts` for PDF generation logic.
- Created `components/report-button.tsx` as a UI entry point.
- Integrated `ReportButton` into the desktop header.
- Implemented robust pagination and markdown-to-text conversion for AI responses in the report.
- Added drawings appendix to the report.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
qcx Ready Ready Preview, Comment May 12, 2026 8:39am

@qodo-code-review
Copy link
Copy Markdown
Contributor

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Warning

Rate limit exceeded

@ngoiyaeric has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 53 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d853701-c3f3-4be1-9208-243293f2ebf9

📥 Commits

Reviewing files that changed from the base of the PR and between e1b6ff0 and e0cda69.

📒 Files selected for processing (2)
  • components/chat.tsx
  • components/report-button.tsx

Walkthrough

This PR adds a PDF report download feature: a ReportButton UI that captures chat messages, map snapshots, and drawn features and invokes generateReport to produce a multi-page PDF (cover, map image, conversation history, and drawings appendix).

Changes

PDF Report Generation Feature

Layer / File(s) Summary
Report generation dependencies
package.json
Adds jspdf, @types/jspdf, and markdown-to-txt runtime/type dependencies to support PDF creation and markdown-to-text conversion.
Report generator utility and data contract
lib/utils/report-generator.ts
Exports ReportData and generateReport; builds multi-page PDF with cover page (title, timestamp), optional map snapshot (paged), conversation history (user turns and AI responses with markdown→plain text), optional resolution search result images, and a drawings & measurements appendix with page-break handling.
ReportButton component
components/report-button.tsx
Client-side React component that captures the map canvas PNG, derives chatTitle from AI state (fallback), guards concurrent generation with isGenerating, calls generateReport with messages/drawnFeatures/mapSnapshot/chatTitle, and shows success/error toasts; supports inline or portal rendering.
Header / Chat / Settings integration
components/header.tsx, components/chat.tsx, components/settings/components/settings.tsx
Adds a portal mount in the desktop header, inserts ReportButton into mobile and desktop header/search areas, and replaces the previous Map settings tab with a Reports tab that renders the ReportButton and map provider settings.

Sequence Diagram

sequenceDiagram
  participant Header
  participant ReportButton
  participant Map as MapCanvas
  participant generateReport
  participant jsPDF
  participant Downloads
  Header->>ReportButton: Render in header/chat/settings
  ReportButton->>Map: Capture canvas PNG (map.getCanvas().toDataURL)
  ReportButton->>generateReport: Invoke with {messages, drawnFeatures, mapSnapshot, chatTitle}
  generateReport->>jsPDF: Build PDF (cover, images, text)
  generateReport->>Downloads: Save PDF file (filename from chatTitle)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • Report Generation #569: This PR implements the requested PDF report feature (ReportButton, map snapshot capture, and generateReport utility).

Poem

🐰 A button blooms within the header's row,
I hop and whisper: "Let the reports flow!"
Maps and chats bundled, pages stacked bright,
Drawings and timestamps tucked in tight—
Click, and a PDF hops into sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: implementing PDF report generation functionality across multiple components and utilities.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/pdf-report-generation-10179943642823521584

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 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/report-button.tsx`:
- Around line 3-11: Remove the redundant top-level "import React from 'react'"
(the JSX runtime no longer needs it) and keep the existing named "useState"
import from 'react'; update the imports near Button, FileDown, Loader2,
useAIState, useMapData, useMap, generateReport, and toast so there are no
duplicate or unused React namespace imports, and verify no code references the
React identifier (e.g., React.something) in this file before committing.

In `@lib/utils/report-generator.ts`:
- Around line 90-94: The code parses userMsg.content into json and directly
reads json.input or json.related_query; add validation to ensure the parsed
value is an object and contains the expected keys before using them. In the try
block where JSON.parse(userMsg.content) is assigned to json, verify typeof json
=== 'object' && json !== null and then use checks like 'input' in json or
'related_query' in json (or optional chaining with fallbacks) to set userContent
safely; keep the existing catch fallback to userMsg.content. Update the logic
around userMsg, json, and userContent to avoid runtime errors when the parsed
structure is unexpected.
- Line 127: The call to doc.addImage(images[i], 'JPEG', ...) hardcodes JPEG but
images (mapboxImage, googleImage, image) may be PNG/WebP/etc.; update the image
handling around the images array and the doc.addImage call to detect each
image's format from its data URL prefix (inspect the "data:*/*;base64," mime
type), map the mime type to jsPDF's expected format string (e.g., 'PNG' for
image/png, 'JPEG' for image/jpeg, etc.), and pass that variable format into
doc.addImage for each image, with a sensible fallback (e.g., 'PNG') if the mime
type is missing or unsupported. Ensure detection logic is colocated with the
images array construction and used when calling doc.addImage so each image is
added with the correct format.
- Around line 162-163: The coordinate string is being naively truncated with
substring(0, 100) which can cut a number or coordinate pair mid-value; modify
the logic that builds coords (using feature.geometry.coordinates) to serialize
coordinates element-by-element and append whole coordinate entries until a
character limit (e.g., 100) would be exceeded, then append an ellipsis, and use
that result in the existing doc.text call (the variable coords and the
doc.text(`Coordinates: ${coords}`, margin + 5, yOffset) should remain but
receive the safely truncated string).
- Around line 112-133: Ensure the parsed JSON from searchResult is an object and
has the expected properties before using them: after const data =
JSON.parse(...), check that data is a non-null object and that summary is a
string before calling addTextWithAutoPageBreak, and verify
mapboxImage/googleImage/image are valid non-empty strings (or valid image
payloads) before adding them to the images array and calling doc.addImage; use
optional chaining/typeof checks and provide safe defaults, only call
checkPageBreak/doc.addImage when image entries pass validation, and keep error
handling around parsing/image rendering (refer to searchResult, data,
addTextWithAutoPageBreak, checkPageBreak, doc.addImage, and yOffset).

In `@package.json`:
- Line 72: Remove the unused "html2canvas" dependency from package.json (delete
the "html2canvas": "^1.4.1" entry) and then update the lockfile by running your
package manager install (npm install or yarn install) so the dependency is
actually removed from node_modules; verify there are no imports of html2canvas
in the codebase (e.g., check report-button.tsx which uses
map.getCanvas().toDataURL('image/png')) and run the test/build to ensure bundle
size and functionality remain intact.
🪄 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: 02755013-44cd-47ec-a497-1ec5561414f1

📥 Commits

Reviewing files that changed from the base of the PR and between d0efce4 and 1ca140d.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • components/header.tsx
  • components/report-button.tsx
  • lib/utils/report-generator.ts
  • package.json
📜 Review details
🔇 Additional comments (6)
lib/utils/report-generator.ts (2)

26-50: LGTM!


105-105: ⚡ Quick win

No action required. The markdownToTxt() function call correctly implements the markdown-to-txt v2.0.1 API, which accepts a markdown string as the first argument (with an optional MarkedOptions second argument). The code usage is appropriate and matches the documented API signature.

components/report-button.tsx (2)

19-43: LGTM!


27-27: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add null safety for chatId before calling substring.

The code constructs chatTitle from aiState.chatId.substring(0, 8), but while the ternary checks for the existence of chatId, it doesn't guard against chatId being an empty string or other edge cases before calling .substring().

🛡️ Suggested fix for safer chatId handling
-      const chatTitle = aiState.chatId ? `Chat-${aiState.chatId.substring(0, 8)}` : 'QCX-Analysis'
+      const chatTitle = aiState.chatId && aiState.chatId.length > 0 
+        ? `Chat-${aiState.chatId.substring(0, 8)}` 
+        : 'QCX-Analysis'
			> Likely an incorrect or invalid review comment.
components/header.tsx (1)

18-18: LGTM!

Also applies to: 75-75

package.json (1)

73-73: ⚡ Quick win

No action required—jsPDF version 4.2.1 is correct.

The specified version is the latest stable release of jsPDF as of May 2026 (published Mar 17, 2026).

			> Likely an incorrect or invalid review comment.

Comment thread components/report-button.tsx Outdated
Comment on lines +3 to +11
import React from 'react'
import { Button } from '@/components/ui/button'
import { FileDown, Loader2 } from 'lucide-react'
import { useAIState } from 'ai/rsc'
import { useMapData } from '@/components/map/map-data-context'
import { useMap } from '@/components/map/map-context'
import { generateReport } from '@/lib/utils/report-generator'
import { useState } from 'react'
import { toast } from 'sonner'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Remove redundant React import.

In React 19, the React import on line 3 is no longer needed for JSX. Additionally, useState is imported on line 10, making the React namespace import on line 3 unnecessary.

🧹 Suggested cleanup
-import React from 'react'
 import { Button } from '@/components/ui/button'
 import { FileDown, Loader2 } from 'lucide-react'
 import { useAIState } from 'ai/rsc'
 import { useMapData } from '@/components/map/map-data-context'
 import { useMap } from '@/components/map/map-context'
 import { generateReport } from '@/lib/utils/report-generator'
 import { useState } from 'react'
 import { toast } from 'sonner'

As per coding guidelines, React 19 no longer requires the React import for JSX transformation, as noted in the React 19 upgrade documentation: "Enable the modern JSX Transform (2020+)."

📝 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.

Suggested change
import React from 'react'
import { Button } from '@/components/ui/button'
import { FileDown, Loader2 } from 'lucide-react'
import { useAIState } from 'ai/rsc'
import { useMapData } from '@/components/map/map-data-context'
import { useMap } from '@/components/map/map-context'
import { generateReport } from '@/lib/utils/report-generator'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { FileDown, Loader2 } from 'lucide-react'
import { useAIState } from 'ai/rsc'
import { useMapData } from '@/components/map/map-data-context'
import { useMap } from '@/components/map/map-context'
import { generateReport } from '@/lib/utils/report-generator'
import { useState } from 'react'
import { toast } from 'sonner'
🤖 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-button.tsx` around lines 3 - 11, Remove the redundant
top-level "import React from 'react'" (the JSX runtime no longer needs it) and
keep the existing named "useState" import from 'react'; update the imports near
Button, FileDown, Loader2, useAIState, useMapData, useMap, generateReport, and
toast so there are no duplicate or unused React namespace imports, and verify no
code references the React identifier (e.g., React.something) in this file before
committing.

Comment on lines +90 to +94
const json = JSON.parse(userMsg.content as string);
userContent = userMsg.type === 'input' ? json.input : json.related_query;
} catch (e) {
userContent = userMsg.content as string;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add validation for parsed JSON structure.

The parsed JSON object is not validated before accessing json.input or json.related_query. If the structure is unexpected, this could fail silently or throw runtime errors.

🛡️ Suggested fix with validation
     let userContent = '';
     try {
         const json = JSON.parse(userMsg.content as string);
-        userContent = userMsg.type === 'input' ? json.input : json.related_query;
+        userContent = userMsg.type === 'input' 
+          ? (json.input || 'No input content') 
+          : (json.related_query || 'No related query');
     } catch (e) {
         userContent = userMsg.content as string;
     }
📝 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.

Suggested change
const json = JSON.parse(userMsg.content as string);
userContent = userMsg.type === 'input' ? json.input : json.related_query;
} catch (e) {
userContent = userMsg.content as string;
}
const json = JSON.parse(userMsg.content as string);
userContent = userMsg.type === 'input'
? (json.input || 'No input content')
: (json.related_query || 'No related query');
} catch (e) {
userContent = userMsg.content as string;
}
🤖 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 90 - 94, The code parses
userMsg.content into json and directly reads json.input or json.related_query;
add validation to ensure the parsed value is an object and contains the expected
keys before using them. In the try block where JSON.parse(userMsg.content) is
assigned to json, verify typeof json === 'object' && json !== null and then use
checks like 'input' in json or 'related_query' in json (or optional chaining
with fallbacks) to set userContent safely; keep the existing catch fallback to
userMsg.content. Update the logic around userMsg, json, and userContent to avoid
runtime errors when the parsed structure is unexpected.

Comment on lines +112 to +133
const data = JSON.parse(searchResult.content as string);

// GeoJSON Summary
if (data.summary) {
addTextWithAutoPageBreak(`Analysis Summary: ${data.summary}`, 11, 'normal', [80, 80, 80]);
}

const images = [data.mapboxImage, data.googleImage, data.image].filter(Boolean);
if (images.length > 0) {
const imgWidth = (contentWidth - 10) / 2;
const imgHeight = (imgWidth * 3) / 4;

checkPageBreak(imgHeight + 10);

for (let i = 0; i < Math.min(images.length, 2); i++) {
doc.addImage(images[i], 'JPEG', margin + (i * (imgWidth + 10)), yOffset, imgWidth, imgHeight);
}
yOffset += imgHeight + 10;
}
} catch (e) {
console.error('Error parsing resolution search result for PDF:', e);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate parsed search result structure before accessing properties.

The code accesses data.summary, data.mapboxImage, data.googleImage, and data.image without validating that these properties exist in the parsed object. This could cause runtime errors if the structure is unexpected.

🛡️ Suggested fix with proper validation
       try {
         const data = JSON.parse(searchResult.content as string);
 
         // GeoJSON Summary
-        if (data.summary) {
+        if (data && typeof data === 'object' && data.summary) {
             addTextWithAutoPageBreak(`Analysis Summary: ${data.summary}`, 11, 'normal', [80, 80, 80]);
         }
 
-        const images = [data.mapboxImage, data.googleImage, data.image].filter(Boolean);
+        const images = data && typeof data === 'object' 
+          ? [data.mapboxImage, data.googleImage, data.image].filter(Boolean)
+          : [];
         if (images.length > 0) {
🤖 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 112 - 133, Ensure the parsed JSON
from searchResult is an object and has the expected properties before using
them: after const data = JSON.parse(...), check that data is a non-null object
and that summary is a string before calling addTextWithAutoPageBreak, and verify
mapboxImage/googleImage/image are valid non-empty strings (or valid image
payloads) before adding them to the images array and calling doc.addImage; use
optional chaining/typeof checks and provide safe defaults, only call
checkPageBreak/doc.addImage when image entries pass validation, and keep error
handling around parsing/image rendering (refer to searchResult, data,
addTextWithAutoPageBreak, checkPageBreak, doc.addImage, and yOffset).

checkPageBreak(imgHeight + 10);

for (let i = 0; i < Math.min(images.length, 2); i++) {
doc.addImage(images[i], 'JPEG', margin + (i * (imgWidth + 10)), yOffset, imgWidth, imgHeight);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify image format before hardcoding 'JPEG'.

The code assumes all images are JPEG format, but the images from different sources (mapboxImage, googleImage, image) may be in different formats (PNG, WebP, etc.). jsPDF may fail or produce corrupted output if the format doesn't match.

Consider detecting the format from the data URL prefix or use a more flexible format like 'PNG' that supports transparency, or handle each image's actual format:

🔍 Suggested improvement
             for (let i = 0; i < Math.min(images.length, 2); i++) {
-                doc.addImage(images[i], 'JPEG', margin + (i * (imgWidth + 10)), yOffset, imgWidth, imgHeight);
+                // Detect format from data URL or default to JPEG
+                const format = images[i].startsWith('data:image/png') ? 'PNG' : 'JPEG';
+                doc.addImage(images[i], format, margin + (i * (imgWidth + 10)), yOffset, imgWidth, imgHeight);
             }
📝 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.

Suggested change
doc.addImage(images[i], 'JPEG', margin + (i * (imgWidth + 10)), yOffset, imgWidth, imgHeight);
for (let i = 0; i < Math.min(images.length, 2); i++) {
// Detect format from data URL or default to JPEG
const format = images[i].startsWith('data:image/png') ? 'PNG' : 'JPEG';
doc.addImage(images[i], format, margin + (i * (imgWidth + 10)), yOffset, imgWidth, imgHeight);
}
🤖 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` at line 127, The call to
doc.addImage(images[i], 'JPEG', ...) hardcodes JPEG but images (mapboxImage,
googleImage, image) may be PNG/WebP/etc.; update the image handling around the
images array and the doc.addImage call to detect each image's format from its
data URL prefix (inspect the "data:*/*;base64," mime type), map the mime type to
jsPDF's expected format string (e.g., 'PNG' for image/png, 'JPEG' for
image/jpeg, etc.), and pass that variable format into doc.addImage for each
image, with a sensible fallback (e.g., 'PNG') if the mime type is missing or
unsupported. Ensure detection logic is colocated with the images array
construction and used when calling doc.addImage so each image is added with the
correct format.

Comment on lines +162 to +163
const coords = JSON.stringify(feature.geometry.coordinates).substring(0, 100) + '...';
doc.text(`Coordinates: ${coords}`, margin + 5, yOffset);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Improve coordinate truncation to avoid cutting mid-value.

The current implementation uses substring(0, 100) which can truncate in the middle of a coordinate pair or number, producing confusing output like [[-122.4, 37.7], [-122.3, 3....

♻️ Suggested improvement for cleaner truncation
-      const coords = JSON.stringify(feature.geometry.coordinates).substring(0, 100) + '...';
-      doc.text(`Coordinates: ${coords}`, margin + 5, yOffset);
+      // Show first few coordinate pairs cleanly
+      const coords = feature.geometry.coordinates;
+      const coordsArray = Array.isArray(coords[0]) ? coords.slice(0, 3) : [coords];
+      const coordsStr = JSON.stringify(coordsArray) + (coords.length > 3 ? '... (truncated)' : '');
+      doc.text(`Coordinates: ${coordsStr}`, margin + 5, yOffset);
📝 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.

Suggested change
const coords = JSON.stringify(feature.geometry.coordinates).substring(0, 100) + '...';
doc.text(`Coordinates: ${coords}`, margin + 5, yOffset);
// Show first few coordinate pairs cleanly
const coords = feature.geometry.coordinates;
const coordsArray = Array.isArray(coords[0]) ? coords.slice(0, 3) : [coords];
const coordsStr = JSON.stringify(coordsArray) + (coords.length > 3 ? '... (truncated)' : '');
doc.text(`Coordinates: ${coordsStr}`, margin + 5, yOffset);
🤖 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 162 - 163, The coordinate string
is being naively truncated with substring(0, 100) which can cut a number or
coordinate pair mid-value; modify the logic that builds coords (using
feature.geometry.coordinates) to serialize coordinates element-by-element and
append whole coordinate entries until a character limit (e.g., 100) would be
exceeded, then append an ellipsis, and use that result in the existing doc.text
call (the variable coords and the doc.text(`Coordinates: ${coords}`, margin + 5,
yOffset) should remain but receive the safely truncated string).

Comment thread package.json Outdated
- Implemented `generateReport` utility with `jsPDF` for comprehensive PDF export.
- Added automatic page breaking and markdown-to-text conversion for long AI responses.
- Included map snapshots, analysis summaries, and a drawings appendix.
- Created `ReportButton` component using React Portals to ensure safe rendering in the global Header.
- Verified successful production build after addressing SSR/prerendering constraints.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
@ngoiyaeric
Copy link
Copy Markdown
Collaborator Author

@coderabbitai the front end of the map settings should be replaced with the report generation feature, place the button their an name that the title of the setting reports.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and read the header component
fd -e tsx -e ts header components/ | head -5
echo "---"
# Find map settings related components
fd -e tsx -e ts -i 'map-settings\|mapsettings\|map_settings' | head -10
echo "---"
# Read the current header file
cat components/header.tsx 2>/dev/null || fd header.tsx | xargs cat
echo "---"
# Find report-button component
cat components/report-button.tsx 2>/dev/null || fd report-button.tsx | xargs cat
echo "---"
# Search for MapToggle usage in header
rg -n 'MapToggle\|map-toggle\|maptoggle' --type tsx -C3 2>/dev/null || rg -n 'MapToggle\|map-toggle\|maptoggle' -C3

Length of output: 5342


@ngoiyaeric Here's the plan — replace <MapToggle /> in the desktop header with the ReportButton rendered inline (dropping the portal), and label it "Reports".


1. components/header.tsx

Remove the MapToggle import/usage and the portal div; import and render ReportButton directly where MapToggle was:

- import { MapToggle } from './map-toggle'
+ import { ReportButton } from './report-button'
  import { ProfileToggle } from './profile-toggle'
  ...

      <div className="flex-1 hidden md:flex justify-center gap-10 items-center z-10">
        <ProfileToggle/>
        
-       <MapToggle />
-
-       <div id="header-report-portal" className="contents" />
+       <ReportButton />
        
        <Button variant="ghost" size="icon" onClick={toggleCalendar} ...>

2. components/report-button.tsx

Strip out the portal logic and rename the button title/label to "Reports":

 'use client'

 import React, { useState } from 'react'
-import { createPortal } from 'react-dom'
 import { Button } from '@/components/ui/button'
 import { FileDown, Loader2 } from 'lucide-react'
 import { useAIState } from 'ai/rsc'
 import { useMapData } from '@/components/map/map-data-context'
 import { useMap } from '@/components/map/map-context'
 import { generateReport } from '@/lib/utils/report-generator'
 import { toast } from 'sonner'

 export const ReportButton = () => {
   const [aiState] = useAIState()
   const { mapData } = useMapData()
   const { map } = useMap()
   const [isGenerating, setIsGenerating] = useState(false)
-  const [portalNode, setPortalNode] = useState<HTMLElement | null>(null)
-
-  useEffect(() => {
-    setPortalNode(document.getElementById('header-report-portal'))
-  }, [])

   const handleDownloadReport = async () => {
     if (isGenerating) return
     setIsGenerating(true)
     try {
       const mapSnapshot = map ? map.getCanvas().toDataURL('image/png') : ''
       const chatTitle = aiState.chatId ? `Chat-${aiState.chatId.substring(0, 8)}` : 'QCX-Analysis'
       await generateReport({
         messages: aiState.messages,
         drawnFeatures: mapData.drawnFeatures || [],
         mapSnapshot,
         chatTitle
       })
       toast.success('Report generated successfully')
     } catch (error) {
       console.error('Failed to generate report:', error)
       toast.error('Failed to generate report')
     } finally {
       setIsGenerating(false)
     }
   }

-  const button = (
-    <Button
-      variant="ghost"
-      size="icon"
-      onClick={handleDownloadReport}
-      title="Download PDF Report"
-      disabled={isGenerating}
-    >
-      {isGenerating ? (
-        <Loader2 className="h-[1.2rem] w-[1.2rem] animate-spin" />
-      ) : (
-        <FileDown className="h-[1.2rem] w-[1.2rem]" />
-      )}
-    </Button>
-  )
-
-  if (!portalNode) return null
-
-  return createPortal(button, portalNode)
+  return (
+    <Button
+      variant="ghost"
+      className="flex items-center gap-1 px-2"
+      onClick={handleDownloadReport}
+      title="Reports"
+      disabled={isGenerating}
+    >
+      {isGenerating ? (
+        <Loader2 className="h-[1.2rem] w-[1.2rem] animate-spin" />
+      ) : (
+        <FileDown className="h-[1.2rem] w-[1.2rem]" />
+      )}
+      <span className="text-sm font-medium">Reports</span>
+    </Button>
+  )
 }

Summary of changes:

  • MapToggle is removed from the desktop header and replaced with ReportButton at the same position.
  • The portal mechanism (header-report-portal div, useEffect, createPortal) is removed — the button renders inline like all other header controls.
  • The button now shows a FileDown icon + "Reports" label, matching the naming you requested.

Would you like me to implement these changes directly?

  • Replace <MapToggle /> with <ReportButton /> inline in components/header.tsx, remove portal div and update imports
  • Refactor components/report-button.tsx to remove portal logic, render inline, add "Reports" label

The requested changes have been implemented and a pull request has been created: View PR

@google-labs-jules
Copy link
Copy Markdown
Contributor

@coderabbitai the front end of the map settings should be replaced with the report generation feature, place the button their an name that the title of the setting reports.

I have updated the Settings view as requested. The 'Map' tab has been renamed to 'Reports' and now features a dedicated section for Report Generation with a prominent button to generate and download the PDF report. I've also moved the map provider settings into this new 'Reports' tab to keep the interface clean.

- Renamed 'Map' tab to 'Reports' in Settings.
- Added 'Report Generation' section to the Reports tab.
- Updated `ReportButton` to support an inline display mode with a label.
- Moved map provider settings into the new Reports tab.
- Maintained the existing 'Download Report' button in the header via portals.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
components/report-button.tsx (1)

3-3: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove redundant React namespace import.

The React namespace import is not used anywhere in this file and is unnecessary in React 19 with the modern JSX transform. Only the named imports (useState, useEffect) are needed.

🧹 Suggested cleanup
-import React, { useState, useEffect } from 'react'
+import { useState, useEffect } from 'react'

As per coding guidelines, React 19 no longer requires the React import for JSX transformation.

🤖 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-button.tsx` at line 3, Remove the unused default React
namespace from the import in components/report-button.tsx; keep only the named
hooks by changing the import that currently reads like "import React, {
useState, useEffect } from 'react'" to import only "{ useState, useEffect }" so
the file uses the modern JSX transform without an unused React symbol.
🤖 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 `@package.json`:
- Line 51: The dependency entry "@types/jspdf" may not match the jsPDF runtime
(4.2.1); audit all places using jsPDF types (search for jsPDF, JsPDF, and
relevant constructors/methods) to verify they are covered by `@types/jspdf`@2.0.0,
and then either replace/update the type source: remove `@types/jspdf` if jsPDF 4.x
includes its own types, upgrade to a community/official types package that lists
4.x support, or add missing type declarations (ambient module augmentations or
specific d.ts shims) for any methods/classes used but missing in `@types/jspdf`;
ensure package.json reflects the chosen approach and run TypeScript type-check
to confirm no new errors.

---

Duplicate comments:
In `@components/report-button.tsx`:
- Line 3: Remove the unused default React namespace from the import in
components/report-button.tsx; keep only the named hooks by changing the import
that currently reads like "import React, { useState, useEffect } from 'react'"
to import only "{ useState, useEffect }" so the file uses the modern JSX
transform without an unused React symbol.
🪄 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: 4c62848b-aa28-41bd-87eb-351a5d794237

📥 Commits

Reviewing files that changed from the base of the PR and between 1ca140d and e1b6ff0.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • components/chat.tsx
  • components/header.tsx
  • components/report-button.tsx
  • components/settings/components/settings.tsx
  • package.json
📜 Review details
🔇 Additional comments (6)
components/header.tsx (1)

73-74: LGTM!

components/report-button.tsx (2)

30-53: LGTM!


55-77: LGTM!

components/settings/components/settings.tsx (1)

26-26: LGTM!

Also applies to: 170-170, 207-240

components/chat.tsx (1)

24-24: LGTM!

Also applies to: 143-143, 190-190

package.json (1)

78-78: ⚡ Quick win

markdown-to-txt@2.0.1 is available and current. Version 2.0.1 exists on npm and is the latest available version. The ^2.0.1 constraint is valid and stable.

Comment thread package.json
"@tailwindcss/typography": "^0.5.16",
"@tavily/core": "^0.6.4",
"@turf/turf": "^7.2.0",
"@types/jspdf": "^2.0.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check jspdf package versions and compatibility

# Check latest available version of jspdf
echo "=== Latest jspdf version ==="
npm view jspdf version

# Check if version 4.2.1 exists
echo -e "\n=== Checking if jspdf@4.2.1 exists ==="
npm view jspdf@4.2.1 version 2>&1 || echo "Version 4.2.1 not found"

# Check available versions of jspdf
echo -e "\n=== Recent jspdf versions ==="
npm view jspdf versions --json | jq '.[-10:]'

# Check latest `@types/jspdf` version
echo -e "\n=== Latest `@types/jspdf` version ==="
npm view `@types/jspdf` version

# Check for security advisories
echo -e "\n=== Security advisories for jspdf ==="
npm audit --package-lock-only --json | jq '.vulnerabilities.jspdf // "No vulnerabilities found"'

Repository: QueueLab/QCX

Length of output: 757


Verify @types/jspdf type coverage for jsPDF 4.2.1.

jsPDF 4.2.1 is the current stable version, not an outdated pre-release. However, @types/jspdf@2.0.0 (the latest available types) may lack type definitions for features introduced in jsPDF 3.x and 4.x. Check that your codebase only uses types from the 2.x definitions or consider if a newer types package is available with proper 4.x support.

🤖 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 `@package.json` at line 51, The dependency entry "@types/jspdf" may not match
the jsPDF runtime (4.2.1); audit all places using jsPDF types (search for jsPDF,
JsPDF, and relevant constructors/methods) to verify they are covered by
`@types/jspdf`@2.0.0, and then either replace/update the type source: remove
`@types/jspdf` if jsPDF 4.x includes its own types, upgrade to a
community/official types package that lists 4.x support, or add missing type
declarations (ambient module augmentations or specific d.ts shims) for any
methods/classes used but missing in `@types/jspdf`; ensure package.json reflects
the chosen approach and run TypeScript type-check to confirm no new errors.

@ngoiyaeric
Copy link
Copy Markdown
Collaborator Author

@coderabbitai can you remove the icon at the top bar and do not notify the user when you've generated it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Tip

For best results, initiate chat on the files or code changes.

@ngoiyaeric Sure! Here are the two changes needed:

  1. Remove the icon — strip FileDown, Loader2, and their JSX; show only the "Reports" text label.
  2. Remove toast notifications — drop toast.success and toast.error calls (and the sonner import).
  • Remove FileDown, Loader2 icon imports and their JSX from ReportButton
  • Remove toast import and toast.success / toast.error calls
  • Keep loading state and error handling logic

The requested changes have been implemented and a pull request has been created: View PR

@google-labs-jules
Copy link
Copy Markdown
Contributor

@coderabbitai can you remove the icon at the top bar and do not notify the user when you've generated it.

I have removed the report icon from the header bar and disabled the success notification after report generation, as requested. The report generation is now primarily accessible through the 'Reports' tab in Settings.

- Removed `ReportButton` from the header.
- Disabled success toast notification after successful report generation.
- Cleaned up unused portal target for the report button in the header.
- Kept the feature accessible via the 'Reports' tab in Settings.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants