Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1,544 changes: 1,544 additions & 0 deletions changes.diff

Large diffs are not rendered by default.

45 changes: 20 additions & 25 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { useCalendarToggle } from './calendar-toggle-context'
import { CalendarNotepad } from './calendar-notepad'
import { MapProvider } from './map/map-provider'
import { useUIState, useAIState } from 'ai/rsc'
import { AI } from '@/app/actions'
import { AIMessage } from '@/lib/types'
import MobileIconsBar from './mobile-icons-bar'
import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle-context";
import { useUsageToggle } from "@/components/usage-toggle-context";
import SettingsView from "@/components/settings/settings-view";
import { UsageView } from "@/components/usage-view";
import { MapDataProvider, useMapData } from './map/map-data-context'; // Add this and useMapData
import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action
import { MapDataProvider, useMapData } from './map/map-data-context';
import { updateDrawingContext } from '@/lib/actions/chat';
import dynamic from 'next/dynamic'
import { HeaderSearchButton } from './header-search-button'

Expand All @@ -29,8 +31,8 @@ type ChatProps = {
export function Chat({ id }: ChatProps) {
const router = useRouter()
const path = usePathname()
const [messages] = useUIState()
const [aiState] = useAIState()
const [messages] = useUIState<typeof AI>()
const [aiState] = useAIState<typeof AI>()
const [isMobile, setIsMobile] = useState(false)
const { activeView } = useProfileToggle();
const { isUsageOpen } = useUsageToggle();
Expand All @@ -41,6 +43,9 @@ export function Chat({ id }: ChatProps) {
const [suggestions, setSuggestions] = useState<PartialRelated | null>(null)
const chatPanelRef = useRef<ChatPanelRef>(null);

// Ref to track the last message ID we refreshed the router for, to prevent infinite loops
const lastRefreshedMessageIdRef = useRef<string | null>(null);

const handleAttachment = () => {
chatPanelRef.current?.handleAttachmentClick();
};
Expand All @@ -54,18 +59,11 @@ export function Chat({ id }: ChatProps) {
}, [messages])

useEffect(() => {
// Check if device is mobile
const checkMobile = () => {
setIsMobile(window.innerWidth < 768)
}

// Initial check
checkMobile()

// Add event listener for window resize
window.addEventListener('resize', checkMobile)

// Cleanup
return () => window.removeEventListener('resize', checkMobile)
}, [])

Expand All @@ -76,13 +74,16 @@ export function Chat({ id }: ChatProps) {
}, [id, path, messages])

useEffect(() => {
if (aiState.messages[aiState.messages.length - 1]?.type === 'response') {
// Refresh the page to chat history updates
router.refresh()
// Check if there is a 'response' message in the history
const responseMessage = aiState.messages.findLast((m: AIMessage) => m.type === 'response');

if (responseMessage && responseMessage.id !== lastRefreshedMessageIdRef.current) {
console.log('Chat.tsx: refreshing router for message:', responseMessage.id);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug console.log before merging.

This log fires on every new AI response message and will be noisy in production. Consider removing it or gating it behind a NODE_ENV === 'development' check.

Proposed fix
    if (responseMessage && responseMessage.id !== lastRefreshedMessageIdRef.current) {
-     console.log('Chat.tsx: refreshing router for message:', responseMessage.id);
      lastRefreshedMessageIdRef.current = responseMessage.id;
      router.refresh();
    }
📝 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
console.log('Chat.tsx: refreshing router for message:', responseMessage.id);
if (responseMessage && responseMessage.id !== lastRefreshedMessageIdRef.current) {
lastRefreshedMessageIdRef.current = responseMessage.id;
router.refresh();
}
🤖 Prompt for AI Agents
In `@components/chat.tsx` at line 81, Remove the noisy debug console.log in
Chat.tsx that logs "Chat.tsx: refreshing router for message:" with
responseMessage.id; either delete that console.log or wrap it behind a
development-only guard (process.env.NODE_ENV === 'development') so it does not
run in production. Locate the statement in the function handling AI responses
(the code referencing responseMessage.id) and update it accordingly to silence
production logs.

lastRefreshedMessageIdRef.current = responseMessage.id;
router.refresh();
}
}, [aiState, router])
}, [aiState.messages, router])
Comment on lines 76 to 85
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 tsconfig.json for target and lib settings
find . -name "tsconfig.json" -type f | head -5

Repository: QueueLab/QCX

Length of output: 373


🏁 Script executed:

# Look for tsconfig.json in common locations and check target/lib
if [ -f tsconfig.json ]; then
  echo "=== tsconfig.json content ===" 
  cat tsconfig.json
fi

Repository: QueueLab/QCX

Length of output: 751


🏁 Script executed:

# Search for findLast usage in the codebase
rg "findLast" -t ts -t tsx -C 2

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Check for polyfills or compatibility layers
rg "polyfill|shim|findLast" --type ts --type tsx --type json | head -30

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Search for findLast in TypeScript/React files
rg "findLast" --type ts

Repository: QueueLab/QCX

Length of output: 158


🏁 Script executed:

# Check the specific file and lines mentioned in the review
if [ -f components/chat.tsx ]; then
  echo "=== components/chat.tsx lines 70-85 ===" 
  sed -n '70,85p' components/chat.tsx
fi

Repository: QueueLab/QCX

Length of output: 685


🏁 Script executed:

# Check for any polyfills or compatibility layers in the project
rg "Array.prototype" --type ts | head -10

Repository: QueueLab/QCX

Length of output: 38


findLast is ES2023 — runtime compatibility issue with target ES2018.

The code at line 76 uses Array.prototype.findLast(), which is an ES2023 feature. The project's tsconfig.json sets target: "ES2018" while using lib: ["dom", "dom.iterable", "esnext"], allowing TypeScript to compile the code without errors. However, this will fail at runtime in environments without ES2023 support (older browsers, Node.js < 18.x).

Replace with an alternative that works in ES2018:

const responseMessage = [...aiState.messages].reverse().find(m => m.type === 'response');

Or upgrade tsconfig.json to "target": "ES2023" if project requirements allow.

🤖 Prompt for AI Agents
In `@components/chat.tsx` around lines 74 - 83, The use of
Array.prototype.findLast on aiState.messages is incompatible with the project's
ES2018 runtime target; replace the findLast call in the useEffect with an
ES2018-safe approach (e.g., iterate from the end or create a reversed copy then
use find) so responseMessage is resolved without relying on ES2023 features,
keeping the rest of the logic that checks lastRefreshedMessageIdRef.current and
calls router.refresh() unchanged (update the code around
aiState.messages.findLast, lastRefreshedMessageIdRef, and router.refresh
accordingly).


// Get mapData to access drawnFeatures
const { mapData } = useMapData();

useEffect(() => {
Expand All @@ -92,10 +93,8 @@ export function Chat({ id }: ChatProps) {
}
}, [isSubmitting])

// useEffect to call the server action when drawnFeatures changes
useEffect(() => {
if (id && mapData.drawnFeatures && mapData.cameraState) {
console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures);
updateDrawingContext(id, {
drawnFeatures: mapData.drawnFeatures,
cameraState: mapData.cameraState,
Expand All @@ -112,7 +111,6 @@ export function Chat({ id }: ChatProps) {
onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setIsSubmitting(true)
}}
onClose={() => setSuggestions(null)}
Expand All @@ -122,10 +120,9 @@ export function Chat({ id }: ChatProps) {
);
};

// Mobile layout
if (isMobile) {
return (
<MapDataProvider> {/* Add Provider */}
<MapDataProvider>
<HeaderSearchButton />
<div className="mobile-layout-container">
<div className="mobile-map-section">
Expand Down Expand Up @@ -169,12 +166,10 @@ export function Chat({ id }: ChatProps) {
);
}

// Desktop layout
return (
<MapDataProvider> {/* Add Provider */}
<MapDataProvider>
<HeaderSearchButton />
<div className="flex justify-start items-start">
{/* This is the new div for scrolling */}
<div className="w-1/2 flex flex-col space-y-3 md:space-y-4 px-8 sm:px-12 pt-16 md:pt-20 pb-4 h-[calc(100vh-0.5in)] overflow-y-auto">
{isCalendarOpen ? (
<CalendarNotepad chatId={id} />
Expand Down Expand Up @@ -206,7 +201,7 @@ export function Chat({ id }: ChatProps) {
</div>
<div
className="w-1/2 p-4 fixed h-[calc(100vh-0.5in)] top-0 right-0 mt-[0.5in]"
style={{ zIndex: 10 }} // Added z-index
style={{ zIndex: 10 }}
>
{activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />}
</div>
Expand Down
38 changes: 31 additions & 7 deletions components/header-search-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useActions, useUIState } from 'ai/rsc'
import { AI } from '@/app/actions'
import { nanoid } from 'nanoid'
import { UserMessage } from './user-message'
import { toast } from 'react-toastify'
import { toast } from 'sonner'
import { useSettingsStore } from '@/lib/store/settings'
import { useMapData } from './map/map-data-context'

Expand All @@ -22,24 +22,46 @@ export function HeaderSearchButton() {
const { map } = useMap()
const { mapProvider } = useSettingsStore()
const { mapData } = useMapData()
// Cast the actions to our defined interface to avoid build errors
const actions = useActions<typeof AI>() as unknown as HeaderActions
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

Double cast as unknown as HeaderActions bypasses type safety.

This silences any type mismatch between the actual actions returned by useActions<typeof AI>() and HeaderActions. If the shape changes, TypeScript won't catch it.

Consider properly typing the AI actions or using a type guard instead.

🤖 Prompt for AI Agents
In `@components/header-search-button.tsx` at line 25, The double-cast "as unknown
as HeaderActions" bypasses TypeScript checks; replace it by giving useActions
the correct generic or by narrowing its return with a type guard. Specifically,
update the call to useActions<typeof AI>() so it returns a properly typed
actions object (e.g., useActions<HeaderActions>() or change the AI type
parameter to match HeaderActions) or add a runtime/type-guard function that
validates the shape before assigning to const actions; reference the
useActions<typeof AI>() call and the HeaderActions type in
header-search-button.tsx to locate and correct the typing.

const [, setMessages] = useUIState<typeof AI>()
const [isAnalyzing, setIsAnalyzing] = useState(false)

// Use state for portals to trigger re-renders when they are found
const [desktopPortal, setDesktopPortal] = useState<HTMLElement | null>(null)
const [mobilePortal, setMobilePortal] = useState<HTMLElement | null>(null)

useEffect(() => {
// Portals can only be used on the client-side after the DOM has mounted
setDesktopPortal(document.getElementById('header-search-portal'))
setMobilePortal(document.getElementById('mobile-header-search-portal'))
// Function to find and set portals
const findPortals = () => {
setDesktopPortal(document.getElementById('header-search-portal'))
setMobilePortal(document.getElementById('mobile-header-search-portal'))
}

// Initial check
findPortals()

// Use a MutationObserver to detect when portals are added to the DOM
const observer = new MutationObserver(() => {
findPortals()
})

observer.observe(document.body, {
childList: true,
subtree: true
})

return () => observer.disconnect()
Comment on lines +44 to +53
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

MutationObserver on document.body with subtree: true is overly broad.

This observer fires on every single DOM mutation (any element added/removed anywhere in the page), just to locate two portal divs that are rendered once. This can cause excessive re-renders and getElementById lookups on busy pages.

Consider a more targeted approach: observe only the direct parent where portals are expected, or simply retry with requestAnimationFrame / a short interval until found, then stop.

Example: polling with cleanup
   useEffect(() => {
-    const findPortals = () => {
-      setDesktopPortal(document.getElementById('header-search-portal'))
-      setMobilePortal(document.getElementById('mobile-header-search-portal'))
-    }
-
-    findPortals()
-
-    const observer = new MutationObserver(() => {
-      findPortals()
-    })
-
-    observer.observe(document.body, {
-      childList: true,
-      subtree: true
-    })
-
-    return () => observer.disconnect()
+    let rafId: number;
+    const findPortals = () => {
+      const desktop = document.getElementById('header-search-portal');
+      const mobile = document.getElementById('mobile-header-search-portal');
+      setDesktopPortal(desktop);
+      setMobilePortal(mobile);
+      if (!desktop || !mobile) {
+        rafId = requestAnimationFrame(findPortals);
+      }
+    };
+    findPortals();
+    return () => cancelAnimationFrame(rafId);
   }, [])
🤖 Prompt for AI Agents
In `@components/header-search-button.tsx` around lines 44 - 53, The
MutationObserver created in this file (observer) is watching document.body with
subtree:true which is too broad; change findPortals mounting logic to stop
observing global DOM mutations: either narrow the observer to the specific
container element where the portal divs are expected (attach observer to that
parent instead of document.body) or replace the observer with a short polling
loop using requestAnimationFrame or setInterval that calls findPortals until the
two portals are found, then clears the raf/interval; ensure the cleanup
currently returning observer.disconnect() is updated to cancel the
interval/animation frame or disconnect the targeted observer and keep references
to the observer/handle so it is stopped as soon as findPortals succeeds.

}, [])

const handleResolutionSearch = async () => {
if (mapProvider === 'mapbox' && !map) {
toast.error('Map is not available yet. Please wait for it to load.')
return
}
if (mapProvider === 'google' && !mapData.cameraState) {
toast.error('Google Maps state is not available. Try moving the map first.')
return
}
if (!actions) {
toast.error('Search actions are not available.')
return
Expand Down Expand Up @@ -102,12 +124,14 @@ export function HeaderSearchButton() {
}
}

const isMapAvailable = mapProvider === 'mapbox' ? !!map : !!mapData.cameraState

const desktopButton = (
<Button
variant="ghost"
size="icon"
onClick={handleResolutionSearch}
disabled={isAnalyzing || !map || !actions}
disabled={isAnalyzing || !isMapAvailable || !actions}
title="Analyze current map view"
>
{isAnalyzing ? (
Expand All @@ -119,7 +143,7 @@ export function HeaderSearchButton() {
)

const mobileButton = (
<Button variant="ghost" size="sm" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions}>
<Button variant="ghost" size="sm" onClick={handleResolutionSearch} disabled={isAnalyzing || !isMapAvailable || !actions}>
<Search className="h-4 w-4 mr-2" />
Search
</Button>
Expand Down
72 changes: 72 additions & 0 deletions components/map/draw-modes/circle-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as turf from '@turf/turf';

const CircleMode: any = {
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding TypeScript types for better maintainability.

The any type is used throughout this file. While mapbox-gl-draw's mode API is loosely typed, adding at least partial type definitions would improve IDE support and catch potential issues.

Example type definitions
interface CircleModeState {
  circle: {
    id: string;
    properties: {
      user_isCircle: boolean;
      user_center: [number, number] | [];
      user_radiusInKm?: number;
    };
    setCoordinates: (coords: number[][][]) => void;
  };
}

interface DrawEvent {
  lngLat: { lng: number; lat: number };
}
🤖 Prompt for AI Agents
In `@components/map/draw-modes/circle-mode.ts` at line 3, Replace the broad "any"
on the CircleMode constant with precise TypeScript interfaces: define a
CircleModeState (with circle.id, circle.properties.user_isCircle, user_center,
optional user_radiusInKm, and setCoordinates signature) and a DrawEvent (with
lngLat {lng, lat}), then type CircleMode as an object implementing the
mapbox-gl-draw mode methods using those interfaces (e.g., handlers like
onMouseMove(event: DrawEvent), onClick(event: DrawEvent), and state:
CircleModeState). Update function and method signatures inside CircleMode to use
these types (referencing CircleMode, CircleModeState, DrawEvent, and methods
like setCoordinates) to improve IDE support and catch type errors.

onSetup: function(opts: any) {
const state: any = {};
state.circle = this.newFeature({
type: 'Feature',
properties: {
user_isCircle: true,
user_center: []
},
geometry: {
type: 'Polygon',
coordinates: [[]]
}
});
this.addFeature(state.circle);
this.clearSelectedFeatures();
this.updateUIClasses({ mouse: 'add' });
this.activateUIButton('circle');
this.setActionableState({
trash: true
});
return state;
},

onTap: function(state: any, e: any) {
this.onClick(state, e);
},

onClick: function(state: any, e: any) {
if (state.circle.properties.user_center.length === 0) {
state.circle.properties.user_center = [e.lngLat.lng, e.lngLat.lat];
// Set initial point-like polygon
state.circle.setCoordinates([[
[e.lngLat.lng, e.lngLat.lat],
[e.lngLat.lng, e.lngLat.lat],
[e.lngLat.lng, e.lngLat.lat],
[e.lngLat.lng, e.lngLat.lat]
]]);
} else {
this.changeMode('simple_select', { featureIds: [state.circle.id] });
}
},

onMouseMove: function(state: any, e: any) {
if (state.circle.properties.user_center.length > 0) {
const center = state.circle.properties.user_center;
const distance = turf.distance(center, [e.lngLat.lng, e.lngLat.lat], { units: 'kilometers' });
const circle = turf.circle(center, distance, { steps: 64, units: 'kilometers' });
state.circle.setCoordinates(circle.geometry.coordinates);
state.circle.properties.user_radiusInKm = distance;
}
},
Comment on lines +46 to +54
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

Zero-radius circle creates degenerate geometry.

If the user double-clicks the same point or clicks very close, distance could be 0 or near-zero, creating a circle with no visible area. Consider enforcing a minimum radius.

🛡️ Proposed fix to enforce minimum radius
   onMouseMove: function(state: any, e: any) {
     if (state.circle.properties.user_center.length > 0) {
       const center = state.circle.properties.user_center;
-      const distance = turf.distance(center, [e.lngLat.lng, e.lngLat.lat], { units: 'kilometers' });
+      const rawDistance = turf.distance(center, [e.lngLat.lng, e.lngLat.lat], { units: 'kilometers' });
+      const distance = Math.max(rawDistance, 0.01); // Minimum 10m radius
       const circle = turf.circle(center, distance, { steps: 64, units: 'kilometers' });
       state.circle.setCoordinates(circle.geometry.coordinates);
       state.circle.properties.user_radiusInKm = distance;
     }
   },
📝 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
onMouseMove: function(state: any, e: any) {
if (state.circle.properties.user_center.length > 0) {
const center = state.circle.properties.user_center;
const distance = turf.distance(center, [e.lngLat.lng, e.lngLat.lat], { units: 'kilometers' });
const circle = turf.circle(center, distance, { steps: 64, units: 'kilometers' });
state.circle.setCoordinates(circle.geometry.coordinates);
state.circle.properties.user_radiusInKm = distance;
}
},
onMouseMove: function(state: any, e: any) {
if (state.circle.properties.user_center.length > 0) {
const center = state.circle.properties.user_center;
const rawDistance = turf.distance(center, [e.lngLat.lng, e.lngLat.lat], { units: 'kilometers' });
const distance = Math.max(rawDistance, 0.01); // Minimum 10m radius
const circle = turf.circle(center, distance, { steps: 64, units: 'kilometers' });
state.circle.setCoordinates(circle.geometry.coordinates);
state.circle.properties.user_radiusInKm = distance;
}
},
🤖 Prompt for AI Agents
In `@components/map/draw-modes/circle-mode.ts` around lines 46 - 54, onMouseMove
in circle-mode.ts can produce a zero/near-zero radius (using turf.distance and
turf.circle) which yields degenerate geometry; clamp the computed distance to a
small minimum (e.g. minRadiusKm) before calling turf.circle, set
state.circle.properties.user_radiusInKm to the clamped value, and then call
state.circle.setCoordinates with the circle generated from the clamped radius so
the circle always has a visible area even when clicks are on the same point.


onKeyUp: function(state: any, e: any) {
if (e.keyCode === 27) return this.changeMode('simple_select');
},
Comment on lines +56 to +58
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

Missing onStop handler for mode cleanup.

The mode doesn't implement onStop to handle cleanup when the mode is exited unexpectedly (e.g., via programmatic mode change). This could leave incomplete features in the draw state.

Add onStop handler
   onKeyUp: function(state: any, e: any) {
     if (e.keyCode === 27) return this.changeMode('simple_select');
   },

+  onStop: function(state: any) {
+    // Clean up incomplete circle if user exits mode without completing
+    if (state.circle && state.circle.properties.user_center.length === 0) {
+      this.deleteFeature(state.circle.id);
+    }
+  },
+
   toDisplayFeatures: function(state: any, geojson: any, display: any) {
📝 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
onKeyUp: function(state: any, e: any) {
if (e.keyCode === 27) return this.changeMode('simple_select');
},
onKeyUp: function(state: any, e: any) {
if (e.keyCode === 27) return this.changeMode('simple_select');
},
onStop: function(state: any) {
// Clean up incomplete circle if user exits mode without completing
if (state.circle && state.circle.properties.user_center.length === 0) {
this.deleteFeature(state.circle.id);
}
},
🤖 Prompt for AI Agents
In `@components/map/draw-modes/circle-mode.ts` around lines 56 - 58, Add an onStop
handler to the circle draw mode to perform cleanup when the mode is exited
(similar to how onKeyUp calls this.changeMode('simple_select')); implement
onStop on the same mode object and make it cancel any in-progress draw action,
remove temporary layers/markers, reset cursor/interaction state, and clear any
mode-specific state so incomplete features aren’t left in the draw state when
changeMode or programmatic exits occur.


toDisplayFeatures: function(state: any, geojson: any, display: any) {
const isActive = geojson.id === state.circle.id;
geojson.properties.active = isActive ? 'true' : 'false';
if (!isActive) return display(geojson);

// Only display if it has a center (and thus coordinates set)
if (geojson.properties.user_center && geojson.properties.user_center.length > 0) {
display(geojson);
}
}
};
Comment on lines +1 to +70
Copy link

Choose a reason for hiding this comment

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

CircleMode is implemented with pervasive any (const CircleMode: any, opts: any, state: any, event e: any, and toDisplayFeatures args). This is type-valid but unsafe and makes it easy to break at runtime (e.g., assuming e.lngLat exists, or state.circle.properties.user_center is always an array).

Also, onSetup ignores opts, and activateUIButton('circle') may not correspond to a registered button name (you create a DOM button with class mapbox-gl-draw_circle, but Mapbox Draw’s internal UI button names typically follow built-in control IDs). If the UI button name is wrong, you may end up with inconsistent “active” styling.

Suggestion

Replace any with the Mapbox Draw mode types (or at least minimal structural types) and avoid relying on activateUIButton('circle') unless you’re sure it’s registered.

Example (minimal, still pragmatic):

import type { MapMouseEvent } from 'mapbox-gl';
import type { DrawCustomMode, DrawFeature } from '@mapbox/mapbox-gl-draw';

type CircleState = {
  circle: DrawFeature;
};

type CircleProps = {
  user_isCircle: true;
  user_center: [number, number] | [];
  user_radiusInKm?: number;
};

const CircleMode: DrawCustomMode<CircleState> = {
  onSetup() {
    const circle = this.newFeature({
      type: 'Feature',
      properties: { user_isCircle: true, user_center: [] } satisfies CircleProps,
      geometry: { type: 'Polygon', coordinates: [[]] }
    });

    this.addFeature(circle);
    this.clearSelectedFeatures();
    this.updateUIClasses({ mouse: 'add' });
    // Consider removing activateUIButton or ensure it matches the control id
    this.setActionableState({ trash: true });

    return { circle };
  },
  onClick(state, e: MapMouseEvent) { /* ... */ },
  onMouseMove(state, e: MapMouseEvent) { /* ... */ },
  /* ... */
};

If you’d like, reply with "@CharlieHelps yes please" and I can add a commit with the typing + safer property handling changes.


export default CircleMode;
3 changes: 2 additions & 1 deletion components/map/map-data-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface MapData {
measurement: string;
geometry: any;
}>;
pendingFeatures?: any[]; // For programmatic drawing commands
markers?: Array<{
latitude: number;
longitude: number;
Expand All @@ -39,7 +40,7 @@ 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: [], pendingFeatures: [], markers: [] });

return (
<MapDataContext.Provider value={{ mapData, setMapData }}>
Expand Down
Loading