Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6eeae28
Integrate billing popup, usage sidebar, and credit preview toggle
CJWTRUST Jan 20, 2026
160e082
Recover lost commit 488b47c and restore branch head
google-labs-jules[bot] Jan 28, 2026
ae085cf
Merge pull request #456 from QueueLab/jules-16847885310673003402-db9c…
ngoiyaeric Jan 28, 2026
73c385c
Restore branch history to 488b47c and integrate latest fixes
google-labs-jules[bot] Jan 28, 2026
1508c04
Restore branch history to 488b47c and resolve build errors
google-labs-jules[bot] Jan 28, 2026
c8c028b
Restore branch history, fix build errors, and re-enable pricing popup
google-labs-jules[bot] Jan 28, 2026
e2c0615
Finalize recovery with performance optimizations and UI fixes
google-labs-jules[bot] Jan 28, 2026
43179e7
Synchronize with main, restore history, and optimize performance
google-labs-jules[bot] Jan 29, 2026
7d45e02
Finalize recovery with prioritized main branch code and usage UI
google-labs-jules[bot] Jan 29, 2026
5623831
Refine usage UI and Lottie animation visibility
google-labs-jules[bot] Jan 29, 2026
44e86b6
Complete recovery, synchronization with main, and usage UI integration
google-labs-jules[bot] Jan 29, 2026
0a48018
feat: recover branch state, optimize resolution search, and fix UI st…
google-labs-jules[bot] Jan 29, 2026
6acfbe5
fix: resolve build error and ESLint warnings
google-labs-jules[bot] Jan 29, 2026
8ae549a
Update LICENSE
ngoiyaeric Feb 1, 2026
3495da1
Update LICENSE
ngoiyaeric Feb 1, 2026
ba36e58
Merge pull request #469 from QueueLab/ngoiyaeric-patch-4
ngoiyaeric Feb 1, 2026
6431b41
fix: reduce padding in mobile chat input area
google-labs-jules[bot] Feb 1, 2026
8a04d49
Merge pull request #470 from QueueLab/fix/mobile-chat-input-padding-1…
ngoiyaeric Feb 1, 2026
10ab3fe
feat: update Stripe checkout links to new URL
google-labs-jules[bot] Feb 1, 2026
250283e
Merge pull request #471 from QueueLab/update-stripe-links-17354207767…
ngoiyaeric Feb 1, 2026
1b56819
feat: implement GeoJSON upload and tool ingestion pipeline
google-labs-jules[bot] Feb 1, 2026
9f79929
feat: implement GeoJSON upload and tool ingestion pipeline
google-labs-jules[bot] Feb 1, 2026
6554775
fix: ensure long sentences wrap to the next line across UI components
google-labs-jules[bot] Feb 1, 2026
bfe62f9
feat: support pasted GeoJSON and fix refresh loop
google-labs-jules[bot] Feb 1, 2026
9450b15
feat: enable GeoJSON uploads, paste support, and fix refresh loop
google-labs-jules[bot] Feb 1, 2026
e95a25b
Merge pull request #473 from QueueLab/fix/text-overflow-wrapping-4739…
ngoiyaeric Feb 1, 2026
f80c87f
Resolve merge conflicts and synchronize with main
CJWTRUST Feb 2, 2026
c4278e9
feat: final stability and performance improvements
google-labs-jules[bot] Feb 2, 2026
813d264
Resolve merge conflicts and synchronize with main, preserving branch …
google-labs-jules[bot] Feb 2, 2026
c868dcd
Merge branch 'feature/billing-integration-recovery-648258468266820429…
ngoiyaeric Feb 2, 2026
3b5ed27
Resolve merge conflicts: synchronization with main while preserving b…
google-labs-jules[bot] Feb 2, 2026
3984b9b
Restore missing features from commit c4278e9: tenttree usage UI, popu…
CJWTRUST Feb 2, 2026
f45f687
Merge branch 'main' into jules-8488824498232079115-26d4e4cd
ngoiyaeric Feb 2, 2026
67c26d5
Merge pull request #476 from QueueLab/jules-8488824498232079115-26d4e4cd
ngoiyaeric Feb 2, 2026
a842df1
chore: update stripe payment links
google-labs-jules[bot] Feb 2, 2026
184f678
Merge pull request #477 from QueueLab/update-stripe-links-20260202-15…
ngoiyaeric Feb 2, 2026
895bf37
Update UsageView to yearly refresh model
google-labs-jules[bot] Feb 2, 2026
dc345b9
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
86013ed
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
2ba1f9e
Merge pull request #479 from QueueLab/update-usage-view-to-yearly-ref…
ngoiyaeric Feb 2, 2026
dda7a32
fix: standardize header icon spacing and remove unused portals
google-labs-jules[bot] Feb 2, 2026
23a1d3f
Merge pull request #480 from QueueLab/fix-header-icon-spacing-1628392…
ngoiyaeric Feb 2, 2026
885dbbe
feat: make zoom controls conditional on drawing mode
google-labs-jules[bot] Feb 2, 2026
dd812c1
Merge pull request #482 from QueueLab/conditional-zoom-controls-22256…
ngoiyaeric Feb 2, 2026
00c2e1a
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
54d9d6e
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
c9bd9b2
Merge pull request #484 from QueueLab/ngoiyaeric-patch-5
ngoiyaeric Feb 2, 2026
85a85c5
Merge branch 'main' into feature/geojson-upload-pipeline-111249932693…
ngoiyaeric Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2024 Yoshiki Miura
Copyright 2025 Eric Ngoiya

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
61 changes: 60 additions & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { BotMessage } from '@/components/message'
import { SearchSection } from '@/components/search-section'
import SearchRelated from '@/components/search-related'
import { GeoJsonLayer } from '@/components/map/geojson-layer'
import { MapDataUpdater } from '@/components/map/map-data-updater'
import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
Expand Down Expand Up @@ -315,8 +316,39 @@ async function submit(formData?: FormData, skip?: boolean) {
image: dataUrl,
mimeType: file.type
})
} else if (file.type === 'text/plain') {
} 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'
}
]
})
Comment on lines +359 to +373
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 validating GeoJSON structure more thoroughly.

The current validation only checks geoJson.type. Invalid GeoJSON (e.g., a FeatureCollection with malformed features) could still pass through and cause rendering issues downstream. Consider using a lightweight validation:

🛡️ Optional: Add feature validation
           if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') {
+            // Basic validation for FeatureCollection
+            if (geoJson.type === 'FeatureCollection' && !Array.isArray(geoJson.features)) {
+              console.warn('Invalid FeatureCollection: missing features array')
+              return
+            }
             const geoJsonId = nanoid()
📝 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
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'
}
]
})
if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') {
// Basic validation for FeatureCollection
if (geoJson.type === 'FeatureCollection' && !Array.isArray(geoJson.features)) {
console.warn('Invalid FeatureCollection: missing features array')
return
}
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'
}
]
})
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 326 - 340, The current branch that creates a
geojson upload message (around geoJson/type check, nanoid generation and
aiState.update that pushes a message with type 'geojson_upload' and content from
file.name) should validate the GeoJSON more thoroughly before updating aiState:
implement or call a lightweight validator (e.g., a new validateGeoJSON function
or an is-geojson/geojson-validation library) to verify
FeatureCollection.features is an array and each Feature has valid properties
(type==='Feature', geometry with type and coordinates) and that
FeatureCollection has valid bbox/crs if present; if validation fails, avoid
pushing the message, log the error (or surface a user-facing error) and return
early. Ensure you reference/replace the simple geoJson.type check with this
validator in the block that generates geoJsonId and calls aiState.update so only
validated GeoJSON gets stored as type 'geojson_upload'.


// Immediately append the updater to the UI stream
uiStream.append(
<MapDataUpdater id={geoJsonId} data={geoJson} filename={file.name} />
)
}
Comment on lines +360 to +379
Copy link

Choose a reason for hiding this comment

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

Server action appends a React client component into the UI stream

submit (a server action) is directly appending <MapDataUpdater /> into uiStream. That means a server-side path is deciding to render a client component as a side effect of file parsing.

This is fragile for a few reasons:

  • It tightly couples server parsing with a specific client synchronization mechanism.
  • It risks odd replay behavior (e.g., streaming replays, retries) causing multiple MapDataUpdater mounts.
  • It makes the UI update dependent on server streaming rather than the persisted AI state reconstruction (which you already added via geojson_upload in getUIStateFromAIState).

Given you already insert a geojson_upload message into aiState, UI reconstruction can naturally render the updater from state without also pushing it immediately.

Suggestion

Remove the immediate uiStream.append(<MapDataUpdater ... />) and rely on the geojson_upload message being rendered via getUIStateFromAIState.

If you need instant feedback before the assistant message is produced, consider appending a lightweight server-renderable placeholder (e.g., “Uploaded X”) and let the client handle map updates based on AI state.

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

} catch (e) {
console.error('Failed to parse GeoJSON:', e)
}
}
Comment on lines +352 to +383
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

Silent error handling may confuse users when GeoJSON parsing fails.

When JSON.parse(textContent) fails on Line 325, the error is only logged to the console. The file content still gets prepended to the message (lines 352-357), which may lead to confusing AI responses. Consider providing user feedback:

🛡️ Proposed improvement
         } catch (e) {
           console.error('Failed to parse GeoJSON:', e)
+          // Optionally notify the user
+          uiStream.append(
+            <BotMessage content={createStreamableValue(`⚠️ Failed to parse ${file.name} as valid GeoJSON. The file will be processed as plain text.`).value} />
+          )
         }
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 319 - 350, The GeoJSON parse failure is only
console.logged and the raw file content may still be prepended to messages;
modify the catch block around JSON.parse(textContent) to (1) prevent adding the
MapDataUpdater/uiStream entry or the geojson_upload assistant message when
parsing fails and (2) surface a user-facing error via aiState.update (add an
assistant or system message with id like geoJsonId or similar explaining the
file failed to parse) and optionally append a UI error component instead of
MapDataUpdater; reference the existing symbols JSON.parse, geoJsonId,
aiState.update, uiStream.append, and MapDataUpdater to locate and change
behavior.


const existingTextPart = messageParts.find(p => p.type === 'text')
if (existingTextPart) {
existingTextPart.text = `${textContent}\n\n${existingTextPart.text}`
Expand Down Expand Up @@ -716,6 +748,13 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
)
}
}
case 'geojson_upload': {
const { data, filename } = JSON.parse(content as string)
return {
id,
component: <MapDataUpdater id={id} data={data} filename={filename} />
}
}
Comment on lines +795 to +801
Copy link

Choose a reason for hiding this comment

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

Unvalidated JSON.parse for geojson_upload can crash UI rendering

getUIStateFromAIState parses content for geojson_upload without a try/catch. Any malformed content (db corruption, older message format, partial writes) will throw and can break UI state mapping.

You already wrap parsing for other message kinds; this one should be consistent.

Suggestion

Wrap the JSON.parse in a try/catch (similar to the tool and data cases) and return { id, component: null } on failure.

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

Comment on lines +795 to +801
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

Missing error handling for malformed geojson_upload content.

The JSON.parse call on line 781 will throw if content is malformed, potentially breaking the entire UI state reconstruction. Other similar cases in this function (e.g., tool role at line 790) wrap parsing in try-catch.

🛡️ Proposed fix
             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 content:', e)
+                return { id, component: null }
+              }
             }
📝 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
case 'geojson_upload': {
const { data, filename } = JSON.parse(content as string)
return {
id,
component: <MapDataUpdater id={id} data={data} filename={filename} />
}
}
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 content:', e)
return { id, component: null }
}
}
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 780 - 786, The JSON.parse in the
'geojson_upload' case can throw for malformed content; wrap the parse of content
in a try-catch (the case handling that returns component: <MapDataUpdater ...
/>) similar to the other role branches: on success return the existing object
with id and component: <MapDataUpdater id={id} data={data} filename={filename}
/>, on parse failure catch the error, log or report it, and return a safe
fallback entry (e.g., an object with id and a lightweight error/display
component or null component) so UI state reconstruction doesn't crash; reference
the 'geojson_upload' case, JSON.parse(content as string), and MapDataUpdater
when applying the fix.

}
break
case 'tool':
Expand Down Expand Up @@ -775,6 +814,26 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
}
}
break
case 'data':
try {
const contextData = JSON.parse(content as string)
if (contextData.uploadedGeoJson && Array.isArray(contextData.uploadedGeoJson)) {
return {
id,
component: (
<>
{contextData.uploadedGeoJson.map((item: any) => (
<MapDataUpdater key={item.id} id={item.id} data={item.data} filename={item.filename} />
))}
</>
)
}
}
return { id, component: null }
} catch (e) {
console.error('Error parsing data message:', e)
return { id, component: null }
}
Comment on lines +863 to +882
Copy link

Choose a reason for hiding this comment

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

data message rendering uses any and replays side effects without guarding

In the data case you iterate uploadedGeoJson.map((item: any) => <MapDataUpdater ... />). Two concerns:

  1. Using any here makes it easy to introduce unsafe shapes that still compile. This is UI-state hydration code; it should be strict.
  2. Rendering many MapDataUpdaters will attempt fitBounds for each layer; on chat reload this can cause multiple sequential camera jumps. The current MapDataUpdater only de-dupes by id for inserting into context, but it does not de-dupe the fitBounds side effect. So reload may result in the map fitting to the last item every time, even if the user previously set a different view.
Suggestion
  1. Replace any with a minimal structural type (e.g., { id: string; data: unknown; filename: string }) and validate required fields before rendering.

  2. Add a flag to MapDataUpdater like shouldFlyTo?: boolean and set it to false when replaying persisted uploadedGeoJson from a data message, or add internal logic to only fitBounds on first-ever insert (e.g., only when it was newly added to context).

Reply with "@CharlieHelps yes please" if you'd like me to add a commit implementing shouldFlyTo and tightening the hydration type.

default:
return {
id,
Expand Down
57 changes: 1 addition & 56 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@

.mobile-chat-input-area {
height: auto;
padding: 10px;
padding: 4px 10px;
background-color: hsl(var(--background));
/* border-top: 1px solid hsl(var(--border)); */ /* Removed for cleaner separation */
border-bottom: 1px solid hsl(var(--border)); /* Added for separation from messages area below */
Expand All @@ -199,61 +199,6 @@
align-items: center;
}

.mobile-chat-input {
/* position: relative; */ /* No longer fixed to bottom */
/* bottom: 0; */
/* left: 0; */ /* Handled by parent flex */
/* right: 0; */ /* Handled by parent flex */
width: 100%; /* Ensure it takes full width of its container */
padding: 10px;
background-color: hsl(var(--background));
/* border-top: 1px solid hsl(var(--border)); */ /* Removed to avoid double border */
/* z-index: 30; */ /* No longer needed */
}

.mobile-chat-input input {
width: 100%;
padding: 8px;
border: 1px solid hsl(var(--input));
border-radius: var(--radius);
background-color: hsl(var(--input));
color: hsl(var(--foreground));
box-sizing: border-box;
}

.mobile-icons-bar-content .icon-button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
cursor: pointer;
}

.mobile-icons-bar-content .icon-button:hover {
background-color: hsl(var(--secondary-foreground));
color: hsl(var(--secondary));
}

.mobile-chat-input .icon-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: transparent;
border: none;
cursor: pointer;
}

.mobile-chat-input .icon-button.paperclip {
right: 40px;
}

.mobile-chat-input .icon-button.arrow-right {
right: 10px;
}
}

/* Added for MapboxDraw controls */
Expand Down
14 changes: 7 additions & 7 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,12 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
onSubmit={handleSubmit}
className={cn(
'max-w-full w-full',
isMobile ? 'px-2 pb-2 pt-1 h-full flex flex-col justify-center' : ''
isMobile ? 'px-2 pb-1 pt-0 h-full flex flex-col justify-center' : ''
)}
>
<div
className={cn(
'relative flex items-start w-full',
isMobile && 'mobile-chat-input' // Apply mobile chat input styling
'relative flex items-start w-full'
)}
>
<input type="hidden" name="mapProvider" value={mapProvider} />
Expand All @@ -214,7 +213,8 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="text/plain,image/png,image/jpeg,image/webp"
accept="text/plain,image/png,image/jpeg,image/webp,.geojson,application/geo+json"
data-testid="file-upload-input"
/>
{!isMobile && (
<Button
Expand All @@ -241,10 +241,10 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
value={input}
data-testid="chat-input"
className={cn(
'resize-none w-full min-h-12 rounded-fill border border-input pl-14 pr-12 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'resize-none w-full rounded-fill border border-input pr-12 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
isMobile
? 'mobile-chat-input input bg-background'
: 'bg-muted'
? 'bg-background min-h-10 pl-4 pt-2 pb-1'
: 'bg-muted min-h-12 pl-14 pt-3 pb-1'
)}
onChange={e => {
setInput(e.target.value)
Expand Down
11 changes: 6 additions & 5 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,17 @@ export function Chat({ id }: ChatProps) {
}
}, [isSubmitting])

// useEffect to call the server action when drawnFeatures changes
// useEffect to call the server action when drawnFeatures or uploadedGeoJson changes
useEffect(() => {
if (id && mapData.drawnFeatures && mapData.cameraState) {
console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures);
if (id && (mapData.drawnFeatures || mapData.uploadedGeoJson) && mapData.cameraState) {
console.log('Chat.tsx: map data changed, calling updateDrawingContext');
updateDrawingContext(id, {
drawnFeatures: mapData.drawnFeatures,
drawnFeatures: mapData.drawnFeatures || [],
cameraState: mapData.cameraState,
uploadedGeoJson: mapData.uploadedGeoJson || []
});
}
}, [id, mapData.drawnFeatures, mapData.cameraState]);
}, [id, mapData.drawnFeatures, mapData.cameraState, mapData.uploadedGeoJson]);
Comment on lines 99 to 118
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

Condition may trigger excessive database writes.

The condition (mapData.drawnFeatures || mapData.uploadedGeoJson) && mapData.cameraState will always evaluate to true once cameraState is set, because drawnFeatures and uploadedGeoJson are initialized as empty arrays (truthy values).

This means updateDrawingContext will be called on every camera state change (pan, zoom, etc.), even when there are no actual drawn features or uploaded GeoJSON to persist.

🐛 Suggested fix to only save when there's actual data
   // useEffect to call the server action when drawnFeatures or uploadedGeoJson changes
   useEffect(() => {
-    if (id && (mapData.drawnFeatures || mapData.uploadedGeoJson) && mapData.cameraState) {
+    const hasDrawnFeatures = mapData.drawnFeatures && mapData.drawnFeatures.length > 0;
+    const hasUploadedGeoJson = mapData.uploadedGeoJson && mapData.uploadedGeoJson.length > 0;
+    
+    if (id && (hasDrawnFeatures || hasUploadedGeoJson) && mapData.cameraState) {
       console.log('Chat.tsx: map data changed, calling updateDrawingContext');
       updateDrawingContext(id, {
         drawnFeatures: mapData.drawnFeatures || [],
         cameraState: mapData.cameraState,
         uploadedGeoJson: mapData.uploadedGeoJson || []
       });
     }
   }, [id, mapData.drawnFeatures, mapData.cameraState, mapData.uploadedGeoJson]);
📝 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
// useEffect to call the server action when drawnFeatures or uploadedGeoJson changes
useEffect(() => {
if (id && mapData.drawnFeatures && mapData.cameraState) {
console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures);
if (id && (mapData.drawnFeatures || mapData.uploadedGeoJson) && mapData.cameraState) {
console.log('Chat.tsx: map data changed, calling updateDrawingContext');
updateDrawingContext(id, {
drawnFeatures: mapData.drawnFeatures,
drawnFeatures: mapData.drawnFeatures || [],
cameraState: mapData.cameraState,
uploadedGeoJson: mapData.uploadedGeoJson || []
});
}
}, [id, mapData.drawnFeatures, mapData.cameraState]);
}, [id, mapData.drawnFeatures, mapData.cameraState, mapData.uploadedGeoJson]);
// useEffect to call the server action when drawnFeatures or uploadedGeoJson changes
useEffect(() => {
const hasDrawnFeatures = mapData.drawnFeatures && mapData.drawnFeatures.length > 0;
const hasUploadedGeoJson = mapData.uploadedGeoJson && mapData.uploadedGeoJson.length > 0;
if (id && (hasDrawnFeatures || hasUploadedGeoJson) && mapData.cameraState) {
console.log('Chat.tsx: map data changed, calling updateDrawingContext');
updateDrawingContext(id, {
drawnFeatures: mapData.drawnFeatures || [],
cameraState: mapData.cameraState,
uploadedGeoJson: mapData.uploadedGeoJson || []
});
}
}, [id, mapData.drawnFeatures, mapData.cameraState, mapData.uploadedGeoJson]);
🤖 Prompt for AI Agents
In `@components/chat.tsx` around lines 92 - 102, The effect's condition always
becomes true once cameraState exists because arrays are truthy; change the
condition in the useEffect that calls updateDrawingContext so it only runs when
id exists, cameraState exists, and at least one of mapData.drawnFeatures or
mapData.uploadedGeoJson contains items (check .length > 0 or truthy length via
optional chaining), e.g., use mapData.drawnFeatures?.length > 0 ||
mapData.uploadedGeoJson?.length > 0 before calling updateDrawingContext(id,
...); keep the same payload shape and dependency array ([id,
mapData.drawnFeatures, mapData.cameraState, mapData.uploadedGeoJson]).

Comment on lines 99 to 118
Copy link

Choose a reason for hiding this comment

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

This effect logs every map-data change and triggers a server action whenever uploadedGeoJson changes. Because uploadedGeoJson elements contain full feature collections, any update that changes object identity (even if semantically the same) can cause repeated writes.

Also, sending full GeoJSON on every change is likely heavy. If MapDataUpdater adds one layer, this effect will upload the entire uploadedGeoJson array back to the server each time.

Suggestion

Reduce redundant writes and payload size:

  • Remove the console.log (or gate it behind a debug flag).
  • Persist only deltas (the newly added layer) instead of the whole array, or persist only IDs + references if you already store the GeoJSON elsewhere.
  • If you keep this approach, debounce the updateDrawingContext call (e.g., 300–500ms) and/or compute a stable hash of uploadedGeoJson to avoid writes on identity-only changes.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing a debounce + change detection.


// Mobile layout
if (isMobile) {
Expand Down
4 changes: 2 additions & 2 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const Header = () => {

<div id="header-search-portal" />

<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
<a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Expand All @@ -67,7 +67,7 @@ export const Header = () => {
{/* Mobile menu buttons */}
<div className="flex md:hidden gap-2">

<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
<a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Expand Down
3 changes: 3 additions & 0 deletions components/map/google-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export function GoogleMapComponent() {
mode="SATELLITE"
/>
<GoogleGeoJsonLayer data={featureCollection} />
{mapData.uploadedGeoJson?.map(item => (
item.visible && <GoogleGeoJsonLayer key={item.id} data={item.data} />
))}
Comment on lines +84 to +86
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

Conditional rendering inside .map() can return false to the array.

When item.visible is falsy, the expression item.visible && <GoogleGeoJsonLayer ... /> returns false, which React will include in the rendered output (though it won't display anything). For cleaner behavior, filter before mapping or use a ternary with null:

♻️ Suggested refactor
-      {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} />
+        ))}
📝 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
{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} />
))}
🤖 Prompt for AI Agents
In `@components/map/google-map.tsx` around lines 84 - 86, The map callback
currently uses a logical && which can push false values into the JSX array;
update the rendering of mapData.uploadedGeoJson so you either filter out
invisible items first (e.g., uploadedGeoJson.filter(item =>
item.visible).map(item => <GoogleGeoJsonLayer key={item.id} data={item.data}
/>)) or use a ternary to return null for invisible items (item.visible ?
<GoogleGeoJsonLayer key={item.id} data={item.data} /> : null); apply this change
where mapData.uploadedGeoJson is iterated to ensure only valid React nodes (no
false values) are returned.

</APIProvider>
)
}
12 changes: 11 additions & 1 deletion components/map/map-data-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export interface MapData {
longitude: number;
title?: string;
}>;
uploadedGeoJson?: Array<{
id: string;
filename: string;
data: any; // FeatureCollection
visible: boolean;
}>;
Comment on lines +32 to +37
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 data as FeatureCollection for type safety.

The data: any type loses type safety. Based on the GeoJSON layer components (e.g., GeoJsonLayerProps in geojson-layer.tsx), this should be a GeoJSON FeatureCollection.

♻️ Suggested type improvement
+import type { FeatureCollection } from 'geojson';
+
 // ... in MapData interface
   uploadedGeoJson?: Array<{
     id: string;
     filename: string;
-    data: any; // FeatureCollection
+    data: FeatureCollection;
     visible: boolean;
   }>;
📝 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
uploadedGeoJson?: Array<{
id: string;
filename: string;
data: any; // FeatureCollection
visible: boolean;
}>;
uploadedGeoJson?: Array<{
id: string;
filename: string;
data: FeatureCollection;
visible: boolean;
}>;
🤖 Prompt for AI Agents
In `@components/map/map-data-context.tsx` around lines 32 - 37, The
uploadedGeoJson entry uses data: any which loses type safety; update the type in
map-data-context.tsx to use GeoJSON's FeatureCollection (import
FeatureCollection from 'geojson' or the project's GeoJSON types) so
uploadedGeoJson?: Array<{ id: string; filename: string; data: FeatureCollection;
visible: boolean; }>; reference the existing GeoJsonLayerProps in
geojson-layer.tsx to ensure the FeatureCollection generic matches the layer's
expected geometry/props types and adjust imports accordingly.

Comment on lines +32 to +37
Copy link

Choose a reason for hiding this comment

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

uploadedGeoJson is typed as data: any in context and MapDataUpdaterProps, and geoJson?: any in tool output. Since this pipeline crosses persistence boundaries and executes viewport operations, this is a good place to tighten runtime validation and narrow types.

Even a lightweight validation (checking type and features array) would prevent downstream runtime errors and reduce the surface area for malicious/accidental payloads.

Suggestion

Replace any with Feature | FeatureCollection (from geojson) and add a small runtime validator at ingestion points (upload + tool) that rejects anything else.

Example types:

import type { Feature, FeatureCollection } from 'geojson'

data: FeatureCollection | Feature

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that tightens types and adds a shared isGeoJsonFeatureOrCollection() helper.

}

interface MapDataContextType {
Expand All @@ -39,7 +45,11 @@ interface MapDataContextType {
const MapDataContext = createContext<MapDataContextType | undefined>(undefined);

export const MapDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [mapData, setMapData] = useState<MapData>({ drawnFeatures: [], markers: [] });
const [mapData, setMapData] = useState<MapData>({
drawnFeatures: [],
markers: [],
uploadedGeoJson: []
});

return (
<MapDataContext.Provider value={{ mapData, setMapData }}>
Expand Down
63 changes: 63 additions & 0 deletions components/map/map-data-updater.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client';

import { useEffect } from 'react';
import { useMapData } from './map-data-context';
import { useMap } from './map-context';
import type { FeatureCollection } from 'geojson';
import * as turf from '@turf/turf';

interface MapDataUpdaterProps {
id: string;
data: any; // FeatureCollection or Feature
filename: string;
}
Comment on lines +9 to +13
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 stronger typing for the data prop.

Using any here loses type safety. Consider using a union type to be explicit about expected inputs:

+import type { Feature } from 'geojson';
+
 interface MapDataUpdaterProps {
   id: string;
-  data: any; // FeatureCollection or Feature
+  data: FeatureCollection | Feature | null;
   filename: string;
 }
🤖 Prompt for AI Agents
In `@components/map/map-data-updater.tsx` around lines 9 - 13, The
MapDataUpdaterProps interface uses a loose any for the data prop; change it to
an explicit GeoJSON union (e.g., GeoJSON.FeatureCollection | GeoJSON.Feature) by
importing the GeoJSON types and replacing the data: any declaration in
MapDataUpdaterProps with the appropriate union type; also update any usages of
the data prop (in the MapDataUpdater component and callers) to satisfy the
stronger type (narrow with type guards where you expect Feature vs
FeatureCollection).


export function MapDataUpdater({ id, data, filename }: MapDataUpdaterProps) {
const { setMapData } = useMapData();
const { map } = useMap();

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

// Ensure it's a FeatureCollection for consistency
const featureCollection: FeatureCollection = data.type === 'FeatureCollection'
? data
: { type: 'FeatureCollection', features: [data] };
Comment on lines +23 to +26
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

Potential silent failure with invalid GeoJSON data.

The normalization logic assumes data is either a FeatureCollection or a valid Feature. If data has an unexpected structure (e.g., data.type is "Point" or "Polygon" — raw geometries rather than Features), wrapping it as { type: 'FeatureCollection', features: [data] } will produce an invalid FeatureCollection that may cause downstream rendering issues.

Consider adding validation:

🛡️ Proposed validation
+    // Validate that data is a Feature or FeatureCollection
+    if (data.type !== 'FeatureCollection' && data.type !== 'Feature') {
+      console.warn('MapDataUpdater: Invalid GeoJSON type, expected Feature or FeatureCollection:', data.type);
+      return;
+    }
+
     // Ensure it's a FeatureCollection for consistency
     const featureCollection: FeatureCollection = data.type === 'FeatureCollection'
       ? data
       : { type: 'FeatureCollection', features: [data] };
📝 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
// Ensure it's a FeatureCollection for consistency
const featureCollection: FeatureCollection = data.type === 'FeatureCollection'
? data
: { type: 'FeatureCollection', features: [data] };
// Validate that data is a Feature or FeatureCollection
if (data.type !== 'FeatureCollection' && data.type !== 'Feature') {
console.warn('MapDataUpdater: Invalid GeoJSON type, expected Feature or FeatureCollection:', data.type);
return;
}
// Ensure it's a FeatureCollection for consistency
const featureCollection: FeatureCollection = data.type === 'FeatureCollection'
? data
: { type: 'FeatureCollection', features: [data] };
🤖 Prompt for AI Agents
In `@components/map/map-data-updater.tsx` around lines 22 - 25, The normalization
blindly treats non-FeatureCollection inputs as Features and can produce invalid
GeoJSON; update the logic around the featureCollection variable to validate the
incoming data.type: if data.type === 'FeatureCollection' keep it, if data.type
=== 'Feature' use it directly, if data.type is a GeoJSON Geometry type (e.g.,
'Point','Polygon','LineString', etc.) wrap it in a Feature object ({ type:
'Feature', geometry: data, properties: {} }), and otherwise handle invalid input
by logging an error via the same logger and returning or falling back to an
empty FeatureCollection ({ type: 'FeatureCollection', features: [] }) so
downstream consumers only ever receive a valid FeatureCollection of Feature
objects. Ensure this uses the same variable names (featureCollection, data) so
the change is easy to locate.


// 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) {
try {
const bbox = turf.bbox(featureCollection);
map.fitBounds(bbox as [number, number, number, number], {
padding: 50,
maxZoom: 15,
duration: 2000
});
Comment on lines 48 to 57
Copy link

Choose a reason for hiding this comment

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

MapDataUpdater uses map.fitBounds(bbox as [number, number, number, number], ...) with a Turf bbox. Two concerns:

  1. fitBounds APIs differ across providers. If useMap() can point at multiple map implementations, this can become a runtime hazard.
  2. fitBounds generally expects a LngLatBoundsLike (often [[west, south],[east, north]]), while Turf returns [minX, minY, maxX, maxY]. Some libs accept the 4-tuple; others do not.

Right now you’re relying on a cast to force it through.

Suggestion

Normalize the bounds shape and isolate provider differences:

  • Convert Turf bbox to a canonical bounds object/tuple your useMap() abstraction guarantees.
  • If useMap() is Mapbox-only, pass [[minX, minY],[maxX, maxY]] to be explicit.

Example:

const [minX, minY, maxX, maxY] = turf.bbox(featureCollection)
const bounds: [[number, number],[number, number]] = [[minX, minY], [maxX, maxY]]
map.fitBounds(bounds, { padding: 50, maxZoom: 15, duration: 2000 })

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this change (and optionally a small adapter in useMap).

} catch (e) {
console.error('Failed to fit bounds for GeoJSON:', e);
}
}
Comment on lines 48 to 61
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:

# Check how useMap is implemented and what map instance it returns
ast-grep --pattern 'export const useMap = $_'

Repository: QueueLab/QCX

Length of output: 489


🏁 Script executed:

# Check for fitBounds usage and context
rg -n 'fitBounds' --type=ts --type=tsx -C3

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Look for Google Maps imports or setup
rg -n 'google\.maps|@react-google-maps|GoogleMap' --type=ts --type=tsx

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Check the context around useMap hook to understand the provider
rg -n 'useMap' --type=ts --type=tsx -B2 -A2 | head -50

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Find MapContextType definition
rg -n 'MapContextType' -A5 | head -30

Repository: QueueLab/QCX

Length of output: 1138


🏁 Script executed:

# Find MapProvider implementation
rg -n 'MapProvider|export.*Provider' components/map/ -A10 | head -50

Repository: QueueLab/QCX

Length of output: 2955


🏁 Script executed:

# Search for fitBounds without type filter
rg -n 'fitBounds' -C3

Repository: QueueLab/QCX

Length of output: 584


🏁 Script executed:

# Search for Google Maps references
rg -n 'google.maps|@react-google-maps|GoogleMap'

Repository: QueueLab/QCX

Length of output: 3033


🏁 Script executed:

# Check package.json to see what mapping libraries are used
cat package.json | grep -A5 -B5 'mapbox\|google\|map'

Repository: QueueLab/QCX

Length of output: 1895


🏁 Script executed:

# Check what map-data-updater.tsx imports and which useMap it uses
head -20 components/map/map-data-updater.tsx

Repository: QueueLab/QCX

Length of output: 584


🏁 Script executed:

# Find the Mapbox component to see if it sets map in context
rg -n 'export.*Mapbox|function Mapbox' components/map/ -A15

Repository: QueueLab/QCX

Length of output: 1615


🏁 Script executed:

# Check GoogleMapComponent implementation
rg -n 'export function GoogleMapComponent' components/map/ -A30

Repository: QueueLab/QCX

Length of output: 1955


🏁 Script executed:

# Check if there's any conditional handling for different map types
rg -n 'MapboxMap|google.maps.Map' components/map/map-data-updater.tsx

Repository: QueueLab/QCX

Length of output: 38


🏁 Script executed:

# Get more of GoogleMapComponent implementation
cat components/map/google-map.tsx | head -80

Repository: QueueLab/QCX

Length of output: 2406


🏁 Script executed:

# Check if google-geojson-layer uses a different approach
cat components/map/google-geojson-layer.tsx

Repository: QueueLab/QCX

Length of output: 1414


🏁 Script executed:

# Verify what providers are actually selectable in settings
rg -n 'mapProvider.*google|mapProvider.*mapbox' components/ -i

Repository: QueueLab/QCX

Length of output: 434


🏁 Script executed:

# Check where MapDataUpdater is imported/used
rg -n 'MapDataUpdater' components/

Repository: QueueLab/QCX

Length of output: 398


🏁 Script executed:

# Check if there are any other places attempting to call fitBounds or similar
rg -n 'fitBounds|flyTo|panTo' components/ -B2 -A2

Repository: QueueLab/QCX

Length of output: 750


🏁 Script executed:

# Verify the actual provider switching logic
cat components/map/map-provider.tsx

Repository: QueueLab/QCX

Length of output: 843


Implement bounds-fitting for Google Maps or restrict MapDataUpdater to Mapbox.

When using the Google Maps provider, the map object in context is null because GoogleMapComponent manages maps independently via @vis.gl/react-google-maps. This causes the fitBounds call (line 51) to silently skip due to the if (map && ...) guard, leaving uploaded GeoJSON uncentered in Google Maps.

Either:

  1. Implement bounds-fitting for Google Maps using its native API (e.g., google.maps.LatLngBounds)
  2. Restrict MapDataUpdater to Mapbox only
  3. Pass the Google Maps instance through context alongside Mapbox

The current code works without errors only because the bounds-fitting is silently skipped for Google Maps.

}, [id, data, filename, setMapData, map]);
Comment on lines 20 to 62
Copy link

Choose a reason for hiding this comment

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

MapDataUpdater triggers camera movement on every mount; this will be disruptive

MapDataUpdater always fitBounds whenever it mounts with data. This is likely fine for a single upload, but it becomes disruptive when:

  • multiple layers are replayed (e.g., from persisted state) → multiple consecutive camera jumps
  • the component remounts due to UI state reconciliation

Because this is a headless “sync” component, controlling side effects is crucial.

Suggestion

Add an explicit prop (e.g., flyTo?: boolean defaulting to true) and/or only run fitBounds if the layer was newly inserted into uploadedGeoJson (i.e., inside the setMapData updater when alreadyExists === false).

That way, replay/hydration can set flyTo={false} and avoid surprising camera behavior.

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


return null; // Headless component
}
14 changes: 13 additions & 1 deletion components/map/map-query-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect } from 'react';
// Removed useMCPMapClient as we'll use data passed via props
import { useMapData } from './map-data-context';
import { MapDataUpdater } from './map-data-updater';

// Define the expected structure of the mcp_response from geospatialTool
interface McpResponseData {
Expand All @@ -13,6 +14,7 @@ interface McpResponseData {
address?: string;
};
mapUrl?: string;
geoJson?: any;
}

interface GeospatialToolOutput {
Expand Down Expand Up @@ -76,7 +78,17 @@ export const MapQueryHandler: React.FC<MapQueryHandlerProps> = ({ toolOutput })
// Its purpose is to trigger map data updates based on AI tool results.
// If it were to use the old useMCPMapClient, mcpLoading and mcpError would be relevant.
// It could return a small status indicator or debug info if needed for development.
return null;
return (
<>
{toolOutput?.mcp_response?.geoJson && (
<MapDataUpdater
id={toolOutput.timestamp}
data={toolOutput.mcp_response.geoJson}
filename={toolOutput.mcp_response.location?.place_name || 'Tool Result'}
/>
)}
</>
);
Comment on lines +81 to +91
Copy link

Choose a reason for hiding this comment

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

Tool-derived GeoJSON id is not stable/unique enough

MapQueryHandler uses toolOutput.timestamp as the MapDataUpdater id. If two tool calls happen in the same timestamp granularity (or if timestamps are reused across reloads), this can:

  • cause unintended de-dupe (layer never appears)
  • overwrite expectations when persisting/replaying

Also, timestamps aren’t semantically tied to the GeoJSON payload.

Suggestion

Generate a unique id for each tool GeoJSON emission (e.g., nanoid() on the client, or use a deterministic hash of the GeoJSON if you want stable ids), and keep timestamp as metadata.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit updating the id generation strategy.

// Example for debugging with previous client:
// return <div data-map-query-processed={originalUserInput} data-mcp-loading={mcpLoading} data-mcp-error={mcpError} style={{display: 'none'}} />;
};
Loading