Skip to content

feat: autonomous map navigation for address inputs#522

Open
ngoiyaeric wants to merge 52 commits intomainfrom
feat/autonomous-map-navigation
Open

feat: autonomous map navigation for address inputs#522
ngoiyaeric wants to merge 52 commits intomainfrom
feat/autonomous-map-navigation

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Feb 15, 2026

This PR implements autonomous map navigation when a user types an address or coordinates in the chat. It uses a heuristic to detect potential addresses and triggers the geospatial tool immediately for a faster user experience.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for uploading and visualizing GeoJSON files on interactive maps.
    • Introduced automatic address and coordinate detection—paste or type locations for instant mapping.
    • GeoJSON data now renders directly in the map view with zoom-to-fit functionality.
  • Chores

    • Enhanced backend data structures to support geospatial features and expanded message metadata.

CJWTRUST and others added 30 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>
- Extend MapData context to support uploaded GeoJSON layers
- Update ChatPanel to support GeoJSON file selection and add test IDs
- 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
- Update AIMessage type to support 'geojson_upload'
- Fix ESLint warnings to ensure clean build performance

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Updated `EmptyScreen` example buttons to use `whitespace-normal`, `text-left`, and `items-start` for better multi-line support.
- Added `break-words` to `BotMessage` container to prevent horizontal overflow from long strings.
- Improved `SuggestionsDropdown` and `SearchRelated` buttons with wrapping and top-alignment.
- Replaced filename truncation with `break-all` wrapping in `ChatPanel` file attachments.
- Ensured consistent icon alignment for multi-line buttons.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Add support for pasting GeoJSON directly into the chat input
- Implement synchronization guard in Chat.tsx to prevent redundant state saves
- Ensure 'geojson_upload' message type is correctly handled in AI state
- Improve GeoJSON extraction from geospatial tool responses

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
This commit implements a comprehensive pipeline for GeoJSON data overlays:
- Updated `ChatPanel` to accept `.geojson` files and handle pasted GeoJSON text.
- Added `MapDataUpdater` headless component to sync GeoJSON to map context and automatically fit viewport bounds using Turf.js.
- Fixed an infinite refresh loop in `Chat.tsx` by introducing `lastRefreshedMessageIdRef` to guard `router.refresh()`.
- Prevented redundant camera movement loops in `MapDataUpdater` using `hasZoomedRef`.
- Updated database schema and message mapping to include `type`, `attachments`, and tool metadata, ensuring map overlays persist across page refreshes.
- Implemented `onConflictDoUpdate` (upsert) for saving messages to prevent primary key collisions.
- Supported GeoJSON ingestion from both file uploads, manual text input, and agent tools.

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

Ensure text wrapping for long sentences across UI components
- 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>
google-labs-jules bot and others added 22 commits February 2, 2026 09:43
…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
…mponent

- Merged origin/main into the branch to ensure latest features and fixes are present.
- Fixed a ReferenceError in components/chat.tsx where lastRefreshedMessageIdRef was used before declaration.
- Improved the refresh logic in components/chat.tsx to reliably update chat history by searching for the last response message, even if followed by related or followup messages.
- Moved useRef declarations to the top of the Chat component for better stability and to prevent hydration issues.
- Verified GeoJSON upload pipeline stability and confirmed successful build.

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

vercel bot commented Feb 15, 2026

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

Project Deployment Actions Updated (UTC)
qcx Error Error Feb 15, 2026 3:21pm

@CLAassistant
Copy link

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 3 committers have signed the CLA.

✅ ngoiyaeric
❌ CJWTRUST
❌ 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 15, 2026

Walkthrough

This PR introduces GeoJSON upload detection and visualization capability throughout the chat application. It extends the database schema with message metadata columns and a new calendar_notes table, updates the geospatial tool to parse GeoJSON responses, and introduces MapDataUpdater components to render uploaded GeoJSON on maps while enhancing message persistence with upsert logic.

Changes

Cohort / File(s) Summary
GeoJSON Upload & Detection
app/actions.tsx, lib/types/index.ts
Detects GeoJSON inputs from text paste or file uploads, creates geojson_upload messages, mounts MapDataUpdater in UI stream. Adds 'geojson_upload' as new AIMessage.type variant. Includes autonomous geospatial navigation via address/coordinate heuristic detection.
Map Components & Data Context
components/map/map-data-updater.tsx, components/map/map-data-context.tsx, components/map/google-map.tsx, components/map/mapbox-map.tsx, components/map/map-query-handler.tsx
Introduces MapDataUpdater headless component for rendering GeoJSON and fitting map bounds. Extends MapData context with uploadedGeoJson field. Updates google-map and mapbox-map to display uploadedGeoJson layers. Integrates geoJson rendering from tool outputs via MapQueryHandler.
Chat & Message State Management
components/chat.tsx, app/search/[id]/page.tsx
Reworked refresh logic to track last 'response' message. Enhanced map data synchronization to include drawnFeatures, cameraState, and uploadedGeoJson. Updated message-to-AIMessage mapping to include type and name fields. Replaced MapDataProvider with fragment wrappers.
Message Persistence & Schema
lib/db/schema.ts, lib/actions/chat-db.ts, drizzle/migrations/0001_aromatic_ultimatum.sql, drizzle/migrations/meta/...*
Added four new message columns: attachments, toolName, toolCallId, type. Created calendar_notes table with foreign keys to users and chats. Implemented upsert logic to update existing data messages instead of creating duplicates. Updated migration metadata snapshots.
Geospatial Tool Enhancement
lib/agents/tools/geospatial.tsx
Extended McpResponse with optional geoJson field. Added parsing logic to extract geoJson from multiple response structures (firstResult, parsedData, location). Updated error messages to include geoJson requirement. Added mapUrl fallback handling.
File Input & UI Polish
components/chat-panel.tsx, components/message.tsx, components/user-message.tsx, components/header-search-button.tsx, components/sidebar/chat-history-client.tsx, package.json
Expanded file input accept types to include .geojson and application/geo+json. Added data-testid attributes for testing. Removed drawnFeatures from resolution search submission. Upgraded drizzle-orm from ^0.29.0 to ^0.45.1.

Sequence Diagram

sequenceDiagram
    actor User
    participant Chat as Chat Actions
    participant MapUpdater as MapDataUpdater
    participant Map as Map Component
    participant DB as Database

    User->>Chat: Upload GeoJSON file or paste text
    Chat->>Chat: Detect GeoJSON content
    Chat->>DB: Create geojson_upload message
    Chat->>Chat: Add to AI state and UI stream
    Chat->>MapUpdater: Mount with GeoJSON data
    MapUpdater->>Map: Extract bounding box (Turf)
    MapUpdater->>DB: Update MapData.uploadedGeoJson
    MapUpdater->>Map: Fit bounds with camera pan
    Map->>Map: Render GeoJsonLayer for uploaded data
    Map-->>User: Display GeoJSON on map
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly Related PRs

Suggested Labels

backend, database, geospatial, map-features, feature-addition

Poem

🐰 A rabbit hopped through GeoJSON dreams,
Uploading data in colorful streams,
Maps now dance with features so bright,
Persistence ensures every point stays right,
Bounds fitted, locations found!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (32 files):

⚔️ app/actions.tsx (content)
⚔️ app/layout.tsx (content)
⚔️ app/page.tsx (content)
⚔️ app/search/[id]/page.tsx (content)
⚔️ bun.lock (content)
⚔️ components/chat-panel.tsx (content)
⚔️ components/chat.tsx (content)
⚔️ components/followup-panel.tsx (content)
⚔️ components/header-search-button.tsx (content)
⚔️ components/history-list.tsx (content)
⚔️ components/map/google-map.tsx (content)
⚔️ components/map/map-data-context.tsx (content)
⚔️ components/map/map-query-handler.tsx (content)
⚔️ components/map/mapbox-map.tsx (content)
⚔️ components/message.tsx (content)
⚔️ components/purchase-credits-popup.tsx (content)
⚔️ components/search-related.tsx (content)
⚔️ components/settings/components/settings.tsx (content)
⚔️ components/settings/components/user-management-form.tsx (content)
⚔️ components/sidebar/chat-history-client.tsx (content)
⚔️ components/user-message.tsx (content)
⚔️ drizzle/migrations/meta/0000_snapshot.json (content)
⚔️ drizzle/migrations/meta/_journal.json (content)
⚔️ lib/actions/chat-db.ts (content)
⚔️ lib/actions/chat.ts (content)
⚔️ lib/actions/users.ts (content)
⚔️ lib/agents/resolution-search.tsx (content)
⚔️ lib/agents/tools/geospatial.tsx (content)
⚔️ lib/db/schema.ts (content)
⚔️ lib/types/index.ts (content)
⚔️ lib/utils/index.ts (content)
⚔️ package.json (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ 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 accurately describes the main feature: autonomous map navigation triggered by address inputs. It directly matches the PR's primary objective of detecting addresses/coordinates and initiating geospatial navigation.

✏️ 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 feat/autonomous-map-navigation
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/autonomous-map-navigation
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Caution

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

⚠️ Outside diff range comments (2)
lib/agents/tools/geospatial.tsx (1)

362-378: 🧹 Nitpick | 🔵 Trivial

Retry with fixed delay — consider exponential backoff.

The retry loop uses a constant 1-second delay. For external MCP service calls, exponential backoff (e.g., 1000 * 2 ** retryCount) would reduce thundering-herd pressure and improve reliability.

Proposed fix
-          await new Promise(resolve => setTimeout(resolve, 1000));
+          await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retryCount)));
lib/db/schema.ts (1)

49-50: ⚠️ Potential issue | 🟡 Minor

updatedAt won't auto-update on row modification.

defaultNow() only sets the value on insert. Without a database trigger or explicit application-level SET updated_at = now() on every update, this column will always reflect the creation time, defeating its purpose.

🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Around line 384-395: The paste and file-upload branches duplicate GeoJSON
parsing, duplicate detection, aiState.update (building the messages entry with
id = geoJsonId, role 'assistant', content JSON.stringify({ data: geoJson,
filename }), type 'geojson_upload') and the MapDataUpdater append/commit steps;
extract a shared helper (e.g. processGeoJsonPayload or handleGeoJsonUpload) that
accepts the parsed geoJson, filename and id and performs the duplicate check,
updates aiState via aiState.update with the same message shape, and calls the
MapDataUpdater append/commit logic, then call that helper from both the paste
handler and the file upload handler to remove the duplicated code and ensure
consistent behavior.
- Around line 759-765: The map callback over aiState.messages (inside the
function that iterates message -> { const { role, content, id, type, name } =
message }) currently has code paths that hit a switch on type for role
'user'/'assistant' and execute a break without returning, causing undefined
entries; update each switch case and the default branch so they always return a
value (e.g., return null for unhandled types) rather than just breaking, or
refactor the case bodies to return the mapped JSX/value directly; ensure every
branch in the map callback returns (including after the initial if (role ===
'data' && index !== actualLastDataIndex') path) to satisfy the linter.
- Around line 246-257: The pasted GeoJSON message is being recorded with role:
'assistant' — update the aiState update so the pushed message object (the one
with id: geoJsonId, content: JSON.stringify({ data: geoJson, filename: 'Pasted
GeoJSON' }), type: 'geojson_upload') uses the correct role (e.g., 'user' or a
neutral 'data') instead of 'assistant'; modify the aiState.update call that
appends to messages to set role: 'user' (or 'data') so downstream logic reads
proper attribution.
- Around line 452-462: The isPotentialAddress heuristic is too permissive;
update the logic in isPotentialAddress to reduce false positives by tightening
the checks: require either isCoordinate OR (hasKeyword && (hasNumber ||
words.length >= 3)) OR (hasNumber && words.length >= 3) instead of the current
(hasNumber && words.length >= 2) rule, and refine addressKeywords (e.g., treat
short tokens like 'dr' as a keyword only when followed/preceded by punctuation
or longer context) so words.some(word => addressKeywords.includes(word)) is less
likely to match titles like "Dr. Smith"; adjust the return expression
accordingly using the existing symbols addressKeywords, words, hasNumber,
hasKeyword, and isCoordinate.
- Around line 472-491: The async IIFE that calls geoTool.execute(...) and then
uiStream.append(MapQueryHandler) can complete after processEvents() calls
uiStream.done(), causing uiStream.append to throw; capture the geocoding promise
(e.g., const geocodePromise = geoTool.execute(...)) instead of fire-and-forget
and ensure processEvents awaits or coordinates with that promise before calling
uiStream.done(); update the code paths referencing geoTool.execute, the IIFE,
uiStream.append and processEvents so the geocodePromise is awaited (or its
resolution handled with safe-checks) and errors are caught so append is never
invoked on a closed stream.

In `@app/search/`[id]/page.tsx:
- Around line 54-55: The assignment unsafely casts nullable dbMsg.toolName to
string; instead, check dbMsg.toolName and only set the AIMessage.name when it's
a real string (e.g. name: dbMsg.toolName !== null ? dbMsg.toolName : undefined),
or omit the name property when null so runtime typeof checks match TypeScript's
types; update the object creation where you set type: dbMsg.type as
AIMessage['type'] and name: dbMsg.toolName to use a conditional/nullable-safe
mapping (and adjust any downstream code expecting string vs undefined
accordingly).

In `@components/chat-panel.tsx`:
- Around line 218-219: handleSubmit currently only creates content parts for
image/* and omits .geojson attachments, and handleFileChange only checks size;
update handleSubmit to detect files with type 'application/geo+json' or filename
ending with .geojson and append a content part (e.g., {type: 'geojson',
filename}) to the message payload so the user message reflects a GeoJSON was
attached, and update handleFileChange to perform a fast validation for .geojson
files by reading the file as text and attempting JSON.parse (rejecting and
showing an error/toast if parse fails) before appending to formData; reference
the formData usage, handleSubmit, and handleFileChange functions and ensure the
attachment bar/preview UI is updated to show a GeoJSON indicator or simple text
preview if parse succeeds.

In `@components/chat.tsx`:
- Around line 100-101: The effect's condition uses mapData.drawnFeatures ||
mapData.uploadedGeoJson which is truthy for empty arrays and causes
updateDrawingContext to run unintentionally; update the condition inside the
useEffect that checks id and mapData.cameraState to explicitly verify there is
actual data by using checks like (Array.isArray(mapData.drawnFeatures) &&
mapData.drawnFeatures.length > 0) || (Array.isArray(mapData.uploadedGeoJson) &&
mapData.uploadedGeoJson.length > 0) before calling updateDrawingContext so the
effect only runs when drawnFeatures or uploadedGeoJson contain items.
- Around line 99-118: The call to the server action updateDrawingContext inside
the useEffect is fire-and-forget and may yield unhandled promise rejections;
wrap the call in an async helper inside the useEffect and either await it or
attach a .catch() to handle errors (for example, create an async function inside
the effect that calls await updateDrawingContext(...) in a try/catch and logs
failures), keeping the same conditions around id, lastSyncedDataRef,
mapData.drawnFeatures, mapData.cameraState and mapData.uploadedGeoJson so you
still update lastSyncedDataRef only on success or handle rollback on error.

In `@components/map/map-data-context.tsx`:
- Around line 32-37: The uploadedGeoJson property is using `any` for `data` but
should be strongly typed as a GeoJSON FeatureCollection; update the type
declaration for `uploadedGeoJson` in map-data-context.tsx to import and use
`FeatureCollection` from the `geojson` package (so the array element becomes {
id: string; filename: string; data: FeatureCollection; visible: boolean }). This
ensures compatibility with downstream consumers like `GoogleGeoJsonLayer` and
`GeoJsonLayer` and preserves type safety across the rendering pipeline.

In `@components/map/map-data-updater.tsx`:
- Around line 23-26: The code currently wraps any non-FeatureCollection into
features: [data], which is invalid if data is a bare Geometry; change the
normalization around the featureCollection variable to detect three cases: if
data.type === 'FeatureCollection' use it as-is; else if data.type === 'Feature'
use data; else (assume Geometry) wrap it into a proper Feature object (e.g., {
type: 'Feature', geometry: data, properties: {} }) before placing into features
array so the resulting featureCollection is always an array of Feature objects.
- Line 7: Replace the heavy wildcard import of Turf in
components/map/map-data-updater.tsx with the specific bbox import: remove
"import * as turf from '@turf/turf'" and import bbox directly from '@turf/bbox',
then update any usage of turf.bbox(...) to call bbox(...) instead (e.g. where
turf.bbox is used around Line 52 in the map-data-updater component).
- Around line 9-13: Replace the loose any type on the MapDataUpdaterProps.data
prop with a GeoJSON union to match map-data-context.tsx: change the data type on
the MapDataUpdaterProps interface (symbol: MapDataUpdaterProps, property: data)
to accept GeoJSON.FeatureCollection | GeoJSON.Feature | GeoJSON.Geometry (or the
equivalent imports FeatureCollection, Feature, Geometry from the 'geojson'
package) and add the necessary import of those types if not already present.

In `@components/map/map-query-handler.tsx`:
- Line 17: The geoJson property is typed as any; change its type to a proper
GeoJSON type by replacing "geoJson?: any" with a union such as "geoJson?:
GeoJSON.FeatureCollection | GeoJSON.Feature | GeoJSON.Geometry" (or at minimum
"GeoJSON.GeoJsonObject"), and ensure you import or reference the GeoJSON types
(e.g., from the "geojson" package or the ambient "@types/geojson") so the
map-query-handler.tsx interface/prop that declares geoJson uses these concrete
types (look for the geoJson property in the component props or interface).

In `@components/map/mapbox-map.tsx`:
- Line 471: The effect that initializes the Mapbox instance is incorrectly
dependent on mapData.cameraState which causes the map to be destroyed and
recreated whenever captureMapCenter calls setMapData; remove mapData.cameraState
from the effect's dependency array (the effect that currently lists setMap,
setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation,
mapData.cameraState, position?.latitude, position?.longitude) so the map is not
torn down on camera moves, and if you need to restore an initial camera position
read mapData.cameraState once into a ref before the effect (e.g., const
initialCameraRef = useRef(mapData.cameraState)) and use that ref inside the
effect instead of the live mapData.cameraState.

In `@drizzle/migrations/0001_aromatic_ultimatum.sql`:
- Around line 1-19: Add indexes on the foreign-key columns and any message-type
filter column to improve query performance: create indexes for
calendar_notes.user_id and calendar_notes.chat_id (e.g., names like
calendar_notes_user_id_idx and calendar_notes_chat_id_idx) and add an index on
messages.type (e.g., messages_type_idx) so queries that filter by these columns
(references: calendar_notes.user_id, calendar_notes.chat_id, messages.type) will
use the indexes; include these CREATE INDEX statements in this migration after
the table/column changes and ensure they are marked as CONCURRENTLY if your DB
supports it to avoid locking live tables.

In `@drizzle/migrations/meta/0001_snapshot.json`:
- Line 238: The migration snapshot shows no secondary indexes for the messages
and calendar_notes tables; add indexes on the foreign key columns (e.g.,
messages.chat_id, messages.user_id and calendar_notes.user_id or calendar_id as
applicable) in your Drizzle schema and create a new migration to apply them so
queries like dbGetMessagesByChatId avoid full table scans; update the table
definitions in the Drizzle schema to include index definitions for those columns
and generate/commit the new migration.

In `@lib/actions/chat.ts`:
- Around line 165-166: The inline 'use server' directive inside the
updateDrawingContext function is redundant because the module already declares
'use server' at the top; remove the duplicate `'use server'` line from within
the updateDrawingContext function to avoid unnecessary repetition and potential
confusion (look for the `'use server'` string inside the updateDrawingContext
definition and delete it).
- Around line 189-205: Replace the full-fetch + in-memory search using
dbGetMessagesByChatId with a targeted DB query and an atomic upsert to avoid
inefficiency and TOCTOU races: add a new helper in chat-db.ts named
findMessageByChatIdAndRole(chatId, role) that queries
db.query.messages.findFirst({ where: and(eq(messages.chatId, chatId),
eq(messages.role, role)) }) and then, in chat.ts, call that instead of
dbGetMessagesByChatId; for the create/update path use a single upsert/ON
CONFLICT (or an insert with onConflict().doUpdate) against messages (using db
and messages symbols) to insert the 'data' message if missing or update its
content atomically, and remove the ad-hoc direct-import-update fallback that
does a separate read then update.
- Around line 195-197: Add an updateMessage function to chat-db.ts (beside
existing saveChat, createMessage, deleteChat, getMessagesByChatId) that accepts
message id and patch data and performs the Drizzle update using db and messages
schema, then export it; in lib/actions/chat.ts remove the dynamic imports of db,
messages, and eq and instead statically import updateMessage from chat-db.ts at
the top, and replace the dynamic-import update logic with a call to
updateMessage(messageId, patch) so the higher-level abstraction is used
consistently.

In `@lib/agents/tools/geospatial.tsx`:
- Line 28: Replace the loose any type on the geoJson property with a stricter
GeoJSON type: import the GeoJSON types (e.g., from the "geojson" or
"@types/geojson" package) and change the declaration of geoJson to a union such
as GeoJSON.FeatureCollection | GeoJSON.Feature | object (or the most specific
GeoJSON subtypes you expect); update any usages in functions/methods that
reference geoJson to satisfy the new type (cast/parsing where necessary) and add
the import for GeoJSON to the top of the file so the type is available for the
geoJson property.
- Around line 420-428: When parsedData.type is 'Feature' or 'FeatureCollection'
and you currently set mcpData.location = {}, compute and populate a meaningful
centroid into mcpData.location instead of leaving it empty: if parsedData.bbox
exists use its [minX,minY,maxX,maxY] to compute centroid longitude=(minX+maxX)/2
and latitude=(minY+maxY)/2; if no top-level bbox but parsedData.features exist,
derive an aggregate bbox from feature bboxes or feature geometries and compute
the centroid similarly; set mcpData.location.latitude,
mcpData.location.longitude and a readable mcpData.location.place_name (e.g.,
properties.name or "GeoJSON centroid") so downstream consumers that read
mcpData.location (used elsewhere in this module) receive coordinates when
geoJson is the primary payload.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be08900 and 209f9be.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • app/actions.tsx
  • app/search/[id]/page.tsx
  • components/chat-panel.tsx
  • components/chat.tsx
  • components/header-search-button.tsx
  • components/map/google-map.tsx
  • components/map/map-data-context.tsx
  • components/map/map-data-updater.tsx
  • components/map/map-query-handler.tsx
  • components/map/mapbox-map.tsx
  • components/message.tsx
  • components/sidebar/chat-history-client.tsx
  • components/user-message.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/agents/tools/geospatial.tsx
  • lib/db/schema.ts
  • lib/types/index.ts
  • package.json
💤 Files with no reviewable changes (1)
  • components/header-search-button.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-20T10:04:04.963Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-20T10:04:04.963Z
Learning: The application has a comprehensive credits and payment UI system with: (1) PurchaseCreditsPopup that shows after user authentication with a 7-day cooldown, (2) History panel on the left side containing CreditsDisplay at the top and ChatHistoryClient below, (3) TentTree icon in header that opens UsageSidebar on the right side showing usage history and billing options. All these components are coordinated through RootLayout and only render for authenticated users.

Applied to files:

  • components/sidebar/chat-history-client.tsx
🧬 Code graph analysis (11)
components/user-message.tsx (2)
tests/chat.spec.ts (1)
  • test (4-89)
components/chat-messages.tsx (2)
  • component (57-59)
  • ChatMessagesProps (7-9)
components/chat-panel.tsx (1)
tests/images.spec.ts (1)
  • test (5-235)
components/map/mapbox-map.tsx (1)
components/map/geojson-layer.tsx (3)
  • GeoJsonLayer (13-100)
  • map (24-80)
  • map (89-96)
components/map/google-map.tsx (1)
components/map/google-geojson-layer.tsx (3)
  • GoogleGeoJsonLayer (10-60)
  • GoogleGeoJsonLayerProps (6-8)
  • newLayer (52-56)
components/message.tsx (1)
tests/chat.spec.ts (1)
  • test (4-89)
components/chat.tsx (3)
components/chat-panel.tsx (1)
  • ChatPanelRef (25-28)
components/map/map-data-context.tsx (1)
  • useMapData (61-67)
lib/actions/chat.ts (1)
  • updateDrawingContext (165-222)
lib/actions/chat.ts (2)
lib/db/index.ts (1)
  • db (25-25)
lib/db/schema.ts (1)
  • messages (26-37)
components/map/map-query-handler.tsx (2)
components/map/map-data-updater.tsx (1)
  • MapDataUpdater (15-65)
components/map/geojson-layer.tsx (1)
  • map (24-80)
app/actions.tsx (3)
components/map/map-data-updater.tsx (1)
  • MapDataUpdater (15-65)
lib/agents/tools/geospatial.tsx (1)
  • geospatialTool (168-451)
components/map/map-query-handler.tsx (1)
  • MapQueryHandler (32-94)
lib/actions/chat-db.ts (1)
lib/db/schema.ts (1)
  • messages (26-37)
components/map/map-data-context.tsx (2)
components/map/geojson-layer.tsx (5)
  • GeoJsonLayer (13-100)
  • map (24-80)
  • GeoJsonLayerProps (8-11)
  • map (89-96)
  • map (16-97)
components/map/google-geojson-layer.tsx (2)
  • GoogleGeoJsonLayerProps (6-8)
  • GoogleGeoJsonLayer (10-60)
🪛 Biome (2.3.14)
app/actions.tsx

[error] 760-760: 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 (13)
components/sidebar/chat-history-client.tsx (1)

136-136: LGTM: Formatting improvement.

The added blank line improves readability by providing visual separation between the collapsible button and the conditional credits display section. This is a harmless cosmetic change.

However, note that this formatting change appears unrelated to the PR's stated objective of implementing autonomous map navigation for address inputs.

components/user-message.tsx (1)

35-35: LGTM!

The data-testid="user-message" attribute aligns with the existing e2e test selectors in tests/chat.spec.ts.

components/message.tsx (1)

21-21: LGTM!

The data-testid="bot-message" attribute matches the e2e test expectations in tests/chat.spec.ts.

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

84-86: LGTM!

The rendering of uploaded GeoJSON layers is correctly gated on item.visible and keyed by item.id. Minor style preference: .filter(item => item.visible).map(...) avoids false entries in the array, but this works correctly as-is.

components/map/map-query-handler.tsx (1)

81-91: LGTM — clean integration of MapDataUpdater.

The conditional rendering when geoJson is present is appropriate. Using toolOutput.timestamp as the deduplication id and falling back to 'Tool Result' for the filename are sensible defaults.

package.json (1)

64-65: Drizzle-orm and drizzle-kit versions are compatible.

drizzle-orm@^0.45.1 and drizzle-kit@^0.31.1 meet Drizzle's compatibility matrix requirements—drizzle-kit@0.31.0+ is the minimum compatible version for drizzle-orm@0.45.x, and these versions were released together as the recommended pairing. No compatibility issues.

components/chat.tsx (1)

42-44: LGTM — Refs for deduplication.

Good use of refs (lastRefreshedMessageIdRef, lastSyncedDataRef) to prevent redundant refreshes and server action calls.

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

1-178: Migration snapshot metadata upgrade looks correct.

Standard Drizzle Kit version 7 format changes (schema-qualified table names, RLS/policies metadata). The initial snapshot correctly represents the base schema without the new columns added in migration 0001.

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

1-19: Journal entry looks correct.

New migration entry aligns with the 0001_aromatic_ultimatum migration file.

lib/types/index.ts (1)

77-77: LGTM — New geojson_upload type variant.

Clean extension of the AIMessage.type union to support GeoJSON uploads.

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

597-599: GeoJsonLayer rendering looks correct.

Filters by item.visible and passes stable id/data props. The GeoJsonLayer component handles its own map interaction via the useMap hook.

lib/actions/chat-db.ts (1)

122-136: API route does not provide message IDs for upsert to work.

The saveChat function in lib/actions/chat-db.ts uses onConflictDoUpdate targeting messages.id, but in app/api/chat/route.ts (line 40–50), the firstMessage object explicitly omits the id field with a comment noting that Drizzle will auto-generate it. This means every insert generates a new UUID and the upsert conflict never triggers, degrading to a plain insert.

The wrapper in lib/actions/chat.ts (line 123) correctly preserves message IDs with id: msg.id, so the upsert works as intended there. However, the API route needs to provide deterministic message IDs if upsert deduplication is required, or the upsert logic should be removed if idempotency is not a concern for that endpoint.

lib/db/schema.ts (1)

33-36: LGTM — new message metadata columns align with migration.

The added attachments, toolName, toolCallId, and type columns are consistent with the SQL migration and snapshot.

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

Comment on lines +246 to +257
aiState.update({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: geoJsonId,
role: 'assistant',
content: JSON.stringify({ data: geoJson, filename: 'Pasted GeoJSON' }),
type: 'geojson_upload'
}
]
})
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

GeoJSON pasted by the user is stored with role: 'assistant' — incorrect attribution.

When a user pastes GeoJSON text, the message is added to AI state with role: 'assistant' (line 252). This misattributes user input to the assistant, which could confuse downstream logic that relies on role for conversation flow.

Consider using role: 'user' with type: 'geojson_upload', or a role: 'data' to keep it neutral.

🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 246 - 257, The pasted GeoJSON message is being
recorded with role: 'assistant' — update the aiState update so the pushed
message object (the one with id: geoJsonId, content: JSON.stringify({ data:
geoJson, filename: 'Pasted GeoJSON' }), type: 'geojson_upload') uses the correct
role (e.g., 'user' or a neutral 'data') instead of 'assistant'; modify the
aiState.update call that appends to messages to set role: 'user' (or 'data') so
downstream logic reads proper attribution.

Comment on lines +384 to +395
aiState.update({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: geoJsonId,
role: 'assistant',
content: JSON.stringify({ data: geoJson, filename: file.name }),
type: 'geojson_upload'
}
]
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicate GeoJSON detection logic between paste (lines 238–266) and file upload (lines 378–405).

The GeoJSON parsing, AI state update, and MapDataUpdater append logic is repeated nearly identically in both code paths. Extract a shared helper to reduce duplication and ensure consistent behavior.

🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 384 - 395, The paste and file-upload branches
duplicate GeoJSON parsing, duplicate detection, aiState.update (building the
messages entry with id = geoJsonId, role 'assistant', content JSON.stringify({
data: geoJson, filename }), type 'geojson_upload') and the MapDataUpdater
append/commit steps; extract a shared helper (e.g. processGeoJsonPayload or
handleGeoJsonUpload) that accepts the parsed geoJson, filename and id and
performs the duplicate check, updates aiState via aiState.update with the same
message shape, and calls the MapDataUpdater append/commit logic, then call that
helper from both the paste handler and the file upload handler to remove the
duplicated code and ensure consistent behavior.

Comment on lines +452 to +462
// Autonomous Map Navigation: Check if input looks like an address
const isPotentialAddress = (text: string) => {
// Simple heuristic: contains numbers and multiple words, or specific keywords
const addressKeywords = ['palace', 'street', 'st', 'avenue', 'ave', 'road', 'rd', 'boulevard', 'blvd', 'drive', 'dr', 'lane', 'ln', 'court', 'ct', 'square', 'sq', 'parkway', 'pkwy'];
const words = text.toLowerCase().split(/\s+/);
const hasNumber = /\d+/.test(text);
const hasKeyword = words.some(word => addressKeywords.includes(word));
const isCoordinate = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/.test(text.trim());

return (hasNumber && words.length >= 2) || hasKeyword || isCoordinate;
};
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

isPotentialAddress heuristic is too broad — will trigger on many non-address inputs.

The condition hasNumber && words.length >= 2 matches inputs like "buy 3 apples", "top 10 movies", "React 19 hooks". The keyword 'dr' matches "Dr. Smith". This will cause spurious geocoding calls on a significant portion of normal chat messages, wasting API quota and injecting unwanted map UI.

Consider tightening the heuristic — e.g., require both a number and a keyword, or use a more specific address regex pattern, or require a minimum of 3+ words when relying on the number heuristic alone.

Suggested tightening
-   return (hasNumber && words.length >= 2) || hasKeyword || isCoordinate;
+   return (hasNumber && hasKeyword) || isCoordinate;

This still won't be perfect but drastically reduces false positives.

🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 452 - 462, The isPotentialAddress heuristic is
too permissive; update the logic in isPotentialAddress to reduce false positives
by tightening the checks: require either isCoordinate OR (hasKeyword &&
(hasNumber || words.length >= 3)) OR (hasNumber && words.length >= 3) instead of
the current (hasNumber && words.length >= 2) rule, and refine addressKeywords
(e.g., treat short tokens like 'dr' as a keyword only when followed/preceded by
punctuation or longer context) so words.some(word =>
addressKeywords.includes(word)) is less likely to match titles like "Dr. Smith";
adjust the return expression accordingly using the existing symbols
addressKeywords, words, hasNumber, hasKeyword, and isCoordinate.

Comment on lines +472 to +491
(async () => {
try {
const isCoordinate = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/.test(userInput.trim());
const result = await geoTool.execute(
isCoordinate
? { queryType: 'reverse', coordinates: {
latitude: parseFloat(userInput.split(',')[0]),
longitude: parseFloat(userInput.split(',')[1])
} }
: { queryType: 'geocode', location: userInput }
);

if (result && result.type === 'MAP_QUERY_TRIGGER') {
uiStream.append(<MapQueryHandler toolOutput={result} />);
}
} catch (error) {
console.error('[AutonomousMap] Quick geocode failed:', error);
}
})();
}
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 | 🔴 Critical

Fire-and-forget async geocoding races with uiStream.done() — will throw on closed stream.

The geocoding IIFE runs concurrently with processEvents() (line 629). When processEvents finishes, it calls uiStream.done() (line 626). If the geocoding promise resolves after that, uiStream.append(...) on line 485 will throw because the stream is already closed.

This is a race condition that will surface as an unhandled runtime error, potentially crashing the request or silently dropping the map result.

Proposed fix: coordinate the geocoding with processEvents

Store the geocoding promise and await it (or handle its completion) before uiStream.done():

+ let autonomousMapPromise: Promise<void> | null = null;
+
  if (userInput && isPotentialAddress(userInput)) {
    console.log('[AutonomousMap] Detected potential address:', userInput);
    const geoTool = geospatialTool({ uiStream, mapProvider });
    
-   (async () => {
+   autonomousMapPromise = (async () => {
      try {
        // ... geocoding logic ...
      } catch (error) {
        console.error('[AutonomousMap] Quick geocode failed:', error);
      }
    })();
  }

Then inside processEvents, before closing the stream:

+ if (autonomousMapPromise) await autonomousMapPromise;
  isGenerating.done(false)
  uiStream.done()
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 472 - 491, The async IIFE that calls
geoTool.execute(...) and then uiStream.append(MapQueryHandler) can complete
after processEvents() calls uiStream.done(), causing uiStream.append to throw;
capture the geocoding promise (e.g., const geocodePromise =
geoTool.execute(...)) instead of fire-and-forget and ensure processEvents awaits
or coordinates with that promise before calling uiStream.done(); update the code
paths referencing geoTool.execute, the IIFE, uiStream.append and processEvents
so the geocodePromise is awaited (or its resolution handled with safe-checks)
and errors are caught so append is never invoked on a closed stream.

Comment on lines 759 to +765
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 return a value for all code paths (confirmed by static analysis).

When role is 'user' or 'assistant' and type doesn't match any of the handled cases in the inner switch, execution falls through to break without returning. This produces undefined elements in the array, which are then filtered out — but it's fragile and triggers the Biome lint error.

Add explicit return null after each break:

Proposed fix
           }
           break
+        return null
         case 'assistant':
           }
           break
+        return null
         case 'tool':

Or better yet, refactor so each case always returns:

         case 'user':
-          switch (type) {
+          return (() => { switch (type) {
             // ...
-          }
-          break
+          default: return null
+          } })()

Also applies to: 803-804, 864-865

🧰 Tools
🪛 Biome (2.3.14)

[error] 760-760: 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 759 - 765, The map callback over
aiState.messages (inside the function that iterates message -> { const { role,
content, id, type, name } = message }) currently has code paths that hit a
switch on type for role 'user'/'assistant' and execute a break without
returning, causing undefined entries; update each switch case and the default
branch so they always return a value (e.g., return null for unhandled types)
rather than just breaking, or refactor the case bodies to return the mapped
JSX/value directly; ensure every branch in the map callback returns (including
after the initial if (role === 'data' && index !== actualLastDataIndex') path)
to satisfy the linter.

Comment on lines +165 to 166
export async function updateDrawingContext(chatId: string, contextData: { drawnFeatures: any[], cameraState: any, uploadedGeoJson?: any[] }) {
'use server';
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

Redundant 'use server' directive.

The file already has 'use server' at the top (line 1), so the inline directive on line 166 is unnecessary.

🤖 Prompt for AI Agents
In `@lib/actions/chat.ts` around lines 165 - 166, The inline 'use server'
directive inside the updateDrawingContext function is redundant because the
module already declares 'use server' at the top; remove the duplicate `'use
server'` line from within the updateDrawingContext function to avoid unnecessary
repetition and potential confusion (look for the `'use server'` string inside
the updateDrawingContext definition and delete it).

Comment on lines +189 to +205
// 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

Fetching all messages to find a single data message is inefficient; race condition risk.

dbGetMessagesByChatId loads every message for the chat into memory just to .find() one with role === 'data'. This scales poorly with conversation length.

Additionally, there's a TOCTOU race: two concurrent calls can both see no existing data message and each create one, or both read the same message and overwrite each other's update.

Consider adding a dedicated query (e.g., findMessageByRole(chatId, 'data')) using a DB-level WHERE clause, and use an upsert (ON CONFLICT) to eliminate the race.

Sketch of a more efficient approach

In chat-db.ts, add a targeted query:

export async function findMessageByChatIdAndRole(chatId: string, role: string) {
  return db.query.messages.findFirst({
    where: and(eq(messages.chatId, chatId), eq(messages.role, role)),
  });
}

Then replace the full-fetch + find with a single call.

🤖 Prompt for AI Agents
In `@lib/actions/chat.ts` around lines 189 - 205, Replace the full-fetch +
in-memory search using dbGetMessagesByChatId with a targeted DB query and an
atomic upsert to avoid inefficiency and TOCTOU races: add a new helper in
chat-db.ts named findMessageByChatIdAndRole(chatId, role) that queries
db.query.messages.findFirst({ where: and(eq(messages.chatId, chatId),
eq(messages.role, role)) }) and then, in chat.ts, call that instead of
dbGetMessagesByChatId; for the create/update path use a single upsert/ON
CONFLICT (or an insert with onConflict().doUpdate) against messages (using db
and messages symbols) to insert the 'data' message if missing or update its
content atomically, and remove the ad-hoc direct-import-update fallback that
does a separate read then update.

Comment on lines +195 to +197
const { db } = await import('@/lib/db');
const { messages } = await import('@/lib/db/schema');
const { eq } = await import('drizzle-orm');
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:

# First, run the provided circular dependency check
rg -n "import.*from.*@/lib/actions/chat" --type=ts -g '!node_modules' | head -20

Repository: QueueLab/QCX

Length of output: 1313


🏁 Script executed:

# Also check what chat.ts imports from
rg -n "import.*from" lib/actions/chat.ts | head -30

Repository: QueueLab/QCX

Length of output: 481


🏁 Script executed:

# Check the structure of db module
ls -la lib/db/

Repository: QueueLab/QCX

Length of output: 349


🏁 Script executed:

# Look at the specific lines in question and surrounding context
sed -n '190,210p' lib/actions/chat.ts

Repository: QueueLab/QCX

Length of output: 906


🏁 Script executed:

# Check if there's a circular dependency by examining db files
cat lib/db/index.ts

Repository: QueueLab/QCX

Length of output: 826


🏁 Script executed:

# Check schema.ts
head -50 lib/db/schema.ts

Repository: QueueLab/QCX

Length of output: 2934


🏁 Script executed:

# Check if chat-db.ts exists and what functions it exports
rg -n "export.*function|export.*const" lib/actions/chat-db.ts | head -30

Repository: QueueLab/QCX

Length of output: 638


🏁 Script executed:

# Check the full context around the dynamic imports to understand the rationale
sed -n '180,220p' lib/actions/chat.ts

Repository: QueueLab/QCX

Length of output: 2000


Consider adding an updateMessage function to chat-db.ts instead of using dynamic imports.

The dynamic imports on lines 195–197 are a workaround for the fact that chat-db.ts doesn't export an updateMessage function. The abstraction layer (chat-db.ts) provides saveChat, createMessage, deleteChat, and getMessagesByChatId, but lacks a corresponding update operation. Rather than accessing low-level db and messages objects directly via dynamic imports, extend the chat-db.ts API to include updateMessage, then use static imports at the top of this file.

No circular dependencies exist between chat.ts and the db modules, so the dynamic imports aren't solving a dependency issue—they're bypassing an incomplete abstraction layer.

🤖 Prompt for AI Agents
In `@lib/actions/chat.ts` around lines 195 - 197, Add an updateMessage function to
chat-db.ts (beside existing saveChat, createMessage, deleteChat,
getMessagesByChatId) that accepts message id and patch data and performs the
Drizzle update using db and messages schema, then export it; in
lib/actions/chat.ts remove the dynamic imports of db, messages, and eq and
instead statically import updateMessage from chat-db.ts at the top, and replace
the dynamic-import update logic with a call to updateMessage(messageId, patch)
so the higher-level abstraction is used consistently.

interface McpResponse {
location: Location;
mapUrl?: string;
geoJson?: any;
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

geoJson?: any — consider a stricter type.

Using any for GeoJSON bypasses all type safety. At minimum, use a GeoJSON.FeatureCollection | GeoJSON.Feature | object type, or leverage @types/geojson if available.

🤖 Prompt for AI Agents
In `@lib/agents/tools/geospatial.tsx` at line 28, Replace the loose any type on
the geoJson property with a stricter GeoJSON type: import the GeoJSON types
(e.g., from the "geojson" or "@types/geojson" package) and change the
declaration of geoJson to a union such as GeoJSON.FeatureCollection |
GeoJSON.Feature | object (or the most specific GeoJSON subtypes you expect);
update any usages in functions/methods that reference geoJson to satisfy the new
type (cast/parsing where necessary) and add the import for GeoJSON to the top of
the file so the type is available for the geoJson property.

Comment on lines +420 to 428
} else if (parsedData.type === 'FeatureCollection' || parsedData.type === 'Feature') {
// Direct GeoJSON response
mcpData = {
location: {}, // Will be derived from bbox if needed, or left empty
geoJson: parsedData
};
} else {
throw new Error("Response missing required 'location' or 'results' field");
throw new Error("Response missing required 'location', 'results', or 'geoJson' field");
}
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

Direct GeoJSON path sets location: {}, which may cause issues downstream.

Line 431 accesses mcpData.location.place_name for the feedback message. With an empty location, this falls back gracefully, but any downstream consumer that expects latitude/longitude to be present when mcpData is non-null will silently produce incorrect behavior (e.g., Google Static Map URL generation on line 435 is guarded, but other consumers may not be).

Consider populating location from the GeoJSON's bounding box centroid here, or at minimum document that location may be empty when geoJson is the primary payload.

🤖 Prompt for AI Agents
In `@lib/agents/tools/geospatial.tsx` around lines 420 - 428, When parsedData.type
is 'Feature' or 'FeatureCollection' and you currently set mcpData.location = {},
compute and populate a meaningful centroid into mcpData.location instead of
leaving it empty: if parsedData.bbox exists use its [minX,minY,maxX,maxY] to
compute centroid longitude=(minX+maxX)/2 and latitude=(minY+maxY)/2; if no
top-level bbox but parsedData.features exist, derive an aggregate bbox from
feature bboxes or feature geometries and compute the centroid similarly; set
mcpData.location.latitude, mcpData.location.longitude and a readable
mcpData.location.place_name (e.g., properties.name or "GeoJSON centroid") so
downstream consumers that read mcpData.location (used elsewhere in this module)
receive coordinates when geoJson is the primary payload.

Copy link

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

The biggest concerns are runtime correctness and performance regressions: removing MapDataProvider in components/chat.tsx can crash useMapData() consumers, and adding mapData.cameraState/position to Mapbox’s initialization effect dependencies risks repeated map teardown/recreation. The autonomous geocode heuristic is overly permissive and likely to trigger unwanted geospatial calls, causing noisy UI updates and potential rate limiting. Finally, the new “single last data message” UI filter conflicts with DB logic that updates the first data message found, which can lead to UI/DB drift and hard-to-debug behavior.

Additional notes (7)
  • Maintainability | app/actions.tsx:234-234
    isGeoJsonInput is set but never used to influence downstream behavior. Right now, a pasted GeoJSON both (a) injects a geojson_upload assistant message + UI updater and (b) still pushes the full raw JSON into messageParts as normal user text. That is likely to bloat prompts and can also confuse the model (it sees huge GeoJSON text plus a separate assistant “upload” message).

This also creates a strange attribution: the upload is recorded as an assistant message, not a user action, which can affect prompting and history semantics.

  • Maintainability | app/actions.tsx:751-751
    You’re filtering UI rendering to only show the last role: 'data' message, but updateDrawingContext was also changed to update an existing role: 'data' message in DB rather than append. These two changes conflict conceptually and can break multi-chat/multi-context scenarios:

  • In DB, you now update the first data message found (find(m => m.role === 'data')), not necessarily the latest. If there are multiple historical data messages, the UI will render the last one, while the server updates an earlier one → UI/DB drift.

  • In UI, you also render contextData.uploadedGeoJson.map((item: any) => ...) using any, which defeats type safety and makes this code fragile.

This looks like it was added to fight duplication, but it needs a consistent single-source-of-truth strategy.

  • Maintainability | components/map/map-data-updater.tsx:15-62
    MapDataUpdater calls map.fitBounds(...) unconditionally if map exists, but useMap() is likely returning a Mapbox map instance. In Google Maps mode, map may be null, so the fit will never happen (OK), but you still store the uploaded GeoJSON in shared MapData, and then both Mapbox and Google map components render layers from mapData.uploadedGeoJson.

This creates an asymmetric UX: GeoJSON upload will fit bounds only for Mapbox. If that’s intended, fine; if not, you’ll need provider-specific camera fitting.

  • Performance | components/map/mapbox-map.tsx:468-471
    The initialization effect dependency array was expanded to include mapData.cameraState and position fields. If this effect is the “create map / attach listeners” effect (as it appears from the cleanup removing the map and geolocation watch), adding frequently-changing values will cause map teardown/recreation during normal operation.

That’s a serious perf regression and can reintroduce leaks or flicker.

  • Performance | lib/actions/chat.ts:165-165
    updateDrawingContext now loads all messages for the chat and finds the first role === 'data' message. This is O(n) per update and will become expensive as chats grow, especially since Chat.tsx calls this whenever map state changes.

Also, updating by importing db/messages dynamically in the hot path is unnecessary overhead and makes the action harder to test/maintain.

  • Security | lib/actions/chat-db.ts:119-136
    lib/actions/chat-db.ts upsert updates content, role, type, etc., but does not update chatId/userId/createdAt. If the same message id is accidentally reused across chats/users, the upsert will silently rewrite the message content while keeping the original chat_id association, leading to hard-to-debug cross-chat corruption.

Upsert is a good idea here, but you need to ensure ids are truly scoped/unique, or include additional conflict targets/constraints.

  • Maintainability | package.json:62-66
    drizzle-orm is bumped from ^0.29.0 to ^0.45.1 in the same PR that changes core persistence. That’s a massive semver jump and increases risk of subtle runtime differences (SQL generation, migration behavior, transaction semantics).

If this upgrade is required for .onConflictDoUpdate or other APIs, it should be isolated into its own PR or at least justified in the PR description with a quick audit of breaking changes.

Summary of changes

Summary

This diff adds autonomous map navigation and GeoJSON ingestion to the chat flow, plus schema/DB support for richer message metadata.

Server-side (app/actions.tsx)

  • Adds GeoJSON detection for pasted JSON input and uploaded .geojson / application/geo+json files, creating geojson_upload assistant messages and appending a headless <MapDataUpdater /> into the UI stream.
  • Introduces an address/coordinate heuristic to trigger geospatialTool immediately and append <MapQueryHandler /> upon MAP_QUERY_TRIGGER outputs.
  • Updates getUIStateFromAIState to:
    • Render geojson_upload messages via <MapDataUpdater />.
    • Render uploaded GeoJSON referenced in role: 'data' messages.
    • Filter to only the last role: 'data' message.

Client-side (chat + map)

  • components/chat-panel.tsx: expands file input accept list to include GeoJSON and adds data-testid.
  • components/chat.tsx: improves refresh guard logic, syncs map context (including uploadedGeoJson) back to server via updateDrawingContext, and removes MapDataProvider wrappers from layouts.
  • components/map/*:
    • Adds new components/map/map-data-updater.tsx to push GeoJSON into MapData context and fit bounds using Turf.
    • Renders uploaded GeoJSON layers in both Mapbox and Google map implementations.
    • Extends geospatial tool output handling to propagate geoJson data.

Persistence / DB

  • Adds migration to create calendar_notes and add attachments, tool_name, tool_call_id, type columns to messages.
  • Updates Drizzle schema to include those columns.
  • Changes chat message saving to upsert on messages.id.
  • Updates app/search/[id]/page.tsx mapping to include type and toolName.

Dependencies

  • Bumps drizzle-orm to ^0.45.1.

Comment on lines +452 to +492
// Autonomous Map Navigation: Check if input looks like an address
const isPotentialAddress = (text: string) => {
// Simple heuristic: contains numbers and multiple words, or specific keywords
const addressKeywords = ['palace', 'street', 'st', 'avenue', 'ave', 'road', 'rd', 'boulevard', 'blvd', 'drive', 'dr', 'lane', 'ln', 'court', 'ct', 'square', 'sq', 'parkway', 'pkwy'];
const words = text.toLowerCase().split(/\s+/);
const hasNumber = /\d+/.test(text);
const hasKeyword = words.some(word => addressKeywords.includes(word));
const isCoordinate = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/.test(text.trim());

return (hasNumber && words.length >= 2) || hasKeyword || isCoordinate;
};

if (userInput && isPotentialAddress(userInput)) {
console.log('[AutonomousMap] Detected potential address:', userInput);
// Trigger geospatial tool directly for immediate map update
const geoTool = geospatialTool({ uiStream, mapProvider });

// Run geocoding in the background or wait briefly
// We don't await it here to avoid blocking the main AI response,
// but we want it to start immediately.
(async () => {
try {
const isCoordinate = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/.test(userInput.trim());
const result = await geoTool.execute(
isCoordinate
? { queryType: 'reverse', coordinates: {
latitude: parseFloat(userInput.split(',')[0]),
longitude: parseFloat(userInput.split(',')[1])
} }
: { queryType: 'geocode', location: userInput }
);

if (result && result.type === 'MAP_QUERY_TRIGGER') {
uiStream.append(<MapQueryHandler toolOutput={result} />);
}
} catch (error) {
console.error('[AutonomousMap] Quick geocode failed:', error);
}
})();
}

Choose a reason for hiding this comment

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

Autonomous geocoding is triggered for any input matching the heuristic, including coordinates, but the heuristic is extremely permissive (e.g., any message containing a number and two words; keywords like st will match many non-address sentences). This will cause lots of unintended geocoding calls and UI side effects.

Additionally:

  • It will also trigger when the user pastes GeoJSON (which commonly includes numbers/keywords), unless the heuristic is explicitly gated.
  • The background task uses console.log/error but doesn’t guard against uiStream lifecycle (e.g., user navigates away) and may append late handlers.
  • You compute isCoordinate twice using the same regex; the parsing uses split(',') without trimming, which is brittle for inputs like "12.3, -45.6".

Net: this feature risks noisy calls, rate limiting, and surprising UI updates.

Suggestion

Tighten and gate the autonomous navigation:

  • Skip auto-geocode when the input is detected as GeoJSON (pasted or uploaded).
  • Use a stricter address heuristic (e.g., require ≥3 words and either a street-type keyword or a postal code/city pattern) and add a minimum length.
  • De-duplicate coordinate parsing:
    • Parse once with a single regex capture and .trim() the parts.
  • Consider debouncing/limiting triggers (e.g., only for direct user input, not related_query; and only once per message id).

If you want, I can add a commit that:

  1. adds a shouldAutonav guard (not GeoJSON, not too short, not tool actions),
  2. centralizes coordinate parse,
  3. reduces false positives.

Reply with "@CharlieHelps yes please" if you’d like me to add that commit.

Comment on lines +857 to +863
case 'geojson_upload': {
const { data, filename } = JSON.parse(content as string)
return {
id,
component: <MapDataUpdater id={id} data={data} filename={filename} />
}
}

Choose a reason for hiding this comment

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

This adds a new geojson_upload case that does JSON.parse(content as string) without any try/catch. A single malformed message content (from older DB rows, partial writes, or tool output changes) will throw and break the entire UI state mapping for the chat.

You already wrap parsing in other branches (e.g. role: 'tool', role: 'data'). This one should be equally defensive.

Suggestion

Wrap the geojson_upload parsing in a try/catch and return { id, component: null } on failure (optionally console.error with message id).

case 'geojson_upload': {
  try {
    const { data, filename } = JSON.parse(content as string)
    return { id, component: <MapDataUpdater id={id} data={data} filename={filename} /> }
  } catch (e) {
    console.error('Error parsing geojson_upload message:', id, e)
    return { id, component: null }
  }
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +15 to +62
export function MapDataUpdater({ id, data, filename }: MapDataUpdaterProps) {
const { setMapData } = useMapData();
const { map } = useMap();
const hasZoomedRef = useRef(false);

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,
data: featureCollection,
visible: true
}
]
};
});

// Fly to the extent of the GeoJSON
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], {
padding: 50,
maxZoom: 15,
duration: 2000
});
} catch (e) {
console.error('Failed to fit bounds for GeoJSON:', e);
}
}
}, [id, data, filename, setMapData, map]);

Choose a reason for hiding this comment

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

MapDataUpdater fits bounds once per component instance (hasZoomedRef). When replaying chat history, you may mount multiple updaters (one per uploaded dataset) and each will call fitBounds (once) causing repeated camera jumps on load. This will feel like the map is "fighting itself".

Also, id is used to dedupe inserts, but when MapQueryHandler uses toolOutput.timestamp as the id, two triggers within the same millisecond (or server-generated timestamps with low precision) could collide and incorrectly drop layers.

Suggestion

Introduce a global/centralized zoom policy instead of per-layer zoom-once:

  • Only auto-zoom for the most recently added dataset (e.g. compare against prev.uploadedGeoJson.length and zoom when you append).
  • Or add a shouldZoom prop and only set it for the newly uploaded dataset.

Also, prefer stable unique ids for tool-derived GeoJSON (e.g. nanoid() generated at the call site) rather than timestamp.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +81 to +91
return (
<>
{toolOutput?.mcp_response?.geoJson && (
<MapDataUpdater
id={toolOutput.timestamp}
data={toolOutput.mcp_response.geoJson}
filename={toolOutput.mcp_response.location?.place_name || 'Tool Result'}
/>
)}
</>
);

Choose a reason for hiding this comment

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

MapQueryHandler now returns a fragment containing <MapDataUpdater ... /> for tool-provided GeoJSON, using toolOutput.timestamp as the id. If multiple tool calls happen within the same timestamp granularity (or if timestamp repeats across sessions), you can collide IDs and dedup logic in MapDataUpdater will prevent later updates.

Also, using timestamp as a stable ID makes it hard to “replace”/refresh layers for the same location.

Suggestion

Use a deterministic-but-unique ID for tool-driven layers:

  • Prefer tool_call_id (if available) or a generated UUID/nanoid passed through tool output.
  • Or derive an ID from (originalUserInput + timestamp) if you must.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that uses a safer ID (and threads tool call id through where available).

@charliecreates charliecreates bot removed the request for review from CharlieHelps February 15, 2026 15:31
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.

3 participants