From 0b2748b187abc18ea98b301364192a79b51ad9ed Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:21:26 +0000 Subject: [PATCH 1/3] feat: Connect generated map to Mapbox MCP workflow This commit integrates the `GeneratedMap` component with the `mapbox_mcp` workflow. The `useMCPMapClient` hook is updated to extract location information from the user's query. This location data is then passed through the `ChatPanel` and `app/actions.tsx` to the `searchTool`. The `searchTool` now includes the location in the search results, which allows the `SearchSection` to render the `GeneratedMap` component with the correct position. This change enables the display of a map related to the user's query in parallel with the existing map render. --- app/actions.tsx | 12 ++++++++---- components/chat-panel.tsx | 10 ++++++---- components/map/generated-map.tsx | 33 ++++++++++++++++++++++++++++++++ components/search-section.tsx | 18 +++++++++++++---- lib/agents/researcher.tsx | 6 +++--- lib/agents/tools/index.tsx | 7 ++++--- lib/agents/tools/search.tsx | 6 +++++- lib/types/index.ts | 4 ++++ mapbox_mcp/hooks.ts | 4 ++-- 9 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 components/map/generated-map.tsx diff --git a/app/actions.tsx b/app/actions.tsx index f3b3c44f..daf5b588 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -32,7 +32,7 @@ type RelatedQueries = { }; // Removed mcp parameter from submit, as geospatialTool now handles its client. -async function submit(formData?: FormData, skip?: boolean) { +async function submit(formData?: FormData, mapLocation?: object, skip?: boolean) { 'use server'; // TODO: Update agent function signatures in lib/agents/researcher.tsx and lib/agents/writer.tsx @@ -149,8 +149,8 @@ async function submit(formData?: FormData, skip?: boolean) { uiStream, streamText, messages, - // mcp, // mcp instance is no longer passed down - useSpecificAPI + useSpecificAPI, + mapLocation, ); answer = fullResponse; toolOutputs = toolResponses; @@ -435,9 +435,13 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { searchResults.done(JSON.stringify(toolOutput)); switch (name) { case 'search': + const searchResult = JSON.parse(searchResults.value); + if (toolOutput.position) { + searchResult.position = toolOutput.position; + } return { id, - component: , + component: , isCollapsed: isCollapsed.value, }; case 'retrieve': diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index c83f4445..deef9182 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -4,7 +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' -// Removed import of useGeospatialToolMcp as it's no longer used/available +import { useMCPMapClient } from '@/mapbox_mcp/hooks' import { cn } from '@/lib/utils' import { UserMessage } from './user-message' import { Button } from './ui/button' @@ -21,7 +21,7 @@ interface ChatPanelProps { export function ChatPanel({ messages, input, setInput }: ChatPanelProps) { const [, setMessages] = useUIState() const { submit } = useActions() - // Removed mcp instance as it's no longer passed to submit + const { processLocationQuery } = useMCPMapClient() const [isButtonPressed, setIsButtonPressed] = useState(false) const [isMobile, setIsMobile] = useState(false) const inputRef = useRef(null) @@ -57,9 +57,11 @@ export function ChatPanel({ messages, input, setInput }: ChatPanelProps) { component: } ]) + + const { mapLocation } = await processLocationQuery(input) + const formData = new FormData(e.currentTarget) - // Removed mcp argument from submit call - const responseMessage = await submit(formData) + const responseMessage = await submit(formData, mapLocation) setMessages(currentMessages => [...currentMessages, responseMessage as any]) } diff --git a/components/map/generated-map.tsx b/components/map/generated-map.tsx new file mode 100644 index 00000000..a218bf24 --- /dev/null +++ b/components/map/generated-map.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useEffect, useRef } from 'react' +import mapboxgl from 'mapbox-gl' +import 'mapbox-gl/dist/mapbox-gl.css' + +mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string + +export const GeneratedMap: React.FC<{ + position: { latitude: number; longitude: number } +}> = ({ position }) => { + const mapContainer = useRef(null) + const map = useRef(null) + + useEffect(() => { + if (map.current) return // initialize map only once + if (mapContainer.current) { + map.current = new mapboxgl.Map({ + container: mapContainer.current, + style: 'mapbox://styles/mapbox/streets-v12', + center: [position.longitude, position.latitude], + zoom: 12 + }) + + // Add a marker to the map + new mapboxgl.Marker() + .setLngLat([position.longitude, position.latitude]) + .addTo(map.current) + } + }, [position]) + + return +} diff --git a/components/search-section.tsx b/components/search-section.tsx index 74695f69..410093f4 100644 --- a/components/search-section.tsx +++ b/components/search-section.tsx @@ -3,6 +3,7 @@ import { SearchResults } from './search-results' import { SearchSkeleton } from './search-skeleton' import { SearchResultsImageSection } from './search-results-image' +import { GeneratedMap } from './map/generated-map' import { Section } from './section' import { ToolBadge } from './tool-badge' import type { SearchResults as TypeSearchResults } from '@/lib/types' @@ -24,10 +25,19 @@ export function SearchSection({ result }: SearchSectionProps) { {searchResults.images && searchResults.images.length > 0 && ( - + + + + + {searchResults.position && ( + + + + )} + )} diff --git a/lib/agents/researcher.tsx b/lib/agents/researcher.tsx index 6ef6d53b..0d59fc3c 100644 --- a/lib/agents/researcher.tsx +++ b/lib/agents/researcher.tsx @@ -16,8 +16,8 @@ export async function researcher( uiStream: ReturnType, streamText: ReturnType>, messages: CoreMessage[], - // mcp: any, // Removed mcp parameter - useSpecificModel?: boolean + useSpecificModel?: boolean, + mapLocation?: object ) { let fullResponse = '' let hasError = false @@ -56,7 +56,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 + mapLocation }) }) diff --git a/lib/agents/tools/index.tsx b/lib/agents/tools/index.tsx index 4c08f373..d516d4a3 100644 --- a/lib/agents/tools/index.tsx +++ b/lib/agents/tools/index.tsx @@ -7,15 +7,16 @@ import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp im export interface ToolProps { uiStream: ReturnType fullResponse: string - // mcp?: any; // Removed mcp property as it's no longer passed down for geospatialTool + mapLocation?: object } // Removed mcp from parameters -export const getTools = ({ uiStream, fullResponse }: ToolProps) => { +export const getTools = ({ uiStream, fullResponse, mapLocation }: ToolProps) => { const tools: any = { search: searchTool({ uiStream, - fullResponse + fullResponse, + mapLocation }), retrieve: retrieveTool({ uiStream, diff --git a/lib/agents/tools/search.tsx b/lib/agents/tools/search.tsx index 3ed9d82d..69cd48d2 100644 --- a/lib/agents/tools/search.tsx +++ b/lib/agents/tools/search.tsx @@ -5,7 +5,7 @@ import { Card } from '@/components/ui/card' import { SearchSection } from '@/components/search-section' import { ToolProps } from '.' -export const searchTool = ({ uiStream, fullResponse }: ToolProps) => ({ +export const searchTool = ({ uiStream, fullResponse, mapLocation }: ToolProps) => ({ description: 'Search the web for information', parameters: searchSchema, execute: async ({ @@ -47,6 +47,10 @@ export const searchTool = ({ uiStream, fullResponse }: ToolProps) => ({ return searchResult } + if (mapLocation) { + searchResult.position = mapLocation + } + streamResults.done(JSON.stringify(searchResult)) return searchResult diff --git a/lib/types/index.ts b/lib/types/index.ts index 0652e848..5842fa55 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -2,6 +2,10 @@ export type SearchResults = { images: string[] results: SearchResultItem[] query: string + position?: { + latitude: number + longitude: number + } } export type ExaSearchResults = { diff --git a/mapbox_mcp/hooks.ts b/mapbox_mcp/hooks.ts index 6e295609..9338a50b 100644 --- a/mapbox_mcp/hooks.ts +++ b/mapbox_mcp/hooks.ts @@ -123,8 +123,8 @@ Focus on extracting and presenting factual data from the tools.`, const coordMatch = response.text.match(coordPattern); if (coordMatch) { mapLocation = { - lat: parseFloat(coordMatch[1]), - lng: parseFloat(coordMatch[2]), + latitude: parseFloat(coordMatch[1]), + longitude: parseFloat(coordMatch[2]), zoom: 12, }; shouldShowMap = true; From 08731db214b24e230df16b3f6e5d896158b7c9fa Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:48:16 +0000 Subject: [PATCH 2/3] fix: Correctly handle search results in getUIStateFromAIState This commit fixes a build error caused by incorrectly handling the search results in the `getUIStateFromAIState` function. The previous implementation attempted to parse a `StreamableValue` object as a string, which resulted in a type error. The logic for adding the map position to the search results was also redundant, as the position is already added in the `searchTool`. This commit reverts the incorrect changes in `app/actions.tsx`, which resolves the build error. --- .gitignore | 3 +++ app/actions.tsx | 6 +----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index fd3dbb57..48ec1aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# Bun +.bun/ + # dependencies /node_modules /.pnp diff --git a/app/actions.tsx b/app/actions.tsx index daf5b588..9c882f74 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -435,13 +435,9 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { searchResults.done(JSON.stringify(toolOutput)); switch (name) { case 'search': - const searchResult = JSON.parse(searchResults.value); - if (toolOutput.position) { - searchResult.position = toolOutput.position; - } return { id, - component: , + component: , isCollapsed: isCollapsed.value, }; case 'retrieve': From 995138e6e7c5a5c8b17fbcf63a0d008045cd4c13 Mon Sep 17 00:00:00 2001 From: EreQ Date: Sun, 24 Aug 2025 14:17:41 +0300 Subject: [PATCH 3/3] Update components/map/generated-map.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- components/map/generated-map.tsx | 56 +++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/components/map/generated-map.tsx b/components/map/generated-map.tsx index a218bf24..892af98f 100644 --- a/components/map/generated-map.tsx +++ b/components/map/generated-map.tsx @@ -12,22 +12,54 @@ export const GeneratedMap: React.FC<{ const mapContainer = useRef(null) const map = useRef(null) +export const GeneratedMap: React.FC<{ + position: { latitude: number; longitude: number; zoom?: number } +}> = ({ position }) => { + const mapContainer = useRef(null) + const map = useRef(null) + const markerRef = useRef(null) + + // Initialize map and marker once, and clean up on unmount useEffect(() => { - if (map.current) return // initialize map only once - if (mapContainer.current) { - map.current = new mapboxgl.Map({ - container: mapContainer.current, - style: 'mapbox://styles/mapbox/streets-v12', - center: [position.longitude, position.latitude], - zoom: 12 - }) + if (!mapContainer.current || map.current) return + if (!mapboxgl.accessToken) { + console.warn('GeneratedMap: NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN is not set.') + return + } + map.current = new mapboxgl.Map({ + container: mapContainer.current, + style: 'mapbox://styles/mapbox/streets-v12', + center: [position.longitude, position.latitude], + zoom: position.zoom ?? 12 + }) + markerRef.current = new mapboxgl.Marker() + .setLngLat([position.longitude, position.latitude]) + .addTo(map.current) + + return () => { + markerRef.current?.remove() + markerRef.current = null + map.current?.remove() + map.current = null + } + }, []) - // Add a marker to the map - new mapboxgl.Marker() - .setLngLat([position.longitude, position.latitude]) + // Update map center/zoom and marker when position changes + useEffect(() => { + if (!map.current) return + const lngLat: [number, number] = [position.longitude, position.latitude] + map.current.flyTo({ + center: lngLat, + zoom: position.zoom ?? map.current.getZoom(), + essential: true + }) + if (markerRef.current) { + markerRef.current.setLngLat(lngLat) + } else { + markerRef.current = new mapboxgl.Marker() + .setLngLat(lngLat) .addTo(map.current) } }, [position]) - return }