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 f3b3c44f..9c882f74 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; 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..892af98f --- /dev/null +++ b/components/map/generated-map.tsx @@ -0,0 +1,65 @@ +'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) + +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 (!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 + } + }, []) + + // 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
+} 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;