Skip to content

Implement PDF Report Generation#602

Closed
ngoiyaeric wants to merge 5 commits into
mainfrom
feature/pdf-report-generation-10179943642823521584
Closed

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

Conversation

@ngoiyaeric

@ngoiyaeric ngoiyaeric commented May 12, 2026

Copy link
Copy Markdown
Collaborator

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 PDF report generation with conversation history, map snapshots, and drawn measurements.
    • New Reports tab in settings for easy access to report generation.
    • Added optional search integration via Tavily API.
  • Improvements

    • Enhanced geospatial tool validation with clearer error messages.
    • Updated AI model provider priority (Grok primary, Gemini secondary).
  • Chores

    • Added dependencies for PDF report functionality.
    • Updated environment configuration documentation.

- 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

vercel Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
qcx Ready Ready Preview, Comment Jun 1, 2026 1:47pm

@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

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR introduces PDF report generation, refactors tool registration and AI provider selection, and restructures geospatial and resolution-search schemas with enhanced runtime validation. The ReportButton component captures map snapshots and generates multi-page PDFs, while search tool availability is now gated on TAVILY_API_KEY and Exa provider support is removed. AI model fallback now prioritizes Grok over Gemini, and geospatial queries are validated at runtime with flattened schema constraints.

Changes

PDF Report Generation

Layer / File(s) Summary
Report generation dependencies
package.json
Adds jsPDF, @types/jspdf, and markdown-to-txt for PDF creation and text conversion.
Report generator utility and data contract
lib/utils/report-generator.ts
Implements generateReport to build multi-page PDFs with cover page, optional map snapshot, conversation history (markdown→text), resolution search results with images, and drawings/measurements appendix; defines ReportData interface.
ReportButton component
components/report-button.tsx
Client React component that captures map canvas PNG, derives chat title, prevents concurrent generation, calls generateReport, and handles errors with toast notifications.
Report UI integration
components/chat.tsx, components/settings/components/settings.tsx
Imports ReportButton into chat; adds "Reports" tab in settings that exposes report generation and relocated map provider selection.

Search Tool & Provider Configuration

Layer / File(s) Summary
Environment configuration
.env.local.example
Adds TAVILY_API_KEY configuration with documentation that search tool is skipped when key is absent.
Conditional search tool registration
lib/agents/tools/index.tsx
Removes searchTool from base tool registry; conditionally registers tools.search only when process.env.TAVILY_API_KEY is set.
Search tool Tavily-only implementation
lib/agents/tools/search.tsx
Removes exa-js import and exaSearch helper; routes all searches exclusively through tavilySearch.
AI model provider fallback order
lib/utils/index.ts, GEMINI_3.1_PRO_INTEGRATION.md
Changes default fallback from Gemini → Grok → Bedrock → OpenAI to Grok → Gemini → Bedrock → OpenAI; updates documentation and inline comments.

Schema Refactoring & GeoJSON Handling

Layer / File(s) Summary
Resolution search schema definition
lib/schema/resolution-search.ts
Introduces resolutionSearchSchema with flattened structure including optional coordinates, COG fields, news context, and GeoJSON FeatureCollection for xAI/OpenAI compatibility.
Resolution search agent wiring
lib/agents/resolution-search.tsx
Imports shared resolutionSearchSchema instead of local definition; updates system prompt to document structured output (summary, geoJson, news context, coordinates).
GeoJSON reconstruction in resolution-search
app/actions.tsx
Rebuilds standard FeatureCollection from analysisResult.geoJson.features; stores reconstructed geoJson in resolution_search_result payload.
Geospatial query schema refactoring
lib/schema/geospatial.tsx
Flattens schema from discriminated union (per-queryType conditional fields) to single flat object; removes Zod-enforced field constraints.
Geospatial tool runtime validation
lib/agents/tools/geospatial.tsx
Adds per-queryType input validation in tool execution; throws descriptive errors when required parameters (origin/destination, coordinates, query, location) are missing.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Chat as Chat UI
  participant ReportButton as ReportButton
  participant Map as Map Canvas
  participant generateReport as generateReport
  participant jsPDF as jsPDF
  participant FileSystem as File System
  User->>Chat: Click "Generate Report"
  Chat->>ReportButton: Render button
  User->>ReportButton: Click download
  ReportButton->>Map: Extract canvas.toDataURL(PNG)
  ReportButton->>generateReport: Call with messages, features, snapshot, chatTitle
  generateReport->>jsPDF: Create document, add cover page
  generateReport->>jsPDF: Embed map snapshot (with paging)
  generateReport->>jsPDF: Render conversation history<br/>(markdown→plain text)
  generateReport->>jsPDF: Embed resolution search result images
  generateReport->>jsPDF: Append drawings measurements section
  generateReport->>FileSystem: Save PDF (chatTitle-based filename)
  FileSystem-->>User: PDF downloaded
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Report Generation #569: Implements the requested PDF report generation feature with ReportButton component, generateReport utility using jsPDF, and integration of messages, drawnFeatures, mapSnapshot, and chatTitle as specified.

Possibly related PRs

  • QueueLab/QCX#621: Both PRs conditionally register the search tool only when process.env.TAVILY_API_KEY is set and update .env.local.example accordingly.
  • QueueLab/QCX#631: Both PRs change how resolution_search_result.geoJson is reconstructed and stored, with this PR using the new schema structure for PDF report embedding.
  • QueueLab/QCX#584: Both PRs modify lib/utils/index.ts's getModel() provider selection logic, though this PR reorders fallback priority (Grok-first) while #584 integrates Gemini 3.1 Pro.

Suggested labels

feature, pdf-export, tool-configuration, schema-refactor

🐰 A rabbit hops through data streams so pure,
PDF pages turning, each report assured,
Maps embedded, chats preserved with care,
Geospatial schemas flattened everywhere,
Downloads flutter like ears in the breeze! 📥

🚥 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 directly and clearly describes the main feature addition: PDF report generation functionality with ReportButton component and report-generator utility.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/pdf-report-generation-10179943642823521584
⚔️ Resolve merge conflicts
  • Resolve merge conflict 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.

@coderabbitai coderabbitai Bot left a comment

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.

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

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

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>

@coderabbitai coderabbitai Bot left a comment

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.

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

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

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>
@ngoiyaeric

Copy link
Copy Markdown
Collaborator Author

@coderabbitai I need to synchronize this branch with the main branch and retain all the code whilst maintain functionality.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how far behind the PR branch is from main
gh api repos/QueueLab/QCX/compare/main...feature/pdf-report-generation-10179943642823521584 \
  --jq '{ahead_by: .ahead_by, behind_by: .behind_by, status: .status}'

echo "---"
# Check if there are any open merge conflicts or blocking status checks
gh pr view 602 --repo QueueLab/QCX --json mergeable,mergeStateStatus,baseRefName,headRefName

Length of output: 646



@ngoiyaeric Good news — GitHub reports your branch is MERGEABLE (no conflicts detected), just 14 commits behind main. I can update the branch for you right now via GitHub's API, or you can do it locally.

Option A — I'll do it for you (reply "yes, go ahead"):
I'll run gh pr update-branch to merge main into your branch automatically.

Option B — Do it locally yourself:

git checkout feature/pdf-report-generation-10179943642823521584
git fetch origin
git merge origin/main
# resolve any conflicts if prompted (GitHub says there are none, so it should be clean)
git push origin feature/pdf-report-generation-10179943642823521584

Option C — Do it via the GitHub UI:
On PR #602, scroll to the bottom and click "Update branch""Update with merge commit".


A note on UNSTABLE status: the mergeStateStatus being unstable is likely due to failing CI checks (not merge conflicts), so syncing with main won't break anything — all your PDF report generation code will be preserved. Let me know which option you'd like to proceed with!

@google-labs-jules

Copy link
Copy Markdown
Contributor

@coderabbitai I need to synchronize this branch with the main branch and retain all the code whilst maintain functionality.

I have successfully merged the latest changes from the main branch into this feature branch. All code for the PDF report generation has been retained, and I've verified that the application still builds successfully and maintains its full functionality.

- Merged the latest changes from the main branch.
- Verified that PDF report generation functionality is preserved.
- Confirmed successful production build.

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.env.local.example (1)

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

Document XAI_API_KEY in the sample env file.

getModel() now prefers xAI before Gemini by default, but this example still only shows Gemini and Tavily keys. New deployments following .env.local.example won't see how to configure the new primary provider without reading source. Add an XAI_API_KEY entry alongside the other AI provider variables.

🤖 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 @.env.local.example around lines 19 - 25, Add an XAI_API_KEY entry to the
sample environment file so deployments can configure xAI as the new preferred
provider; update the block that currently lists GEMINI_3_PRO_API_KEY and
TAVILY_API_KEY to include a clearly named XAI_API_KEY=your_xai_api_key_here
entry (matching the key name expected by getModel()/configuration), with the
same comment style as the other provider keys.
components/report-button.tsx (1)

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

Add an accessible name to the icon-only button.

When inline is false, this control renders without visible text. title alone is not a reliable accessible name, so add an aria-label for the icon-only variant.

Suggested change
     <Button
       variant={inline ? "default" : "ghost"}
       size={inline ? "default" : "icon"}
       onClick={handleDownloadReport}
       title="Download PDF Report"
+      aria-label={inline ? undefined : "Download PDF Report"}
       disabled={isGenerating}
       className={inline ? "w-full" : ""}
     >
🤖 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 48 - 61, The icon-only Button
rendered in ReportButton (props/vars: inline, isGenerating, Button,
handleDownloadReport) lacks a reliable accessible name; add an aria-label to the
Button when inline is false (the icon-only variant) — e.g. conditionally pass
aria-label={inline ? undefined : isGenerating ? "Generating report" : "Generate
report"} so screen readers receive a clear name while keeping the existing title
and behavior.
🤖 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 `@lib/agents/resolution-search.tsx`:
- Line 130: The prompt text in lib/agents/resolution-search.tsx asks for "news
context" which doesn't match the schema; update the prompt (the string that
contains "7. **Structured Output:**") to use the exact schema field names:
replace "news context" with the concrete fields hasRecentNews and newsItems and
instruct the model to populate those keys (hasRecentNews: boolean, newsItems:
array of objects) so the output aligns with the expected schema and validation
(locate the prompt string in the resolution-search.tsx prompt builder /
generatePrompt function and update it accordingly).

In `@lib/agents/tools/geospatial.tsx`:
- Around line 347-368: The parameter validation and toolArgs construction
(switch on queryType that references params, includeMap, etc.) must be executed
before establishing the MCP connection and calling getConnectedMcpClient() /
listTools(); move the guard logic that builds toolArgs (the switch block
handling 'directions', 'distance', 'reverse', 'search', 'geocode', 'map') to run
first, throw early on malformed params (missing origin/destination, coordinates,
query, or location), and only after toolArgs is successfully created proceed to
call getConnectedMcpClient() and listTools() so invalid requests short-circuit
without opening external connections.

In `@lib/schema/geospatial.tsx`:
- Around line 42-45: The geospatial schema currently allows an unsupported
'transit' routing mode which can be passed through to Mapbox and cause runtime
failures; update the zod enum in lib/schema/geospatial.tsx (the field defined as
mode: z.enum([...]).optional().default('driving').describe(...)) to only include
'driving', 'walking', and 'cycling' (remove 'transit'), keep the default
'driving', and ensure any validation or docs tied to that schema reflect the
three supported modes; also verify callers in lib/agents/tools/geospatial.tsx
that pass params.mode (used as profile/mode) will only receive those three
values.

In `@lib/schema/resolution-search.ts`:
- Around line 12-24: The geoJson shape currently lets any geometryType string
and only supports up to Polygon coordinate nesting; update the geoJson schema
(the geoJson object in lib/schema/resolution-search.ts) to validate known
GeoJSON geometry types and their matching coordinate structures: replace
geometryType: z.string() with a z.enum listing
"Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection","Feature","FeatureCollection"
and make coordinates a discriminated union (or use a z.discriminatedUnion keyed
on geometryType) that enforces 1D arrays for Point/MultiPoint, 2D for
LineString/MultiLineString, 3D for Polygon/MultiPolygon, and supports nested
arrays for Multi* types so valid MultiPolygon coordinates are accepted and
unsupported shapes like "Circle" are rejected; ensure this aligns with how
app/actions.tsx and GeoJsonLayer expect GeoJSON so rendered payloads match valid
geometry.

---

Outside diff comments:
In @.env.local.example:
- Around line 19-25: Add an XAI_API_KEY entry to the sample environment file so
deployments can configure xAI as the new preferred provider; update the block
that currently lists GEMINI_3_PRO_API_KEY and TAVILY_API_KEY to include a
clearly named XAI_API_KEY=your_xai_api_key_here entry (matching the key name
expected by getModel()/configuration), with the same comment style as the other
provider keys.

In `@components/report-button.tsx`:
- Around line 48-61: The icon-only Button rendered in ReportButton (props/vars:
inline, isGenerating, Button, handleDownloadReport) lacks a reliable accessible
name; add an aria-label to the Button when inline is false (the icon-only
variant) — e.g. conditionally pass aria-label={inline ? undefined : isGenerating
? "Generating report" : "Generate report"} so screen readers receive a clear
name while keeping the existing title and behavior.
🪄 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: 0504f067-de43-4f88-b42c-908246134f6b

📥 Commits

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

📒 Files selected for processing (12)
  • .env.local.example
  • GEMINI_3.1_PRO_INTEGRATION.md
  • app/actions.tsx
  • components/chat.tsx
  • components/report-button.tsx
  • lib/agents/resolution-search.tsx
  • lib/agents/tools/geospatial.tsx
  • lib/agents/tools/index.tsx
  • lib/agents/tools/search.tsx
  • lib/schema/geospatial.tsx
  • lib/schema/resolution-search.ts
  • lib/utils/index.ts
💤 Files with no reviewable changes (1)
  • components/chat.tsx
📜 Review details
🔇 Additional comments (7)
lib/agents/tools/index.tsx (1)

27-32: LGTM!

lib/agents/tools/search.tsx (1)

42-52: LGTM!

lib/utils/index.ts (1)

82-104: LGTM!

GEMINI_3.1_PRO_INTEGRATION.md (1)

13-14: LGTM!

Also applies to: 64-65

lib/schema/geospatial.tsx (1)

3-41: LGTM!

Also applies to: 46-61

components/report-button.tsx (2)

3-3: Drop the unused default React import.

Already raised on an earlier revision; React is not referenced in this file, and the JSX runtime does not need the default import here.


27-35: ⚡ Quick win

Make map snapshot export best-effort in components/report-button.tsx

Wrap map.getCanvas().toDataURL('image/png') so a canvas export failure doesn’t abort generateReport; fall back to '' and continue generating the PDF.

Suggested change
-      const mapSnapshot = map ? map.getCanvas().toDataURL('image/png') : ''
+      let mapSnapshot = ''
+      if (map) {
+        try {
+          mapSnapshot = map.getCanvas().toDataURL('image/png')
+        } catch (error) {
+          console.warn('Map snapshot unavailable for report:', error)
+        }
+      }

5. **COG Applicability:** Determine if this location would benefit from Cloud Optimized GeoTIFF (COG) analysis for high-precision temporal or spectral data.
6. **News Integration:** Reference any recent news or events that may be relevant to the current state of the location.
7. **Structured Output:** Return your findings in a structured JSON format including summary, geoJson, and newsContext.
7. **Structured Output:** Return your findings in a structured JSON format including summary, geoJson (if any), news context, and any extracted coordinates or COG information. Use the provided schema.

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 | 🟠 Major | ⚡ Quick win

Use the exact schema field names in the prompt.

Line 130 asks for “news context”, but the schema only accepts hasRecentNews and newsItems. That mismatch makes the model more likely to emit the wrong key or bury the data in summary, which then gets lost at validation time. Please name the concrete fields the schema expects.

🤖 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/agents/resolution-search.tsx` at line 130, The prompt text in
lib/agents/resolution-search.tsx asks for "news context" which doesn't match the
schema; update the prompt (the string that contains "7. **Structured Output:**")
to use the exact schema field names: replace "news context" with the concrete
fields hasRecentNews and newsItems and instruct the model to populate those keys
(hasRecentNews: boolean, newsItems: array of objects) so the output aligns with
the expected schema and validation (locate the prompt string in the
resolution-search.tsx prompt builder / generatePrompt function and update it
accordingly).

Comment on lines 347 to +368
const toolArgs = (() => {
switch (queryType) {
case 'directions': return { waypoints: [params.origin, params.destination], includeMapPreview: includeMap, profile: params.mode };
case 'distance': return { places: [params.origin, params.destination], includeMapPreview: includeMap, mode: params.mode || 'driving' };
case 'reverse': return { searchText: `${params.coordinates.latitude},${params.coordinates.longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 };
case 'search': return { searchText: params.query, includeMapPreview: includeMap, maxResults: params.maxResults || 5, ...(params.coordinates && { proximity: `${params.coordinates.latitude},${params.coordinates.longitude}` }), ...(params.radius && { radius: params.radius }) };
case 'geocode':
case 'map': return { searchText: params.location, includeMapPreview: includeMap, maxResults: queryType === 'geocode' ? params.maxResults || 5 : undefined };
case 'directions': {
if (!params.origin || !params.destination) throw new Error("'directions' query requires origin and destination");
return { waypoints: [params.origin, params.destination], includeMapPreview: includeMap, profile: params.mode };
}
case 'distance': {
if (!params.origin || !params.destination) throw new Error("'distance' query requires origin and destination");
return { places: [params.origin, params.destination], includeMapPreview: includeMap, mode: params.mode || 'driving' };
}
case 'reverse': {
if (!params.coordinates) throw new Error("'reverse' query requires coordinates");
return { searchText: `${params.coordinates.latitude},${params.coordinates.longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 };
}
case 'search': {
if (!params.query) throw new Error("'search' query requires query");
return { searchText: params.query, includeMapPreview: includeMap, maxResults: params.maxResults || 5, ...(params.coordinates && { proximity: `${params.coordinates.latitude},${params.coordinates.longitude}` }), ...(params.radius && { radius: params.radius }) };
}
case 'geocode':
case 'map': {
if (!params.location) throw new Error(`'${queryType}' query requires location`);
return { searchText: params.location, includeMapPreview: includeMap, maxResults: queryType === 'geocode' ? params.maxResults || 5 : undefined };

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 | 🟠 Major | ⚡ Quick win

Validate params before opening the MCP connection.

These guards run after getConnectedMcpClient() and the listTools() probe. With the flatter schema, malformed inputs now still pay for an outbound connection and tool discovery before we reject them locally. Move the argument-building/validation step before Line 314 so bad requests short-circuit without external I/O.

Suggested direction
-    const mcpClient = await getConnectedMcpClient();
+    const toolArgs = buildToolArgs(params, includeMap);
+    const mcpClient = await getConnectedMcpClient();
     if (!mcpClient) {
       feedbackMessage = 'Geospatial functionality is unavailable. Please check configuration.';
       uiFeedbackStream.update(feedbackMessage);
       uiFeedbackStream.done();
       uiStream.update(<BotMessage content={uiFeedbackStream.value} />);
       return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), timestamp: new Date().toISOString(), mcp_response: null, error: 'MCP client initialization failed' };
     }
@@
-      const toolArgs = (() => {
-        switch (queryType) {
-          ...
-        }
-      })();
🤖 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/agents/tools/geospatial.tsx` around lines 347 - 368, The parameter
validation and toolArgs construction (switch on queryType that references
params, includeMap, etc.) must be executed before establishing the MCP
connection and calling getConnectedMcpClient() / listTools(); move the guard
logic that builds toolArgs (the switch block handling 'directions', 'distance',
'reverse', 'search', 'geocode', 'map') to run first, throw early on malformed
params (missing origin/destination, coordinates, query, or location), and only
after toolArgs is successfully created proceed to call getConnectedMcpClient()
and listTools() so invalid requests short-circuit without opening external
connections.

Comment thread lib/schema/geospatial.tsx
Comment on lines +42 to +45
mode: z.enum(['driving', 'walking', 'cycling', 'transit'])
.optional()
.default('driving')
.describe("Transportation mode (used by 'directions' and 'distance')"),

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does the Mapbox Directions API or Matrix API support a "transit" routing profile? What routing profiles are officially supported?

💡 Result:

No, neither the Mapbox Directions API nor the Matrix API supports a transit routing profile [1][2][3]. The Mapbox Directions and Matrix APIs officially support the following four routing profiles [1][2][3]: 1. mapbox/driving: For automotive routing, prioritizing the fastest routes [1][2][3]. 2. mapbox/driving-traffic: For automotive routing, factoring in current and historic traffic conditions [1][2][3]. 3. mapbox/walking: For pedestrian and hiking routing [1][2][3]. 4. mapbox/cycling: For bicycle routing, preferring paths with bike lanes and avoiding highways [1][2][3]. While Mapbox provides tools and data that can be used within transit applications—such as geocoding, base maps, and navigation SDKs for vehicle-based transit services—it does not offer a native API profile for multi-modal public transit journey planning [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the exact schema and tool mapping for "mode"
rg -n "mode:\s*z\.enum\(" lib/schema/geospatial.tsx || true
rg -n "'driving'|'walking'|'cycling'|'transit'" lib/schema/geospatial.tsx || true

rg -n "geospatial\.tsx" -S lib/agents/tools || true
rg -n "directions|distance|matrix|routing profile|routing_profile|profile" lib/agents/tools/geospatial.tsx || true

# Print the parts of the tool that mention mode/profile mapping
sed -n '1,220p' lib/agents/tools/geospatial.tsx | cat -n
sed -n '1,120p' lib/schema/geospatial.tsx | cat -n

Repository: QueueLab/QCX

Length of output: 14984


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the execute branch around where profile: params.mode is set
sed -n '300,420p' lib/agents/tools/geospatial.tsx | cat -n

# Show how "profile" is used in downstream request construction
rg -n "profile:\s*params\.mode|profile\W*:" lib/agents/tools/geospatial.tsx
rg -n "Directions|Matrix|matrix|directions" lib/agents/tools/geospatial.tsx

# If profile is transformed anywhere, capture those lines
rg -n "params\.mode|mode\s*===" lib/agents/tools/geospatial.tsx

Repository: QueueLab/QCX

Length of output: 8421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any mention of transit in the repo
rg -n "transit" lib || true

# Find any explicit Mapbox routing profile strings
rg -n "mapbox/(driving|driving-traffic|walking|cycling)" lib || true

# Find where "profile" or "mode" are translated/mapped for Mapbox requests (heuristic)
rg -n "profile:\s*|routing profile|routing_profile|driving-traffic|driving_traffic" lib/agents/tools lib/schema || true

Repository: QueueLab/QCX

Length of output: 554


Remove the unsupported transit routing mode from the geospatial schema.

lib/agents/tools/geospatial.tsx passes params.mode straight through to the downstream MCP/Mapbox calls (profile: params.mode for directions, mode: params.mode for distance), while the tool description only advertises driving, walking, and cycling. Mapbox Directions/Matrix routing profiles do not include transit, so allowing it in the schema can lead to runtime failures.

-  mode: z.enum(['driving', 'walking', 'cycling', 'transit'])
+  mode: z.enum(['driving', 'walking', 'cycling'])
🤖 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/schema/geospatial.tsx` around lines 42 - 45, The geospatial schema
currently allows an unsupported 'transit' routing mode which can be passed
through to Mapbox and cause runtime failures; update the zod enum in
lib/schema/geospatial.tsx (the field defined as mode:
z.enum([...]).optional().default('driving').describe(...)) to only include
'driving', 'walking', and 'cycling' (remove 'transit'), keep the default
'driving', and ensure any validation or docs tied to that schema reflect the
three supported modes; also verify callers in lib/agents/tools/geospatial.tsx
that pass params.mode (used as profile/mode) will only receive those three
values.

Comment on lines +12 to +24
geoJson: z.object({
type: z.string().describe("Must be 'FeatureCollection'"),
features: z.array(z.object({
type: z.string().describe("Must be 'Feature'"),
geometryType: z.string().describe("The type of geometry, e.g., 'Point', 'Polygon'"),
coordinates: z.array(z.number())
.or(z.array(z.array(z.number())))
.or(z.array(z.array(z.array(z.number()))))
.describe('Coordinates for the geometry'),
name: z.string().describe('Name of the feature or point of interest'),
description: z.string().optional().describe('Description of the feature')
}))
}).optional().describe('A collection of points of interest and classified land features to be overlaid on the map.'),

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 | 🟠 Major | ⚡ Quick win

Tighten the GeoJSON shape contract before it reaches the map layer.

This schema accepts any geometryType string, but coordinates only support up to Polygon nesting. That means invalid values like Circle can pass validation, while valid GeoJSON shapes like MultiPolygon are rejected. Since app/actions.tsx reconstructs and renders this payload as GeoJSON, one bad model output will either fail the structured response or reach GeoJsonLayer as invalid geometry.

🤖 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/schema/resolution-search.ts` around lines 12 - 24, The geoJson shape
currently lets any geometryType string and only supports up to Polygon
coordinate nesting; update the geoJson schema (the geoJson object in
lib/schema/resolution-search.ts) to validate known GeoJSON geometry types and
their matching coordinate structures: replace geometryType: z.string() with a
z.enum listing
"Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection","Feature","FeatureCollection"
and make coordinates a discriminated union (or use a z.discriminatedUnion keyed
on geometryType) that enforces 1D arrays for Point/MultiPoint, 2D for
LineString/MultiLineString, 3D for Polygon/MultiPolygon, and supports nested
arrays for Multi* types so valid MultiPolygon coordinates are accepted and
unsupported shapes like "Circle" are rejected; ensure this aligns with how
app/actions.tsx and GeoJsonLayer expect GeoJSON so rendered payloads match valid
geometry.

@ngoiyaeric ngoiyaeric closed this Jun 1, 2026
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