From 424fbcd04c5118df1e34ea87c8e5327bcfd007dd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:02:52 +0000 Subject: [PATCH 1/3] Implement automatic coordinate rendering and map panning - Update MapData to support multiple markers with unique IDs - Enhance Mapbox component to synchronize markers with MapData context - Create CoordinateLink component for interactive coordinate labels - Implement coordinate detection in BotMessage for automatic marker placement and panning - Update MapQueryHandler to add tool-provided locations to the map markers - Fix TypeScript error in BotMessage component for production build compatibility --- components/map/coordinate-link.tsx | 49 ++++++++++++++++++++++++++++ components/map/map-data-context.tsx | 1 + components/map/map-query-handler.tsx | 27 +++++++++------ components/map/mapbox-map.tsx | 28 ++++++++++++++++ components/message.tsx | 38 +++++++++++++++++++++ server.log | 40 ++++++++++++++++++----- 6 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 components/map/coordinate-link.tsx diff --git a/components/map/coordinate-link.tsx b/components/map/coordinate-link.tsx new file mode 100644 index 00000000..d36885d3 --- /dev/null +++ b/components/map/coordinate-link.tsx @@ -0,0 +1,49 @@ +'use client'; + +import React, { useEffect } from 'react'; +import { useMapData } from './map-data-context'; + +interface CoordinateLinkProps { + lat: number; + lng: number; + label?: string; +} + +export const CoordinateLink: React.FC = ({ lat, lng, label }) => { + const { setMapData } = useMapData(); + const id = `${lat},${lng}`; + + useEffect(() => { + // Automatically add marker and pan to it when the component mounts + setMapData(prev => { + const exists = prev.markers?.some(m => m.id === id); + const newMarkers = exists ? prev.markers : [...(prev.markers || []), { id, latitude: lat, longitude: lng, title: label || id }]; + + return { + ...prev, + targetPosition: { lat, lng }, + markers: newMarkers + }; + }); + }, [lat, lng, id, label, setMapData]); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + setMapData(prev => ({ + ...prev, + targetPosition: { lat, lng } + })); + }; + + return ( + + {label || `${lat.toFixed(4)}, ${lng.toFixed(4)}`} + + ); +}; diff --git a/components/map/map-data-context.tsx b/components/map/map-data-context.tsx index b96d7018..581367c1 100644 --- a/components/map/map-data-context.tsx +++ b/components/map/map-data-context.tsx @@ -24,6 +24,7 @@ export interface MapData { geometry: any; }>; markers?: Array<{ + id: string; latitude: number; longitude: number; title?: string; diff --git a/components/map/map-query-handler.tsx b/components/map/map-query-handler.tsx index ea460170..d5053538 100644 --- a/components/map/map-query-handler.tsx +++ b/components/map/map-query-handler.tsx @@ -36,16 +36,23 @@ export const MapQueryHandler: React.FC = ({ toolOutput }) if (typeof latitude === 'number' && typeof longitude === 'number') { console.log(`MapQueryHandler: Received data from geospatialTool. Place: ${place_name}, Lat: ${latitude}, Lng: ${longitude}`); - setMapData(prevData => ({ - ...prevData, - targetPosition: { lat: latitude, lng: longitude }, - // Optionally store more info from mcp_response if needed by MapboxMap component later - mapFeature: { - place_name, - // Potentially add mapUrl or other details from toolOutput.mcp_response - mapUrl: toolOutput.mcp_response?.mapUrl - } - })); + const id = `${latitude},${longitude}`; + setMapData(prevData => { + const exists = prevData.markers?.some(m => m.id === id); + const newMarkers = exists ? prevData.markers : [...(prevData.markers || []), { id, latitude, longitude, title: place_name || id }]; + + return { + ...prevData, + targetPosition: { lat: latitude, lng: longitude }, + markers: newMarkers, + // Optionally store more info from mcp_response if needed by MapboxMap component later + mapFeature: { + place_name, + // Potentially add mapUrl or other details from toolOutput.mcp_response + mapUrl: toolOutput.mcp_response?.mapUrl + } + }; + }); } else { console.warn("MapQueryHandler: Invalid latitude/longitude in toolOutput.mcp_response:", toolOutput.mcp_response.location); // Clear target position if data is invalid diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index 3dd390cd..2a71d96e 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -23,6 +23,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number const rotationFrameRef = useRef(null) const polygonLabelsRef = useRef<{ [id: string]: mapboxgl.Marker }>({}) const lineLabelsRef = useRef<{ [id: string]: mapboxgl.Marker }>({}) + const markersRef = useRef<{ [id: string]: mapboxgl.Marker }>({}) const lastInteractionRef = useRef(Date.now()) const isRotatingRef = useRef(false) const isUpdatingPositionRef = useRef(false) @@ -554,6 +555,33 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number // } }, [mapData.targetPosition, mapData.mapFeature, updateMapPosition]); + // Effect to synchronize markers from MapDataContext + useEffect(() => { + if (!map.current || !initializedRef.current) return; + + // Remove markers that are no longer in mapData + const currentMarkerIds = new Set(mapData.markers?.map(m => m.id) || []); + Object.keys(markersRef.current).forEach(id => { + if (!currentMarkerIds.has(id)) { + markersRef.current[id].remove(); + delete markersRef.current[id]; + } + }); + + // Add or update markers from mapData + mapData.markers?.forEach(markerData => { + if (!markersRef.current[markerData.id]) { + const marker = new mapboxgl.Marker() + .setLngLat([markerData.longitude, markerData.latitude]) + .setPopup(markerData.title ? new mapboxgl.Popup().setHTML(`

${markerData.title}

`) : null) + .addTo(map.current!); + markersRef.current[markerData.id] = marker; + } else { + markersRef.current[markerData.id].setLngLat([markerData.longitude, markerData.latitude]); + } + }); + }, [mapData.markers]); + // Long-press handlers const handleMouseDown = useCallback(() => { // Only activate long press if not in real-time mode (as that mode has its own interactions) diff --git a/components/message.tsx b/components/message.tsx index 264aa1f6..b1860739 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' import 'katex/dist/katex.min.css' +import { CoordinateLink } from './map/coordinate-link' export function BotMessage({ content }: { content: StreamableValue }) { const [data, error, pending] = useStreamableValue(content) @@ -23,6 +24,43 @@ export function BotMessage({ content }: { content: StreamableValue }) { rehypePlugins={[[rehypeExternalLinks, { target: '_blank' }], rehypeKatex]} remarkPlugins={[remarkGfm, remarkMath]} className="prose-sm prose-neutral prose-a:text-accent-foreground/50" + components={{ + text: (props) => { + const { children } = props; + if (typeof children !== 'string') return <>{children}; + const value = children; + const coordRegex = /(-?\d+\.\d+),\s*(-?\d+\.\d+)/g; + const parts = []; + let lastIndex = 0; + let match; + + while ((match = coordRegex.exec(value)) !== null) { + const lat = parseFloat(match[1]); + const lng = parseFloat(match[2]); + + // Basic validation for lat/lng ranges + if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + if (match.index > lastIndex) { + parts.push(value.substring(lastIndex, match.index)); + } + parts.push( + + ); + lastIndex = coordRegex.lastIndex; + } + } + + if (lastIndex < value.length) { + parts.push(value.substring(lastIndex)); + } + + return <>{parts.length > 0 ? parts : value}; + } + }} > {processedData} diff --git a/server.log b/server.log index 45044476..251d4835 100644 --- a/server.log +++ b/server.log @@ -1,11 +1,35 @@ -$ next dev --turbo - ⚠ Port 3000 is in use, using available port 3003 instead. + +> QCX@0.1.0 dev +> next dev --turbo + ▲ Next.js 15.3.6 (Turbopack) - - Local: http://localhost:3003 - - Network: http://192.168.0.2:3003 - - Environments: .env.local, .env + - Local: http://localhost:3000 + - Network: http://192.168.0.2:3000 + - Environments: .env ✓ Starting... - ○ Compiling middleware ... - ✓ Compiled middleware in 648ms - ✓ Ready in 2.5s + ✓ Compiled middleware in 359ms + ✓ Ready in 1716ms + ○ Compiling / ... + ✓ Compiled / in 27.5s +Chat DB actions loaded. Ensure getCurrentUserId() is correctly implemented for server-side usage if applicable. +[Upstash Redis] The 'url' property is missing or undefined in your Redis config. +[Upstash Redis] The 'token' property is missing or undefined in your Redis config. + GET / 200 in 31388ms + GET / 200 in 934ms + ○ Compiling /search/[id] ... + ✓ Compiled /search/[id] in 12.1s +Chat DB actions loaded. Ensure getCurrentUserId() is correctly implemented for server-side usage if applicable. +[Upstash Redis] The 'url' property is missing or undefined in your Redis config. +[Upstash Redis] The 'token' property is missing or undefined in your Redis config. + ⨯ [TypeError: action is not a function] { digest: '2341665709' } + POST /search/4hlC3SIEkZPxlRUV5KUmP 500 in 15389ms +[DEBUG] getSelectedModel - Reading from path: "/app/config/model.json" +[DEBUG] getSelectedModel - Raw file content: "{ + "selectedModel": null +} +" + POST /search/4hlC3SIEkZPxlRUV5KUmP 200 in 945ms +Chat DB actions loaded. Ensure getCurrentUserId() is correctly implemented for server-side usage if applicable. +[Upstash Redis] The 'url' property is missing or undefined in your Redis config. +[Upstash Redis] The 'token' property is missing or undefined in your Redis config. From 2dd6df77b5447b15d0eab1a73ccf4b375848a5ff Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:30:42 +0000 Subject: [PATCH 2/3] Improve coordinate detection to support degree symbols and N/S/E/W suffixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update regex in BotMessage to handle formats like '65.2500° N, 52.7500° W' - Implement suffix parsing to correctly convert to decimal degrees - Enhance validation to avoid false positives on plain numbers without context --- components/message.tsx | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/components/message.tsx b/components/message.tsx index b1860739..30861877 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -29,17 +29,39 @@ export function BotMessage({ content }: { content: StreamableValue }) { const { children } = props; if (typeof children !== 'string') return <>{children}; const value = children; - const coordRegex = /(-?\d+\.\d+),\s*(-?\d+\.\d+)/g; + // Improved regex to handle various coordinate formats: + // - Decimal: 40.7128, -74.0060 + // - With degrees and suffixes: 65.2500° N, 52.7500° W + // - With just degrees: 40.71° -74.00° + const coordRegex = /(-?\d+(?:\.\d+)?)(°?\s*[NS]?|°)?[\s,°]+(-?\d+(?:\.\d+)?)(°?\s*[EW]?|°)?/gi; const parts = []; let lastIndex = 0; let match; while ((match = coordRegex.exec(value)) !== null) { - const lat = parseFloat(match[1]); - const lng = parseFloat(match[2]); + let lat = parseFloat(match[1]); + const latSuffix = match[2]; + let lng = parseFloat(match[3]); + const lngSuffix = match[4]; + + // Handle N/S/E/W suffixes + if (latSuffix) { + if (/S/i.test(latSuffix)) lat = -Math.abs(lat); + else if (/N/i.test(latSuffix)) lat = Math.abs(lat); + } + if (lngSuffix) { + if (/W/i.test(lngSuffix)) lng = -Math.abs(lng); + else if (/E/i.test(lngSuffix)) lng = Math.abs(lng); + } + + // Validation for lat/lng ranges and basic context to avoid false positives + if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + const hasContext = !!(latSuffix || lngSuffix || match[0].includes('°') || match[0].includes(',')); + + // Also check if it's just two plain numbers without decimal points - likely not coordinates unless context is strong + const isPlainNumbers = !match[1].includes('.') && !match[3].includes('.'); + if (isPlainNumbers && !hasContext) continue; - // Basic validation for lat/lng ranges - if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { if (match.index > lastIndex) { parts.push(value.substring(lastIndex, match.index)); } From 9ae874f45bd112d26ecf85b2217ccc5e594b3e9f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:11:41 +0000 Subject: [PATCH 3/3] Refine coordinate detection and add visible map labels - Use matchAll for more reliable coordinate extraction in BotMessage - Make CoordinateLink more prominent with highlighted styling - Implement visible labels for Mapbox markers using custom HTML elements - Ensure coordinate detection handles multiple variants of degree symbols --- components/map/coordinate-link.tsx | 4 +- components/map/mapbox-map.tsx | 21 +++++++++- components/message.tsx | 62 ++++++++++++++++-------------- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/components/map/coordinate-link.tsx b/components/map/coordinate-link.tsx index d36885d3..f748fc2a 100644 --- a/components/map/coordinate-link.tsx +++ b/components/map/coordinate-link.tsx @@ -41,9 +41,9 @@ export const CoordinateLink: React.FC = ({ lat, lng, label target="_blank" rel="noopener noreferrer" onClick={handleClick} - className="text-accent-foreground underline decoration-dotted hover:text-primary transition-colors cursor-pointer" + className="text-primary font-semibold underline decoration-solid hover:text-primary/80 transition-colors cursor-pointer inline-flex items-center gap-1" > - {label || `${lat.toFixed(4)}, ${lng.toFixed(4)}`} + {label || `${lat.toFixed(4)}, ${lng.toFixed(4)}`} ); }; diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index 2a71d96e..00e74af3 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -571,9 +571,26 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number // Add or update markers from mapData mapData.markers?.forEach(markerData => { if (!markersRef.current[markerData.id]) { - const marker = new mapboxgl.Marker() + // Create a custom element for the marker to include a label + const el = document.createElement('div'); + el.className = 'custom-marker'; + + // Marker icon/dot + const dot = document.createElement('div'); + dot.className = 'w-4 h-4 bg-primary border-2 border-white rounded-full shadow-lg'; + el.appendChild(dot); + + // Label + if (markerData.title) { + const label = document.createElement('div'); + label.className = 'absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 bg-white/90 text-primary text-[10px] font-bold rounded shadow-sm whitespace-nowrap pointer-events-none border border-primary/20'; + label.textContent = markerData.title; + el.appendChild(label); + } + + const marker = new mapboxgl.Marker({ element: el }) .setLngLat([markerData.longitude, markerData.latitude]) - .setPopup(markerData.title ? new mapboxgl.Popup().setHTML(`

${markerData.title}

`) : null) + .setPopup(markerData.title ? new mapboxgl.Popup({ offset: 25 }).setHTML(`

${markerData.title}

`) : null) .addTo(map.current!); markersRef.current[markerData.id] = marker; } else { diff --git a/components/message.tsx b/components/message.tsx index 30861877..98c9c56c 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -29,58 +29,62 @@ export function BotMessage({ content }: { content: StreamableValue }) { const { children } = props; if (typeof children !== 'string') return <>{children}; const value = children; - // Improved regex to handle various coordinate formats: - // - Decimal: 40.7128, -74.0060 - // - With degrees and suffixes: 65.2500° N, 52.7500° W - // - With just degrees: 40.71° -74.00° - const coordRegex = /(-?\d+(?:\.\d+)?)(°?\s*[NS]?|°)?[\s,°]+(-?\d+(?:\.\d+)?)(°?\s*[EW]?|°)?/gi; + + // Highly inclusive regex for coordinates: + // Matches numbers with optional degrees/ordinal indicators and N/S/E/W suffixes. + const coordRegex = /(-?\d+(?:\.\d+)?)\s*[°\u00B0\u00BA]?\s*([NS])?[\s,°\u00B0\u00BA]+(-?\d+(?:\.\d+)?)\s*[°\u00B0\u00BA]?\s*([EW])?/gi; + const parts = []; let lastIndex = 0; - let match; + const matches = Array.from(value.matchAll(coordRegex)); - while ((match = coordRegex.exec(value)) !== null) { + for (const match of matches) { + const fullMatch = match[0]; let lat = parseFloat(match[1]); const latSuffix = match[2]; let lng = parseFloat(match[3]); const lngSuffix = match[4]; - // Handle N/S/E/W suffixes + // Apply suffixes if (latSuffix) { - if (/S/i.test(latSuffix)) lat = -Math.abs(lat); - else if (/N/i.test(latSuffix)) lat = Math.abs(lat); + if (latSuffix.toUpperCase() === 'S') lat = -Math.abs(lat); + else if (latSuffix.toUpperCase() === 'N') lat = Math.abs(lat); } if (lngSuffix) { - if (/W/i.test(lngSuffix)) lng = -Math.abs(lng); - else if (/E/i.test(lngSuffix)) lng = Math.abs(lng); + if (lngSuffix.toUpperCase() === 'W') lng = -Math.abs(lng); + else if (lngSuffix.toUpperCase() === 'E') lng = Math.abs(lng); } - // Validation for lat/lng ranges and basic context to avoid false positives + // Validate range if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { - const hasContext = !!(latSuffix || lngSuffix || match[0].includes('°') || match[0].includes(',')); + // Ensure we have some context (suffix, comma, or degree symbol) to avoid matching arbitrary pairs of numbers + const hasContext = !!(latSuffix || lngSuffix || fullMatch.includes('°') || fullMatch.includes('\u00B0') || fullMatch.includes('\u00BA') || fullMatch.includes(',')); + const isDecimal = match[1].includes('.') || match[3].includes('.'); - // Also check if it's just two plain numbers without decimal points - likely not coordinates unless context is strong - const isPlainNumbers = !match[1].includes('.') && !match[3].includes('.'); - if (isPlainNumbers && !hasContext) continue; - - if (match.index > lastIndex) { - parts.push(value.substring(lastIndex, match.index)); + if (hasContext || isDecimal) { + if (match.index! > lastIndex) { + parts.push(value.substring(lastIndex, match.index!)); + } + parts.push( + + ); + lastIndex = match.index! + fullMatch.length; } - parts.push( - - ); - lastIndex = coordRegex.lastIndex; } } + if (parts.length === 0) return <>{value}; + if (lastIndex < value.length) { parts.push(value.substring(lastIndex)); } - return <>{parts.length > 0 ? parts : value}; + return <>{parts}; } }} >