-
-
Notifications
You must be signed in to change notification settings - Fork 7
Map Image Overlays and State Restoration #461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add `imageOverlays` to `MapData` state and `MapDataProvider`. - Implement `ImageOverlayLayer` component for rendering and positioning images on Mapbox. - Add "Place on Map" button to `UserMessage` for image attachments. - Implement map state restoration logic by parsing the latest 'data' message in `app/search/[id]/page.tsx`. - Update `updateDrawingContext` server action to persist `imageOverlays`. - Fix redundant `MapDataProvider` instances in `Chat.tsx` that were shadowing state. - Ensure proper cleanup of Mapbox resources and React roots. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
WalkthroughAdds image overlay support across the map stack: new ImageOverlay type and ImageOverlayLayer component; MapDataProvider now accepts initialData; pages seed initial map state from latest chat data; UserMessage gains "Place on Map" UI; map change persistence API updated to include imageOverlays and debounced client calls updated. Changes
Sequence DiagramsequenceDiagram
actor User
participant UserMessage
participant MapContext as Map Context\n(useMapData)
participant Chat
participant MapboxMap
participant ImageOverlayLayer
User->>UserMessage: Click "Place on Map"
UserMessage->>MapContext: setMapData(imageOverlays: [new overlay])
UserMessage->>MapContext: setMapType(DrawingMode)
MapContext-->>Chat: mapData updated -> re-render
Chat->>MapboxMap: render with updated mapData
MapboxMap->>ImageOverlayLayer: instantiate layer for overlay
ImageOverlayLayer->>ImageOverlayLayer: create draggable corner markers + delete control
User->>ImageOverlayLayer: drag corner / click delete
ImageOverlayLayer->>MapContext: update overlay coordinates or remove overlay
MapContext-->>Chat: propagate changes -> re-render / persist (debounced)
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Suggested Labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
ⓘ Your approaching your monthly quota for Qodo. Upgrade your plan PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||||||||
ⓘ Your approaching your monthly quota for Qodo. Upgrade your plan PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This introduces a solid end-to-end overlay feature, but there are a few high-impact issues around persistence frequency and Mapbox/React resource lifecycle. The biggest concern is unthrottled updateDrawingContext calls (likely many DB writes during marker drag) and leaking createRoot() trees / Mapbox load listeners in ImageOverlayLayer. State restoration should also normalize/whitelist parsed JSON to avoid shape drift across older chats and prevent undefined state fields from propagating.
Additional notes (1)
- Security |
app/search/[id]/page.tsx:62-62
Persisted state restoration parseslatestDataMessage.contentand passes it directly asinitialData. If older chats contain different shapes (e.g., onlydrawnFeatures, noimageOverlays, or different keys), the provider may start with missing defaults. Also,JSON.parsehere is a trust boundary: even if it’s your own data, you should minimally validate shape to avoid runtime crashes deeper in the app.
At minimum, normalize the subset you care about (arrays + cameraState) and ignore unknown keys.
Summary of changes
What changed
✅ Map state restoration on chat load
app/search/[id]/page.tsxnow scans DB messages for the most recent message withrole === 'data', parses its JSON, and passes it intoMapDataProviderviainitialData.
✅ Removed redundant providers (fixes state shadowing)
components/chat.tsxno longer wraps mobile/desktop layouts with nestedMapDataProvider, relying on the page-level provider instead.
✅ Added persisted image overlay state
components/map/map-data-context.tsxintroducesImageOverlayand addsimageOverlaystoMapData.MapDataProvidernow acceptsinitialData?: MapDataand initializes state from it.
✅ Render + manipulate overlays on Mapbox
- New
components/map/image-overlay-layer.tsxcreates a Mapboximagesource + raster layer per overlay, syncs coordinates/opacity, and adds draggable corner markers + a delete UI in drawing mode. components/map/mapbox-map.tsxrenders anImageOverlayLayerfor each entry inmapData.imageOverlays.
✅ UX: “Place on Map” for image attachments
components/user-message.tsxadds a hover button that creates a new overlay from the current map bounds and switches to drawing mode.
✅ Persist overlays server-side
lib/actions/chat.tsupdatesupdateDrawingContextsignature to accept optionalimageOverlaysalongsidedrawnFeaturesandcameraState.
| export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => { | ||
| const [mapData, setMapData] = useState<MapData>(initialData || { drawnFeatures: [], markers: [], imageOverlays: [] }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MapDataProvider initializes state from initialData verbatim, which can leave drawnFeatures, markers, and imageOverlays as undefined depending on what the persisted payload contains. You partially work around this elsewhere with || [], but leaving the canonical state shape inconsistent tends to create subtle bugs (e.g., code that maps without guarding, or merge logic that assumes arrays exist).
Because initialData comes from JSON.parse, it’s especially important to normalize defaults at the boundary.
Suggestion
Normalize initialData into a full MapData object with array defaults rather than storing it raw.
For example:
export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => {
const normalized: MapData = {
drawnFeatures: [],
markers: [],
imageOverlays: [],
...initialData,
drawnFeatures: initialData?.drawnFeatures ?? [],
markers: initialData?.markers ?? [],
imageOverlays: initialData?.imageOverlays ?? [],
}
const [mapData, setMapData] = useState<MapData>(normalized)
...
}Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
components/chat.tsx
Outdated
| // useEffect to call the server action when map data changes | ||
| useEffect(() => { | ||
| if (id && mapData.drawnFeatures && mapData.cameraState) { | ||
| console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures); | ||
| if (id && (mapData.drawnFeatures || mapData.imageOverlays) && mapData.cameraState) { | ||
| console.log('Chat.tsx: map data changed, calling updateDrawingContext'); | ||
| updateDrawingContext(id, { | ||
| drawnFeatures: mapData.drawnFeatures, | ||
| drawnFeatures: mapData.drawnFeatures || [], | ||
| imageOverlays: mapData.imageOverlays || [], | ||
| cameraState: mapData.cameraState, | ||
| }); | ||
| } | ||
| }, [id, mapData.drawnFeatures, mapData.cameraState]); | ||
| }, [id, mapData.drawnFeatures, mapData.imageOverlays, mapData.cameraState]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server action is now triggered for any change to drawnFeatures/imageOverlays and requires cameraState. Two correctness concerns:
mapData.drawnFeatures || mapData.imageOverlayswill be truthy even when both are empty arrays (because[]is truthy), which can cause unnecessary writes on initialization.- On rapid marker drags, you’ll call
updateDrawingContexton every intermediate state, likely creating manydatamessages and hammering the server/DB.
This should be debounced/throttled and should distinguish “data actually changed” from “arrays exist”.
Suggestion
Debounce persistence and tighten the condition so it only runs when there is meaningful content to persist.
Example approach:
- Compute counts:
const hasMapEdits = (mapData.drawnFeatures?.length ?? 0) > 0 || (mapData.imageOverlays?.length ?? 0) > 0 - Debounce the call (e.g., 300–800ms) and cancel on unmount.
Pseudo-code:
useEffect(() => {
if (!id || !mapData.cameraState) return
const hasMapEdits = (mapData.drawnFeatures?.length ?? 0) > 0 || (mapData.imageOverlays?.length ?? 0) > 0
if (!hasMapEdits) return
const t = setTimeout(() => {
updateDrawingContext(id, {
drawnFeatures: mapData.drawnFeatures ?? [],
imageOverlays: mapData.imageOverlays ?? [],
cameraState: mapData.cameraState,
})
}, 500)
return () => clearTimeout(t)
}, [id, mapData.drawnFeatures, mapData.imageOverlays, mapData.cameraState])Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
| const deleteRootRef = useRef<any>(null) | ||
|
|
||
| const sourceId = `image-source-${overlay.id}` | ||
| const layerId = `image-layer-${overlay.id}` | ||
|
|
||
| // Update overlay coordinates in global state | ||
| const updateCoordinates = useCallback((index: number, lngLat: [number, number]) => { | ||
| setMapData(prev => { | ||
| const overlays = prev.imageOverlays || [] | ||
| const newOverlays = overlays.map(o => { | ||
| if (o.id === overlay.id) { | ||
| const newCoords = [...o.coordinates] as [[number, number], [number, number], [number, number], [number, number]] | ||
| newCoords[index] = lngLat | ||
| return { ...o, coordinates: newCoords } | ||
| } | ||
| return o | ||
| }) | ||
| return { ...prev, imageOverlays: newOverlays } | ||
| }) | ||
| }, [overlay.id, setMapData]) | ||
|
|
||
| const removeOverlay = useCallback(() => { | ||
| setMapData(prev => ({ | ||
| ...prev, | ||
| imageOverlays: (prev.imageOverlays || []).filter(o => o.id !== overlay.id) | ||
| })) | ||
| }, [overlay.id, setMapData]) | ||
|
|
||
| useEffect(() => { | ||
| if (!map) return | ||
|
|
||
| const onMapLoad = () => { | ||
| if (!map.getSource(sourceId)) { | ||
| map.addSource(sourceId, { | ||
| type: 'image', | ||
| url: overlay.url, | ||
| coordinates: overlay.coordinates | ||
| }) | ||
|
|
||
| map.addLayer({ | ||
| id: layerId, | ||
| type: 'raster', | ||
| source: sourceId, | ||
| paint: { | ||
| 'raster-opacity': overlay.opacity || 0.7 | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| if (map.isStyleLoaded()) { | ||
| onMapLoad() | ||
| } else { | ||
| map.on('load', onMapLoad) | ||
| } | ||
|
|
||
| return () => { | ||
| if (map.getLayer(layerId)) map.removeLayer(layerId) | ||
| if (map.getSource(sourceId)) map.removeSource(sourceId) | ||
| } | ||
| }, [map, sourceId, layerId, overlay.url]) // coordinates handled separately | ||
|
|
||
| // Sync coordinates with map source | ||
| useEffect(() => { | ||
| if (!map) return | ||
| const source = map.getSource(sourceId) as mapboxgl.ImageSource | ||
| if (source) { | ||
| source.setCoordinates(overlay.coordinates) | ||
| } | ||
| }, [map, overlay.coordinates, sourceId]) | ||
|
|
||
| // Sync opacity | ||
| useEffect(() => { | ||
| if (!map) return | ||
| if (map.getLayer(layerId)) { | ||
| map.setPaintProperty(layerId, 'raster-opacity', overlay.opacity || 0.7) | ||
| } | ||
| }, [map, overlay.opacity, layerId]) | ||
|
|
||
| // Draggable markers for corners | ||
| useEffect(() => { | ||
| if (!map) return | ||
|
|
||
| // Clean up existing markers | ||
| markersRef.current.forEach(m => m.remove()) | ||
| markersRef.current = [] | ||
| if (deleteMarkerRef.current) { | ||
| deleteMarkerRef.current.remove() | ||
| deleteMarkerRef.current = null | ||
| } | ||
|
|
||
| if (isDrawingMode) { | ||
| // Add 4 corner markers | ||
| overlay.coordinates.forEach((coord, index) => { | ||
| const el = document.createElement('div') | ||
| el.className = 'w-4 h-4 bg-white border-2 border-primary rounded-full cursor-move shadow-md' | ||
|
|
||
| const marker = new mapboxgl.Marker({ | ||
| element: el, | ||
| draggable: true | ||
| }) | ||
| .setLngLat(coord) | ||
| .addTo(map) | ||
|
|
||
| marker.on('drag', () => { | ||
| const newLngLat = marker.getLngLat() | ||
| updateCoordinates(index, [newLngLat.lng, newLngLat.lat]) | ||
| }) | ||
|
|
||
| markersRef.current.push(marker) | ||
| }) | ||
|
|
||
| // Add delete button near top-right corner | ||
| const deleteEl = document.createElement('div') | ||
| const root = createRoot(deleteEl) | ||
| deleteRootRef.current = root | ||
| root.render( | ||
| <Button | ||
| variant="destructive" | ||
| size="icon" | ||
| className="w-6 h-6 rounded-full shadow-lg" | ||
| onClick={removeOverlay} | ||
| > | ||
| <X size={14} /> | ||
| </Button> | ||
| ) | ||
|
|
||
| const deleteMarker = new mapboxgl.Marker({ | ||
| element: deleteEl, | ||
| offset: [0, -20] | ||
| }) | ||
| .setLngLat(overlay.coordinates[1]) // Near Top-right | ||
| .addTo(map) | ||
|
|
||
| deleteMarkerRef.current = deleteMarker | ||
| } | ||
|
|
||
| return () => { | ||
| markersRef.current.forEach(m => m.remove()) | ||
| if (deleteMarkerRef.current) deleteMarkerRef.current.remove() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleteRootRef is typed as any and never unmounted. Because you create a React root inside a Mapbox marker element, you should call root.unmount() during cleanup; otherwise you risk leaking React trees and event handlers when toggling drawing mode, changing overlay coordinates, or unmounting the layer.
Also, you remove the marker element in cleanup but don’t clear deleteRootRef.current, which makes repeated toggles harder to reason about.
Suggestion
Avoid any and always unmount the React root you create.
Suggested changes:
- Type:
const deleteRootRef = useRef<import('react-dom/client').Root | null>(null) - Cleanup:
return () => {
markersRef.current.forEach(m => m.remove())
markersRef.current = []
deleteMarkerRef.current?.remove()
deleteMarkerRef.current = null
deleteRootRef.current?.unmount()
deleteRootRef.current = null
}Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
| useEffect(() => { | ||
| if (!map) return | ||
|
|
||
| const onMapLoad = () => { | ||
| if (!map.getSource(sourceId)) { | ||
| map.addSource(sourceId, { | ||
| type: 'image', | ||
| url: overlay.url, | ||
| coordinates: overlay.coordinates | ||
| }) | ||
|
|
||
| map.addLayer({ | ||
| id: layerId, | ||
| type: 'raster', | ||
| source: sourceId, | ||
| paint: { | ||
| 'raster-opacity': overlay.opacity || 0.7 | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| if (map.isStyleLoaded()) { | ||
| onMapLoad() | ||
| } else { | ||
| map.on('load', onMapLoad) | ||
| } | ||
|
|
||
| return () => { | ||
| if (map.getLayer(layerId)) map.removeLayer(layerId) | ||
| if (map.getSource(sourceId)) map.removeSource(sourceId) | ||
| } | ||
| }, [map, sourceId, layerId, overlay.url]) // coordinates handled separately |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The effect that adds the Mapbox source/layer subscribes to map.on('load', onMapLoad) when style isn’t loaded, but it never unregisters the listener. If the component unmounts before load fires, the handler can still run later and attempt to add sources/layers for an overlay that no longer exists.
You should call map.off('load', onMapLoad) in cleanup when you register it.
Suggestion
Track whether you registered the load handler and unregister it in cleanup.
Example:
let subscribed = false
if (!map.isStyleLoaded()) {
map.on('load', onMapLoad)
subscribed = true
} else {
onMapLoad()
}
return () => {
if (subscribed) map.off('load', onMapLoad)
if (map.getLayer(layerId)) map.removeLayer(layerId)
if (map.getSource(sourceId)) map.removeSource(sourceId)
}Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
| // Draggable markers for corners | ||
| useEffect(() => { | ||
| if (!map) return | ||
|
|
||
| // Clean up existing markers | ||
| markersRef.current.forEach(m => m.remove()) | ||
| markersRef.current = [] | ||
| if (deleteMarkerRef.current) { | ||
| deleteMarkerRef.current.remove() | ||
| deleteMarkerRef.current = null | ||
| } | ||
|
|
||
| if (isDrawingMode) { | ||
| // Add 4 corner markers | ||
| overlay.coordinates.forEach((coord, index) => { | ||
| const el = document.createElement('div') | ||
| el.className = 'w-4 h-4 bg-white border-2 border-primary rounded-full cursor-move shadow-md' | ||
|
|
||
| const marker = new mapboxgl.Marker({ | ||
| element: el, | ||
| draggable: true | ||
| }) | ||
| .setLngLat(coord) | ||
| .addTo(map) | ||
|
|
||
| marker.on('drag', () => { | ||
| const newLngLat = marker.getLngLat() | ||
| updateCoordinates(index, [newLngLat.lng, newLngLat.lat]) | ||
| }) | ||
|
|
||
| markersRef.current.push(marker) | ||
| }) | ||
|
|
||
| // Add delete button near top-right corner | ||
| const deleteEl = document.createElement('div') | ||
| const root = createRoot(deleteEl) | ||
| deleteRootRef.current = root | ||
| root.render( | ||
| <Button | ||
| variant="destructive" | ||
| size="icon" | ||
| className="w-6 h-6 rounded-full shadow-lg" | ||
| onClick={removeOverlay} | ||
| > | ||
| <X size={14} /> | ||
| </Button> | ||
| ) | ||
|
|
||
| const deleteMarker = new mapboxgl.Marker({ | ||
| element: deleteEl, | ||
| offset: [0, -20] | ||
| }) | ||
| .setLngLat(overlay.coordinates[1]) // Near Top-right | ||
| .addTo(map) | ||
|
|
||
| deleteMarkerRef.current = deleteMarker | ||
| } | ||
|
|
||
| return () => { | ||
| markersRef.current.forEach(m => m.remove()) | ||
| if (deleteMarkerRef.current) deleteMarkerRef.current.remove() | ||
| } | ||
| }, [map, isDrawingMode, overlay.coordinates, updateCoordinates, removeOverlay]) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You set the delete marker location to overlay.coordinates[1] and comment it as “Near Top-right”, but the coordinate ordering is documented as [NW, NE, SE, SW], so [1] is exactly NE. That’s fine, but the marker won’t follow when coordinates change unless drawing mode is re-entered because you only create the delete marker inside the drawing-mode effect.
Since that effect depends on overlay.coordinates, it will re-run and recreate the delete marker on every drag (because you update global state on every drag). Combined with the lack of debouncing, this means you’re constantly destroying/recreating the React root and marker during drags.
This is avoidable churn.
Suggestion
Don’t recreate the delete marker/root on every coordinate update.
Option A: Create markers once when entering drawing mode, then update their positions in a separate effect that runs on overlay.coordinates.
Option B: Keep the current effect but move the delete marker position update to a lighter-weight branch:
// when already exists
deleteMarkerRef.current?.setLngLat(overlay.coordinates[1])Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
| // } | ||
|
|
||
| export async function updateDrawingContext(chatId: string, contextData: { drawnFeatures: any[], cameraState: any }) { | ||
| export async function updateDrawingContext(chatId: string, contextData: { drawnFeatures?: any[], imageOverlays?: any[], cameraState: any }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateDrawingContext now accepts imageOverlays, but the signature still uses any[] and cameraState: any. While tsc passes, this is an unsafe-but-type-valid boundary: you’re persisting untyped blobs to the DB and later JSON.parse them into runtime structures.
Given this feature’s complexity (Mapbox sources/layers depend on coordinate ordering and URL), it’s worth strongly typing the persisted payload to prevent silent corruption.
Suggestion
Replace any with your shared types (MapData, ImageOverlay, CameraState) for the server action payload and validate minimally before writing.
Example:
import type { CameraState, ImageOverlay } from '@/components/map/map-data-context'
export async function updateDrawingContext(
chatId: string,
contextData: {
drawnFeatures?: MapData['drawnFeatures']
imageOverlays?: ImageOverlay[]
cameraState: CameraState
}
) { ... }Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In @.env:
- Around line 1-2: Remove the committed Mapbox token by replacing the real value
for NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN with a placeholder (e.g.
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=YOUR_MAPBOX_TOKEN_HERE) or move the current .env
contents into .env.example, add .env to .gitignore so secrets aren’t tracked,
and update deployment/config to load the real token from environment/secret
storage; reference the NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN variable in the repo to
locate and update all usages and rotate the exposed token in your Mapbox
account.
In `@app/search/`[id]/page.tsx:
- Around line 62-71: The parsed JSON assigned to initialMapData (from
latestDataMessage) must be validated to ensure it matches the expected MapData
shape before passing to MapDataProvider; update the parsing block
(latestDataMessage / initialMapData) to: try parsing, then check that the result
is an object and that drawnFeatures, markers, and imageOverlays exist and are
arrays (or coerce to empty arrays as a safe default); on validation failure, log
a clear error and set initialMapData to a safe fallback object ({ drawnFeatures:
[], markers: [], imageOverlays: [] }) so MapDataProvider and downstream
components never receive malformed data.
In `@components/chat.tsx`:
- Around line 92-102: The effect calling updateDrawingContext on every
mapData.imageOverlays change should be debounced or committed only on drag end
to avoid per-drag DB writes; modify the useEffect that references id,
mapData.drawnFeatures, mapData.imageOverlays, and mapData.cameraState to buffer
rapid updates (e.g., use a debounce timer via useRef/useEffect or a
useCallback-wrapped debounce) or replace the frequent trigger with a single
commit on the map dragend event, and ensure updateDrawingContext is only invoked
after the debounce delay or on drag end with the latest
drawnFeatures/imageOverlays/cameraState.
In `@components/map/image-overlay-layer.tsx`:
- Around line 51-83: The useEffect's onMapLoad listener isn't removed on
cleanup, the image layer opacity uses || causing 0 to default to 0.7, and the
React root created for the delete button is never unmounted; fix by (1) storing
onMapLoad and, in the cleanup return, call map.off('load', onMapLoad) if you
previously attached it (or always call map.off to be safe) alongside removing
layer/source in useEffect, (2) change any 'raster-opacity': overlay.opacity ||
0.7 (and the other occurrence) to use nullish coalescing 'raster-opacity':
overlay.opacity ?? 0.7, and (3) keep the createRoot instance (e.g.,
deleteButtonRoot or similar variable used where you call createRoot) in scope
and call its unmount() in the same cleanup; also wrap forEach callbacks in
explicit { } blocks for clarity.
- Around line 62-69: The raster layer's opacity uses a truthy fallback
(overlay.opacity || 0.7) which treats 0 as false and prevents fully transparent
overlays; update both uses (the paint 'raster-opacity' in the map.addLayer call
where layerId is added and the other occurrence around line 98) to use the
nullish coalescing operator (overlay.opacity ?? 0.7) so that explicit 0 is
honored while still defaulting to 0.7 when opacity is undefined or null.
In `@components/map/map-data-context.tsx`:
- Around line 49-50: MapDataProvider currently sets state to initialData
directly which can omit drawnFeatures/markers/imageOverlays; change the
initialization in the MapDataProvider (where mapData and setMapData are created)
to merge initialData with a default MapData object so missing fields fall back
to empty arrays (e.g. create a default like { drawnFeatures: [], markers: [],
imageOverlays: [] } and shallow-merge with initialData), and ensure any later
setters that replace mapData preserve those array defaults when merging updates.
In `@components/user-message.tsx`:
- Around line 82-101: The hover-only reveal for the Place on Map button is not
accessible on touch devices; update the wrapper and button visibility so the
control is reachable via keyboard and touch by adding focus-based styles (e.g.,
include group-focus-within:opacity-100 alongside group-hover:opacity-100 on the
absolute button container), ensure the container is keyboard-focusable (add a
tabindex or use a semantic button wrapper) and confirm handlePlaceOnMap remains
the click/keyboard activation handler on the Button component (refer to the
Button element with onClick={handlePlaceOnMap} and the surrounding div using
classes "group" and "absolute top-4 right-4 ...").
- Around line 28-30: The current hooks (useMap, useMapData, useMapToggle) assume
their contexts always exist which will break if UserMessage is used outside
Chat; change the hooks to be defensive (return null/undefined or safe no-op
functions instead of throwing when their Provider is missing) and update
UserMessage to safely read from them (e.g., const mapCtx = useMap(); const map =
mapCtx?.map; and const setMapDataFn = useMapData()?.setMapData; const
setMapTypeFn = useMapToggle()?.setMapType) and keep the existing guard in the
Place on Map handler that checks for map and imagePart before using them. Ensure
hook return types reflect optional values so callers (UserMessage) can
null-check before invoking methods like setMapData and setMapType.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (8)
.envapp/search/[id]/page.tsxcomponents/chat.tsxcomponents/map/image-overlay-layer.tsxcomponents/map/map-data-context.tsxcomponents/map/mapbox-map.tsxcomponents/user-message.tsxlib/actions/chat.ts
🧰 Additional context used
🧬 Code graph analysis (4)
app/search/[id]/page.tsx (2)
components/map/map-data-context.tsx (1)
MapDataProvider(49-57)app/page.tsx (1)
Page(9-18)
components/user-message.tsx (5)
components/map/map-context.tsx (1)
useMap(24-30)components/map/map-data-context.tsx (1)
useMapData(59-65)components/map-toggle-context.tsx (1)
useMapToggle(36-42)components/ui/button.tsx (1)
Button(56-56)app/page.tsx (1)
Page(9-18)
components/map/mapbox-map.tsx (1)
components/map/image-overlay-layer.tsx (1)
ImageOverlayLayer(16-167)
components/map/image-overlay-layer.tsx (4)
components/map/map-data-context.tsx (1)
ImageOverlay(15-20)components/map/map-context.tsx (1)
useMap(24-30)components/map-toggle-context.tsx (1)
useMapToggle(36-42)components/ui/button.tsx (1)
Button(56-56)
🪛 Biome (2.3.13)
components/map/image-overlay-layer.tsx
[error] 107-107: This callback passed to forEach() iterable method should not return a value.
Either remove this return or remove the returned value.
(lint/suspicious/useIterableCallbackReturn)
[error] 161-161: This callback passed to forEach() iterable method should not return a value.
Either remove this return or remove the returned value.
(lint/suspicious/useIterableCallbackReturn)
🔇 Additional comments (6)
lib/actions/chat.ts (1)
165-165: LGTM: overlay data included in persisted context.This extension aligns with the new map overlay state.
components/map/mapbox-map.tsx (1)
573-575: LGTM: overlays rendered viaImageOverlayLayer.Clean integration with the new overlay state.
components/map/image-overlay-layer.tsx (1)
107-108: The Biome linter ruleuseBlockStatementsis explicitly disabled in this project'sbiome.jsonconfiguration ("useBlockStatements": "off"). Therefore, Biome does not flag these forEach callbacks as problematic, and this change is not required.Likely an incorrect or invalid review comment.
app/search/[id]/page.tsx (1)
82-84: LGTM!The
initialDataprop is correctly passed toMapDataProvider, matching its optionalinitialData?: MapDatasignature shown in the relevant context.components/user-message.tsx (2)
1-11: LGTM!The
'use client'directive is correctly added since the component now uses React hooks (useMap,useMapData,useMapToggle). Imports are appropriate for the new functionality.
45-76: LGTM!The
handlePlaceOnMapfunction is well-structured:
- Defensive early returns handle missing
map,imagePart, orbounds.- Coordinates use
[lng, lat]order consistent with Mapbox/GeoJSON conventions.- The overlay is positioned in the center 50% of the viewport, providing reasonable initial placement.
- Fallback
prev.imageOverlays || []handles undefined gracefully.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| DATABASE_URL="postgresql://user:password@host:port/db" | ||
| NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=pk.eyJ1IjoiZHVtbXkiLCJhIjoiY2p1bW15In0.ZHVtbXk= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove committed Mapbox token from tracked .env.
Even public Mapbox tokens can be abused and tied to account quotas; committed .env files are a common leak vector. Keep the real token in deployment secrets and use a placeholder (or move to .env.example) in the repo.
🛠️ Suggested placeholder
-NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=pk.eyJ1IjoiZHVtbXkiLCJhIjoiY2p1bW15In0.ZHVtbXk=
+NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=YOUR_MAPBOX_PUBLIC_TOKEN📝 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.
| DATABASE_URL="postgresql://user:password@host:port/db" | |
| NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=pk.eyJ1IjoiZHVtbXkiLCJhIjoiY2p1bW15In0.ZHVtbXk= | |
| DATABASE_URL="postgresql://user:password@host:port/db" | |
| NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=YOUR_MAPBOX_PUBLIC_TOKEN |
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 1-1: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🤖 Prompt for AI Agents
In @.env around lines 1 - 2, Remove the committed Mapbox token by replacing the
real value for NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN with a placeholder (e.g.
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=YOUR_MAPBOX_TOKEN_HERE) or move the current .env
contents into .env.example, add .env to .gitignore so secrets aren’t tracked,
and update deployment/config to load the real token from environment/secret
storage; reference the NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN variable in the repo to
locate and update all usages and rotate the exposed token in your Mapbox
account.
| // Extract initial map data from the latest 'data' message | ||
| const latestDataMessage = [...dbMessages].reverse().find(m => m.role === 'data'); | ||
| let initialMapData = undefined; | ||
| if (latestDataMessage) { | ||
| try { | ||
| initialMapData = JSON.parse(latestDataMessage.content); | ||
| } catch (e) { | ||
| console.error('Failed to parse initial map data:', e); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate parsed map data structure before use.
The parsed JSON is passed directly to MapDataProvider without validating that it conforms to the expected MapData shape ({ drawnFeatures, markers, imageOverlays }). Malformed data in the database could cause runtime errors in child components that expect these arrays.
Consider adding schema validation or at minimum structural checks:
🛡️ Proposed fix to add basic validation
// Extract initial map data from the latest 'data' message
const latestDataMessage = [...dbMessages].reverse().find(m => m.role === 'data');
let initialMapData = undefined;
if (latestDataMessage) {
try {
- initialMapData = JSON.parse(latestDataMessage.content);
+ const parsed = JSON.parse(latestDataMessage.content);
+ // Basic validation: ensure expected arrays exist
+ if (
+ parsed &&
+ typeof parsed === 'object' &&
+ Array.isArray(parsed.drawnFeatures) &&
+ Array.isArray(parsed.imageOverlays)
+ ) {
+ initialMapData = parsed;
+ } else {
+ console.warn('Parsed map data has unexpected shape, ignoring');
+ }
} catch (e) {
console.error('Failed to parse initial map data:', e);
}
}🤖 Prompt for AI Agents
In `@app/search/`[id]/page.tsx around lines 62 - 71, The parsed JSON assigned to
initialMapData (from latestDataMessage) must be validated to ensure it matches
the expected MapData shape before passing to MapDataProvider; update the parsing
block (latestDataMessage / initialMapData) to: try parsing, then check that the
result is an object and that drawnFeatures, markers, and imageOverlays exist and
are arrays (or coerce to empty arrays as a safe default); on validation failure,
log a clear error and set initialMapData to a safe fallback object ({
drawnFeatures: [], markers: [], imageOverlays: [] }) so MapDataProvider and
downstream components never receive malformed data.
| useEffect(() => { | ||
| if (!map) return | ||
|
|
||
| const onMapLoad = () => { | ||
| if (!map.getSource(sourceId)) { | ||
| map.addSource(sourceId, { | ||
| type: 'image', | ||
| url: overlay.url, | ||
| coordinates: overlay.coordinates | ||
| }) | ||
|
|
||
| map.addLayer({ | ||
| id: layerId, | ||
| type: 'raster', | ||
| source: sourceId, | ||
| paint: { | ||
| 'raster-opacity': overlay.opacity || 0.7 | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| if (map.isStyleLoaded()) { | ||
| onMapLoad() | ||
| } else { | ||
| map.on('load', onMapLoad) | ||
| } | ||
|
|
||
| return () => { | ||
| if (map.getLayer(layerId)) map.removeLayer(layerId) | ||
| if (map.getSource(sourceId)) map.removeSource(sourceId) | ||
| } | ||
| }, [map, sourceId, layerId, overlay.url]) // coordinates handled separately |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd "image-overlay-layer.tsx" --type fRepository: QueueLab/QCX
Length of output: 94
🏁 Script executed:
cat -n components/map/image-overlay-layer.tsxRepository: QueueLab/QCX
Length of output: 6094
Fix Mapbox and React resource leaks in overlay lifecycle.
Three issues cause resource leaks when overlays are added/removed:
- The
map.on('load', onMapLoad)listener (line 76) isn't detached in cleanup, leaving orphaned listeners if the component unmounts before the map style loads. - The React root for the delete button (line 137–138) is never unmounted in cleanup (line 160–163), leaking the React tree.
- Opacity uses
||instead of??(lines 67, 98), so setting opacity to0incorrectly defaults to0.7instead.
Additionally, consider wrapping forEach callbacks in explicit blocks for clarity (lines 107, 161).
Suggested fixes
Line 79–82: Add listener cleanup
return () => {
+ map.off('load', onMapLoad)
if (map.getLayer(layerId)) map.removeLayer(layerId)
if (map.getSource(sourceId)) map.removeSource(sourceId)
}Lines 160–163: Unmount React root
return () => {
markersRef.current.forEach(m => m.remove())
if (deleteMarkerRef.current) deleteMarkerRef.current.remove()
+ if (deleteRootRef.current) {
+ deleteRootRef.current.unmount()
+ deleteRootRef.current = null
+ }
}Lines 67, 98: Use nullish coalescing
- 'raster-opacity': overlay.opacity || 0.7
+ 'raster-opacity': overlay.opacity ?? 0.7Lines 107, 161: Wrap forEach callbacks explicitly
- markersRef.current.forEach(m => m.remove())
+ markersRef.current.forEach(m => { m.remove() })📝 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.
| useEffect(() => { | |
| if (!map) return | |
| const onMapLoad = () => { | |
| if (!map.getSource(sourceId)) { | |
| map.addSource(sourceId, { | |
| type: 'image', | |
| url: overlay.url, | |
| coordinates: overlay.coordinates | |
| }) | |
| map.addLayer({ | |
| id: layerId, | |
| type: 'raster', | |
| source: sourceId, | |
| paint: { | |
| 'raster-opacity': overlay.opacity || 0.7 | |
| } | |
| }) | |
| } | |
| } | |
| if (map.isStyleLoaded()) { | |
| onMapLoad() | |
| } else { | |
| map.on('load', onMapLoad) | |
| } | |
| return () => { | |
| if (map.getLayer(layerId)) map.removeLayer(layerId) | |
| if (map.getSource(sourceId)) map.removeSource(sourceId) | |
| } | |
| }, [map, sourceId, layerId, overlay.url]) // coordinates handled separately | |
| useEffect(() => { | |
| if (!map) return | |
| const onMapLoad = () => { | |
| if (!map.getSource(sourceId)) { | |
| map.addSource(sourceId, { | |
| type: 'image', | |
| url: overlay.url, | |
| coordinates: overlay.coordinates | |
| }) | |
| map.addLayer({ | |
| id: layerId, | |
| type: 'raster', | |
| source: sourceId, | |
| paint: { | |
| 'raster-opacity': overlay.opacity ?? 0.7 | |
| } | |
| }) | |
| } | |
| } | |
| if (map.isStyleLoaded()) { | |
| onMapLoad() | |
| } else { | |
| map.on('load', onMapLoad) | |
| } | |
| return () => { | |
| map.off('load', onMapLoad) | |
| if (map.getLayer(layerId)) map.removeLayer(layerId) | |
| if (map.getSource(sourceId)) map.removeSource(sourceId) | |
| } | |
| }, [map, sourceId, layerId, overlay.url]) // coordinates handled separately |
🤖 Prompt for AI Agents
In `@components/map/image-overlay-layer.tsx` around lines 51 - 83, The useEffect's
onMapLoad listener isn't removed on cleanup, the image layer opacity uses ||
causing 0 to default to 0.7, and the React root created for the delete button is
never unmounted; fix by (1) storing onMapLoad and, in the cleanup return, call
map.off('load', onMapLoad) if you previously attached it (or always call map.off
to be safe) alongside removing layer/source in useEffect, (2) change any
'raster-opacity': overlay.opacity || 0.7 (and the other occurrence) to use
nullish coalescing 'raster-opacity': overlay.opacity ?? 0.7, and (3) keep the
createRoot instance (e.g., deleteButtonRoot or similar variable used where you
call createRoot) in scope and call its unmount() in the same cleanup; also wrap
forEach callbacks in explicit { } blocks for clarity.
| map.addLayer({ | ||
| id: layerId, | ||
| type: 'raster', | ||
| source: sourceId, | ||
| paint: { | ||
| 'raster-opacity': overlay.opacity || 0.7 | ||
| } | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "image-overlay-layer.tsx" -type fRepository: QueueLab/QCX
Length of output: 96
🏁 Script executed:
wc -l ./components/map/image-overlay-layer.tsxRepository: QueueLab/QCX
Length of output: 100
🏁 Script executed:
sed -n '55,75p' ./components/map/image-overlay-layer.tsxRepository: QueueLab/QCX
Length of output: 507
🏁 Script executed:
sed -n '90,105p' ./components/map/image-overlay-layer.tsxRepository: QueueLab/QCX
Length of output: 446
🏁 Script executed:
grep -n 'overlay.opacity' ./components/map/image-overlay-layer.tsxRepository: QueueLab/QCX
Length of output: 234
🏁 Script executed:
cat -n ./components/map/image-overlay-layer.tsx | sed -n '1,30p'Repository: QueueLab/QCX
Length of output: 1366
🏁 Script executed:
grep -A 5 'interface ImageOverlay' ./components/map/map-data-context.tsxRepository: QueueLab/QCX
Length of output: 246
Allow explicit opacity = 0.
overlay.opacity || 0.7 treats 0 as falsy and forces 0.7, preventing fully transparent overlays. Use ?? instead.
Suggested fix
- 'raster-opacity': overlay.opacity || 0.7
+ 'raster-opacity': overlay.opacity ?? 0.7- map.setPaintProperty(layerId, 'raster-opacity', overlay.opacity || 0.7)
+ map.setPaintProperty(layerId, 'raster-opacity', overlay.opacity ?? 0.7)Applies to lines 67 and 98.
🤖 Prompt for AI Agents
In `@components/map/image-overlay-layer.tsx` around lines 62 - 69, The raster
layer's opacity uses a truthy fallback (overlay.opacity || 0.7) which treats 0
as false and prevents fully transparent overlays; update both uses (the paint
'raster-opacity' in the map.addLayer call where layerId is added and the other
occurrence around line 98) to use the nullish coalescing operator
(overlay.opacity ?? 0.7) so that explicit 0 is honored while still defaulting to
0.7 when opacity is undefined or null.
| export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => { | ||
| const [mapData, setMapData] = useState<MapData>(initialData || { drawnFeatures: [], markers: [], imageOverlays: [] }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merge defaults when initialData is supplied.
initialData bypasses defaults, so partial data (older chats) can leave drawnFeatures, markers, or imageOverlays undefined. Merge with defaults to keep array invariants stable.
✅ Suggested fix
-export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => {
- const [mapData, setMapData] = useState<MapData>(initialData || { drawnFeatures: [], markers: [], imageOverlays: [] });
+export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => {
+ const [mapData, setMapData] = useState<MapData>(() => ({
+ drawnFeatures: [],
+ markers: [],
+ imageOverlays: [],
+ ...initialData
+ }));📝 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.
| export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => { | |
| const [mapData, setMapData] = useState<MapData>(initialData || { drawnFeatures: [], markers: [], imageOverlays: [] }); | |
| export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => { | |
| const [mapData, setMapData] = useState<MapData>(() => ({ | |
| drawnFeatures: [], | |
| markers: [], | |
| imageOverlays: [], | |
| ...initialData | |
| })); |
🤖 Prompt for AI Agents
In `@components/map/map-data-context.tsx` around lines 49 - 50, MapDataProvider
currently sets state to initialData directly which can omit
drawnFeatures/markers/imageOverlays; change the initialization in the
MapDataProvider (where mapData and setMapData are created) to merge initialData
with a default MapData object so missing fields fall back to empty arrays (e.g.
create a default like { drawnFeatures: [], markers: [], imageOverlays: [] } and
shallow-merge with initialData), and ensure any later setters that replace
mapData preserve those array defaults when merging updates.
| const { map } = useMap() | ||
| const { setMapData } = useMapData() | ||
| const { setMapType } = useMapToggle() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
# Search for UserMessage component usage
rg -n '<UserMessage' --type tsx --type ts -C 3Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Find the hook implementations to see if they throw errors
fd -e tsx -e ts | xargs rg -l 'export.*useMap|export.*useMapData|export.*useMapToggle' | head -10Repository: QueueLab/QCX
Length of output: 260
🏁 Script executed:
# Look for provider setup in the app
rg -n 'MapProvider|MapDataProvider|MapToggleProvider' --type tsx --type ts -C 5Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Search for UserMessage component usage
rg -n '<UserMessage' -C 3Repository: QueueLab/QCX
Length of output: 2371
🏁 Script executed:
# Find app.tsx or page.tsx to see provider setup
fd -name 'page.tsx' -o -name 'app.tsx' -o -name 'layout.tsx'Repository: QueueLab/QCX
Length of output: 286
🏁 Script executed:
# Look at the hook implementations
cat components/map/map-context.tsx | head -50Repository: QueueLab/QCX
Length of output: 860
🏁 Script executed:
# Check MapDataContext
cat components/map/map-data-context.tsx | head -50Repository: QueueLab/QCX
Length of output: 1734
🏁 Script executed:
# Check MapToggleContext
cat components/map-toggle-context.tsx | head -50Repository: QueueLab/QCX
Length of output: 1154
🏁 Script executed:
# Search for app.tsx or main layout/page setup
fd -name 'page.tsx' | head -5Repository: QueueLab/QCX
Length of output: 286
🏁 Script executed:
# Check the app page structure
cat app/page.tsx | head -100Repository: QueueLab/QCX
Length of output: 470
🏁 Script executed:
# Find and view useMapData hook implementation
rg -n 'export const useMapData' -A 10Repository: QueueLab/QCX
Length of output: 544
🏁 Script executed:
# Check if there's a root layout with providers
find . -name 'layout.tsx' -type fRepository: QueueLab/QCX
Length of output: 72
🏁 Script executed:
# Check the root layout for providers
cat app/layout.tsx | head -150Repository: QueueLab/QCX
Length of output: 2861
🏁 Script executed:
# Check the Chat component to see what it renders and where UserMessage is used
cat components/chat.tsx | head -100Repository: QueueLab/QCX
Length of output: 3498
🏁 Script executed:
# Search for where MapProvider and MapToggleProvider are used
rg -n 'MapProvider|MapToggleProvider' -B 2 -A 2Repository: QueueLab/QCX
Length of output: 8703
🏁 Script executed:
# Check where search-related.tsx and header-search-button.tsx are used
rg -n 'search-related|header-search-button' --type jsx --type jsRepository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Check followup-panel.tsx context
rg -n 'FollowupPanel|followup-panel' --type jsx --type js -B 2 -A 2Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Check if MapDataProvider is wrapped globally or only in page.tsx
rg -n 'MapDataProvider'Repository: QueueLab/QCX
Length of output: 842
🏁 Script executed:
# View the complete user-message.tsx file to see all hook usage
cat components/user-message.tsxRepository: QueueLab/QCX
Length of output: 3414
🏁 Script executed:
# Check where search-related.tsx is rendered
rg -n 'search-related|SearchRelated' -B 2 -A 2Repository: QueueLab/QCX
Length of output: 2123
🏁 Script executed:
# Check where header-search-button.tsx is rendered
rg -n 'header-search-button|HeaderSearchButton' -B 2 -A 2Repository: QueueLab/QCX
Length of output: 1232
🏁 Script executed:
# Check where followup-panel.tsx is rendered
rg -n 'followup-panel|FollowupPanel' -B 2 -A 2Repository: QueueLab/QCX
Length of output: 1546
🏁 Script executed:
# View header.tsx to see if it's at root level
cat components/header.tsx | head -80Repository: QueueLab/QCX
Length of output: 2481
🏁 Script executed:
# Verify if Chat component is used anywhere outside app/page.tsx and app/search/[id]/page.tsx
rg -n 'import.*Chat|<Chat' | grep -v 'ChatPanel\|ChatMessages\|ChatShare'Repository: QueueLab/QCX
Length of output: 1700
🏁 Script executed:
# Double-check: are there any other entry points that render Chat without MapDataProvider?
fd -name 'page.tsx' -o -name 'layout.tsx' | xargs rg -l 'Chat'Repository: QueueLab/QCX
Length of output: 1004
🏁 Script executed:
# Check if app/actions.tsx is a server action file and see how UserMessage is used there
head -10 app/actions.tsxRepository: QueueLab/QCX
Length of output: 273
🏁 Script executed:
# Check the context where UserMessage components are rendered - verify they're all within Chat
grep -n "component:" app/actions.tsx | head -20Repository: QueueLab/QCX
Length of output: 848
🏁 Script executed:
# Verify HeaderSearchButton is only used in Chat
rg -n 'HeaderSearchButton' -B 3 -A 1Repository: QueueLab/QCX
Length of output: 1254
🏁 Script executed:
# Final check: verify "Place on Map" button is always rendered or only when specific conditions are met
rg -n 'Place on Map|handlePlaceOnMap' -B 5 -A 2Repository: QueueLab/QCX
Length of output: 1280
All three hooks are currently used safely within the Chat context.
UserMessage is always rendered through components within the Chat component, which is wrapped with MapDataProvider in both entry points (app/page.tsx and app/search/[id]/page.tsx). MapProvider and MapToggleProvider wrap the entire application in the root layout. Therefore, useMap(), useMapData(), and useMapToggle() will not throw errors in current usage.
The "Place on Map" button is only rendered when an image is attached (imagePart exists), and the handler has guard clauses (if (!map || !imagePart) return), so the feature gracefully handles edge cases.
If UserMessage is ever extracted and used outside the Chat component in the future, the hooks would fail. Consider adopting defensive context access if you anticipate broader reusability.
🤖 Prompt for AI Agents
In `@components/user-message.tsx` around lines 28 - 30, The current hooks (useMap,
useMapData, useMapToggle) assume their contexts always exist which will break if
UserMessage is used outside Chat; change the hooks to be defensive (return
null/undefined or safe no-op functions instead of throwing when their Provider
is missing) and update UserMessage to safely read from them (e.g., const mapCtx
= useMap(); const map = mapCtx?.map; and const setMapDataFn =
useMapData()?.setMapData; const setMapTypeFn = useMapToggle()?.setMapType) and
keep the existing guard in the Place on Map handler that checks for map and
imagePart before using them. Ensure hook return types reflect optional values so
callers (UserMessage) can null-check before invoking methods like setMapData and
setMapType.
| <div className="group relative p-2 border rounded-lg bg-muted w-fit"> | ||
| <Image | ||
| src={imagePart} | ||
| alt="attachment" | ||
| width={300} | ||
| height={300} | ||
| className="max-w-xs max-h-64 rounded-md object-contain" | ||
| /> | ||
| <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity"> | ||
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| className="h-8 px-2 shadow-md" | ||
| onClick={handlePlaceOnMap} | ||
| > | ||
| <MapPin size={14} className="mr-1" /> | ||
| Place on Map | ||
| </Button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider touch device accessibility for the hover-revealed button.
The button only becomes visible on hover (opacity-0 group-hover:opacity-100), which works for mouse users but may not be discoverable on touch devices where hover states don't exist.
Consider adding group-focus-within:opacity-100 or making the button always visible (perhaps with reduced prominence):
♿ Suggested accessibility improvement
- <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
+ <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-within:opacity-100 transition-opacity">📝 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.
| <div className="group relative p-2 border rounded-lg bg-muted w-fit"> | |
| <Image | |
| src={imagePart} | |
| alt="attachment" | |
| width={300} | |
| height={300} | |
| className="max-w-xs max-h-64 rounded-md object-contain" | |
| /> | |
| <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <Button | |
| variant="secondary" | |
| size="sm" | |
| className="h-8 px-2 shadow-md" | |
| onClick={handlePlaceOnMap} | |
| > | |
| <MapPin size={14} className="mr-1" /> | |
| Place on Map | |
| </Button> | |
| </div> | |
| </div> | |
| <div className="group relative p-2 border rounded-lg bg-muted w-fit"> | |
| <Image | |
| src={imagePart} | |
| alt="attachment" | |
| width={300} | |
| height={300} | |
| className="max-w-xs max-h-64 rounded-md object-contain" | |
| /> | |
| <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-within:opacity-100 transition-opacity"> | |
| <Button | |
| variant="secondary" | |
| size="sm" | |
| className="h-8 px-2 shadow-md" | |
| onClick={handlePlaceOnMap} | |
| > | |
| <MapPin size={14} className="mr-1" /> | |
| Place on Map | |
| </Button> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
In `@components/user-message.tsx` around lines 82 - 101, The hover-only reveal for
the Place on Map button is not accessible on touch devices; update the wrapper
and button visibility so the control is reachable via keyboard and touch by
adding focus-based styles (e.g., include group-focus-within:opacity-100
alongside group-hover:opacity-100 on the absolute button container), ensure the
container is keyboard-focusable (add a tabindex or use a semantic button
wrapper) and confirm handlePlaceOnMap remains the click/keyboard activation
handler on the Button component (refer to the Button element with
onClick={handlePlaceOnMap} and the surrounding div using classes "group" and
"absolute top-4 right-4 ...").
|
@coderabbitai all the token generation features are not working such as the query input or the resolution search. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain
🏁 Script executed: Length of output: 268 🏁 Script executed: Length of output: 17240 🏁 Script executed: Length of output: 11308 🏁 Script executed: Length of output: 23126 🏁 Script executed: Length of output: 3637
The issue is that your AI API keys are missing from your environment configuration. Here's what I found: Root CauseYour current However, the "token generation" features (query input suggestions and resolution search) require AI model API keys to function. The
Without at least one of these configured, the AI features cannot generate tokens/responses. Solution
# AI Provider - Choose at least one
GEMINI_3_PRO_API_KEY=your_gemini_api_key_here
# OR
OPENAI_API_KEY=your_openai_api_key_here
# Also ensure these are configured:
COMPOSIO_API_KEY=your_composio_api_key
COMPOSIO_MAPBOX_AUTH_CONFIG_ID=ac_YOUR_MAPBOX_CONFIG_ID
COMPOSIO_USER_ID=user@example.com
# Supabase credentials
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL_HERE
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE
SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY_HERE
The map image overlay features in this PR work correctly, but they depend on the existing AI infrastructure being properly configured. |
I've identified and fixed the issue causing the token generation and query features to fail. The problem was related to top-level Redis initialization in |
…ty fixes - Add `imageOverlays` to `MapData` state and root `MapDataProvider`. - Implement `ImageOverlayLayer` component for rendering and georeferencing images on the map. - Add "Place on Map" interactive button for image attachments in chat messages. - Implement full map state restoration by parsing 'data' messages during chat load. - Fix "action is not a function" errors by making Redis initialization lazy. - Debounce map state persistence calls to improve performance and stability. - Ensure proper cleanup of Mapbox resources and React roots. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/actions/chat.ts (1)
187-199: Data messages accumulate indefinitely.Each map state change creates a new
'data'message. Since the restoration logic only uses the latest one, consider periodically pruning older data messages or using an upsert pattern to update a single data message per chat.
🤖 Fix all issues with AI agents
In `@components/chat.tsx`:
- Around line 93-99: The debouncedUpdateDrawingContext created via
useRef(debounce(...)) can fire after the component unmounts; add a cleanup
useEffect that calls debouncedUpdateDrawingContext.cancel() on unmount to
prevent stale writes and errors, referencing the existing
debouncedUpdateDrawingContext and updateDrawingContext symbols (ensure the
debounce implementation provides a cancel method or wrap it so cancel is
available).
- Line 22: Replace the current named import of debounce from the full lodash
package with a direct import of the debounce module to reduce bundle size:
change the import that brings in "debounce" (currently imported as { debounce }
from 'lodash') to a default import from 'lodash/debounce' (i.e., import debounce
from 'lodash/debounce') and update any usages of the debounce identifier
accordingly in components/chat.tsx.
- Around line 102-110: The current useEffect calls debouncedUpdateDrawingContext
even when drawnFeatures or imageOverlays are empty arrays because
`(mapData.drawnFeatures || mapData.imageOverlays)` is truthy for [] — update the
condition in the useEffect (the block that references id, mapData.drawnFeatures,
mapData.imageOverlays, mapData.cameraState, and debouncedUpdateDrawingContext)
to check array lengths explicitly (e.g. only proceed if
(mapData.drawnFeatures?.length ?? 0) > 0 or (mapData.imageOverlays?.length ?? 0)
> 0) along with a valid cameraState and id so empty arrays won’t trigger
unnecessary persistence.
In `@lib/actions/chat.ts`:
- Around line 217-218: The environment token is not being normalized like the
URL: trim UPSTASH_REDIS_REST_TOKEN as well to avoid authentication failures from
trailing/leading whitespace. Update the code where UPSTASH_REDIS_REST_URL and
UPSTASH_REDIS_REST_TOKEN are read (the url and token variables in
lib/actions/chat.ts) so both use .trim() (or equivalent null-safe trimming) and
keep the same null/undefined handling as url to ensure consistent credential
processing.
- Around line 209-210: The import of Redis from '@upstash/redis' is declared
mid-file with a // `@ts-ignore`; move that import to the top with the other
imports (place the statement importing Redis alongside the other imports near
the top of lib/actions/chat.ts), remove the inline // `@ts-ignore` and resolve the
underlying typing issue instead (either fix the import syntax, install/update
types, or add a proper declaration file), and delete the duplicate mid-file
import lines that currently reference Redis.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
components/chat.tsxlib/actions/chat.ts
🧰 Additional context used
🧬 Code graph analysis (1)
components/chat.tsx (4)
lib/actions/chat.ts (1)
updateDrawingContext(165-204)app/page.tsx (1)
Page(9-18)components/chat-panel.tsx (2)
ref(31-308)e(90-125)components/map/mapbox-map.tsx (1)
prevData(166-166)
🔇 Additional comments (5)
lib/actions/chat.ts (3)
240-247: LGTM!The lazy Redis client pattern with graceful degradation is well implemented. The early return with appropriate logging when Redis is not configured prevents crashes in environments without Redis.
263-270: LGTM!Consistent application of the lazy Redis client pattern in
getSystemPrompt.
179-179: The review comment's concern is unfounded. The database schema definesroleas a simplevarchar(50)column with no constraints, enums, or CHECK conditions. The comment on the schema merely lists examples ('user', 'assistant', 'system', 'tool') but does not enforce them. The 'data' role will insert successfully without any constraint violation. The type assertion is unnecessary since the inferredNewMessagetype already definesroleasstring.Likely an incorrect or invalid review comment.
components/chat.tsx (2)
115-115: LGTM!Removing the redundant
MapDataProviderwrapper in the mobile layout is correct. Theapp/page.tsxsnippet showsChatis already wrapped byMapDataProviderat the page level, so the inner provider was shadowing the global map state.
175-175: LGTM!Same fix for the desktop layout - removing the redundant inner
MapDataProviderto use the outer context properly.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action | ||
| import dynamic from 'next/dynamic' | ||
| import { HeaderSearchButton } from './header-search-button' | ||
| import { debounce } from 'lodash' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider importing from lodash/debounce for smaller bundles.
While modern bundlers tree-shake { debounce } from 'lodash', importing directly from lodash/debounce guarantees only the debounce module is included.
🔧 Suggested change
-import { debounce } from 'lodash'
+import debounce from 'lodash/debounce'📝 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.
| import { debounce } from 'lodash' | |
| import debounce from 'lodash/debounce' |
🤖 Prompt for AI Agents
In `@components/chat.tsx` at line 22, Replace the current named import of debounce
from the full lodash package with a direct import of the debounce module to
reduce bundle size: change the import that brings in "debounce" (currently
imported as { debounce } from 'lodash') to a default import from
'lodash/debounce' (i.e., import debounce from 'lodash/debounce') and update any
usages of the debounce identifier accordingly in components/chat.tsx.
| // Debounced version of updateDrawingContext | ||
| const debouncedUpdateDrawingContext = useRef( | ||
| debounce((chatId: string, contextData: any) => { | ||
| console.log('Chat.tsx: calling debounced updateDrawingContext'); | ||
| updateDrawingContext(chatId, contextData); | ||
| }, 2000) | ||
| ).current; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cancel debounced function on unmount to prevent stale writes.
The debounced function may fire after the component unmounts, potentially causing errors or writing to a stale chat. Add cleanup to cancel pending calls.
🛡️ Proposed fix
const debouncedUpdateDrawingContext = useRef(
debounce((chatId: string, contextData: any) => {
console.log('Chat.tsx: calling debounced updateDrawingContext');
updateDrawingContext(chatId, contextData);
}, 2000)
).current;
+ useEffect(() => {
+ return () => {
+ debouncedUpdateDrawingContext.cancel();
+ };
+ }, [debouncedUpdateDrawingContext]);📝 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.
| // Debounced version of updateDrawingContext | |
| const debouncedUpdateDrawingContext = useRef( | |
| debounce((chatId: string, contextData: any) => { | |
| console.log('Chat.tsx: calling debounced updateDrawingContext'); | |
| updateDrawingContext(chatId, contextData); | |
| }, 2000) | |
| ).current; | |
| // Debounced version of updateDrawingContext | |
| const debouncedUpdateDrawingContext = useRef( | |
| debounce((chatId: string, contextData: any) => { | |
| console.log('Chat.tsx: calling debounced updateDrawingContext'); | |
| updateDrawingContext(chatId, contextData); | |
| }, 2000) | |
| ).current; | |
| useEffect(() => { | |
| return () => { | |
| debouncedUpdateDrawingContext.cancel(); | |
| }; | |
| }, [debouncedUpdateDrawingContext]); |
🤖 Prompt for AI Agents
In `@components/chat.tsx` around lines 93 - 99, The debouncedUpdateDrawingContext
created via useRef(debounce(...)) can fire after the component unmounts; add a
cleanup useEffect that calls debouncedUpdateDrawingContext.cancel() on unmount
to prevent stale writes and errors, referencing the existing
debouncedUpdateDrawingContext and updateDrawingContext symbols (ensure the
debounce implementation provides a cancel method or wrap it so cancel is
available).
| useEffect(() => { | ||
| if (id && mapData.drawnFeatures && mapData.cameraState) { | ||
| console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures); | ||
| updateDrawingContext(id, { | ||
| drawnFeatures: mapData.drawnFeatures, | ||
| if (id && (mapData.drawnFeatures || mapData.imageOverlays) && mapData.cameraState) { | ||
| debouncedUpdateDrawingContext(id, { | ||
| drawnFeatures: mapData.drawnFeatures || [], | ||
| imageOverlays: mapData.imageOverlays || [], | ||
| cameraState: mapData.cameraState, | ||
| }); | ||
| } | ||
| }, [id, mapData.drawnFeatures, mapData.cameraState]); | ||
| }, [id, mapData.drawnFeatures, mapData.imageOverlays, mapData.cameraState, debouncedUpdateDrawingContext]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Empty arrays trigger persistence unnecessarily.
The condition (mapData.drawnFeatures || mapData.imageOverlays) is truthy for empty arrays []. Consider checking array length to avoid writing empty state repeatedly.
🔧 Suggested improvement
useEffect(() => {
- if (id && (mapData.drawnFeatures || mapData.imageOverlays) && mapData.cameraState) {
+ const hasFeatures = mapData.drawnFeatures?.length > 0;
+ const hasOverlays = mapData.imageOverlays?.length > 0;
+ if (id && (hasFeatures || hasOverlays) && mapData.cameraState) {
debouncedUpdateDrawingContext(id, {
drawnFeatures: mapData.drawnFeatures || [],
imageOverlays: mapData.imageOverlays || [],
cameraState: mapData.cameraState,
});
}
}, [id, mapData.drawnFeatures, mapData.imageOverlays, mapData.cameraState, debouncedUpdateDrawingContext]);📝 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.
| useEffect(() => { | |
| if (id && mapData.drawnFeatures && mapData.cameraState) { | |
| console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures); | |
| updateDrawingContext(id, { | |
| drawnFeatures: mapData.drawnFeatures, | |
| if (id && (mapData.drawnFeatures || mapData.imageOverlays) && mapData.cameraState) { | |
| debouncedUpdateDrawingContext(id, { | |
| drawnFeatures: mapData.drawnFeatures || [], | |
| imageOverlays: mapData.imageOverlays || [], | |
| cameraState: mapData.cameraState, | |
| }); | |
| } | |
| }, [id, mapData.drawnFeatures, mapData.cameraState]); | |
| }, [id, mapData.drawnFeatures, mapData.imageOverlays, mapData.cameraState, debouncedUpdateDrawingContext]); | |
| useEffect(() => { | |
| const hasFeatures = mapData.drawnFeatures?.length > 0; | |
| const hasOverlays = mapData.imageOverlays?.length > 0; | |
| if (id && (hasFeatures || hasOverlays) && mapData.cameraState) { | |
| debouncedUpdateDrawingContext(id, { | |
| drawnFeatures: mapData.drawnFeatures || [], | |
| imageOverlays: mapData.imageOverlays || [], | |
| cameraState: mapData.cameraState, | |
| }); | |
| } | |
| }, [id, mapData.drawnFeatures, mapData.imageOverlays, mapData.cameraState, debouncedUpdateDrawingContext]); |
🤖 Prompt for AI Agents
In `@components/chat.tsx` around lines 102 - 110, The current useEffect calls
debouncedUpdateDrawingContext even when drawnFeatures or imageOverlays are empty
arrays because `(mapData.drawnFeatures || mapData.imageOverlays)` is truthy for
[] — update the condition in the useEffect (the block that references id,
mapData.drawnFeatures, mapData.imageOverlays, mapData.cameraState, and
debouncedUpdateDrawingContext) to check array lengths explicitly (e.g. only
proceed if (mapData.drawnFeatures?.length ?? 0) > 0 or
(mapData.imageOverlays?.length ?? 0) > 0) along with a valid cameraState and id
so empty arrays won’t trigger unnecessary persistence.
| // @ts-ignore - Ignoring Redis import error for now as it might be removed or replaced | ||
| import { Redis } from '@upstash/redis'; // This will cause issues if REDIS_URL is not configured. | ||
| const redis = new Redis({ | ||
| url: process.env.UPSTASH_REDIS_REST_URL?.trim() || '', | ||
| token: process.env.UPSTASH_REDIS_REST_TOKEN || '' | ||
| }); | ||
| import { Redis } from '@upstash/redis'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Move import to the top of the file.
The @upstash/redis import is placed mid-file after function definitions. While hoisting makes this work, conventional placement at the top improves readability. The @ts-ignore also suppresses potential legitimate type errors.
🔧 Suggested placement
Move the import to the top with other imports (after line 18), and address the underlying type issue rather than suppressing it:
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'
+import { Redis } from '@upstash/redis'Then remove lines 209-210.
🤖 Prompt for AI Agents
In `@lib/actions/chat.ts` around lines 209 - 210, The import of Redis from
'@upstash/redis' is declared mid-file with a // `@ts-ignore`; move that import to
the top with the other imports (place the statement importing Redis alongside
the other imports near the top of lib/actions/chat.ts), remove the inline //
`@ts-ignore` and resolve the underlying typing issue instead (either fix the
import syntax, install/update types, or add a proper declaration file), and
delete the duplicate mid-file import lines that currently reference Redis.
| const url = process.env.UPSTASH_REDIS_REST_URL?.trim(); | ||
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Inconsistent whitespace handling for credentials.
UPSTASH_REDIS_REST_URL is trimmed but UPSTASH_REDIS_REST_TOKEN is not. For consistency and to avoid subtle auth failures from trailing whitespace in env files, consider trimming both.
🔧 Suggested fix
const url = process.env.UPSTASH_REDIS_REST_URL?.trim();
- const token = process.env.UPSTASH_REDIS_REST_TOKEN;
+ const token = process.env.UPSTASH_REDIS_REST_TOKEN?.trim();📝 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.
| const url = process.env.UPSTASH_REDIS_REST_URL?.trim(); | |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; | |
| const url = process.env.UPSTASH_REDIS_REST_URL?.trim(); | |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN?.trim(); |
🤖 Prompt for AI Agents
In `@lib/actions/chat.ts` around lines 217 - 218, The environment token is not
being normalized like the URL: trim UPSTASH_REDIS_REST_TOKEN as well to avoid
authentication failures from trailing/leading whitespace. Update the code where
UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are read (the url and token
variables in lib/actions/chat.ts) so both use .trim() (or equivalent null-safe
trimming) and keep the same null/undefined handling as url to ensure consistent
credential processing.
User description
This PR implements the ability for users to place images from the chat onto the map as positioned overlays.
Key features:
drawnFeaturesandimageOverlaysto the map.PR created automatically by Jules for task 8494928424595183080 started by @ngoiyaeric
PR Type
Enhancement
Description
Implement image overlay placement on map with draggable corner markers
Add "Place on Map" button to image attachments in chat messages
Persist and restore image overlays from database via 'data' messages
Fix redundant MapDataProvider instances shadowing global state
Support imageOverlays in map state alongside drawnFeatures
Diagram Walkthrough
File Walkthrough
chat.ts
Add imageOverlays persistence to server actionlib/actions/chat.ts
updateDrawingContextfunction signature to accept optionalimageOverlaysparameterdrawnFeaturesandimageOverlaysto databasepage.tsx
Restore map state from database messagesapp/search/[id]/page.tsx
initialDatatoMapDataProviderfor state restorationimage-overlay-layer.tsx
New image overlay layer component with markerscomponents/map/image-overlay-layer.tsx
drawing mode
map-data-context.tsx
Add imageOverlays to map data contextcomponents/map/map-data-context.tsx
ImageOverlayinterface with id, url, coordinates, and opacityproperties
imageOverlaysarray toMapDatainterfaceMapDataProviderto accept optionalinitialDataprop for staterestoration
mapbox-map.tsx
Render image overlays on Mapboxcomponents/map/mapbox-map.tsx
ImageOverlayLayercomponentuser-message.tsx
Add place on map button to image attachmentscomponents/user-message.tsx
handlePlaceOnMapfunction to calculate initial overlaycoordinates based on current map bounds
.env
Add Mapbox access token configuration.env
NEXT_PUBLIC_MAPBOX_ACCESS_TOKENenvironment variable with dummytoken
chat.tsx
Fix provider nesting and add imageOverlays trackingcomponents/chat.tsx
drawnFeaturesandimageOverlayschangesupdateDrawingContextserver actionMapDataProviderwrappers from mobile and desktoplayouts
Summary by CodeRabbit
New Features
Performance
Chores
✏️ Tip: You can customize this high-level summary in your review settings.