Skip to content
Closed
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
5 changes: 4 additions & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ async function submit(formData?: FormData, skip?: boolean) {
? `{"action": "skip"}`
: (formData?.get('input') as string);

const mapDataString = formData?.get('map_data') as string | undefined;
const mapData = mapDataString ? JSON.parse(mapDataString) : undefined;

Comment on lines +66 to +68
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

Untrusted JSON.parse on client-provided map_data can crash the action

map_data comes from the browser. A malformed payload will throw and break the request. Wrap in try/catch and default to undefined.

Apply this defensive parsing:

-  const mapDataString = formData?.get('map_data') as string | undefined;
-  const mapData = mapDataString ? JSON.parse(mapDataString) : undefined;
+  const mapDataString = formData?.get('map_data') as string | undefined
+  let mapData: unknown = undefined
+  if (mapDataString) {
+    try {
+      mapData = JSON.parse(mapDataString)
+    } catch (e) {
+      console.warn('submit(): Ignoring invalid map_data JSON in formData', e)
+      mapData = undefined
+    }
+  }

Optional follow-up: avoid including the raw map_data blob in the persisted user message content (JSON.stringify(Object.fromEntries(formData))) to prevent inflating message history and storage. Instead, construct the user-visible content from a filtered copy of FormData that excludes map_data. I can provide a patch if you want that change now.

📝 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
const mapDataString = formData?.get('map_data') as string | undefined;
const mapData = mapDataString ? JSON.parse(mapDataString) : undefined;
const mapDataString = formData?.get('map_data') as string | undefined
let mapData: unknown = undefined
if (mapDataString) {
try {
mapData = JSON.parse(mapDataString)
} catch (e) {
console.warn('submit(): Ignoring invalid map_data JSON in formData', e)
mapData = undefined
}
}
🤖 Prompt for AI Agents
In app/actions.tsx around lines 66 to 68, the code directly calls JSON.parse on
client-provided map_data which can throw on malformed input; update the code to
wrap the JSON.parse call in a try/catch and set mapData to undefined on parse
errors (optionally log/debug the error), and when building persisted user
message content (JSON.stringify(Object.fromEntries(formData))) exclude the raw
map_data entry by creating a filtered copy of FormData that omits 'map_data' so
the large blob isn't stored.

const content = skip
? userInput
: formData
Expand Down Expand Up @@ -149,7 +152,7 @@ async function submit(formData?: FormData, skip?: boolean) {
uiStream,
streamText,
messages,
// mcp, // mcp instance is no longer passed down
mapData, // Pass mapData here
useSpecificAPI
);
answer = fullResponse;
Expand Down
5 changes: 5 additions & 0 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect, useState, useRef } from 'react'
import { useRouter } from 'next/navigation'
import type { AI, UIState } from '@/app/actions'
import { useUIState, useActions } from 'ai/rsc'
import { useMapData } from './map/map-data-context'
// Removed import of useGeospatialToolMcp as it's no longer used/available
import { cn } from '@/lib/utils'
import { UserMessage } from './user-message'
Expand All @@ -21,6 +22,7 @@ interface ChatPanelProps {
export function ChatPanel({ messages, input, setInput }: ChatPanelProps) {
const [, setMessages] = useUIState<typeof AI>()
const { submit } = useActions()
const { mapData } = useMapData()
// Removed mcp instance as it's no longer passed to submit
const [isButtonPressed, setIsButtonPressed] = useState(false)
const [isMobile, setIsMobile] = useState(false)
Expand Down Expand Up @@ -58,6 +60,9 @@ export function ChatPanel({ messages, input, setInput }: ChatPanelProps) {
}
])
const formData = new FormData(e.currentTarget)
if (mapData) {
formData.append('map_data', JSON.stringify(mapData))
}
// Removed mcp argument from submit call
const responseMessage = await submit(formData)
setMessages(currentMessages => [...currentMessages, responseMessage as any])
Expand Down
3 changes: 3 additions & 0 deletions components/map/map-data-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export interface MapData {
measurement: string;
geometry: any;
}>;
center?: [number, number];
zoom?: number;
pitch?: number;
}

interface MapDataContextType {
Expand Down
11 changes: 9 additions & 2 deletions components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,16 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
const center = map.current.getCenter();
const zoom = map.current.getZoom();
const pitch = map.current.getPitch();
currentMapCenterRef.current = { center: [center.lng, center.lat], zoom, pitch };
const newCenter: [number, number] = [center.lng, center.lat];
currentMapCenterRef.current = { center: newCenter, zoom, pitch };
setMapData(prevData => ({
...prevData,
center: newCenter,
zoom: zoom,
pitch: pitch,
}));
}
}, [])
}, [setMapData])

// Set up idle rotation checker
useEffect(() => {
Expand Down
21 changes: 18 additions & 3 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { BotMessage } from '@/components/message'
import { getTools } from './tools'
import { getModel } from '../utils'

import { MapData } from '@/components/map/map-data-context'

export async function researcher(
dynamicSystemPrompt: string, // New parameter
uiStream: ReturnType<typeof createStreamableUI>,
streamText: ReturnType<typeof createStreamableValue<string>>,
messages: CoreMessage[],
// mcp: any, // Removed mcp parameter
mapData: MapData | undefined, // Add mapData parameter
useSpecificModel?: boolean
) {
let fullResponse = ''
Expand All @@ -28,11 +30,24 @@ export async function researcher(
)

const currentDate = new Date().toLocaleString()
let mapContext = '';
if (mapData) {
mapContext += 'The user is currently viewing a map with the following properties:\n';
if (mapData.center) {
mapContext += `- Center: [${mapData.center.join(', ')}]\n`;
}
if (mapData.zoom) {
mapContext += `- Zoom level: ${mapData.zoom}\n`;
}
if (mapData.drawnFeatures && mapData.drawnFeatures.length > 0) {
mapContext += `- Drawn features on map: ${JSON.stringify(mapData.drawnFeatures.map(f => ({ type: f.type, measurement: f.measurement })))} \n`;
}
}
// Default system prompt, used if dynamicSystemPrompt is not provided
const default_system_prompt = `As a comprehensive AI assistant, you can search the web, retrieve information from URLs, and understand geospatial queries to assist the user and display information on a map.
Current date and time: ${currentDate}.

Tool Usage Guide:
${mapContext}Tool Usage Guide:
- For general web searches for factual information: Use the 'search' tool.
- For retrieving content from specific URLs provided by the user: Use the 'retrieve' tool. (Do not use this for URLs found in search results).
- **For any questions involving locations, places, addresses, geographical features, finding businesses or points of interest, distances between locations, or directions: You MUST use the 'geospatialQueryTool'. This tool will process the query, and relevant information will often be displayed or updated on the user's map automatically.**
Expand All @@ -56,7 +71,7 @@ Match the language of your response to the user's language.`;
tools: getTools({
uiStream,
fullResponse,
// mcp // mcp parameter is no longer passed to getTools
mapData
})
})

Expand Down
3 changes: 2 additions & 1 deletion lib/agents/tools/geospatial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Fixed geospatial tool with improved error handling and schema
*/
import { createStreamableUI, createStreamableValue } from 'ai/rsc';
import { MapData } from '@/components/map/map-data-context';
import { BotMessage } from '@/components/message';
import { geospatialQuerySchema } from '@/lib/schema/geospatial';
import { Client as MCPClientClass } from '@modelcontextprotocol/sdk/client/index.js';
Expand Down Expand Up @@ -141,7 +142,7 @@ async function closeClient(client: McpClient | null) {
/**
* Main geospatial tool executor.
*/
export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof createStreamableUI> }) => ({
export const geospatialTool = ({ uiStream, mapData }: { uiStream: ReturnType<typeof createStreamableUI>, mapData?: MapData }) => ({
description: `Use this tool for location-based queries including:
- Finding specific places, addresses, or landmarks
- Getting coordinates for locations
Expand Down
13 changes: 6 additions & 7 deletions lib/agents/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { createStreamableUI } from 'ai/rsc'
import { retrieveTool } from './retrieve'
import { searchTool } from './search'
import { videoSearchTool } from './video-search'
import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp import
import { geospatialTool } from './geospatial'
import { MapData } from '@/components/map/map-data-context'
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

Import MapData as a type to avoid bundling a client module on the server

MapData is declared in a 'use client' file under components. Importing it as a value risks pulling a client-only module (and its deps like mapbox-gl) into server bundles. Use a type-only import, or better, move the type to a shared, non-client module (e.g., lib/types/map.ts) and import from there.

Apply the minimal fix:

-import { MapData } from '@/components/map/map-data-context'
+import type { MapData } from '@/components/map/map-data-context'

Longer-term, extract MapData into a server-safe shared types module (e.g., lib/types/map.ts) and update all imports in lib/* to reference that instead of a client component file.

📝 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 { MapData } from '@/components/map/map-data-context'
import type { MapData } from '@/components/map/map-data-context'
🤖 Prompt for AI Agents
In lib/agents/tools/index.tsx around line 6, the file imports MapData from a
client-side component which can pull client-only modules into server bundles;
update the import to be type-only by replacing the value import with a type
import (e.g., "import type { MapData } from
'@/components/map/map-data-context'") or, preferably for a longer-term fix, move
the MapData type to a shared server-safe module like lib/types/map.ts and import
the type from there across lib/* so no client component is bundled on the
server.


export interface ToolProps {
uiStream: ReturnType<typeof createStreamableUI>
fullResponse: string
// mcp?: any; // Removed mcp property as it's no longer passed down for geospatialTool
mapData?: MapData
}

// Removed mcp from parameters
export const getTools = ({ uiStream, fullResponse }: ToolProps) => {
export const getTools = ({ uiStream, fullResponse, mapData }: ToolProps) => {
const tools: any = {
search: searchTool({
uiStream,
Expand All @@ -21,10 +21,9 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => {
uiStream,
fullResponse
}),
// geospatialTool now only requires uiStream
geospatialQueryTool: geospatialTool({
uiStream
// mcp: mcp || null // Removed mcp argument
uiStream,
mapData
})
}

Expand Down