Refactor MapComponent and complete find place tool with Mapbox API

This commit is contained in:
zaidmukaddam 2024-11-06 20:22:26 +05:30
parent 0ae2e4a2c6
commit 4583c93730
3 changed files with 618 additions and 913 deletions

View File

@ -31,14 +31,6 @@ function sanitizeUrl(url: string): string {
return url.replace(/\s+/g, '%20')
}
type SearchResultImage =
| string
| {
url: string
description: string
number_of_results?: number
}
// Helper function to geocode an address
const geocodeAddress = async (address: string) => {
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
@ -61,7 +53,7 @@ export async function POST(req: Request) {
topP: 0.5,
frequencyPenalty: 0,
presencePenalty: 0,
experimental_activeTools: ["get_weather_data", "programming", "web_search", "text_translate"],
experimental_activeTools: ["get_weather_data", "programming", "web_search", "text_translate", "find_place"],
system: `
You are an expert AI web search engine called MiniPerplx, that helps users find information on the internet with no bullshit talks.
Always start with running the tool(s) and then and then only write your response AT ALL COSTS!!
@ -444,47 +436,28 @@ When asked a "What is" question, maintain the same format as the question and an
},
}),
find_place: tool({
description: "Find a specific place using Mapbox API.",
description: "Find a place using Mapbox v6 reverse geocoding API.",
parameters: z.object({
input: z.string().describe("The place to search for (e.g., 'Museum of Contemporary Art Australia')."),
inputtype: z.enum(["textquery", "phonenumber"]).describe("The type of input (textquery or phonenumber)."),
latitude: z.number().describe("The latitude of the location."),
longitude: z.number().describe("The longitude of the location."),
}),
execute: async ({ input, inputtype }: {
input: string;
inputtype: "textquery" | "phonenumber";
}) => {
execute: async ({ latitude, longitude }: { latitude: number; longitude: number }) => {
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
let searchEndpoint = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(input)}.json`;
if (inputtype === "phonenumber") {
// Note: Mapbox doesn't support phone number search directly
// We'll just search the number as text
searchEndpoint = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(input)}.json`;
}
const response = await fetch(`${searchEndpoint}?types=poi&access_token=${mapboxToken}`);
const response = await fetch(
`https://api.mapbox.com/search/geocode/v6/reverse?longitude=${longitude}&latitude=${latitude}&access_token=${mapboxToken}`
);
const data = await response.json();
if (!data.features || data.features.length === 0) {
return { candidates: [] };
return { features: [] };
}
const place = data.features[0];
return {
candidates: [{
name: place.text,
formatted_address: place.place_name,
geometry: {
location: {
lat: place.center[1],
lng: place.center[0]
}
},
// Note: Mapbox doesn't provide these fields
rating: null,
opening_hours: null
}]
features: data.features.map((feature: any) => ({
name: feature.properties.name_preferred || feature.properties.name,
formatted_address: feature.properties.full_address,
geometry: feature.geometry,
})),
};
},
}),

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,8 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { MapPin, Star } from 'lucide-react';
import { Star } from 'lucide-react';
import { Skeleton } from "@/components/ui/skeleton";
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
@ -28,89 +26,83 @@ interface MapProps {
zoom?: number;
}
const MapComponent = React.memo(({ center, places = [], zoom = 14 }: MapProps) => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map | null>(null);
const markers = useRef<mapboxgl.Marker[]>([]);
const [mapError, setMapError] = useState<string | null>(null);
const MapComponent = ({ center, places = [], zoom = 14 }: MapProps) => {
const mapRef = useRef<HTMLDivElement>(null);
const mapInstance = useRef<mapboxgl.Map | null>(null);
const markersRef = useRef<mapboxgl.Marker[]>([]);
// Initialize the map only once
useEffect(() => {
if (!mapContainer.current) return;
if (!mapRef.current || mapInstance.current) return;
try {
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v12',
center: [center.lng, center.lat],
zoom: zoom
});
// Add navigation control
map.current.addControl(new mapboxgl.NavigationControl(), 'top-right');
// Clean up markers when component unmounts
return () => {
markers.current.forEach(marker => marker.remove());
map.current?.remove();
};
} catch (error) {
console.error('Error initializing map:', error);
setMapError('Failed to initialize map');
if (!mapboxgl.accessToken) {
console.error('Mapbox access token is not set');
return;
}
mapInstance.current = new mapboxgl.Map({
container: mapRef.current,
style: 'mapbox://styles/mapbox/standard',
center: [center.lng, center.lat],
zoom,
});
return () => {
mapInstance.current?.remove();
mapInstance.current = null;
};
}, [center.lat, center.lng, zoom]);
// Update map center when 'center' prop changes
useEffect(() => {
if (!map.current) return;
if (mapInstance.current) {
mapInstance.current.flyTo({
center: [center.lng, center.lat],
zoom,
essential: true,
});
}
}, [center, zoom]);
// Update center when it changes
map.current.flyTo({
center: [center.lng, center.lat],
essential: true
});
// Update markers when 'places' prop changes
useEffect(() => {
if (!mapInstance.current) return;
// Clear existing markers
markers.current.forEach(marker => marker.remove());
markers.current = [];
// Remove existing markers
markersRef.current.forEach((marker) => marker.remove());
markersRef.current = [];
// Add new markers
places.forEach(place => {
const el = document.createElement('div');
el.className = 'marker';
el.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>';
el.style.color = 'hsl(var(--primary))';
el.style.width = '24px';
el.style.height = '24px';
el.style.cursor = 'pointer';
const marker = new mapboxgl.Marker(el)
places.forEach((place) => {
const marker = new mapboxgl.Marker()
.setLngLat([place.location.lng, place.location.lat])
.setPopup(
new mapboxgl.Popup({ offset: 25 })
.setHTML(
`<strong>${place.name}</strong>${place.rating ?
`<br>Rating: ${place.rating} ⭐ (${place.user_ratings_total} reviews)` :
''}`
)
new mapboxgl.Popup({ offset: 25 }).setText(
`${place.name}${place.vicinity ? `\n${place.vicinity}` : ''}`
)
)
.addTo(map.current!);
.addTo(mapInstance.current!);
markers.current.push(marker);
markersRef.current.push(marker);
});
}, [center, places]);
}, [places]);
if (mapError) {
return (
<div className="h-64 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 text-neutral-800 dark:text-neutral-200">
{mapError}
</div>
);
}
return (
<div className="w-full h-64 rounded-lg overflow-hidden shadow-lg">
<div ref={mapRef} className="w-full h-full" />
</div>
);
};
return <div ref={mapContainer} className="w-full h-64" />;
export default React.memo(MapComponent, (prevProps, nextProps) => {
return (
prevProps.center.lat === nextProps.center.lat &&
prevProps.center.lng === nextProps.center.lng &&
prevProps.zoom === nextProps.zoom &&
JSON.stringify(prevProps.places) === JSON.stringify(nextProps.places)
);
});
MapComponent.displayName = 'MapComponent';
const MapSkeleton = () => (
<Skeleton className="w-full h-64 bg-neutral-200 dark:bg-neutral-700" />
);
@ -118,15 +110,14 @@ const MapSkeleton = () => (
const PlaceDetails = ({ place }: { place: Place }) => (
<div className="flex justify-between items-start py-2">
<div>
<h4 className="font-semibold text-neutral-800 dark:text-neutral-200">{place.name}</h4>
{place.vicinity && (
<p className="text-sm text-neutral-600 dark:text-neutral-400 max-w-[200px]" title={place.vicinity}>
{place.vicinity}
</p>
)}
<h2 className="text-lg font-semibold">{place.name}</h2>
{place.vicinity && <p className="text-sm text-gray-600">{place.vicinity}</p>}
</div>
{place.rating && (
<Badge variant="secondary" className="flex items-center bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200">
<Badge
variant="secondary"
className="flex items-center bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200"
>
<Star className="h-3 w-3 mr-1 text-yellow-400" />
{place.rating} ({place.user_ratings_total})
</Badge>
@ -141,48 +132,29 @@ interface MapContainerProps {
loading?: boolean;
}
const MapContainer: React.FC<MapContainerProps> = ({ title, center, places = [], loading = false }) => {
const MapContainer: React.FC<MapContainerProps> = ({
title,
center,
places = [],
loading = false,
}) => {
if (loading) {
return (
<Card className="w-full my-4 bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
<CardHeader>
<Skeleton className="h-6 w-3/4 bg-neutral-200 dark:bg-neutral-700" />
</CardHeader>
<CardContent className="p-0 rounded-t-none rounded-b-xl">
<MapSkeleton />
</CardContent>
</Card>
<div className="my-4">
<MapSkeleton />
<p>Loading map...</p>
</div>
);
}
return (
<Card className="w-full my-4 overflow-hidden bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-neutral-800 dark:text-neutral-100">
<MapPin className="h-5 w-5 text-primary" />
<span>{title}</span>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<MapComponent center={center} places={places} />
{places.length > 0 && (
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="place-details">
<AccordionTrigger className="px-4 text-neutral-800 dark:text-neutral-200">
Place Details
</AccordionTrigger>
<AccordionContent>
<div className="px-4 space-y-4 max-h-64 overflow-y-auto">
{places.map((place, index) => (
<PlaceDetails key={index} place={place} />
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</CardContent>
</Card>
<div className="my-4">
<h2 className="text-xl font-semibold mb-2">{title}</h2>
<MapComponent center={center} places={places} />
{places.map((place, index) => (
<PlaceDetails key={index} place={place} />
))}
</div>
);
};