Skip to content

GeoJSON Upload and Tool Ingestion Pipeline#472

Open
ngoiyaeric wants to merge 48 commits intomainfrom
feature/geojson-upload-pipeline-11124993269366174034
Open

GeoJSON Upload and Tool Ingestion Pipeline#472
ngoiyaeric wants to merge 48 commits intomainfrom
feature/geojson-upload-pipeline-11124993269366174034

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Feb 1, 2026

User description

This change enables users to upload GeoJSON files (.geojson, application/geo+json) directly through the chat interface. Uploaded data is automatically rendered on the map (both Mapbox and Google Maps) and the viewport is adjusted to frame the data's extent using Turf.js.

The implementation includes:

  • A new uploadedGeoJson field in MapDataContext.
  • Server-side parsing and validation of GeoJSON in the submit action.
  • A headless MapDataUpdater component that manages state synchronization and "fly to" behavior.
  • Database persistence for GeoJSON layers, ensuring they reappear when a chat is reloaded.
  • Extension of the geospatialTool to allow AI agents to return GeoJSON that is processed through the same pipeline.
  • Improved testability with new data-testid attributes.

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


PR Type

Enhancement


Description

  • Enable GeoJSON file uploads (.geojson, application/geo+json) through chat interface

  • Automatically render uploaded GeoJSON on maps with viewport framing using Turf.js

  • Persist GeoJSON layers in database for chat session reload capability

  • Allow AI agents to return GeoJSON through geospatial tool pipeline

  • Add MapDataUpdater component for state synchronization and map navigation


Diagram Walkthrough

flowchart LR
  A["User uploads GeoJSON file"] --> B["Parse and validate GeoJSON"]
  B --> C["MapDataUpdater component"]
  C --> D["Update MapData context"]
  D --> E["Render on Mapbox/Google Maps"]
  E --> F["Fit map bounds with Turf.js"]
  F --> G["Persist to database"]
  H["AI tool returns GeoJSON"] --> I["MapQueryHandler processes result"]
  I --> C
Loading

File Walkthrough

Relevant files
Enhancement
10 files
chat.ts
Add uploadedGeoJson field to context update                           
+1/-1     
actions.tsx
Implement GeoJSON file parsing and upload pipeline             
+60/-1   
chat-panel.tsx
Add GeoJSON file type to upload input accept                         
+2/-1     
chat.tsx
Sync uploadedGeoJson to server via updateDrawingContext   
+6/-5     
map-data-context.tsx
Add uploadedGeoJson array to MapData interface                     
+11/-1   
map-data-updater.tsx
Create headless component for GeoJSON context sync             
+63/-0   
map-query-handler.tsx
Integrate MapDataUpdater for tool GeoJSON results               
+13/-1   
mapbox-map.tsx
Render uploaded GeoJSON layers on Mapbox                                 
+4/-0     
google-map.tsx
Render uploaded GeoJSON layers on Google Maps                       
+3/-0     
geospatial.tsx
Extract and pass GeoJSON from geospatial tool results       
+28/-3   
Tests
2 files
message.tsx
Add data-testid attribute to bot message                                 
+1/-1     
user-message.tsx
Add data-testid attribute to user message                               
+1/-1     

Summary by CodeRabbit

  • New Features

    • Upload or paste GeoJSON (files or inline) and have it added to chat as a data item and rendered as map layers.
    • Multiple uploaded GeoJSON items supported with visibility toggles; map auto-zooms to uploaded data.
    • GeoJSON returned by geospatial tools is automatically added to the map.
  • Bug Fixes

    • Improved detection and error handling for invalid GeoJSON inputs.
  • Tests

    • Added test IDs to file upload and message elements for easier automated testing.

CJWTRUST and others added 21 commits January 20, 2026 01:20
Restored the branch to the expected head at commit 488b47c.
This recovers several missing features and architectural improvements:
- Integrated HistoryToggleProvider and HistorySidebar.
- Integrated UsageToggleProvider and the new UsageView component.
- Removed legacy usage-sidebar.tsx.
- Restored pricing and feature updates in PurchaseCreditsPopup.
- Fixed layout structure in app/layout.tsx to support these new global state providers.

Resolved previous merge conflicts and incorrect force-push state.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Reset branch HEAD to historical recovery point 488b47c.
- Applied latest code fixes from orphan commit 166aee9, including Mapbox rendering optimizations and GeoJSON streaming.
- Improved type safety in app/actions.tsx by removing 'as any' casts and properly typing message arrays.
- Refined getModel utility to support vision-aware model fallback.
- Documented non-blocking background task pattern in server actions.
- Updated feature/billing-integration branch with the restored state.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Reset branch HEAD to historical recovery point 488b47c.
- Applied latest code fixes from orphan commit 166aee9.
- Resolved TypeScript build error in app/actions.tsx by casting GeoJSON data to FeatureCollection.
- Improved type safety for AIState message filtering and content handling.
- Implemented background processing for resolution search with immediate UI streaming.
- Optimized chat history fetching to trigger only when the sidebar is open.
- Ensured Mapbox style loading is robust against re-renders.
- Cleaned up dead code and improved documentation in server actions.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Reset branch HEAD to historical recovery point 488b47c.
- Applied latest code fixes from orphan commit 166aee9.
- Resolved TypeScript build error in app/actions.tsx by casting GeoJSON data to FeatureCollection.
- Improved type safety for AIState message filtering and content handling.
- Implemented background processing for resolution search with immediate UI streaming.
- Re-enabled auto-opening of the pricing popup in components/header.tsx.
- Optimized chat history fetching to trigger only when the sidebar is open.
- Ensured Mapbox style loading is robust against re-renders.
- Cleaned up dead code and improved documentation in server actions.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Restored historical state from 488b47c and merged latest changes.
- Implemented streaming for resolution search in lib/agents/resolution-search.tsx and app/actions.tsx to improve response time.
- Re-enabled auto-opening pricing popup in components/header.tsx.
- Resolved all TypeScript build errors in app/actions.tsx.
- Restored Grok model support for vision tasks in lib/utils/index.ts.
- Optimized chat history loading in components/sidebar/chat-history-client.tsx.
- Improved Mapbox style loading robustness in components/map/mapbox-map.tsx.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Synchronized branch with origin/main, prioritizing main's code structure and latest features.
- Bridged the history with historical recovery point 488b47c.
- Implemented streaming for resolution search in lib/agents/resolution-search.tsx and app/actions.tsx to resolve performance issues.
- Restored the auto-opening Pricing Popup and Usage View in the Header component.
- Integrated the Timezone Clock and time context into the restored resolution search logic.
- Resolved TypeScript build errors with proper type casting and fixed a missing 'use client' directive in components/history.tsx.
- Ensured all required providers (History, Usage, etc.) are correctly wrapped in the root layout.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Synchronized branch with origin/main, prioritizing main's code structure and latest features.
- Bridged the history with historical recovery point 488b47c.
- Integrated UsageView and billing UI into the Chat component, toggled by the tent tree icon.
- Implemented streaming for resolution search in lib/agents/resolution-search.tsx and app/actions.tsx for better performance.
- Restored the auto-opening Pricing Popup in the Header component.
- Improved type safety across server actions and Mapbox components.
- Ensured mutual exclusion between Settings, Usage, and Map views.
- Fixed a missing 'use client' directive in components/history.tsx.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Integrated UsageView into the main Chat component for both mobile and desktop.
- Ensured mutual exclusion between Settings, Usage, and Map views in the UI panel.
- Updated ConditionalLottie to hide the loading animation when the Usage View is open.
- Synchronized with origin/main while prioritizing its code structure.
- Maintained historical recovery and performance optimizations for resolution search.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Synchronized with origin/main, prioritizing main branch code and features.
- Fully restored historical context and missing changes from commit 488b47c.
- Integrated Usage and Billing UI (UsageView) into the Chat component.
- Implemented streaming for resolution search analysis to improve perceived performance.
- Re-enabled auto-opening pricing popup in components/header.tsx.
- Refined ConditionalLottie visibility to avoid overlaps with the Usage UI.
- Ensured mutual exclusion between Settings, Usage, and Map views in the side panel.
- Improved type safety across server actions and Mapbox components.
- Resolved build failures related to missing client directives and type mismatches.

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

- Recovered historical state from commit 488b47c and synchronized with main.
- Optimized resolution search by refactoring to stream partial summaries.
- Fixed Mapbox memory leaks by ensuring all event listeners are removed on cleanup.
- Improved StreamableValue stability by initializing with default values.
- Integrated Usage View with mutual exclusion logic and Lottie player visibility fixes.
- Refined model selection for Grok vision and Gemini 1.5 Pro.
- Integrated timezone-aware analysis using tz-lookup.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Fixed Type error in `app/actions.tsx` by casting `content` to `string` in `createStreamableValue`.
- Addressed ESLint warnings in `components/map/mapbox-map.tsx` and `components/chat-panel.tsx` by adding missing dependencies to `useEffect` and `useCallback` hooks.
- Ensured `relatedQueries` streamable value is initialized with an empty state for stability.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Reduced vertical padding in `.mobile-chat-input-area` in `globals.css`.
- Removed redundant `.mobile-chat-input` class and associated styles from `globals.css`.
- Adjusted `ChatPanel` to use more compact padding and a smaller minimum height on mobile.
- Removed excessive left padding (`pl-14`) on the mobile chat input since the attachment button is moved to the icons bar.
- Cleaned up unused mobile-specific CSS classes in `globals.css`.

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

Reduce padding in mobile chat input area
Replaced all occurrences of the old Stripe checkout link with the new URL:
https://buy.stripe.com/14A3cv7K72TR3go14Nasg02

Updated files:
- components/mobile-icons-bar.tsx
- components/header.tsx

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Extend MapData context to support uploaded GeoJSON layers
- Update ChatPanel to support GeoJSON file selection
- Implement MapDataUpdater component for automatic context sync and map framing
- Update Mapbox and Google Maps components to render uploaded GeoJSON
- Enable AI tools to ingest GeoJSON into the map pipeline via MapQueryHandler
- Ensure persistence of GeoJSON data across chat sessions via database sync
- Add test IDs to key components for improved observability

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

vercel bot commented Feb 1, 2026

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

Project Deployment Actions Updated (UTC)
qcx Ready Ready Preview, Comment Feb 3, 2026 11:26am

@google-labs-jules
Copy link
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.

@charliecreates charliecreates bot requested a review from CharlieHelps February 1, 2026 16:57
@CLAassistant
Copy link

CLAassistant commented Feb 1, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ ngoiyaeric
❌ google-labs-jules[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 1, 2026

Walkthrough

Detects inline or uploaded GeoJSON on submit, emits a geojson_upload AI message, streams a headless MapDataUpdater UI item to append entries to mapData.uploadedGeoJson, renders those entries as GeoJSON layers in Mapbox/Google maps, and persists uploadedGeoJson in the drawing-context message (deduplicated/upserted).

Changes

Cohort / File(s) Summary
File submit & AI state
app/actions.tsx, components/chat-panel.tsx
Detect/parses inline JSON and .geojson/application/geo+json uploads; emit geojson_upload AI messages and stream a MapDataUpdater UI message. Added data-testid="file-upload-input" on file input.
Chat sync / drawing context
components/chat.tsx, lib/actions/chat.ts
Include uploadedGeoJson in drawing-context payload; add snapshot/dedup guards to avoid redundant updates. updateDrawingContext signature now accepts optional uploadedGeoJson and will update existing data-type messages instead of always creating new ones.
Map data model & updater
components/map/map-data-context.tsx, components/map/map-data-updater.tsx
Add uploadedGeoJson array to MapData. New MapDataUpdater normalizes Feature/FeatureCollection, appends unique uploaded entries ({id, filename, data, visible}), and fits map bounds once using turf.bbox.
Map rendering layers
components/map/google-map.tsx, components/map/mapbox-map.tsx
Render additional GeoJSON layers for each visible item in mapData.uploadedGeoJson (GoogleGeoJsonLayer / GeoJsonLayer).
Tooling & parsing
lib/agents/tools/geospatial.tsx, components/map/map-query-handler.tsx
Surface geoJson from MCP/tool responses (added optional geoJson to McpResponse); render MapDataUpdater for tool-produced GeoJSON results.
DB schema & persistence
drizzle/migrations/..., lib/db/schema.ts, lib/actions/chat-db.ts
Add calendar_notes table and add attachments, tool_name, tool_call_id, type to messages (migration + schema). Message insert changed to upsert (on conflict do update) to deduplicate by id.
Types & test attributes
lib/types/index.ts, components/message.tsx, components/user-message.tsx
Add geojson_upload to AIMessage.type; add data-testid attributes to message components for testing.
Deps / snapshots
package.json, drizzle/migrations/meta/*
Bump drizzle-orm dependency and add/update migration snapshot and journal metadata files.

Sequence Diagram

sequenceDiagram
    actor User
    participant ChatPanel as Chat Panel
    participant Actions as app/actions
    participant AIState as AI State
    participant MapUpdater as MapDataUpdater
    participant MapContext as MapData Context
    participant MapRenderer as Map Components

    User->>ChatPanel: paste or upload GeoJSON
    ChatPanel->>Actions: submit(text/file)
    Actions->>Actions: detect & parse GeoJSON
    Actions->>AIState: push 'geojson_upload' message (id, filename, data)
    Actions->>MapUpdater: stream MapDataUpdater component
    MapUpdater->>MapContext: add uploadedGeoJson entry {id, filename, data, visible:true}
    MapUpdater->>MapUpdater: compute bbox (turf.bbox)
    MapUpdater->>MapRenderer: call fitBounds(bbox)
    MapRenderer->>User: render GeoJsonLayer(s)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Backend

Poem

🐇 I found a GeoJSON neat and round,

I hopped its features into town,
Layers bloomed where bounds took flight,
The map stretched wide — what a sight,
Cheers from a rabbit, lightly bound!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'GeoJSON Upload and Tool Ingestion Pipeline' accurately reflects the main objective of the PR, which enables end-to-end GeoJSON ingestion through the chat interface and AI tool results.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/geojson-upload-pipeline-11124993269366174034

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


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.

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 1, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Denial of service

Description: Unbounded GeoJSON ingestion (Buffer -> JSON.parse) on user-uploaded files without
enforcing file size/feature-count limits can be exploited to cause server memory/CPU
exhaustion (e.g., uploading a very large .geojson or deeply nested JSON).
actions.tsx [319-350]

Referred Code
} else if (file.type === 'text/plain' || file.name.endsWith('.geojson') || file.type === 'application/geo+json') {
  const textContent = Buffer.from(buffer).toString('utf-8')
  const isGeoJson = file.name.endsWith('.geojson') || file.type === 'application/geo+json'

  if (isGeoJson) {
    try {
      const geoJson = JSON.parse(textContent)
      if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') {
        const geoJsonId = nanoid()
        // Add a special message to track the GeoJSON upload
        aiState.update({
          ...aiState.get(),
          messages: [
            ...aiState.get().messages,
            {
              id: geoJsonId,
              role: 'assistant',
              content: JSON.stringify({ data: geoJson, filename: file.name }),
              type: 'geojson_upload'
            }
          ]


 ... (clipped 11 lines)
Client-side DoS

Description: Rendering and processing untrusted GeoJSON on the client (notably
turf.bbox(featureCollection) and map.fitBounds) without complexity limits can freeze the
UI or crash the tab when given extremely large/complex GeoJSON from uploads or tool
output.
map-data-updater.tsx [19-60]

Referred Code
useEffect(() => {
  if (!data) return;

  // Ensure it's a FeatureCollection for consistency
  const featureCollection: FeatureCollection = data.type === 'FeatureCollection'
    ? data
    : { type: 'FeatureCollection', features: [data] };

  // Update MapData context
  setMapData(prev => {
    // Avoid duplicate entries
    const alreadyExists = prev.uploadedGeoJson?.some(item => item.id === id);
    if (alreadyExists) return prev;

    return {
      ...prev,
      uploadedGeoJson: [
        ...(prev.uploadedGeoJson || []),
        {
          id,
          filename,


 ... (clipped 21 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: The GeoJSON upload/ingestion path adds and persists user-supplied GeoJSON but does not
emit a structured audit log including user ID, action description, and outcome to
reconstruct who uploaded what and whether it succeeded.

Referred Code
} else if (file.type === 'text/plain' || file.name.endsWith('.geojson') || file.type === 'application/geo+json') {
  const textContent = Buffer.from(buffer).toString('utf-8')
  const isGeoJson = file.name.endsWith('.geojson') || file.type === 'application/geo+json'

  if (isGeoJson) {
    try {
      const geoJson = JSON.parse(textContent)
      if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') {
        const geoJsonId = nanoid()
        // Add a special message to track the GeoJSON upload
        aiState.update({
          ...aiState.get(),
          messages: [
            ...aiState.get().messages,
            {
              id: geoJsonId,
              role: 'assistant',
              content: JSON.stringify({ data: geoJson, filename: file.name }),
              type: 'geojson_upload'
            }
          ]


 ... (clipped 11 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent parse failure: GeoJSON parsing failures are only logged via console.error and the request continues
without surfacing an actionable error to the user or recording a failure outcome, making
troubleshooting and UX degraded.

Referred Code
if (isGeoJson) {
  try {
    const geoJson = JSON.parse(textContent)
    if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') {
      const geoJsonId = nanoid()
      // Add a special message to track the GeoJSON upload
      aiState.update({
        ...aiState.get(),
        messages: [
          ...aiState.get().messages,
          {
            id: geoJsonId,
            role: 'assistant',
            content: JSON.stringify({ data: geoJson, filename: file.name }),
            type: 'geojson_upload'
          }
        ]
      })

      // Immediately append the updater to the UI stream
      uiStream.append(


 ... (clipped 6 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Logs raw tool data: The tool logs potentially sensitive unredacted content and echoes params via
JSON.stringify(params) which may include user-provided locations/addresses or other
sensitive details in unstructured console logs.

Referred Code
try { content = JSON.parse(content); }
catch { console.warn('[GeospatialTool] Content is not JSON, using as string:', content); }

// Process results
if (typeof content === 'object' && content !== null) {
  const parsedData = content as any;
  if (parsedData.results?.length > 0) {
    const firstResult = parsedData.results[0];
    mcpData = {
      location: {
        latitude: firstResult.coordinates?.latitude,
        longitude: firstResult.coordinates?.longitude,
        place_name: firstResult.name || firstResult.place_name,
        address: firstResult.full_address || firstResult.address
      },
      mapUrl: parsedData.mapUrl,
      geoJson: parsedData.geoJson || parsedData.geojson || firstResult.geoJson || firstResult.geojson
    };
  } else if (parsedData.location) {
    mcpData = {
      location: {


 ... (clipped 20 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Weak GeoJSON validation: Uploaded GeoJSON is accepted after only a shallow type check (Feature/FeatureCollection)
without schema validation, size/feature-count limits, or sanitization, increasing risk of
malformed/hostile input affecting storage and rendering.

Referred Code
} else if (file.type === 'text/plain' || file.name.endsWith('.geojson') || file.type === 'application/geo+json') {
  const textContent = Buffer.from(buffer).toString('utf-8')
  const isGeoJson = file.name.endsWith('.geojson') || file.type === 'application/geo+json'

  if (isGeoJson) {
    try {
      const geoJson = JSON.parse(textContent)
      if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') {
        const geoJsonId = nanoid()
        // Add a special message to track the GeoJSON upload
        aiState.update({
          ...aiState.get(),
          messages: [
            ...aiState.get().messages,
            {
              id: geoJsonId,
              role: 'assistant',
              content: JSON.stringify({ data: geoJson, filename: file.name }),
              type: 'geojson_upload'
            }
          ]


 ... (clipped 7 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Overuse of any: New GeoJSON pipeline types use broad any (e.g., data: any, uploadedGeoJson entries with
data: any) which reduces self-documentation and makes intent and constraints unclear.

Referred Code
interface MapDataUpdaterProps {
  id: string;
  data: any; // FeatureCollection or Feature
  filename: string;
}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 1, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent repeated map view updates
Suggestion Impact:The commit adds a hasZoomedRef guard so map.fitBounds() runs only once, preventing repeated map view updates, though it uses a ref-based approach instead of checking existing GeoJSON in context.

code diff:

-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
 import { useMapData } from './map-data-context';
 import { useMap } from './map-context';
 import type { FeatureCollection } from 'geojson';
@@ -15,6 +15,7 @@
 export function MapDataUpdater({ id, data, filename }: MapDataUpdaterProps) {
   const { setMapData } = useMapData();
   const { map } = useMap();
+  const hasZoomedRef = useRef(false);
 
   useEffect(() => {
     if (!data) return;
@@ -45,7 +46,8 @@
     });
 
     // Fly to the extent of the GeoJSON
-    if (map && featureCollection.features.length > 0) {
+    if (map && featureCollection.features.length > 0 && !hasZoomedRef.current) {
+      hasZoomedRef.current = true;
       try {
         const bbox = turf.bbox(featureCollection);
         map.fitBounds(bbox as [number, number, number, number], {

In MapDataUpdater, prevent the useEffect from repeatedly calling map.fitBounds()
by checking if the GeoJSON data has already been processed and exiting early.

components/map/map-data-updater.tsx [15-63]

 export function MapDataUpdater({ id, data, filename }: MapDataUpdaterProps) {
-  const { setMapData } = useMapData();
+  const { mapData, setMapData } = useMapData();
   const { map } = useMap();
 
   useEffect(() => {
     if (!data) return;
+
+    // Prevent re-processing if the data is already in the context.
+    const alreadyExists = mapData.uploadedGeoJson?.some(item => item.id === id);
+    if (alreadyExists) {
+      return;
+    }
 
     // Ensure it's a FeatureCollection for consistency
     const featureCollection: FeatureCollection = data.type === 'FeatureCollection'
       ? data
       : { type: 'FeatureCollection', features: [data] };
 
     // Update MapData context
-    setMapData(prev => {
-      // Avoid duplicate entries
-      const alreadyExists = prev.uploadedGeoJson?.some(item => item.id === id);
-      if (alreadyExists) return prev;
+    setMapData(prev => ({
+      ...prev,
+      uploadedGeoJson: [
+        ...(prev.uploadedGeoJson || []),
+        {
+          id,
+          filename,
+          data: featureCollection,
+          visible: true
+        }
+      ]
+    }));
 
-      return {
-        ...prev,
-        uploadedGeoJson: [
-          ...(prev.uploadedGeoJson || []),
-          {
-            id,
-            filename,
-            data: featureCollection,
-            visible: true
-          }
-        ]
-      };
-    });
-
-    // Fly to the extent of the GeoJSON
+    // Fly to the extent of the GeoJSON only once
     if (map && featureCollection.features.length > 0) {
       try {
         const bbox = turf.bbox(featureCollection);
         map.fitBounds(bbox as [number, number, number, number], {
           padding: 50,
           maxZoom: 15,
           duration: 2000
         });
       } catch (e) {
         console.error('Failed to fit bounds for GeoJSON:', e);
       }
     }
-  }, [id, data, filename, setMapData, map]);
+  }, [id, data, filename, setMapData, map, mapData.uploadedGeoJson]);
 
   return null; // Headless component
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that map.fitBounds() is called on every render, causing a poor user experience, and provides a correct fix to run the effect only once per GeoJSON upload.

Medium
Safely access nested response properties

In lib/agents/tools/geospatial.tsx, use optional chaining (?.) instead of
logical OR (||) to safely access nested geoJson properties from the API
response, preventing potential runtime errors.

lib/agents/tools/geospatial.tsx [393]

-geoJson: parsedData.geoJson || parsedData.geojson || firstResult.geoJson || firstResult.geojson
+geoJson: parsedData.geoJson ?? parsedData.geojson ?? firstResult?.geoJson ?? firstResult?.geojson
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly proposes using optional chaining (?.) for safer property access, which improves code robustness against potentially missing nested properties in the API response.

Low
General
Provide feedback for invalid uploads

In app/actions.tsx, provide user feedback for invalid GeoJSON uploads by
appending a BotMessage to the uiStream within the catch block and for files that
are not valid GeoJSON features.

app/actions.tsx [323-350]

 if (isGeoJson) {
   try {
     const geoJson = JSON.parse(textContent)
     if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') {
       const geoJsonId = nanoid()
       // Add a special message to track the GeoJSON upload
       aiState.update({
         ...aiState.get(),
         messages: [
           ...aiState.get().messages,
           {
             id: geoJsonId,
             role: 'assistant',
             content: JSON.stringify({ data: geoJson, filename: file.name }),
             type: 'geojson_upload'
           }
         ]
       })
 
       // Immediately append the updater to the UI stream
       uiStream.append(
         <MapDataUpdater id={geoJsonId} data={geoJson} filename={file.name} />
       )
+    } else {
+      // Handle cases where JSON is valid but not a GeoJSON Feature or FeatureCollection
+      uiStream.append(<BotMessage content="The uploaded file is not a valid GeoJSON Feature or FeatureCollection." />)
     }
   } catch (e) {
     console.error('Failed to parse GeoJSON:', e)
+    uiStream.append(<BotMessage content="The uploaded file could not be parsed as valid GeoJSON." />)
   }
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion improves user experience by providing feedback on invalid file uploads, which is a crucial part of error handling for this new feature.

Medium
Use case-insensitive extension check

In app/actions.tsx, make the GeoJSON file detection case-insensitive by
converting file.name to lowercase before checking if it ends with .geojson.

app/actions.tsx [319-321]

-} else if (file.type === 'text/plain' || file.name.endsWith('.geojson') || file.type === 'application/geo+json') {
+} else if (
+  file.type === 'text/plain' ||
+  file.name.toLowerCase().endsWith('.geojson') ||
+  file.type === 'application/geo+json'
+) {
   const textContent = Buffer.from(buffer).toString('utf-8')
-  const isGeoJson = file.name.endsWith('.geojson') || file.type === 'application/geo+json'
+  const isGeoJson =
+    file.name.toLowerCase().endsWith('.geojson') ||
+    file.type === 'application/geo+json'
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion makes the file type detection more robust by handling case-insensitive file extensions, which is a good practice for file handling.

Low
Filter items before rendering

In GoogleMapComponent, refactor the rendering of uploadedGeoJson to use
.filter(item => item.visible) before .map() to avoid returning boolean values in
the render array.

components/map/google-map.tsx [84-86]

-{mapData.uploadedGeoJson?.map(item => (
-  item.visible && <GoogleGeoJsonLayer key={item.id} data={item.data} />
-))}
+{mapData.uploadedGeoJson
+  ?.filter(item => item.visible)
+  .map(item => (
+    <GoogleGeoJsonLayer key={item.id} data={item.data} />
+  ))}
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion improves code style and robustness by using .filter() before .map(), which is a clearer and safer pattern for conditional rendering in React than using &&.

Low
  • Update

Copy link
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: 5

Caution

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

⚠️ Outside diff range comments (1)
app/actions.tsx (1)

879-886: ⚠️ Potential issue | 🟡 Minor

Add explicit return after the outer switch to handle unmapped type combinations.

The case 'user' and case 'assistant' branches contain inner switches without default cases. When a message type doesn't match any inner switch case, execution falls through the outer switch's break statement and returns undefined instead of a value. The subsequent .filter(message => message !== null) will not remove these undefined entries.

🛠️ Proposed fix
     .map((message, index) => {
       const { role, content, id, type, name } = message
       ...
       switch (role) {
         ...
       }
+      return null
     })
-    .filter(message => message !== null) as UIState
+    .filter((message): message is UIState[number] => message != null)
🤖 Fix all issues with AI agents
In `@app/search/`[id]/page.tsx:
- Around line 51-55: The mapping of DB records to AIMessage in page.tsx
currently casts nullable columns directly (dbMsg.type, dbMsg.toolName) which can
leave nulls where AIMessage expects undefined; update the mapping in the
function that constructs the AIMessage object (the block assigning
role/content/createdAt/type/name) to convert nullable values to undefined (e.g.,
use null-coalescing like dbMsg.type ?? undefined and dbMsg.toolName ?? undefined
or conditional expressions) so type and name are undefined when the DB returns
null.

In `@drizzle/migrations/0001_aromatic_ultimatum.sql`:
- Around line 1-12: This migration uses gen_random_uuid() in the calendar_notes
table but doesn't enable the pgcrypto extension; add a CREATE EXTENSION IF NOT
EXISTS "pgcrypto"; statement at the top of the first migration (before the
CREATE TABLE "calendar_notes") so gen_random_uuid() is available when the
migration runs.

In `@lib/actions/chat-db.ts`:
- Around line 123-136: The upsert currently uses
tx.insert(messages).values(messagesToInsert).onConflictDoUpdate targeting
messages.id which can overwrite rows across chats/users; modify the
onConflictDoUpdate call to include a setWhere clause that restricts updates to
rows where messages.chat_id = excluded.chat_id (or messages.chatId =
excluded.chatId) and messages.user_id = excluded.user_id (or messages.userId =
excluded.userId) so updates only apply when the incoming row matches the same
chatId and userId as the existing row; keep the same set mapping (content, role,
type, tool_name, tool_call_id, attachments) but add the setWhere guard to the
onConflictDoUpdate invocation on tx.insert/messages/messagesToInsert.

In `@lib/actions/chat.ts`:
- Around line 188-205: The current update branch in save/update logic uses
dbGetMessagesByChatId and existingMessages.find(...) which returns the first
(oldest) 'data' message and can leave the UI showing the last 'data' entry
out-of-sync with getUIStateFromAIState; change the selection to target the most
recent 'data' message (e.g., pick the last matching element by timestamp or id)
or iterate and update all messages with role === 'data' so the DB update against
messages (used in the db.update(...) block) updates the latest/each 'data'
record rather than the first one found; update references:
dbGetMessagesByChatId, existingMessages, dataMessage, and the db.update(...)
call accordingly.

In `@lib/db/schema.ts`:
- Around line 33-36: The attachments column is declared with
jsonb('attachments') which yields an untyped any; define and use a concrete type
for the JSON shape (e.g., Attachment or Attachment[] depending on expected
payload) and pass it into jsonb to lock down typings (e.g.,
jsonb<Attachment[]>('attachments')), or if null is not meaningful set a typed
default value instead; update the schema declaration that references attachments
and any related uses of toolName/toolCallId/type to use the new Attachment type
for downstream safety and remove unnecessary null checks.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfe62f9 and 9450b15.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • app/actions.tsx
  • app/search/[id]/page.tsx
  • components/chat.tsx
  • components/map/map-data-updater.tsx
  • drizzle/migrations/0001_aromatic_ultimatum.sql
  • drizzle/migrations/meta/0000_snapshot.json
  • drizzle/migrations/meta/0001_snapshot.json
  • drizzle/migrations/meta/_journal.json
  • lib/actions/chat-db.ts
  • lib/actions/chat.ts
  • lib/db/schema.ts
  • package.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-13T13:26:30.086Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.086Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.

Applied to files:

  • app/actions.tsx
🧬 Code graph analysis (5)
lib/actions/chat-db.ts (1)
lib/db/schema.ts (1)
  • messages (26-37)
lib/db/schema.ts (1)
lib/types/index.ts (1)
  • Chat (49-57)
app/search/[id]/page.tsx (1)
lib/types/index.ts (1)
  • AIMessage (59-78)
components/chat.tsx (3)
components/map/map-data-context.tsx (2)
  • MapData (15-32)
  • mapData (41-49)
lib/actions/chat.ts (1)
  • updateDrawingContext (165-222)
components/map/mapbox-map.tsx (3)
  • prevData (166-166)
  • prevData (330-339)
  • feature (78-164)
components/map/map-data-updater.tsx (2)
components/map/map-data-context.tsx (4)
  • useMapData (61-67)
  • mapData (41-49)
  • MapData (15-32)
  • MapDataContextType (34-37)
components/map/map-context.tsx (1)
  • useMap (24-30)
🪛 Biome (2.3.13)
app/actions.tsx

[error] 698-698: This callback passed to map() iterable method should always return a value.

Add missing return statements so that this callback returns a value on all execution paths.

(lint/suspicious/useIterableCallbackReturn)

🔇 Additional comments (10)
drizzle/migrations/meta/0000_snapshot.json (1)

1-178: Snapshot metadata update looks consistent.

drizzle/migrations/meta/0001_snapshot.json (1)

1-305: Snapshot looks consistent with the updated schema.

package.json (1)

65-65: No action required—drizzle-orm 0.45.1 has no breaking changes.

drizzle-orm 0.45.1 (released December 10) contains only a single bug fix for pg-native Pool detection and has no breaking changes to migrations or query APIs. The current drizzle-kit 0.31.1 is compatible with this version; ideally, both should be upgraded together, but this is an optional alignment—not a blocker.

drizzle/migrations/meta/_journal.json (1)

11-18: Journal entry addition looks consistent.

app/actions.tsx (3)

26-26: Import wiring is fine.


693-703: Filtering to the latest data message makes restoration deterministic.


859-878: Data-message GeoJSON reconstruction looks good.

components/chat.tsx (3)

75-88: Refresh guard looks solid.

The last-message ID guard should prevent repeated refresh cycles on the same response.


96-115: Snapshot-based map sync reduces redundant updates.


120-174: Layout-only wrapper changes.

Also applies to: 180-238

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +51 to +55
role: dbMsg.role as AIMessage['role'],
content: dbMsg.content,
createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined,
// 'type' and 'name' are not in the basic Drizzle 'messages' schema.
// These would be undefined unless specific logic is added to derive them.
// For instance, if a message with role 'tool' should have a 'name',
// or if some messages have a specific 'type' based on content or other flags.
// This mapping assumes standard user/assistant messages primarily.
type: dbMsg.type as AIMessage['type'],
name: dbMsg.toolName as string,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd app/search && find . -type f -name "page.tsx" | head -5

Repository: QueueLab/QCX

Length of output: 71


🏁 Script executed:

cd app && find . -type f -name "*.tsx" -o -name "*.ts" | grep -E "(page|types|message)" | head -20

Repository: QueueLab/QCX

Length of output: 89


🏁 Script executed:

cat -n app/search/\[id\]/page.tsx | sed -n '40,65p'

Repository: QueueLab/QCX

Length of output: 1170


🏁 Script executed:

# Find AIMessage type definition
rg -n "type AIMessage|interface AIMessage" --type=ts --type=tsx

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Check dbMsg source and context
rg -n "dbMsg" app/search/\[id\]/page.tsx -B 5 -A 2

Repository: QueueLab/QCX

Length of output: 607


🏁 Script executed:

rg -n "type AIMessage|interface AIMessage" -A 10

Repository: QueueLab/QCX

Length of output: 1274


🏁 Script executed:

rg -n "type DrizzleMessage|interface DrizzleMessage" -A 10

Repository: QueueLab/QCX

Length of output: 38


🏁 Script executed:

# Search for where these types are defined
fd -e ts -e tsx | xargs rg "export (type|interface) AIMessage"

Repository: QueueLab/QCX

Length of output: 100


🏁 Script executed:

fd -e ts -e tsx | xargs rg "export (type|interface) DrizzleMessage"

Repository: QueueLab/QCX

Length of output: 40


🏁 Script executed:

# Check for downstream usages of message.name and message.type
rg "\.name\s*\?" -t=ts -t=tsx | head -20

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Look for optional chaining or null checks on message fields
rg "message\.(name|type|toolName)" | head -30

Repository: QueueLab/QCX

Length of output: 501


🏁 Script executed:

# Find DrizzleMessage or Message type from Drizzle schema
rg -n "export type Message|export type DrizzleMessage" -A 15

Repository: QueueLab/QCX

Length of output: 1189


🏁 Script executed:

# Check the database schema or Drizzle definitions
fd -e ts -e tsx | xargs rg "messages.*table\|export const messages"

Repository: QueueLab/QCX

Length of output: 40


🏁 Script executed:

# Look at the Drizzle schema file
find . -name "*schema*" -o -name "*drizzle*" | head -10

Repository: QueueLab/QCX

Length of output: 549


🏁 Script executed:

# Check db directory structure
ls -la db/ 2>/dev/null || ls -la lib/db/ 2>/dev/null || find . -type d -name "db" | head -3

Repository: QueueLab/QCX

Length of output: 349


🏁 Script executed:

cat -n lib/db/schema.ts

Repository: QueueLab/QCX

Length of output: 4470


🏁 Script executed:

# Check the actual schema definition for the messages table
cat -n lib/schema/messages.ts 2>/dev/null || cat -n lib/db/schema.ts | grep -A 30 "messages"

Repository: QueueLab/QCX

Length of output: 3827


🏁 Script executed:

# Check the AIMessage type definition more completely
cat -n lib/types/index.ts | sed -n '59,75p'

Repository: QueueLab/QCX

Length of output: 614


🏁 Script executed:

# Check if there are any null checks on message.type or message.name in the codebase
rg "message\.(type|name)\s*\?" -A 2 -B 2

Repository: QueueLab/QCX

Length of output: 38


🏁 Script executed:

# Check how the initialMessages are used downstream
rg "initialMessages" app/search/\[id\]/page.tsx -A 5 -B 5

Repository: QueueLab/QCX

Length of output: 836


🏁 Script executed:

# Check how the type and name fields are accessed in downstream code
rg "\.type\s*==|\.type\s*!=|\.type\s*\?|\.name\s*==|\.name\s*!=" app/ -A 2 -B 2

Repository: QueueLab/QCX

Length of output: 3401


🏁 Script executed:

# Look at the AI component to see how it uses these fields
rg -n "initialAIState" app/ -A 10 | head -50

Repository: QueueLab/QCX

Length of output: 1910


🏁 Script executed:

# Check if there are assertions or strict equality checks on type/name
rg "(type|name)\s*!==" app/ -B 2 -A 2

Repository: QueueLab/QCX

Length of output: 981


Convert nullable database fields to undefined when mapping to AIMessage.

Lines 54–55 cast dbMsg.type and dbMsg.toolName without converting null values to undefined. Since the database schema defines these columns as nullable (no .notNull() constraint) and the AIMessage type expects undefined for optional fields, assign null-coalesced values to maintain type consistency.

Recommended fix
-      type: dbMsg.type as AIMessage['type'],
-      name: dbMsg.toolName as string,
+      type: dbMsg.type ?? undefined,
+      name: dbMsg.toolName ?? undefined,
📝 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
role: dbMsg.role as AIMessage['role'],
content: dbMsg.content,
createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined,
// 'type' and 'name' are not in the basic Drizzle 'messages' schema.
// These would be undefined unless specific logic is added to derive them.
// For instance, if a message with role 'tool' should have a 'name',
// or if some messages have a specific 'type' based on content or other flags.
// This mapping assumes standard user/assistant messages primarily.
type: dbMsg.type as AIMessage['type'],
name: dbMsg.toolName as string,
role: dbMsg.role as AIMessage['role'],
content: dbMsg.content,
createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined,
type: dbMsg.type ?? undefined,
name: dbMsg.toolName ?? undefined,
🤖 Prompt for AI Agents
In `@app/search/`[id]/page.tsx around lines 51 - 55, The mapping of DB records to
AIMessage in page.tsx currently casts nullable columns directly (dbMsg.type,
dbMsg.toolName) which can leave nulls where AIMessage expects undefined; update
the mapping in the function that constructs the AIMessage object (the block
assigning role/content/createdAt/type/name) to convert nullable values to
undefined (e.g., use null-coalescing like dbMsg.type ?? undefined and
dbMsg.toolName ?? undefined or conditional expressions) so type and name are
undefined when the DB returns null.

Comment on lines +1 to +12
CREATE TABLE "calendar_notes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"chat_id" uuid,
"date" timestamp with time zone NOT NULL,
"content" text NOT NULL,
"location_tags" jsonb,
"user_tags" text[],
"map_feature_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "CREATE EXTENSION.*pgcrypto|gen_random_uuid" drizzle/migrations

Repository: QueueLab/QCX

Length of output: 1209


🏁 Script executed:

rg -n "CREATE EXTENSION" drizzle/

Repository: QueueLab/QCX

Length of output: 38


🏁 Script executed:

head -20 drizzle/migrations/0000_sweet_metal_master.sql

Repository: QueueLab/QCX

Length of output: 763


🏁 Script executed:

find . -maxdepth 2 -type f \( -name "README*" -o -name "*.md" -o -name "init*" -o -name "setup*" \) | head -20

Repository: QueueLab/QCX

Length of output: 356


🏁 Script executed:

rg -i "pgcrypto|database.*init|extension" README.md ./docs/ ./CLOUD_BUILD_SETUP.md 2>/dev/null | head -20

Repository: QueueLab/QCX

Length of output: 38


Ensure pgcrypto is enabled before using gen_random_uuid().

gen_random_uuid() requires the pgcrypto extension; it is not created in any migration file. This migration will fail unless pgcrypto is already enabled in the database. Add CREATE EXTENSION IF NOT EXISTS "pgcrypto"; at the top of the first migration, or confirm it exists in your database setup.

🛠️ Proposed fix
+CREATE EXTENSION IF NOT EXISTS "pgcrypto";
+
 CREATE TABLE "calendar_notes" (
📝 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
CREATE TABLE "calendar_notes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"chat_id" uuid,
"date" timestamp with time zone NOT NULL,
"content" text NOT NULL,
"location_tags" jsonb,
"user_tags" text[],
"map_feature_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TABLE "calendar_notes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"chat_id" uuid,
"date" timestamp with time zone NOT NULL,
"content" text NOT NULL,
"location_tags" jsonb,
"user_tags" text[],
"map_feature_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
🤖 Prompt for AI Agents
In `@drizzle/migrations/0001_aromatic_ultimatum.sql` around lines 1 - 12, This
migration uses gen_random_uuid() in the calendar_notes table but doesn't enable
the pgcrypto extension; add a CREATE EXTENSION IF NOT EXISTS "pgcrypto";
statement at the top of the first migration (before the CREATE TABLE
"calendar_notes") so gen_random_uuid() is available when the migration runs.

Comment on lines +123 to +136
// Use upsert to avoid duplicate primary keys and prevent redundant appends
await tx.insert(messages)
.values(messagesToInsert)
.onConflictDoUpdate({
target: messages.id,
set: {
content: sql`excluded.content`,
role: sql`excluded.role`,
type: sql`excluded.type`,
toolName: sql`excluded.tool_name`,
toolCallId: sql`excluded.tool_call_id`,
attachments: sql`excluded.attachments`
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine the chat-db.ts file
find . -name "chat-db.ts" -type f

Repository: QueueLab/QCX

Length of output: 80


🏁 Script executed:

# Read the file to see the full context around lines 123-136
cat -n lib/actions/chat-db.ts | sed -n '100,150p'

Repository: QueueLab/QCX

Length of output: 2263


🏁 Script executed:

# Check the messages table schema definition
fd -e ts -e js | xargs grep -l "messages.*Table\|export.*messages" | head -5

Repository: QueueLab/QCX

Length of output: 95


🏁 Script executed:

# Examine the messages table schema
cat -n lib/db/schema.ts | head -150

Repository: QueueLab/QCX

Length of output: 4470


🏁 Script executed:

# Search specifically for the messages table definition
grep -n "messages.*=" lib/db/schema.ts | head -10

Repository: QueueLab/QCX

Length of output: 176


🏁 Script executed:

# Check if onConflictDoUpdate with where clause is used elsewhere in the codebase
rg "onConflictDoUpdate" -A 5 lib/

Repository: QueueLab/QCX

Length of output: 391


🏁 Script executed:

# Check Drizzle ORM version in package.json to understand API capabilities
cat package.json | grep -A 2 -B 2 "drizzle"

Repository: QueueLab/QCX

Length of output: 230


🌐 Web query:

Drizzle ORM onConflictDoUpdate where clause support version 0.45

💡 Result:

In Drizzle ORM v0.45.x, .onConflictDoUpdate() does support WHERE clauses, but not via a single where field. Instead you use:

  • targetWhere: adds WHERE ... to the conflict target (for partial indexes / ON CONFLICT (...) WHERE ...)
  • setWhere: adds WHERE ... to the DO UPDATE part (... DO UPDATE SET ... WHERE ...)

Example:

await db.insert(employees).values({...}).onConflictDoUpdate({
  target: employees.employeeId,
  targetWhere: sql`name <> 'John Doe'`,
  set: { name: sql`excluded.name` },
});

await db.insert(employees).values({...}).onConflictDoUpdate({
  target: employees.employeeId,
  set: { name: 'John Doe' },
  setWhere: sql`name <> 'John Doe'`,
});

This split (wheretargetWhere + setWhere) was introduced earlier (v0.30.8 for Postgres; v0.30.9 for SQLite), so it’s available in v0.45.0 as well. [1] [2]

Notes:

  • This applies to PostgreSQL and SQLite upserts (ON CONFLICT ...). [1]
  • MySQL uses .onDuplicateKeyUpdate(...) (and doesn’t have the same ON CONFLICT ... WHERE ... structure). [1]

Sources:
[1] Drizzle ORM Insert docs (where clauses, targetWhere/setWhere)
[2] Drizzle ORM v0.30.8 release note (split into setWhere and targetWhere)


🏁 Script executed:

# Check if there are any imports of `and`, `eq` functions from drizzle-orm in the file
grep -n "import.*from.*drizzle" lib/actions/chat-db.ts

Repository: QueueLab/QCX

Length of output: 172


🌐 Web query:

Drizzle ORM setWhere syntax and condition with eq and chatId userId

💡 Result:

In Drizzle ORM:

1) Normal UPDATE / SELECT / DELETE: use .where(...) (not setWhere)

import { and, eq } from "drizzle-orm";

await db
  .update(chatMembers)
  .set({ lastReadAt: new Date() })
  .where(and(
    eq(chatMembers.chatId, chatId),
    eq(chatMembers.userId, userId),
  ));

(.where(...) takes a single condition; combine multiple with and(...).) (orm.drizzle.team)

2) INSERT ... onConflictDoUpdate(...): setWhere is for the DO UPDATE’s WHERE

import { and, eq } from "drizzle-orm";

await db.insert(chatMembers).values({ chatId, userId, ... })
  .onConflictDoUpdate({
    target: [chatMembers.chatId, chatMembers.userId],
    set: { lastReadAt: new Date() },
    setWhere: and(
      eq(chatMembers.chatId, chatId),
      eq(chatMembers.userId, userId),
    ),
  });

setWhere / targetWhere are specific to onConflictDoUpdate. (orm.drizzle.team)

Citations:


Add setWhere guard to prevent cross-chat/user overwrites in upsert.

The upsert targets messages.id alone without guarding against conflicts. If an id collision occurs (or a malicious id is supplied), this can overwrite a message belonging to another chat or user. Add a setWhere clause that restricts updates to rows matching the same chatId and userId being inserted.

🔒 Guarded onConflict update
       await tx.insert(messages)
         .values(messagesToInsert)
         .onConflictDoUpdate({
           target: messages.id,
           set: {
             content: sql`excluded.content`,
             role: sql`excluded.role`,
             type: sql`excluded.type`,
             toolName: sql`excluded.tool_name`,
             toolCallId: sql`excluded.tool_call_id`,
             attachments: sql`excluded.attachments`
-          }
+          },
+          setWhere: and(
+            eq(messages.chatId, sql`excluded.chat_id`),
+            eq(messages.userId, sql`excluded.user_id`)
+          )
         });
🤖 Prompt for AI Agents
In `@lib/actions/chat-db.ts` around lines 123 - 136, The upsert currently uses
tx.insert(messages).values(messagesToInsert).onConflictDoUpdate targeting
messages.id which can overwrite rows across chats/users; modify the
onConflictDoUpdate call to include a setWhere clause that restricts updates to
rows where messages.chat_id = excluded.chat_id (or messages.chatId =
excluded.chatId) and messages.user_id = excluded.user_id (or messages.userId =
excluded.userId) so updates only apply when the incoming row matches the same
chatId and userId as the existing row; keep the same set mapping (content, role,
type, tool_name, tool_call_id, attachments) but add the setWhere guard to the
onConflictDoUpdate invocation on tx.insert/messages/messagesToInsert.

Comment on lines 188 to +205
try {
// We need to ensure the message is associated with the chat.
// dbCreateMessage requires chatId.
// Check if a 'data' message already exists to avoid duplication
const existingMessages = await dbGetMessagesByChatId(chatId);
const dataMessage = existingMessages.find(m => m.role === 'data');

if (dataMessage) {
// Update existing message content using direct db call since chat-db.ts doesn't have an updateMessage
const { db } = await import('@/lib/db');
const { messages } = await import('@/lib/db/schema');
const { eq } = await import('drizzle-orm');

await db.update(messages)
.set({ content: JSON.stringify(contextData) })
.where(eq(messages.id, dataMessage.id));

console.log('Drawing context updated for chat:', chatId, 'messageId:', dataMessage.id);
return { success: true, messageId: dataMessage.id };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update the most recent data message to stay in sync with UI restoration.

getUIStateFromAIState renders the last data message, but this update path uses find() which may pick the oldest entry if duplicates exist (legacy data). That can leave the rendered context stale after reloads. Prefer updating the latest data message (or updating all data messages for the chat) to match restoration behavior.

🐛 Proposed fix (pick latest data message)
-    const dataMessage = existingMessages.find(m => m.role === 'data');
+    const dataMessage = [...existingMessages]
+      .filter(m => m.role === 'data')
+      .sort(
+        (a, b) =>
+          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+      )[0];
📝 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
try {
// We need to ensure the message is associated with the chat.
// dbCreateMessage requires chatId.
// Check if a 'data' message already exists to avoid duplication
const existingMessages = await dbGetMessagesByChatId(chatId);
const dataMessage = existingMessages.find(m => m.role === 'data');
if (dataMessage) {
// Update existing message content using direct db call since chat-db.ts doesn't have an updateMessage
const { db } = await import('@/lib/db');
const { messages } = await import('@/lib/db/schema');
const { eq } = await import('drizzle-orm');
await db.update(messages)
.set({ content: JSON.stringify(contextData) })
.where(eq(messages.id, dataMessage.id));
console.log('Drawing context updated for chat:', chatId, 'messageId:', dataMessage.id);
return { success: true, messageId: dataMessage.id };
}
try {
// Check if a 'data' message already exists to avoid duplication
const existingMessages = await dbGetMessagesByChatId(chatId);
const dataMessage = [...existingMessages]
.filter(m => m.role === 'data')
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)[0];
if (dataMessage) {
// Update existing message content using direct db call since chat-db.ts doesn't have an updateMessage
const { db } = await import('@/lib/db');
const { messages } = await import('@/lib/db/schema');
const { eq } = await import('drizzle-orm');
await db.update(messages)
.set({ content: JSON.stringify(contextData) })
.where(eq(messages.id, dataMessage.id));
console.log('Drawing context updated for chat:', chatId, 'messageId:', dataMessage.id);
return { success: true, messageId: dataMessage.id };
}
🤖 Prompt for AI Agents
In `@lib/actions/chat.ts` around lines 188 - 205, The current update branch in
save/update logic uses dbGetMessagesByChatId and existingMessages.find(...)
which returns the first (oldest) 'data' message and can leave the UI showing the
last 'data' entry out-of-sync with getUIStateFromAIState; change the selection
to target the most recent 'data' message (e.g., pick the last matching element
by timestamp or id) or iterate and update all messages with role === 'data' so
the DB update against messages (used in the db.update(...) block) updates the
latest/each 'data' record rather than the first one found; update references:
dbGetMessagesByChatId, existingMessages, dataMessage, and the db.update(...)
call accordingly.

Comment on lines +33 to +36
attachments: jsonb('attachments'),
toolName: varchar('tool_name', { length: 100 }),
toolCallId: varchar('tool_call_id', {length: 100}),
type: varchar('type', { length: 50 })
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider typing attachments to avoid any propagation.

Line 33 uses jsonb('attachments') without a type, which makes downstream typings less safe. If null isn’t meaningful, a typed default also removes null checks.

♻️ Suggested typing (adjust shape as needed)
-  attachments: jsonb('attachments'),
+  attachments: jsonb('attachments').$type<unknown[]>().default([]),
📝 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
attachments: jsonb('attachments'),
toolName: varchar('tool_name', { length: 100 }),
toolCallId: varchar('tool_call_id', {length: 100}),
type: varchar('type', { length: 50 })
attachments: jsonb('attachments').$type<unknown[]>().default([]),
toolName: varchar('tool_name', { length: 100 }),
toolCallId: varchar('tool_call_id', {length: 100}),
type: varchar('type', { length: 50 })
🤖 Prompt for AI Agents
In `@lib/db/schema.ts` around lines 33 - 36, The attachments column is declared
with jsonb('attachments') which yields an untyped any; define and use a concrete
type for the JSON shape (e.g., Attachment or Attachment[] depending on expected
payload) and pass it into jsonb to lock down typings (e.g.,
jsonb<Attachment[]>('attachments')), or if null is not meaningful set a typed
default value instead; update the schema declaration that references attachments
and any related uses of toolName/toolCallId/type to use the new Attachment type
for downstream safety and remove unnecessary null checks.

…911856121500176

Ensure text wrapping for long sentences across UI components
@ngoiyaeric ngoiyaeric added help wanted Extra attention is needed Bug fix 1.0.0 Standard release labels Feb 1, 2026
CJWTRUST and others added 22 commits February 2, 2026 01:48
- Optimized resolution search with streaming.
- Improved timezone handling and AI context.
- Fixed UI mutual exclusion and loading state logic.
- Stabilized Mapbox cleanup and event listeners.
- Resolved build errors and ESLint warnings.

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

- Reverted branch to commit 6acfbe5.
- Merged changes from main (e95a25b).
- Preserved Tentree Icon (UsageView logic), History layout (HistoryContainer), and PurchaseCreditsPopup from 6acfbe5 as requested.
- Updated Stripe checkout URL in PurchaseCreditsPopup to the official one (14A3cv7K72TR3go14Nasg02).
- Re-applied bug fixes from 6acfbe5 (type casting in app/actions.tsx, ESLint dependencies in components/map/mapbox-map.tsx and components/chat-panel.tsx, and relatedQueries initialization).
- Verified build and synchronization with main branch features (e.g., resolution search enhancements).

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

- Reverted branch to commit 6acfbe5 and merged with main (e95a25b).
- Preserved Tentree Icon (Usage View), history layout, and PurchaseCreditsPopup from 6acfbe5.
- Restored branch-specific UI components: Header, History button, Profile toggle, and Usage integration in Chat.
- Synchronized all other logic with main, including resolution search enhancements.
- Fixed an infinite re-render loop in Mapbox map by optimizing useEffect dependencies (removed cameraState from initialization effect).
- Re-applied critical bug fixes from 6acfbe5:
  - Type casting in app/actions.tsx (getUIStateFromAIState).
  - Streamable value initialization for stability.
  - setSuggestions dependency in ChatPanel.
- Updated Stripe checkout URL in PurchaseCreditsPopup to the official link.
- Verified successful build with 'bun run build'.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
Replace all occurrences of the old Stripe checkout link with https://buy.stripe.com/14A3cv7K72TR3go14Nasg02 in:
- components/mobile-icons-bar.tsx
- components/purchase-credits-popup.tsx
- components/usage-view.tsx

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

Update Stripe payment links
- Updated 'Daily refresh credits' to 'Yearly refresh credits'
- Updated refresh credit value from 300 to 500
- Updated subtext to 'Refresh to 500 every year.'
- Replaced task list with 'QCX-TERRA Crop yield Analysis', 'QCX-TERRA Flood predictions', and 'Green OS climate synchronization'
- Set task dates to 'upcoming' and credit changes to single digits (-7, -5, -3)

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

Update Usage View to Yearly Refresh
This commit refactors the desktop header icon container in `components/header.tsx` to ensure all icons are equidistant.

Key changes:
- Changed the icon container from `justify-between w-1/2` to `justify-center flex-1` with a fixed `gap-10`.
- Removed the unused `<div id="timezone-clock-portal" />` which was causing irregular spacing between the TentTree and ModeToggle icons.
- Applied `className="contents"` to the `<div id="header-search-portal" />` so that it doesn't affect the flex layout when empty.
- These changes ensure that icons are always perfectly centered and equidistant regardless of the number of active icons.

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

Standardize Header Icon Spacing
- Modified `mapbox-map.tsx` to only show `NavigationControl` when in `DrawingMode` on desktop.
- Fixed a bug in the mode change `useEffect` where previous state was being updated prematurely, preventing cleanup logic from executing correctly.
- Updated `tests/map.spec.ts` to switch to `DrawingMode` before verifying zoom control visibility.

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

Conditional Map Zoom Controls and Mode Transition Fix
Copy link
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: 2

🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Around line 697-703: The map callback over aiState.messages (inside the
function handling message rendering where actualLastDataIndex is used) does not
return on every path — update the switch inside that callback (the switch
handling message.type/name/etc.) to include an explicit return in the
default/fallthrough case (e.g., return null or the intended fallback JSX/value)
so the callback always returns a value and you no longer rely on later filtering
to remove undefined results; ensure you reference the same variables (message,
index, role, content, id, type, name) and keep existing early returns for role
=== 'data' and other explicit cases.

In `@components/map/mapbox-map.tsx`:
- Line 471: The map initialization effect is re-running and tearing down the map
because it lists mapData.cameraState and position?.latitude/position?.longitude
as dependencies; since the effect's cleanup sets map.current = null this causes
full reinitialization on every camera or position update. Fix by removing
mapData.cameraState and position?.latitude and position?.longitude from the
dependency array of the effect (keep only setup callbacks like setMap,
setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation), so the
map is initialized once and position/camera updates are handled by their
dedicated effects.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9450b15 and 85a85c5.

📒 Files selected for processing (4)
  • app/actions.tsx
  • components/chat-panel.tsx
  • components/chat.tsx
  • components/map/mapbox-map.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-13T13:26:30.086Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.086Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.

Applied to files:

  • app/actions.tsx
🧬 Code graph analysis (1)
components/map/mapbox-map.tsx (1)
components/map/geojson-layer.tsx (4)
  • GeoJsonLayer (13-100)
  • map (24-80)
  • map (89-96)
  • GeoJsonLayerProps (8-11)
🪛 Biome (2.3.13)
app/actions.tsx

[error] 698-698: This callback passed to map() iterable method should always return a value.

Add missing return statements so that this callback returns a value on all execution paths.

(lint/suspicious/useIterableCallbackReturn)

🔇 Additional comments (4)
components/chat-panel.tsx (1)

217-218: LGTM! GeoJSON file type support added correctly.

The accept attribute properly includes both the file extension (.geojson) and the MIME type (application/geo+json) for broader compatibility. The data-testid attribute improves testability.

components/chat.tsx (1)

79-85: Good fix for the infinite refresh loop.

The lastRefreshedMessageIdRef guard effectively prevents repeated router.refresh() calls for the same response message. This addresses the refresh loop issue mentioned in the PR comments.

app/actions.tsx (1)

863-882: Good error handling for data message parsing.

The try-catch wrapper around JSON parsing and the validation of uploadedGeoJson array structure is a good defensive pattern. This correctly handles malformed data messages without crashing the UI state reconstruction.

components/map/mapbox-map.tsx (1)

597-599: No issues with GeoJsonLayer placement.

The GeoJsonLayer components are properly positioned as siblings in the JSX return statement. Since GeoJsonLayer returns null and accesses the Mapbox map instance exclusively via the useMap() context hook, the DOM relationship is irrelevant. The context is available because Mapbox is rendered within MapContextProvider (as shown in app/layout.tsx). The component's useEffect safely handles the async nature of map initialization with a if (!map || !data) return guard. This is a correct pattern for side-effect components that manipulate the map via context without rendering DOM elements.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 697 to +703
return aiState.messages
.map((message, index) => {
const { role, content, id, type, name } = message

if (role === 'data' && index !== actualLastDataIndex) {
return null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Map callback does not explicitly return on all paths.

The static analysis tool correctly identifies that the callback passed to map() at line 698 doesn't always return a value. While the early returns at lines 701-703 and 711 handle some cases, the switch statement inside may fall through without returning.

The current code relies on the filter at line 890 to remove undefined values, but explicit returns improve clarity and prevent subtle bugs.

🛡️ Proposed fix - add explicit return at switch default
         default:
           return {
             id,
             component: null
           }
       }
+      return null // Explicit return for unhandled cases within switch
     })
     .filter(message => message !== null) as UIState
📝 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
return aiState.messages
.map((message, index) => {
const { role, content, id, type, name } = message
if (role === 'data' && index !== actualLastDataIndex) {
return null
}
return aiState.messages
.map((message, index) => {
const { role, content, id, type, name } = message
if (role === 'data' && index !== actualLastDataIndex) {
return null
}
switch (type) {
// ... switch cases ...
default:
return {
id,
component: null
}
}
return null // Explicit return for unhandled cases within switch
})
.filter(message => message !== null) as UIState
🧰 Tools
🪛 Biome (2.3.13)

[error] 698-698: This callback passed to map() iterable method should always return a value.

Add missing return statements so that this callback returns a value on all execution paths.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 697 - 703, The map callback over
aiState.messages (inside the function handling message rendering where
actualLastDataIndex is used) does not return on every path — update the switch
inside that callback (the switch handling message.type/name/etc.) to include an
explicit return in the default/fallthrough case (e.g., return null or the
intended fallback JSX/value) so the callback always returns a value and you no
longer rely on later filtering to remove undefined results; ensure you reference
the same variables (message, index, role, content, id, type, name) and keep
existing early returns for role === 'data' and other explicit cases.

}
}
}, [setMap, setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation])
}, [setMap, setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation, mapData.cameraState, position?.latitude, position?.longitude])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the effect at line 471 with context
sed -n '460,485p' components/map/mapbox-map.tsx | cat -n

Repository: QueueLab/QCX

Length of output: 1181


🏁 Script executed:

#!/bin/bash
# Find where mapData.cameraState is defined/updated
rg -n "cameraState" components/map/mapbox-map.tsx -B2 -A2

Repository: QueueLab/QCX

Length of output: 708


🏁 Script executed:

#!/bin/bash
# Search for setCameraState to understand update patterns
rg -n "setCameraState" components/map/ -A1

Repository: QueueLab/QCX

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Look for the full effect hook starting from line 450
sed -n '440,480p' components/map/mapbox-map.tsx | cat -n

Repository: QueueLab/QCX

Length of output: 1780


🏁 Script executed:

#!/bin/bash
# Find the complete effect hook starting from an earlier line
sed -n '330,380p' components/map/mapbox-map.tsx | cat -n

Repository: QueueLab/QCX

Length of output: 2010


🏁 Script executed:

#!/bin/bash
# Search for initializedRef to see the guard logic
rg -n "initializedRef" components/map/mapbox-map.tsx -B2 -A2

Repository: QueueLab/QCX

Length of output: 619


🏁 Script executed:

#!/bin/bash
# Check if position object is frequently updated in state
rg -n "setPosition|position:" components/map/mapbox-map.tsx -C1 | head -40

Repository: QueueLab/QCX

Length of output: 198


🏁 Script executed:

#!/bin/bash
# Find where position state is updated
rg -n "const \[position" components/map/mapbox-map.tsx -A5

Repository: QueueLab/QCX

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Look for geolocation updates that would change position
rg -n "watchPosition|success.*position" components/map/mapbox-map.tsx -B2 -A5

Repository: QueueLab/QCX

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Get component function signature to see if position is a prop
sed -n '1,50p' components/map/mapbox-map.tsx | cat -n

Repository: QueueLab/QCX

Length of output: 2981


🏁 Script executed:

#!/bin/bash
# Search for where position prop/state is passed or defined
rg -n "position" components/map/mapbox-map.tsx | head -20

Repository: QueueLab/QCX

Length of output: 1147


🏁 Script executed:

#!/bin/bash
# Check the component's type definition or interface
rg -n "interface.*Props|type.*Props" components/map/mapbox-map.tsx -A20

Repository: QueueLab/QCX

Length of output: 38


Remove position and cameraState from map initialization effect dependencies—they cause the map to be torn down and rebuilt on every change.

The effect's cleanup function sets map.current = null, so when these dependencies change, the cleanup runs and the guard condition (!map.current) becomes true again, causing the map to fully reinitialize rather than just re-running setup logic.

  • position?.latitude and position?.longitude: A separate effect already handles position updates properly (lines 541-544). Including them here is redundant and causes map destruction/recreation on every prop change.
  • mapData.cameraState: Updates frequently on every moveend event (line 412), triggering reinitializations during normal map interaction. The initial effect only needs to read it once; it should not be a dependency.

Only callbacks that are called during setup should be dependencies. Remove position properties and cameraState from the dependency array.

🤖 Prompt for AI Agents
In `@components/map/mapbox-map.tsx` at line 471, The map initialization effect is
re-running and tearing down the map because it lists mapData.cameraState and
position?.latitude/position?.longitude as dependencies; since the effect's
cleanup sets map.current = null this causes full reinitialization on every
camera or position update. Fix by removing mapData.cameraState and
position?.latitude and position?.longitude from the dependency array of the
effect (keep only setup callbacks like setMap, setIsMapLoaded, captureMapCenter,
handleUserInteraction, stopRotation), so the map is initialized once and
position/camera updates are handled by their dedicated effects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.0.0 Standard release Bug fix help wanted Extra attention is needed Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants