Skip to content
Open
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
11 changes: 11 additions & 0 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler' // Add this import
import { MapboxElevationDisplay } from '@/components/mapbox-elevation-display'

// Define the type for related queries
type RelatedQueries = {
Expand Down Expand Up @@ -653,6 +654,16 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
isCollapsed: false
}
}
if (
toolOutput.type === 'ELEVATION_QUERY_RESULT' &&
name === 'elevationQueryTool'
) {
return {
id,
component: <MapboxElevationDisplay toolOutput={toolOutput} />,
isCollapsed: false
};
}

const searchResults = createStreamableValue()
searchResults.done(JSON.stringify(toolOutput))
Expand Down
63 changes: 63 additions & 0 deletions components/mapbox-elevation-display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client';

import { useEffect } from 'react';
import { useMapData } from './map/map-data-context';
import Image from 'next/image';

interface ElevationData {
latitude: number;
longitude: number;
elevation: number;
mapUrl?: string;
}

interface ElevationToolOutput {
type: string;
originalUserInput: string;
timestamp: string;
elevation_response: ElevationData | null;
}

interface MapboxElevationDisplayProps {
toolOutput?: ElevationToolOutput | null;
}

export const MapboxElevationDisplay: React.FC<MapboxElevationDisplayProps> = ({ toolOutput }) => {
const { setMapData } = useMapData();

useEffect(() => {
if (toolOutput && toolOutput.elevation_response) {
const { latitude, longitude } = toolOutput.elevation_response;
if (typeof latitude === 'number' && typeof longitude === 'number') {
setMapData(prevData => ({
...prevData,
targetPosition: [longitude, latitude],
}));
}
}
}, [toolOutput, setMapData]);

if (!toolOutput || !toolOutput.elevation_response) {
return null;
}

const { elevation, mapUrl } = toolOutput.elevation_response;

return (
<div className="bg-zinc-900 border-zinc-700 border rounded-lg p-4 my-4">
<h3 className="text-lg font-semibold text-white">Elevation Information (Mapbox)</h3>
<p className="text-zinc-300">Elevation: {elevation} meters</p>
{mapUrl && (
<div className="mt-4">
<Image
src={mapUrl}
alt="Map preview"
width={600}
height={400}
className="rounded-lg"
/>
</div>
)}
</div>
);
};
1 change: 1 addition & 0 deletions dev.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ next dev --turbo
29 changes: 29 additions & 0 deletions jules-scratch/verification/verify_elevation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import re
from playwright.sync_api import Playwright, sync_playwright, expect

def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
page.goto("http://localhost:3000/")

# Wait for the loading overlay to disappear
loading_overlay = page.locator('div[class*="z-[9999]"]')
expect(loading_overlay).to_be_hidden(timeout=60000)

# Use the correct placeholder text: "Explore"
input_field = page.get_by_placeholder("Explore")
expect(input_field).to_be_visible(timeout=30000)
expect(input_field).to_be_enabled(timeout=30000)

input_field.click()
input_field.fill("what is the elevation of mount everest at latitude 27.9881 and longitude 86.9250?")
page.get_by_role("button", name="Send message").click()
expect(page.get_by_text("Elevation Information (Mapbox)")).to_be_visible(timeout=90000)
page.screenshot(path="jules-scratch/verification/elevation-display.png")

context.close()
browser.close()

with sync_playwright() as playwright:
run(playwright)
94 changes: 94 additions & 0 deletions lib/agents/tools/elevation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Elevation tool to fetch elevation data for a given location using Mapbox Tilequery API.
*/
import { createStreamableUI, createStreamableValue } from 'ai/rsc';
import { BotMessage } from '@/components/message';
import { z } from 'zod';

// Define the schema for the elevation tool's parameters
export const elevationQuerySchema = z.object({
latitude: z.number().describe('The latitude of the location.'),
longitude: z.number().describe('The longitude of the location.'),
includeMap: z.boolean().optional().default(true).describe('Whether to include a map preview.'),
});

// Main elevation tool executor
export const elevationTool = ({ uiStream }: { uiStream: ReturnType<typeof createStreamableUI> }) => ({
description: 'Use this tool to get the elevation for a specific location (latitude and longitude) using Mapbox.',
parameters: elevationQuerySchema,
execute: async (params: z.infer<typeof elevationQuerySchema>) => {
const { latitude, longitude, includeMap } = params;
const mapboxAccessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;

const uiFeedbackStream = createStreamableValue<string>();
uiStream.append(<BotMessage content={uiFeedbackStream.value} />);

if (!mapboxAccessToken) {
const errorMessage = 'Mapbox access token is not configured. This tool is unavailable.';
uiFeedbackStream.done(errorMessage);
return {
type: 'ELEVATION_QUERY_RESULT',
error: errorMessage,
};
}

let feedbackMessage = `Processing elevation query for coordinates: ${latitude}, ${longitude}...`;
uiFeedbackStream.update(feedbackMessage);

let elevationData: {
latitude: number;
longitude: number;
elevation: number;
mapUrl?: string
} | null = null;
let toolError: string | null = null;

try {
const apiUrl = `https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2/tilequery/${longitude},${latitude}.json?access_token=${mapboxAccessToken}`;
const response = await fetch(apiUrl);

if (!response.ok) {
throw new Error(`Failed to fetch elevation data from Mapbox. Status: ${response.status}`);
}

const data = await response.json();

if (data.features && data.features.length > 0) {
// Find the highest elevation value from the contour features
const maxElevation = data.features
.filter((feature: any) => feature.properties && typeof feature.properties.ele !== 'undefined')
.reduce((max: number, feature: any) => Math.max(max, feature.properties.ele), -Infinity);

if (maxElevation === -Infinity) {
throw new Error('No elevation data found in the response features.');
}

elevationData = { latitude, longitude, elevation: maxElevation };

if (includeMap) {
const mapUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/pin-s-marker+285A98(${longitude},${latitude})/${longitude},${latitude},12,0/600x400?access_token=${mapboxAccessToken}`;
elevationData.mapUrl = mapUrl;
}

feedbackMessage = `Elevation at ${latitude}, ${longitude}: ${maxElevation} meters.`;
uiFeedbackStream.update(feedbackMessage);
} else {
throw new Error('No features returned from Mapbox for the given coordinates.');
}
} catch (error: any) {
toolError = `Error fetching elevation data from Mapbox: ${error.message}`;
uiFeedbackStream.update(toolError);
console.error('[ElevationTool] Tool execution failed:', error);
} finally {
uiFeedbackStream.done();
}

return {
type: 'ELEVATION_QUERY_RESULT',
originalUserInput: JSON.stringify(params),
timestamp: new Date().toISOString(),
elevation_response: elevationData,
error: toolError,
};
},
});
4 changes: 4 additions & 0 deletions lib/agents/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { retrieveTool } from './retrieve'
import { searchTool } from './search'
import { videoSearchTool } from './video-search'
import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp import
import { elevationTool } from './elevation'

export interface ToolProps {
uiStream: ReturnType<typeof createStreamableUI>
Expand All @@ -25,6 +26,9 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => {
geospatialQueryTool: geospatialTool({
uiStream
// mcp: mcp || null // Removed mcp argument
}),
elevationQueryTool: elevationTool({
uiStream
})
Comment on lines +30 to 32

Choose a reason for hiding this comment

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

Consider conditionally registering elevationQueryTool based on the presence of a Mapbox access token, similar to how videoSearch is gated by SERPER_API_KEY. Without gating, environments lacking the token may still expose this tool in the UI, leading to avoidable runtime failures when invoked. Aligning with the existing conditional registration pattern improves robustness and avoids presenting unusable tools.

Suggestion

To match the existing pattern used for videoSearch, remove the inline elevationQueryTool property from the object initializer and register it conditionally after the object is created:

// Remove from the object initializer:
// elevationQueryTool: elevationTool({ uiStream })

// After tools is created:
if (process.env.MAPBOX_ACCESS_TOKEN) {
  tools.elevationQueryTool = elevationTool({ uiStream })
}

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

}

Expand Down