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
65 changes: 57 additions & 8 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Spinner } from '@/components/ui/spinner'
import { Section } from '@/components/section'
import { FollowupPanel } from '@/components/followup-panel'
import { inquire, researcher, taskManager, querySuggestor } from '@/lib/agents'
import { geojsonEnricher } from '@/lib/agents/geojson-enricher'
// Removed import of useGeospatialToolMcp as it no longer exists and was incorrectly used here.
// The geospatialTool (if used by agents like researcher) now manages its own MCP client.
import { writer } from '@/lib/agents/writer'
Expand All @@ -25,11 +26,10 @@ import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler' // Add this import

// Define the type for related queries
type RelatedQueries = {
items: { query: string }[]
}
import { LocationResponseHandler } from '@/components/map/location-response-handler'
import { isLocationResponse } from '@/lib/utils/type-guards'
import { PartialRelated } from '@/lib/schema/related'
import { LocationResponse } from '@/lib/types/custom'

// Removed mcp parameter from submit, as geospatialTool now handles its client.
async function submit(formData?: FormData, skip?: boolean) {
Expand Down Expand Up @@ -222,7 +222,7 @@ async function submit(formData?: FormData, skip?: boolean) {
{
id: nanoid(),
role: 'assistant',
content: `inquiry: ${inquiry?.question}`
content: `inquiry: ${(inquiry as any)?.question}`
}
]
})
Expand Down Expand Up @@ -296,6 +296,29 @@ async function submit(formData?: FormData, skip?: boolean) {
}

if (!errorOccurred) {
let locationResponse: LocationResponse;
try {
// Create a timeout promise that rejects after 30 seconds.
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('GeoJSON enrichment timed out after 30 seconds')), 30000)
);

// Race the agent call against the timeout.
locationResponse = (await Promise.race([
geojsonEnricher(answer),
timeoutPromise
])) as LocationResponse;
} catch (e) {
console.error("Error during geojson enrichment:", e);
// Fallback to a response without location data, ensuring consistent structure.
locationResponse = {
type: 'tool',
text: answer,
geojson: null,
map_commands: null,
};
}
Comment on lines +299 to +320
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear timeout after Promise.race to avoid leaked timer/dangling event loop

The timeout isn’t cleared if enrichment resolves quickly. Clear it in a finally block.

Apply this diff:

-      let locationResponse: LocationResponse;
+      let locationResponse: LocationResponse;
+      let timeoutId: ReturnType<typeof setTimeout> | undefined;
       try {
-        // Create a timeout promise that rejects after 30 seconds.
-        const timeoutPromise = new Promise((_, reject) =>
-          setTimeout(() => reject(new Error('GeoJSON enrichment timed out after 30 seconds')), 30000)
-        );
+        // Create a timeout promise that rejects after 30 seconds.
+        const timeoutPromise = new Promise((_, reject) => {
+          timeoutId = setTimeout(
+            () => reject(new Error('GeoJSON enrichment timed out after 30 seconds')),
+            30_000
+          );
+        });
 
         // Race the agent call against the timeout.
         locationResponse = (await Promise.race([
           geojsonEnricher(answer),
           timeoutPromise
         ])) as LocationResponse;
       } catch (e) {
         console.error("Error during geojson enrichment:", e);
         // Fallback to a response without location data, ensuring consistent structure.
         locationResponse = {
           type: 'tool',
           text: answer,
           geojson: null,
           map_commands: null,
         };
-      }
+      } finally {
+        if (timeoutId) clearTimeout(timeoutId);
+      }
🤖 Prompt for AI Agents
In app/actions.tsx around lines 299 to 320, the timeout created for the 30s
Promise.race is not cleared if geojsonEnricher resolves quickly, leaking a
timer; store the setTimeout return value in a variable (typed appropriately),
build the timeoutPromise using that timer id, and ensure you call
clearTimeout(timerId) in a finally block after the Promise.race finishes (both
success and error paths) so the timer is cancelled and no dangling event-loop
callback remains.


const relatedQueries = await querySuggestor(uiStream, messages)
uiStream.append(
<Section title="Follow-up">
Expand All @@ -312,9 +335,16 @@ async function submit(formData?: FormData, skip?: boolean) {
{
id: groupeId,
role: 'assistant',
content: answer,
content: locationResponse.text,
type: 'response'
},
{
id: nanoid(),
role: 'tool',
name: 'geojsonEnrichment',
content: JSON.stringify(locationResponse),
type: 'tool',
},
{
id: groupeId,
role: 'assistant',
Expand Down Expand Up @@ -518,7 +548,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
)
}
case 'related':
const relatedQueries = createStreamableValue<RelatedQueries>()
const relatedQueries = createStreamableValue<PartialRelated>()
relatedQueries.done(JSON.parse(content as string))
return {
id,
Expand Down Expand Up @@ -556,6 +586,25 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
}
}

if (name === 'geojsonEnrichment') {
// Use the type guard for robust runtime validation.
if (isLocationResponse(toolOutput)) {
return {
id,
component: (
<LocationResponseHandler locationResponse={toolOutput} />
),
isCollapsed: false,
};
} else {
// The type guard already logs the specific validation error.
console.warn(
'Skipping render for geojsonEnrichment due to invalid data structure.'
);
return { id, component: null };
}
}

const searchResults = createStreamableValue()
searchResults.done(JSON.stringify(toolOutput))
switch (name) {
Expand Down
Binary file modified bun.lockb
Binary file not shown.
28 changes: 28 additions & 0 deletions components/map/location-response-handler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { useEffect } from 'react';
import { useMapData } from './map-data-context';
import { LocationResponse } from '@/lib/types/custom';

interface LocationResponseHandlerProps {
locationResponse: LocationResponse;
}

export const LocationResponseHandler: React.FC<LocationResponseHandlerProps> = ({ locationResponse }) => {
const { setMapData } = useMapData();

useEffect(() => {
if (locationResponse) {
const { geojson, map_commands } = locationResponse;
console.log('LocationResponseHandler: Received data', locationResponse);
setMapData(prevData => ({
...prevData,
geojson: geojson,
mapCommands: map_commands,
}));
}
}, [locationResponse, setMapData]);

// This component handles logic and does not render any UI.
return null;
};
6 changes: 6 additions & 0 deletions components/map/map-data-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import React, { createContext, useContext, useState, ReactNode } from 'react';
import { LngLatLike } from 'mapbox-gl'; // Import LngLatLike
import {
GeoJSONFeatureCollection,
MapCommand
} from '@/lib/types/custom';

// Define the shape of the map data you want to share
export interface MapData {
Expand All @@ -14,6 +18,8 @@ export interface MapData {
measurement: string;
geometry: any;
}>;
geojson?: GeoJSONFeatureCollection | null;
mapCommands?: MapCommand[] | null;
}
Comment on lines +21 to 23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add defaults for new fields to avoid undefined checks.

Optional: initialize geojson and mapCommands in state to null to simplify consumer logic and avoid undefined/tri-state handling.

Example:

const [mapData, setMapData] = useState<MapData>({
  drawnFeatures: [],
  geojson: null,
  mapCommands: null,
});
🤖 Prompt for AI Agents
In components/map/map-data-context.tsx around lines 21 to 23, the MapData type
allows geojson and mapCommands to be optional which forces consumers to handle
undefined/tri-state; update the context state initialization to set explicit
defaults (initialize geojson and mapCommands to null and drawnFeatures to an
empty array) so consumers can rely on null instead of undefined, and update the
useState<MapData> call to supply these defaults when creating the initial
mapData state.


interface MapDataContextType {
Expand Down
81 changes: 81 additions & 0 deletions components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,87 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
// }
}, [mapData.targetPosition, mapData.mapFeature, updateMapPosition]);

// Effect to handle GeoJSON data updates
useEffect(() => {
if (!map.current) return;

const mapInstance = map.current;
const source = mapInstance.getSource('geojson-data');

// If GeoJSON data is present, add or update the source and layers
if (mapData.geojson) {
if (source) {
(source as mapboxgl.GeoJSONSource).setData(mapData.geojson as any);
} else {
mapInstance.addSource('geojson-data', {
type: 'geojson',
data: mapData.geojson as any,
});

// Add layer for points
mapInstance.addLayer({
id: 'geojson-points',
type: 'circle',
source: 'geojson-data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
},
filter: ['==', '$type', 'Point'],
});

// Add layer for lines
mapInstance.addLayer({
id: 'geojson-lines',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': '#ff4500',
'line-width': 3,
},
filter: ['==', '$type', 'LineString'],
});
}
} else {
// If no GeoJSON data, remove layers and source if they exist
if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
if (source) mapInstance.removeSource('geojson-data');
}
}, [mapData.geojson]);
Comment on lines +527 to +576
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against adding sources/layers before style is loaded.

If this effect runs before the map style is loaded, addSource/addLayer can throw. Add an isStyleLoaded check (or register a one‑time load handler) before mutating the style.

Apply this diff:

   useEffect(() => {
-    if (!map.current) return;
+    if (!map.current) return;
 
     const mapInstance = map.current;
+    // Ensure style is loaded before adding/removing sources/layers
+    if (!mapInstance.isStyleLoaded()) {
+      const onLoad = () => {
+        // re-run the logic once style is ready
+        const sourceAfterLoad = mapInstance.getSource('geojson-data');
+        if (mapData.geojson) {
+          if (sourceAfterLoad) {
+            (sourceAfterLoad as mapboxgl.GeoJSONSource).setData(mapData.geojson as any);
+          } else {
+            mapInstance.addSource('geojson-data', { type: 'geojson', data: mapData.geojson as any });
+            mapInstance.addLayer({
+              id: 'geojson-points',
+              type: 'circle',
+              source: 'geojson-data',
+              paint: { 'circle-radius': 8, 'circle-color': '#007cbf', 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff' },
+              filter: ['==', '$type', 'Point'],
+            });
+            mapInstance.addLayer({
+              id: 'geojson-lines',
+              type: 'line',
+              source: 'geojson-data',
+              paint: { 'line-color': '#ff4500', 'line-width': 3 },
+              filter: ['==', '$type', 'LineString'],
+            });
+          }
+        } else {
+          if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
+          if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
+          if (sourceAfterLoad) mapInstance.removeSource('geojson-data');
+        }
+        mapInstance.off('load', onLoad);
+      };
+      mapInstance.on('load', onLoad);
+      return;
+    }
 
     const source = mapInstance.getSource('geojson-data');

Optional: add a polygon fill layer if you expect Polygon features.

📝 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
// Effect to handle GeoJSON data updates
useEffect(() => {
if (!map.current) return;
const mapInstance = map.current;
const source = mapInstance.getSource('geojson-data');
// If GeoJSON data is present, add or update the source and layers
if (mapData.geojson) {
if (source) {
(source as mapboxgl.GeoJSONSource).setData(mapData.geojson as any);
} else {
mapInstance.addSource('geojson-data', {
type: 'geojson',
data: mapData.geojson as any,
});
// Add layer for points
mapInstance.addLayer({
id: 'geojson-points',
type: 'circle',
source: 'geojson-data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
},
filter: ['==', '$type', 'Point'],
});
// Add layer for lines
mapInstance.addLayer({
id: 'geojson-lines',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': '#ff4500',
'line-width': 3,
},
filter: ['==', '$type', 'LineString'],
});
}
} else {
// If no GeoJSON data, remove layers and source if they exist
if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
if (source) mapInstance.removeSource('geojson-data');
}
}, [mapData.geojson]);
// Effect to handle GeoJSON data updates
useEffect(() => {
if (!map.current) return;
const mapInstance = map.current;
// Ensure style is loaded before adding/removing sources/layers
if (!mapInstance.isStyleLoaded()) {
const onLoad = () => {
// re-run the logic once style is ready
const sourceAfterLoad = mapInstance.getSource('geojson-data');
if (mapData.geojson) {
if (sourceAfterLoad) {
(sourceAfterLoad as mapboxgl.GeoJSONSource).setData(mapData.geojson as any);
} else {
mapInstance.addSource('geojson-data', { type: 'geojson', data: mapData.geojson as any });
mapInstance.addLayer({
id: 'geojson-points',
type: 'circle',
source: 'geojson-data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
},
filter: ['==', '$type', 'Point'],
});
mapInstance.addLayer({
id: 'geojson-lines',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': '#ff4500',
'line-width': 3
},
filter: ['==', '$type', 'LineString'],
});
}
} else {
if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
if (sourceAfterLoad) mapInstance.removeSource('geojson-data');
}
mapInstance.off('load', onLoad);
};
mapInstance.on('load', onLoad);
return;
}
const source = mapInstance.getSource('geojson-data');
// If GeoJSON data is present, add or update the source and layers
if (mapData.geojson) {
if (source) {
(source as mapboxgl.GeoJSONSource).setData(mapData.geojson as any);
} else {
mapInstance.addSource('geojson-data', {
type: 'geojson',
data: mapData.geojson as any,
});
// Add layer for points
mapInstance.addLayer({
id: 'geojson-points',
type: 'circle',
source: 'geojson-data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
},
filter: ['==', '$type', 'Point'],
});
// Add layer for lines
mapInstance.addLayer({
id: 'geojson-lines',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': '#ff4500',
'line-width': 3,
},
filter: ['==', '$type', 'LineString'],
});
}
} else {
// If no GeoJSON data, remove layers and source if they exist
if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
if (source) mapInstance.removeSource('geojson-data');
}
}, [mapData.geojson]);
🤖 Prompt for AI Agents
components/map/mapbox-map.tsx around lines 527 to 576: the effect mutates map
style (addSource/addLayer) without verifying the style is loaded which can throw
if executed too early; change the logic to check mapInstance.isStyleLoaded()
before calling addSource/addLayer and if the style is not loaded, register a
one‑time mapInstance.once('load', ...) handler to perform the source/layer
addition or updates; ensure the same guard is applied before removing
layers/sources (or perform removals inside the load handler if appropriate) and
optionally add a polygon fill layer when Polygon features are expected.


// Effect to execute map commands
useEffect(() => {
if (!map.current || !mapData.mapCommands || mapData.mapCommands.length === 0) return;

const mapInstance = map.current;

mapData.mapCommands.forEach(command => {
switch (command.command) {
case 'flyTo':
mapInstance.flyTo(command.params);
break;
case 'easeTo':
mapInstance.easeTo(command.params);
break;
case 'fitBounds':
// Destructure params to pass 'bounds' and 'options' separately,
// ensuring the function is called with the correct signature.
const { bounds, options } = command.params;
mapInstance.fitBounds(bounds, options || {});
break;
default:
console.warn(`Unknown map command: ${command.command}`);
}
});

// Clear commands after execution to prevent re-triggering
setMapData(prev => ({ ...prev, mapCommands: null }));

}, [mapData.mapCommands, setMapData]);
Comment on lines +579 to +606
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix switch‑case declaration in fitBounds case.

Declare variables inside a block to avoid leaking across cases.

Apply this diff (and satisfy linter):

-        case 'fitBounds':
-          // Destructure params to pass 'bounds' and 'options' separately,
-          // ensuring the function is called with the correct signature.
-          const { bounds, options } = command.params;
-          mapInstance.fitBounds(bounds, options || {});
-          break;
+        case 'fitBounds': {
+          // Destructure params to pass 'bounds' and 'options' separately,
+          // ensuring the function is called with the correct signature.
+          const { bounds, options } = command.params;
+          mapInstance.fitBounds(bounds, options || {});
+          break;
+        }

Based on static analysis.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!map.current || !mapData.mapCommands || mapData.mapCommands.length === 0) return;
const mapInstance = map.current;
mapData.mapCommands.forEach(command => {
switch (command.command) {
case 'flyTo':
mapInstance.flyTo(command.params);
break;
case 'easeTo':
mapInstance.easeTo(command.params);
break;
case 'fitBounds':
// Destructure params to pass 'bounds' and 'options' separately,
// ensuring the function is called with the correct signature.
const { bounds, options } = command.params;
mapInstance.fitBounds(bounds, options || {});
break;
default:
console.warn(`Unknown map command: ${command.command}`);
}
});
// Clear commands after execution to prevent re-triggering
setMapData(prev => ({ ...prev, mapCommands: null }));
}, [mapData.mapCommands, setMapData]);
useEffect(() => {
if (!map.current || !mapData.mapCommands || mapData.mapCommands.length === 0) return;
const mapInstance = map.current;
mapData.mapCommands.forEach(command => {
switch (command.command) {
case 'flyTo':
mapInstance.flyTo(command.params);
break;
case 'easeTo':
mapInstance.easeTo(command.params);
break;
case 'fitBounds': {
// Destructure params to pass 'bounds' and 'options' separately,
// ensuring the function is called with the correct signature.
const { bounds, options } = command.params;
mapInstance.fitBounds(bounds, options || {});
break;
}
default:
console.warn(`Unknown map command: ${command.command}`);
}
});
// Clear commands after execution to prevent re-triggering
setMapData(prev => ({ ...prev, mapCommands: null }));
}, [mapData.mapCommands, setMapData]);
🧰 Tools
🪛 Biome (2.1.2)

[error] 595-595: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In components/map/mapbox-map.tsx around lines 579-606, the fitBounds case
declares const variables at case scope which can leak into other cases; fix by
adding a block for that case (case 'fitBounds': { const { bounds, options } =
command.params; mapInstance.fitBounds(bounds, options || {}); break; }) so the
variables are properly scoped and include the missing break to satisfy the
linter.


// Long-press handlers
const handleMouseDown = useCallback(() => {
// Only activate long press if not in real-time mode (as that mode has its own interactions)
Expand Down
7 changes: 0 additions & 7 deletions dev.log

This file was deleted.

92 changes: 92 additions & 0 deletions lib/agents/geojson-enricher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { CoreMessage, LanguageModel, streamText } from 'ai';
import { getModel } from '../utils';
import { LocationResponse } from '../types/custom';
import { isLocationResponse } from '../utils/type-guards';

// A specialized prompt instructing the LLM to parse a textual response
// and extract structured GeoJSON data and map commands.
const GEOJSON_ENRICHMENT_PROMPT = `
You are an AI assistant specializing in geospatial data extraction.
The final output MUST be a single JSON object that strictly follows the LocationResponse interface.
Return ONLY the raw JSON object with no surrounding markdown, code fences, or any additional text or explanation.

Your task is to process a given text and extract the following information:

1. "type": This field must always be the string 'tool'.
2. "text": The original textual response that should be displayed to the user.
3. "geojson": A valid GeoJSON FeatureCollection representing any locations, addresses, coordinates, or routes mentioned in the text.
4. "map_commands": A list of map camera commands to control the map view, such as flying to a location.

Rules for GeoJSON:
- Convert all found locations into appropriate GeoJSON features (Point, LineString).
- Use the correct coordinate format: [Longitude, Latitude] in WGS84.
- Include meaningful properties for each feature (e.g., "name", "description").
- If no geographic data can be extracted, set "geojson" to null.

Rules for Map Commands:
- Identify actions in thetext that imply map movements (e.g., "fly to," "center on," "zoom to").
- Create a list of command objects, for example: { "command": "flyTo", "params": { "center": [-71.05633, 42.356823], "zoom": 15 } }.
- If no map commands can be inferred, set "map_commands" to null.
Comment on lines +27 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor typo in prompt.

“inthetext” → “in the text”.

Apply this diff:

- - Identify actions in thetext that imply map movements (e.g., "fly to," "center on," "zoom to").
+ - Identify actions in the text that imply map movements (e.g., "fly to," "center on," "zoom to").
📝 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
- Identify actions in thetext that imply map movements (e.g., "fly to," "center on," "zoom to").
- Create a list of command objects, for example: { "command": "flyTo", "params": { "center": [-71.05633, 42.356823], "zoom": 15 } }.
- If no map commands can be inferred, set "map_commands" to null.
- Identify actions in the text that imply map movements (e.g., "fly to," "center on," "zoom to").
- Create a list of command objects, for example: { "command": "flyTo", "params": { "center": [-71.05633, 42.356823], "zoom": 15 } }.
- If no map commands can be inferred, set "map_commands" to null.
🤖 Prompt for AI Agents
In lib/agents/geojson-enricher.tsx around lines 27 to 29, fix the typo in the
prompt text by changing "inthetext" to "in the text"; update the prompt string
where that fragment appears so it reads "in the text" (preserve surrounding
wording and punctuation).


Here is the text to process:
`;

/**
* An asynchronous agent that enriches a textual response with GeoJSON data and map commands.
* @param researcherResponse The text generated by the researcher agent.
* @returns A promise that resolves to a LocationResponse object.
*/
export async function geojsonEnricher(
researcherResponse: string
): Promise<LocationResponse> {
const model = getModel() as LanguageModel;
const messages: CoreMessage[] = [
{
role: 'user',
content: `${GEOJSON_ENRICHMENT_PROMPT}\n\n${researcherResponse}`,
},
];

try {
// Await the streaming text promise to get the full response.
const { text } = await streamText({
model,
messages,
maxTokens: 4096, // Increased maxTokens for potentially larger GeoJSON payloads.
});
let responseText = await text;

// Strip any surrounding markdown code fences.
const jsonMatch = responseText.match(/```(json)?\n([\s\S]*?)\n```/);
if (jsonMatch && jsonMatch[2]) {
responseText = jsonMatch[2].trim();
}

// Parse the cleaned text into a JSON object.
const parsedJson = JSON.parse(responseText);

Comment on lines +59 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Harden JSON extraction beyond code‑fence stripping.

Models sometimes return bare JSON with stray pre/post text. Consider slicing the first balanced {...} block before JSON.parse as a fallback, to reduce defaulting to null on minor formatting drift.

// Validate the parsed object against the LocationResponse interface.
if (isLocationResponse(parsedJson)) {
return parsedJson;
} else {
// If validation fails, log the error and the invalid object.
console.error('Validation failed: The parsed object does not match the LocationResponse interface.', parsedJson);
// Return a default response to avoid crashing the application.
return {
type: 'tool',
text: researcherResponse,
geojson: null,
map_commands: null,
};
}
} catch (error) {
console.error('Error enriching response with GeoJSON:', error);
// If parsing or any other part of the process fails, return a default response.
return {
type: 'tool',
text: researcherResponse,
geojson: null,
map_commands: null,
};
}
}
34 changes: 34 additions & 0 deletions lib/types/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Defines the structure for a map command, like 'flyTo' or 'easeTo'.
export interface MapCommand {
command: 'flyTo' | 'easeTo' | 'fitBounds'; // Add other valid map commands as needed
params: any; // Parameters for the command, e.g., { center: [lon, lat], zoom: 10 }
}
Comment on lines +2 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

MapCommand.params typed as any; consider a discriminated union

Stronger typing catches mistakes early and documents contract.

Example:

-export interface MapCommand {
-  command: 'flyTo' | 'easeTo' | 'fitBounds'; // Add other valid map commands as needed
-  params: any; // Parameters for the command, e.g., { center: [lon, lat], zoom: 10 }
-}
+type LngLat = [number, number];
+type Bounds = [LngLat, LngLat] | [number, number, number, number];
+
+export type MapCommand =
+  | { command: 'flyTo'; params: { center: LngLat; zoom?: number; bearing?: number; pitch?: number } }
+  | { command: 'easeTo'; params: { center?: LngLat; zoom?: number; bearing?: number; pitch?: number; duration?: number } }
+  | { command: 'fitBounds'; params: { bounds: Bounds; padding?: number | { top?: number; bottom?: number; left?: number; right?: number }; maxZoom?: number } };
📝 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
export interface MapCommand {
command: 'flyTo' | 'easeTo' | 'fitBounds'; // Add other valid map commands as needed
params: any; // Parameters for the command, e.g., { center: [lon, lat], zoom: 10 }
}
type LngLat = [number, number];
type Bounds = [LngLat, LngLat] | [number, number, number, number];
export type MapCommand =
| { command: 'flyTo'; params: { center: LngLat; zoom?: number; bearing?: number; pitch?: number } }
| { command: 'easeTo'; params: { center?: LngLat; zoom?: number; bearing?: number; pitch?: number; duration?: number } }
| { command: 'fitBounds'; params: { bounds: Bounds; padding?: number | { top?: number; bottom?: number; left?: number; right?: number }; maxZoom?: number } };
🤖 Prompt for AI Agents
In lib/types/custom.ts around lines 2 to 5, MapCommand.params is currently typed
as any; replace it with a discriminated union so each command has a precise
params shape. Define individual param interfaces (e.g., FlyToParams { center:
[number, number]; zoom?: number }, EaseToParams { center?: [number, number];
duration?: number; zoom?: number }, FitBoundsParams { bounds: [[number, number],
[number, number]]; padding?: number | number[] }) then change MapCommand to a
union like { command: 'flyTo'; params: FlyToParams } | { command: 'easeTo';
params: EaseToParams } | { command: 'fitBounds'; params: FitBoundsParams };
update any imports/usages to the new types and run type checks to fix resulting
call sites.


// Defines the structure for the geometry part of a GeoJSON feature.
export interface GeoJSONGeometry {
type: 'Point' | 'LineString' | 'Polygon'; // Can be extended with other GeoJSON geometry types
coordinates: number[] | number[][] | number[][][];
}

// Defines a single feature in a GeoJSON FeatureCollection.
export interface GeoJSONFeature {
type: 'Feature';
geometry: GeoJSONGeometry;
properties: {
[key: string]: any; // Features can have any number of properties
};
}

// Defines the structure for a GeoJSON FeatureCollection.
export interface GeoJSONFeatureCollection {
type: 'FeatureCollection';
features: GeoJSONFeature[];
}
Comment on lines +8 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Prefer standard GeoJSON types to avoid drift

If feasible, reuse types from @types/geojson to cover Multi* geometries and properties shape.

Example:

import type { Feature, FeatureCollection, Geometry } from 'geojson';

export type GeoJSONGeometry = Geometry;
export type GeoJSONFeature = Feature;
export type GeoJSONFeatureCollection = FeatureCollection;

Verify downstream code compatibility before changing exported types.

🤖 Prompt for AI Agents
In lib/types/custom.ts around lines 8 to 26, the file defines bespoke GeoJSON
interfaces that omit Multi* geometries and differ from the community standard;
replace these custom types by importing and re-exporting the canonical types
from the @types/geojson package (Geometry, Feature, FeatureCollection) so all
geometry variants and property typings are covered, update the exported type
names to alias those imports (GeoJSONGeometry = Geometry, GeoJSONFeature =
Feature, GeoJSONFeatureCollection = FeatureCollection), and then run a quick
search/compile to verify and adjust any downstream code that relied on the
narrower custom shapes to ensure compatibility before committing the change.


// Defines the structured response that includes textual data, GeoJSON, and map commands.
export interface LocationResponse {
type: 'tool';
text: string;
geojson: GeoJSONFeatureCollection | null;
map_commands?: MapCommand[] | null;
}
Loading