Skip to content

Conversation

@google-labs-jules
Copy link
Contributor

@google-labs-jules google-labs-jules bot commented Jan 16, 2026

This change ensures that whenever coordinates (latitude and longitude) are mentioned in AI responses or returned by tools, they are automatically rendered as markers on the map and the map pans to that location. Coordinates in the chat are also clickable links that can re-trigger the panning.


PR created automatically by Jules for task 14070818144172209940 started by @ngoiyaeric

Summary by CodeRabbit

  • New Features
    • Coordinates in messages are now interactive links that connect to Google Maps.
    • Map markers are automatically synchronized and managed with unique identifiers.
    • Clicking on coordinate links updates the map to display the specified location.

✏️ Tip: You can customize this high-level summary in your review settings.

- 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
@google-labs-jules
Copy link
Contributor Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link

vercel bot commented Jan 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
qcx Ready Ready Preview, Comment Jan 16, 2026 1:13pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The changes introduce an interactive coordinate link feature that detects latitude and longitude patterns within message content, renders them as clickable links to Google Maps, and synchronizes corresponding markers on a Mapbox map display using a deduplicating marker system with unique coordinate-based identifiers.

Changes

Cohort / File(s) Summary
Coordinate Link Component
components/map/coordinate-link.tsx
New React client component that renders coordinate links. On mount, adds or updates a marker in map state; on click, navigates to the same coordinates on the map. Renders an anchor element linking to Google Maps with coordinate or custom label.
Map Data Structure Updates
components/map/map-data-context.tsx
Updated MapData.markers interface to include a required id: string field alongside existing latitude, longitude, and optional title fields.
Map State Management
components/map/map-query-handler.tsx
Refactored to implement a deduplicating marker system using coordinate-based unique IDs. Now conditionally appends new markers and returns them in map state alongside targetPosition and mapFeature.
Map Rendering & Synchronization
components/map/mapbox-map.tsx
Added marker tracking via markersRef and an effect that synchronizes markers from MapDataContext, adding new markers to the map and removing those no longer present.
Message Integration
components/message.tsx
Integrated coordinate detection and rendering within Markdown content. Custom text renderer detects coordinate patterns, validates ranges, and renders CoordinateLink components for valid coordinates.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Message
    participant CoordinateLink
    participant MapDataContext
    participant MapboxMap
    participant GoogleMaps

    User->>Message: View message with coordinates
    Message->>Message: Parse text for coordinate patterns
    Message->>CoordinateLink: Render detected coordinates
    CoordinateLink-->>User: Display as clickable link

    User->>CoordinateLink: Click coordinate link
    CoordinateLink->>MapDataContext: Update map state with marker
    MapDataContext->>MapDataContext: Add/update marker with ID
    MapDataContext->>MapboxMap: Notify of marker changes
    MapboxMap->>MapboxMap: Synchronize markers on map
    MapboxMap-->>User: Display marker on map
    
    CoordinateLink->>GoogleMaps: Navigate to coordinates (new tab)
    GoogleMaps-->>User: Show maps view
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested Labels

Review effort 3/5

Poem

🐰 A rabbit hops through coordinates so bright,
Coordinates leap from messages in sight!
Markers bloom on maps with glee,
Each link a path where you can be.
Navigate and explore wild and free! 🗺️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Coordinate Rendering and Map Panning' directly and accurately summarizes the main objectives of the pull request, which implements automatic coordinate rendering as map markers and map panning functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

The biggest issue is a fatal XSS risk in Mapbox via Popup().setHTML(...) using tool/AI-provided strings. Additionally, CoordinateLink auto-pans on mount, which will likely cause unpredictable/jumpy map behavior when chat content renders or re-renders. Coordinate parsing in BotMessage is also brittle and may accidentally transform content inside code-like text nodes, and marker dedupe IDs should be normalized to avoid float-precision duplicates.

Additional notes (1)
  • Compatibility | components/map/map-data-context.tsx:26-30
    This change adds id: string to MapData.markers items. Any existing marker creation paths elsewhere in the codebase must now provide id. In this diff, you updated CoordinateLink and MapQueryHandler, and Mapbox reads markerData.id, but ensure there are no other writers that still push { latitude, longitude } only; otherwise markers may silently break/dedupe incorrectly.

If you expect backward compatibility, consider making id optional and deriving one as a fallback at the render layer.

Summary of changes

What this PR changes

  • Adds a new client component components/map/coordinate-link.tsx that:
    • renders coordinates as an <a> link to Google Maps
    • on mount, adds a marker to MapDataContext and sets targetPosition to pan the map
    • on click, re-sets targetPosition to re-pan
  • Extends MapData.markers items to include a required id.
  • Updates MapQueryHandler to:
    • create a marker from tool-provided latitude/longitude
    • dedupe markers by id
    • set targetPosition and keep mapFeature metadata
  • Updates Mapbox to synchronize Mapbox GL markers with mapData.markers via a new markersRef and a marker sync useEffect.
  • Updates BotMessage markdown rendering to detect inline coordinate patterns and replace them with CoordinateLink components.
  • Updates server.log (runtime output changes).

Comment on lines +16 to +28
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]);

Choose a reason for hiding this comment

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

The useEffect always sets targetPosition on mount, which means simply rendering historical chat messages containing coordinates will keep panning the map as the user scrolls / as React re-mounts nodes. This can create a very jumpy UX and makes map position dependent on render order rather than explicit user intent.

This is especially problematic because BotMessage will create a CoordinateLink for every match, so a single message with multiple coordinates can pan multiple times during initial render.

Suggestion

Consider not panning on mount. Split responsibilities:

  • On mount: only ensure the marker exists.
  • On click: pan (targetPosition) and optionally select/focus the marker.

Example:

useEffect(() => {
  setMapData(prev => {
    const exists = prev.markers?.some(m => m.id === id)
    if (exists) return prev

    return {
      ...prev,
      markers: [
        ...(prev.markers || []),
        { id, latitude: lat, longitude: lng, title: label || id }
      ]
    }
  })
}, [id, lat, lng, label, setMapData])

const handleClick = (e: React.MouseEvent) => {
  e.preventDefault()
  setMapData(prev => ({ ...prev, targetPosition: { lat, lng } }))
}

If you still want auto-pan, gate it behind a flag (e.g. autoPanOnMount?: boolean) and only enable it for tool outputs, not for every coordinate mention.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +14 to +21
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 }];

Choose a reason for hiding this comment

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

The id is derived from raw lat,lng numbers. The same real-world coordinate can appear with different precision (e.g. 37.77 vs 37.7700) and become duplicate markers. Also, floating-point stringification can be inconsistent across sources.

A stable ID strategy (rounding to a fixed precision for IDs, or using an explicit unique ID from the tool when available) will prevent marker spam and make dedupe reliable.

Suggestion

Normalize coordinate IDs to a fixed precision (and consider including a namespace) so dedupe is consistent:

const norm = (n: number) => n.toFixed(6)
const id = `coord:${norm(lat)},${norm(lng)}`

Then use the same normalization in MapQueryHandler when generating IDs.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 558 to 583
// 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(`<h3>${markerData.title}</h3>`) : null)
.addTo(map.current!);
markersRef.current[markerData.id] = marker;
} else {
markersRef.current[markerData.id].setLngLat([markerData.longitude, markerData.latitude]);
}
});
}, [mapData.markers]);

Choose a reason for hiding this comment

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

Using setHTML(`<h3>${markerData.title}</h3>`) injects untrusted content into the DOM. title comes from AI/tool output (place_name) and can contain HTML/JS payloads.

This is a direct XSS vector inside the map popup.

Suggestion

Avoid setHTML for untrusted strings. Use setDOMContent and assign textContent:

const popup = markerData.title
  ? (() => {
      const el = document.createElement('div')
      const h3 = document.createElement('h3')
      h3.textContent = markerData.title
      el.appendChild(h3)
      return new mapboxgl.Popup().setDOMContent(el)
    })()
  : undefined

const marker = new mapboxgl.Marker()
  .setLngLat([markerData.longitude, markerData.latitude])
  .setPopup(popup ?? null)
  .addTo(map.current!)

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 571 to 582
// 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(`<h3>${markerData.title}</h3>`) : null)
.addTo(map.current!);
markersRef.current[markerData.id] = marker;
} else {
markersRef.current[markerData.id].setLngLat([markerData.longitude, markerData.latitude]);
}
});

Choose a reason for hiding this comment

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

This effect only updates marker position for existing markers, but it does not update popup/title when markerData.title changes. If the same id is reused with a new title (e.g., tool later resolves a nicer place name), the UI will remain stale.

Also, marker id is derived from raw floats elsewhere; small precision differences can accidentally create duplicates that look identical on the map.

Suggestion

Update popup when title changes, and consider normalizing IDs to a fixed precision.

  • Popup update approach:
const existing = markersRef.current[markerData.id]
existing.setLngLat([markerData.longitude, markerData.latitude])
if (markerData.title) {
  // recreate popup safely (see XSS comment) and reattach
}
  • ID normalization (shared helper used by CoordinateLink + MapQueryHandler):
const coordId = (lat: number, lng: number) => `${lat.toFixed(6)},${lng.toFixed(6)}`

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 27 to 63
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(
<CoordinateLink
key={`${lat}-${lng}-${match.index}`}
lat={lat}
lng={lng}
/>
);
lastIndex = coordRegex.lastIndex;
}
}

if (lastIndex < value.length) {
parts.push(value.substring(lastIndex));
}

return <>{parts.length > 0 ? parts : value}</>;
}
}}

Choose a reason for hiding this comment

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

The coordinate detection regex only matches decimals with a dot (e.g. 12.34, 56.78). It will miss many common coordinate formats such as integers (40, -74), degrees/minutes/seconds, or coordinates wrapped in parentheses.

More importantly, this replaces all matching text nodes, including ones inside code blocks/inline code depending on how react-markdown emits nodes. That can corrupt code samples or JSON snippets that contain numbers.

Finally, parts is built as an untyped array; mixing strings and React elements is fine, but the logic always returns parts even when there are no matches (parts.length > 0 becomes true because the full string is pushed at the end). That makes the : value branch effectively dead and can hide bugs.

Suggestion

Tighten the behavior:

  1. Only transform when at least one valid coordinate match was found.
  2. Skip transformations for code/inlineCode nodes by overriding those components (or by moving this logic to a remark/rehype plugin that can respect AST node types).
  3. Expand the regex minimally to support integer degrees:
const coordRegex = /(-?\d{1,2}(?:\.\d+)?),\s*(-?\d{1,3}(?:\.\d+)?)/g

And track whether you inserted any CoordinateLink:

let inserted = false
...
if (valid) {
  inserted = true
  ...
}
...
return <>{inserted ? parts : value}</>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +39 to +55
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
}
};
});

Choose a reason for hiding this comment

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

id is computed as ${latitude},${longitude} directly from floats. Tool outputs often vary slightly between calls; this can lead to duplicated markers that represent the same real-world location.

Also, place_name is used as title and later used in Mapbox popup creation (currently via HTML). Even after fixing XSS, you probably still want a consistent normalization strategy shared with CoordinateLink.

Suggestion

Introduce and reuse a shared helper to generate marker IDs with fixed precision, e.g. coordId(lat, lng) using toFixed(6).

const id = coordId(latitude, longitude)

That keeps deduping stable and avoids near-duplicate markers.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

@charliecreates charliecreates bot removed the request for review from CharlieHelps January 16, 2026 11:08
@ngoiyaeric
Copy link
Collaborator

@coderabbitai do a full review of the pull request

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

@ngoiyaeric: I'll perform a full review of the pull request now. I'll analyze the coordinate rendering and map panning implementation, including how coordinates are detected, rendered as markers, and made clickable in the chat interface.

✅ Actions performed

Full review triggered.

@ngoiyaeric
Copy link
Collaborator

Screenshot 2026-01-16 152046 @coderabbitai its not rendering these on the map.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the BotMessage coordinate detection implementation
cat components/message.tsx

Length of output: 2815


🏁 Script executed:

#!/bin/bash
# Read the CoordinateLink component implementation
cat components/map/coordinate-link.tsx

Length of output: 1357


🏁 Script executed:

#!/bin/bash
# Read the Mapbox map marker synchronization code
cat components/map/mapbox-map.tsx

Length of output: 23658


🏁 Script executed:

#!/bin/bash
# Read the MapData context to understand state structure
cat components/map/map-data-context.tsx

Length of output: 1686


🏁 Script executed:

#!/bin/bash
# Search for any test files or examples that might show how coordinates are displayed
rg -n "latitude|longitude|coordinate" --type tsx --type ts -C 2

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Check if there are any map-related test files or examples
fd -e tsx -e ts test map

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Look for MapQueryHandler to see how coordinates are returned from tools
cat components/map/map-query-handler.tsx

Length of output: 4048


🏁 Script executed:

#!/bin/bash
# Look for how coordinates might be formatted in responses
rg -n "latitude|longitude" -g "*.tsx" -g "*.ts" -C 2

Length of output: 12894


🏁 Script executed:

#!/bin/bash
# Check the structure of how text is rendered in paragraphs
# Let's see if there are other message rendering components
fd -t f "message" -e tsx -e ts

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Look for examples of how markdown is being processed
rg -n "MemoizedReactMarkdown|ReactMarkdown" -g "*.tsx" -C 3

Length of output: 1702


@ngoiyaeric I found the issue! The problem is with how the coordinate detection is implemented in components/message.tsx.

@ngoiyaeric I found the issue! The problem is that text component renderer functionality was removed in react-markdown version 6.0.0. The components={{ text: ... }} approach in message.tsx isn't being called at all, so coordinates are never detected.

The fix is to use the paragraph (p) component renderer instead, which processes all text content within paragraphs. Here's the corrected implementation:

'use client'

import { StreamableValue, useStreamableValue } from 'ai/rsc'
import { MemoizedReactMarkdown } from './ui/markdown'
import rehypeExternalLinks from 'rehype-external-links'
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'
import React from 'react'

export function BotMessage({ content }: { content: StreamableValue<string> }) {
  const [data, error, pending] = useStreamableValue(content)

  // Currently, sometimes error occurs after finishing the stream.
  if (error) return <div>Error</div>

  //modify the content to render LaTeX equations
  const processedData = preprocessLaTeX(data || '')

  // Helper function to process text and replace coordinates with CoordinateLink
  const processTextForCoordinates = (text: string): React.ReactNode[] => {
    const coordRegex = /(-?\d+\.\d+),\s*(-?\d+\.\d+)/g;
    const parts: React.ReactNode[] = [];
    let lastIndex = 0;
    let match;

    while ((match = coordRegex.exec(text)) !== 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(text.substring(lastIndex, match.index));
        }
        parts.push(
          <CoordinateLink
            key={`${lat}-${lng}-${match.index}`}
            lat={lat}
            lng={lng}
          />
        );
        lastIndex = coordRegex.lastIndex;
      }
    }

    if (lastIndex < text.length) {
      parts.push(text.substring(lastIndex));
    }

    return parts.length > 0 ? parts : [text];
  };

  // Recursively process children to find and replace coordinates
  const processChildren = (children: React.ReactNode): React.ReactNode => {
    return React.Children.map(children, (child) => {
      if (typeof child === 'string') {
        return processTextForCoordinates(child);
      }
      if (React.isValidElement(child) && child.props.children) {
        return React.cloneElement(child as React.ReactElement<any>, {
          children: processChildren(child.props.children)
        });
      }
      return child;
    });
  };

  return (
    <div className="overflow-x-auto">
      <MemoizedReactMarkdown
        rehypePlugins={[[rehypeExternalLinks, { target: '_blank' }], rehypeKatex]}
        remarkPlugins={[remarkGfm, remarkMath]}
        className="prose-sm prose-neutral prose-a:text-accent-foreground/50"
        components={{
          p: ({ children, ...props }) => {
            return <p {...props}>{processChildren(children)}</p>;
          },
          li: ({ children, ...props }) => {
            return <li {...props}>{processChildren(children)}</li>;
          }
        }}
      >
        {processedData}
      </MemoizedReactMarkdown>
    </div>
  )
}

// Preprocess LaTeX equations to be rendered by KaTeX
// ref: https://github.com/remarkjs/react-markdown/issues/785
const preprocessLaTeX = (content: string) => {
  const blockProcessedContent = content.replace(
    /\\\[([\s\S]*?)\\\]/g,
    (_, equation) => `$$${equation}$$`
  )
  const inlineProcessedContent = blockProcessedContent.replace(
    /\\\(([\s\S]*?)\\\)/g,
    (_, equation) => `$${equation}$`
  )
  return inlineProcessedContent
}

Key changes:

  1. Removed the non-functional text component renderer - it doesn't exist in modern react-markdown
  2. Added p and li component renderers - these intercept paragraphs and list items where coordinates typically appear
  3. Created helper functions to recursively process children and detect coordinate patterns
  4. Maintained the same validation logic - lat (-90 to 90) and lng (-180 to 180)

This approach ensures that coordinates like "40.7128, -74.0060" appearing in the chat will be detected, converted to clickable CoordinateLink components, and rendered as markers on the map.


💡 Use @coderabbitai plan to create a plan for code changes

@google-labs-jules
Copy link
Contributor Author

Screenshot 2026-01-16 152046 @coderabbitai its not rendering these on the map.

Thank you for the feedback. I've updated the coordinate detection logic to support formats with degree symbols and N/S/E/W suffixes (e.g., '65.2500° N, 52.7500° W'). It should now correctly render these locations on the map.

…ffixes

- 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
@ngoiyaeric
Copy link
Collaborator

@coderabbitai its still not rendering the coordinates as links

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the current implementation of message.tsx to see what's there
cat components/message.tsx

Length of output: 4039


🏁 Script executed:

#!/bin/bash
# Check if there are any example responses or test data showing coordinate format
rg -n "latitude|longitude|coordinate" -g "*.tsx" -g "*.ts" -A 2 -B 2 | head -100

Length of output: 7865


🏁 Script executed:

#!/bin/bash
# Look at the CoordinateLink component to ensure it's working correctly
cat components/map/coordinate-link.tsx

Length of output: 1357


🏁 Script executed:

#!/bin/bash
# Check package.json to see which version of react-markdown is being used
cat package.json | grep -A 2 -B 2 "react-markdown"

Length of output: 168


@ngoiyaeric I see the issue now. The text component renderer doesn't work in react-markdown v9. The solution is to preprocess the markdown content to convert coordinates into special markdown links, then use a custom link renderer to convert those into CoordinateLink components.

Here's the corrected implementation for components/message.tsx:

'use client'

import { StreamableValue, useStreamableValue } from 'ai/rsc'
import { MemoizedReactMarkdown } from './ui/markdown'
import rehypeExternalLinks from 'rehype-external-links'
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<string> }) {
  const [data, error, pending] = useStreamableValue(content)

  // Currently, sometimes error occurs after finishing the stream.
  if (error) return <div>Error</div>

  // First process LaTeX equations
  let processedData = preprocessLaTeX(data || '')
  
  // Then process coordinates - convert them to special markdown links
  processedData = preprocessCoordinates(processedData)

  return (
    <div className="overflow-x-auto">
      <MemoizedReactMarkdown
        rehypePlugins={[[rehypeExternalLinks, { target: '_blank' }], rehypeKatex]}
        remarkPlugins={[remarkGfm, remarkMath]}
        className="prose-sm prose-neutral prose-a:text-accent-foreground/50"
        components={{
          a: ({ node, href, children, ...props }) => {
            // Check if this is a coordinate link (our special format)
            if (href && href.startsWith('coord://')) {
              const coords = href.replace('coord://', '').split(',')
              if (coords.length === 2) {
                const lat = parseFloat(coords[0])
                const lng = parseFloat(coords[1])
                if (!isNaN(lat) && !isNaN(lng)) {
                  return (
                    <CoordinateLink
                      key={`${lat}-${lng}`}
                      lat={lat}
                      lng={lng}
                      label={children as string}
                    />
                  )
                }
              }
            }
            // Regular link
            return <a href={href} {...props}>{children}</a>
          }
        }}
      >
        {processedData}
      </MemoizedReactMarkdown>
    </div>
  )
}

// Preprocess coordinates to convert them into markdown links
const preprocessCoordinates = (content: string): string => {
  // Match coordinate patterns: decimal numbers with comma separation
  // Examples: 40.7128, -74.0060 or -33.8688, 151.2093
  const coordRegex = /(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)/g
  
  return content.replace(coordRegex, (match, lat, lng) => {
    const latNum = parseFloat(lat)
    const lngNum = parseFloat(lng)
    
    // Validate lat/lng ranges
    if (latNum >= -90 && latNum <= 90 && lngNum >= -180 && lngNum <= 180) {
      // Convert to special markdown link format
      return `[${match}](coord://${lat},${lng})`
    }
    
    // Not valid coordinates, return unchanged
    return match
  })
}

// Preprocess LaTeX equations to be rendered by KaTeX
// ref: https://github.com/remarkjs/react-markdown/issues/785
const preprocessLaTeX = (content: string) => {
  const blockProcessedContent = content.replace(
    /\\\[([\s\S]*?)\\\]/g,
    (_, equation) => `$$${equation}$$`
  )
  const inlineProcessedContent = blockProcessedContent.replace(
    /\\\(([\s\S]*?)\\\)/g,
    (_, equation) => `$${equation}$`
  )
  return inlineProcessedContent
}

How this works:

  1. Preprocessing step: The preprocessCoordinates function scans the markdown text for coordinate patterns (e.g., 40.7128, -74.0060) and converts them to special markdown links with a coord:// protocol: [40.7128, -74.0060](coord://40.7128,-74.0060)

  2. Custom link renderer: The a component renderer intercepts all links. When it finds a link with the coord:// protocol, it renders a CoordinateLink component instead of a regular anchor tag.

  3. Validation: Only converts valid latitude/longitude pairs (lat: -90 to 90, lng: -180 to 180)

This approach is more reliable because:

  • It works with react-markdown v9's architecture
  • It doesn't try to intercept text nodes directly
  • It leverages markdown's native link syntax
  • The preprocessing happens before markdown parsing

The coordinates in your chat should now appear as clickable links and automatically add markers to the map! 🗺️

@google-labs-jules
Copy link
Contributor Author

@coderabbitai its still not rendering the coordinates as links

I've further improved the coordinate detection logic using matchAll and a more inclusive regex that covers various degree symbols (including ° and º). I also updated the CoordinateLink component to be much more prominent in the chat with a highlighted background and primary color. Additionally, I updated the map markers to include a visible label by default so they are easier to identify on the map.

- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants