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
56 changes: 48 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,8 @@ 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 { PartialRelated } from '@/lib/schema/related'

// Removed mcp parameter from submit, as geospatialTool now handles its client.
async function submit(formData?: FormData, skip?: boolean) {
Expand Down Expand Up @@ -222,7 +220,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 +294,19 @@ async function submit(formData?: FormData, skip?: boolean) {
}

if (!errorOccurred) {
let locationResponse;
try {
locationResponse = await geojsonEnricher(answer);
} catch (e) {
console.error("Error during geojson enrichment:", e);
// Fallback to a response without location data
locationResponse = {
text: answer,
geojson: null,
map_commands: null,
};
}

const relatedQueries = await querySuggestor(uiStream, messages)
uiStream.append(
<Section title="Follow-up">
Expand All @@ -312,9 +323,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 +536,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 +574,28 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
}
}

if (name === 'geojsonEnrichment') {
// Runtime validation for the toolOutput
if (
toolOutput &&
typeof toolOutput.text === 'string' &&
(toolOutput.geojson === null ||
(typeof toolOutput.geojson === 'object' &&
toolOutput.geojson.type === 'FeatureCollection'))
) {
return {
id,
component: (
<LocationResponseHandler locationResponse={toolOutput} />
),
isCollapsed: false,
};
} else {
console.warn('Invalid toolOutput for geojsonEnrichment:', toolOutput);
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;
}

interface MapDataContextType {
Expand Down
79 changes: 79 additions & 0 deletions components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,85 @@ 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]);

// 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':
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]);

// 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
75 changes: 75 additions & 0 deletions lib/agents/geojson-enricher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { CoreMessage, LanguageModel, streamText } from 'ai';
import { getModel }from '../utils';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix import spacing to restore build.

The statement currently reads import { getModel }from '../utils'; which is invalid syntax and matches the build failure reported by CI. Insert the missing space:

-import { getModel }from '../utils';
+import { getModel } from '../utils';
📝 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
import { getModel }from '../utils';
import { getModel } from '../utils';
🤖 Prompt for AI Agents
In lib/agents/geojson-enricher.tsx around line 2, the import statement lacks a
space between the named import and the from keyword causing a syntax error;
update the import to add the missing space so it reads with proper spacing
between the closing brace and the from keyword (e.g., ensure "import { getModel
} from '../utils';"), save the file and run the build/lint to confirm the fix.

import { LocationResponse } from '../types/custom';

// 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.
Your task is to process a given text and extract the following information:

1. "text": The original textual response that should be displayed to the user.
2. "geojson": A valid GeoJSON FeatureCollection representing any locations, addresses, coordinates, or routes mentioned in the text.
3. "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.

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.

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 {
const { text } = await streamText({
model,
messages,
maxTokens: 2048,
});

// Assuming the LLM returns a valid JSON string, parse it.
let responseText = await text;

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

const enrichedData = JSON.parse(responseText) as LocationResponse;
return enrichedData;
} catch (error) {
console.error('Error enriching response with GeoJSON:', error);
// If parsing fails, return a default response that includes the original text.
return {
text: researcherResponse,
geojson: null,
map_commands: null,
};
}
}
33 changes: 33 additions & 0 deletions lib/types/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 }
}

// 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[];
}

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