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') return url.replace(/\s+/g, '%20')
} }
type SearchResultImage =
| string
| {
url: string
description: string
number_of_results?: number
}
// Helper function to geocode an address // Helper function to geocode an address
const geocodeAddress = async (address: string) => { const geocodeAddress = async (address: string) => {
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN; const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
@ -61,7 +53,7 @@ export async function POST(req: Request) {
topP: 0.5, topP: 0.5,
frequencyPenalty: 0, frequencyPenalty: 0,
presencePenalty: 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: ` system: `
You are an expert AI web search engine called MiniPerplx, that helps users find information on the internet with no bullshit talks. 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!! 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({ find_place: tool({
description: "Find a specific place using Mapbox API.", description: "Find a place using Mapbox v6 reverse geocoding API.",
parameters: z.object({ parameters: z.object({
input: z.string().describe("The place to search for (e.g., 'Museum of Contemporary Art Australia')."), latitude: z.number().describe("The latitude of the location."),
inputtype: z.enum(["textquery", "phonenumber"]).describe("The type of input (textquery or phonenumber)."), longitude: z.number().describe("The longitude of the location."),
}), }),
execute: async ({ input, inputtype }: { execute: async ({ latitude, longitude }: { latitude: number; longitude: number }) => {
input: string;
inputtype: "textquery" | "phonenumber";
}) => {
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN; const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
const response = await fetch(
let searchEndpoint = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(input)}.json`; `https://api.mapbox.com/search/geocode/v6/reverse?longitude=${longitude}&latitude=${latitude}&access_token=${mapboxToken}`
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 data = await response.json(); const data = await response.json();
if (!data.features || data.features.length === 0) { if (!data.features || data.features.length === 0) {
return { candidates: [] }; return { features: [] };
} }
const place = data.features[0];
return { return {
candidates: [{ features: data.features.map((feature: any) => ({
name: place.text, name: feature.properties.name_preferred || feature.properties.name,
formatted_address: place.place_name, formatted_address: feature.properties.full_address,
geometry: { geometry: feature.geometry,
location: { })),
lat: place.center[1],
lng: place.center[0]
}
},
// Note: Mapbox doesn't provide these fields
rating: null,
opening_hours: null
}]
}; };
}, },
}), }),

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 mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'; 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 { Badge } from "@/components/ui/badge";
import { MapPin, Star } from 'lucide-react'; import { Star } from 'lucide-react';
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || ''; mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
@ -28,89 +26,83 @@ interface MapProps {
zoom?: number; zoom?: number;
} }
const MapComponent = React.memo(({ center, places = [], zoom = 14 }: MapProps) => { const MapComponent = ({ center, places = [], zoom = 14 }: MapProps) => {
const mapContainer = useRef<HTMLDivElement>(null); const mapRef = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map | null>(null); const mapInstance = useRef<mapboxgl.Map | null>(null);
const markers = useRef<mapboxgl.Marker[]>([]); const markersRef = useRef<mapboxgl.Marker[]>([]);
const [mapError, setMapError] = useState<string | null>(null);
// Initialize the map only once
useEffect(() => { useEffect(() => {
if (!mapContainer.current) return; if (!mapRef.current || mapInstance.current) return;
try { if (!mapboxgl.accessToken) {
map.current = new mapboxgl.Map({ console.error('Mapbox access token is not set');
container: mapContainer.current, return;
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');
} }
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]); }, [center.lat, center.lng, zoom]);
// Update map center when 'center' prop changes
useEffect(() => { 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 // Update markers when 'places' prop changes
map.current.flyTo({ useEffect(() => {
center: [center.lng, center.lat], if (!mapInstance.current) return;
essential: true
});
// Clear existing markers // Remove existing markers
markers.current.forEach(marker => marker.remove()); markersRef.current.forEach((marker) => marker.remove());
markers.current = []; markersRef.current = [];
// Add new markers // Add new markers
places.forEach(place => { places.forEach((place) => {
const el = document.createElement('div'); const marker = new mapboxgl.Marker()
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)
.setLngLat([place.location.lng, place.location.lat]) .setLngLat([place.location.lng, place.location.lat])
.setPopup( .setPopup(
new mapboxgl.Popup({ offset: 25 }) new mapboxgl.Popup({ offset: 25 }).setText(
.setHTML( `${place.name}${place.vicinity ? `\n${place.vicinity}` : ''}`
`<strong>${place.name}</strong>${place.rating ? )
`<br>Rating: ${place.rating} ⭐ (${place.user_ratings_total} reviews)` :
''}`
)
) )
.addTo(map.current!); .addTo(mapInstance.current!);
markers.current.push(marker); markersRef.current.push(marker);
}); });
}, [center, places]); }, [places]);
if (mapError) { return (
return ( <div className="w-full h-64 rounded-lg overflow-hidden shadow-lg">
<div className="h-64 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 text-neutral-800 dark:text-neutral-200"> <div ref={mapRef} className="w-full h-full" />
{mapError} </div>
</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 = () => ( const MapSkeleton = () => (
<Skeleton className="w-full h-64 bg-neutral-200 dark:bg-neutral-700" /> <Skeleton className="w-full h-64 bg-neutral-200 dark:bg-neutral-700" />
); );
@ -118,15 +110,14 @@ const MapSkeleton = () => (
const PlaceDetails = ({ place }: { place: Place }) => ( const PlaceDetails = ({ place }: { place: Place }) => (
<div className="flex justify-between items-start py-2"> <div className="flex justify-between items-start py-2">
<div> <div>
<h4 className="font-semibold text-neutral-800 dark:text-neutral-200">{place.name}</h4> <h2 className="text-lg font-semibold">{place.name}</h2>
{place.vicinity && ( {place.vicinity && <p className="text-sm text-gray-600">{place.vicinity}</p>}
<p className="text-sm text-neutral-600 dark:text-neutral-400 max-w-[200px]" title={place.vicinity}>
{place.vicinity}
</p>
)}
</div> </div>
{place.rating && ( {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" /> <Star className="h-3 w-3 mr-1 text-yellow-400" />
{place.rating} ({place.user_ratings_total}) {place.rating} ({place.user_ratings_total})
</Badge> </Badge>
@ -141,48 +132,29 @@ interface MapContainerProps {
loading?: boolean; loading?: boolean;
} }
const MapContainer: React.FC<MapContainerProps> = ({ title, center, places = [], loading = false }) => { const MapContainer: React.FC<MapContainerProps> = ({
title,
center,
places = [],
loading = false,
}) => {
if (loading) { if (loading) {
return ( return (
<Card className="w-full my-4 bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700"> <div className="my-4">
<CardHeader> <MapSkeleton />
<Skeleton className="h-6 w-3/4 bg-neutral-200 dark:bg-neutral-700" /> <p>Loading map...</p>
</CardHeader> </div>
<CardContent className="p-0 rounded-t-none rounded-b-xl">
<MapSkeleton />
</CardContent>
</Card>
); );
} }
return ( return (
<Card className="w-full my-4 overflow-hidden bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700"> <div className="my-4">
<CardHeader> <h2 className="text-xl font-semibold mb-2">{title}</h2>
<CardTitle className="flex items-center gap-2 text-neutral-800 dark:text-neutral-100"> <MapComponent center={center} places={places} />
<MapPin className="h-5 w-5 text-primary" /> {places.map((place, index) => (
<span>{title}</span> <PlaceDetails key={index} place={place} />
</CardTitle> ))}
</CardHeader> </div>
<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>
); );
}; };