wip: nearby search
This commit is contained in:
parent
fb19021c91
commit
9f8b8a856e
@ -1,3 +1,4 @@
|
||||
// /app/api/chat/route.ts
|
||||
import { z } from "zod";
|
||||
import { createAzure } from '@ai-sdk/azure';
|
||||
import { anthropic } from '@ai-sdk/anthropic'
|
||||
@ -53,7 +54,7 @@ export async function POST(req: Request) {
|
||||
topP: 0.5,
|
||||
frequencyPenalty: 0,
|
||||
presencePenalty: 0,
|
||||
experimental_activeTools: ["get_weather_data", "find_place", "programming", "web_search", "text_translate",],
|
||||
experimental_activeTools: ["get_weather_data", "find_place", "programming", "web_search", "text_translate", "nearby_search"],
|
||||
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!!
|
||||
@ -91,6 +92,7 @@ DO's:
|
||||
- The programming tool runs the code in a 'safe' and 'sandboxed' jupyper notebook environment. Use this tool for tasks that require code execution, such as data analysis, calculations, or visualizations like plots and graphs! Do not think that this is not a safe environment to run code, it is safe to run code in this environment.
|
||||
- The programming tool can be used to install libraries using !pip install <library_name> in the code. This will help in running the code successfully. Always remember to install the libraries using !pip install <library_name> in the code at all costs!!
|
||||
- For queries about finding a specific place, use the find_place tool. Provide the information about the location and then compose your response based on the information gathered.
|
||||
- For queries about nearby places, use the nearby_search tool. Provide the location and radius in the parameters, then compose your response based on the information gathered.
|
||||
- Adding Country name in the location search will help in getting the accurate results. Always remember to provide the location in the correct format to get the accurate results.
|
||||
- For text translation queries, use the text_translate tool. Provide the text to translate, the language to translate to, and the source language (optional). Then, compose your response based on the translated text.
|
||||
- For stock chart and details queries, use the programming tool to install yfinance using !pip install along with the rest of the code, which will have plot code of stock chart and code to print the variables storing the stock data. Then, compose your response based on the output of the code execution.
|
||||
@ -119,6 +121,7 @@ Follow the format and guidelines for each tool and provide the response accordin
|
||||
## Trip based queries:
|
||||
- For queries related to trips, always use the find_place tool for map location and then run the web_search tool to find information about places, directions, or reviews.
|
||||
- Calling web and find place tools in the same response is allowed, but do not call the same tool in a response at all costs!!
|
||||
- For nearby search queries, use the nearby_search tool to find places around a location. Provide the location and radius in the parameters, then compose your response based on the information gathered.
|
||||
|
||||
## Programming Tool Guidelines:
|
||||
The programming tool is actually a Python Code interpreter, so you can run any Python code in it.
|
||||
@ -373,66 +376,6 @@ When asked a "What is" question, maintain the same format as the question and an
|
||||
return { message: message.trim(), images, chart: execution.results[0].chart ?? "" };
|
||||
},
|
||||
}),
|
||||
nearby_search: tool({
|
||||
description: "Search for nearby places using Mapbox API.",
|
||||
parameters: z.object({
|
||||
location: z.string().describe("The location to search near (e.g., 'New York City' or '1600 Amphitheatre Parkway, Mountain View, CA')."),
|
||||
type: z.string().describe("The type of place to search for (e.g., restaurant, cafe, park)."),
|
||||
keyword: z.string().describe("An optional keyword to refine the search."),
|
||||
radius: z.number().default(3000).describe("The radius of the search area in meters (max 50000, default 3000)."),
|
||||
}),
|
||||
execute: async ({ location, type, keyword, radius }: {
|
||||
location: string;
|
||||
type: string;
|
||||
keyword?: string;
|
||||
radius: number;
|
||||
}) => {
|
||||
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
|
||||
|
||||
// First geocode the location
|
||||
const locationData = await geocodeAddress(location);
|
||||
const [lng, lat] = locationData.center;
|
||||
|
||||
// Construct search query
|
||||
let searchQuery = type;
|
||||
if (keyword) {
|
||||
searchQuery = `${keyword} ${type}`;
|
||||
}
|
||||
|
||||
// Search for places using Mapbox Geocoding API
|
||||
const response = await fetch(
|
||||
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(searchQuery)}.json?proximity=${lng},${lat}&limit=10&types=poi&access_token=${mapboxToken}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
// Filter results by distance
|
||||
const radiusInDegrees = radius / 111320; // Approximate conversion from meters to degrees
|
||||
const results = data.features
|
||||
.filter((feature: any) => {
|
||||
const [placeLng, placeLat] = feature.center;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(placeLng - lng, 2) + Math.pow(placeLat - lat, 2)
|
||||
);
|
||||
return distance <= radiusInDegrees;
|
||||
})
|
||||
.map((feature: any) => ({
|
||||
name: feature.text,
|
||||
vicinity: feature.place_name,
|
||||
place_id: feature.id,
|
||||
location: {
|
||||
lat: feature.center[1],
|
||||
lng: feature.center[0]
|
||||
},
|
||||
// Note: Mapbox doesn't provide ratings, so we'll exclude those
|
||||
}));
|
||||
|
||||
return {
|
||||
results: results.slice(0, 5),
|
||||
center: { lat, lng },
|
||||
formatted_address: locationData.place_name,
|
||||
};
|
||||
},
|
||||
}),
|
||||
find_place: tool({
|
||||
description: "Find a place using Mapbox v6 reverse geocoding API.",
|
||||
parameters: z.object({
|
||||
@ -543,8 +486,60 @@ When asked a "What is" question, maintain the same format as the question and an
|
||||
};
|
||||
},
|
||||
}),
|
||||
nearby_search: tool({
|
||||
description: "Search for nearby places, such as restaurants or hotels.",
|
||||
parameters: z.object({
|
||||
latitude: z.number().describe("The latitude of the location."),
|
||||
longitude: z.number().describe("The longitude of the location."),
|
||||
type: z.string().describe("The type of place to search for (e.g., restaurant, hotel, attraction)."),
|
||||
radius: z.number().default(3000).describe("The radius of the search area in meters (max 50000, default 3000)."),
|
||||
}),
|
||||
execute: async ({ latitude, longitude, type, radius }: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
type: string;
|
||||
radius: number;
|
||||
}) => {
|
||||
const apiKey = process.env.TRIPADVISOR_API_KEY;
|
||||
console.log("Latitude:", latitude);
|
||||
console.log("Longitude:", longitude);
|
||||
console.log("Type:", type);
|
||||
console.log("Radius:", radius);
|
||||
const response = await fetch(
|
||||
`https://api.content.tripadvisor.com/api/v1/location/nearby_search?latLong=${latitude},${longitude}&category=${type}&radius=${radius}&language=en&key=${apiKey}`
|
||||
);
|
||||
|
||||
// check for error
|
||||
if (!response.ok) {
|
||||
console.log(response);
|
||||
throw new Error(`HTTP error! status: ${response.status} ${response}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
results: data.data.map((place: any) => ({
|
||||
name: place.name,
|
||||
location: {
|
||||
lat: parseFloat(place.latitude),
|
||||
lng: parseFloat(place.longitude)
|
||||
},
|
||||
place_id: place.location_id,
|
||||
vicinity: place.address_obj.address_string,
|
||||
distance: place.distance,
|
||||
bearing: place.bearing,
|
||||
type: type
|
||||
})),
|
||||
center: { lat: latitude, lng: longitude }
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
},
|
||||
toolChoice: "auto",
|
||||
onChunk(event) {
|
||||
console.log("Call Type: ", event.chunk.type);
|
||||
},
|
||||
});
|
||||
|
||||
return result.toDataStreamResponse();
|
||||
|
||||
@ -103,7 +103,7 @@ import {
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
import FormComponent from '@/components/ui/form-component';
|
||||
import WeatherChart from '@/components/weather-chart';
|
||||
import { MapComponent, MapContainer, MapSkeleton, PlaceDetails } from '@/components/map-components';
|
||||
import { MapComponent, MapContainer, MapSkeleton, MapView, Place, PlaceDetails } from '@/components/map-components';
|
||||
import InteractiveChart from '@/components/interactive-charts';
|
||||
|
||||
export const maxDuration = 60;
|
||||
@ -271,6 +271,7 @@ const HomeContent = () => {
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [viewMode, setViewMode] = useState<'map' | 'list'>('map');
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
@ -554,7 +555,6 @@ GPT-4o has been re-enabled! You can use it by selecting the model from the dropd
|
||||
|
||||
|
||||
|
||||
|
||||
interface TableData {
|
||||
title: string;
|
||||
content: string;
|
||||
@ -667,7 +667,6 @@ GPT-4o has been re-enabled! You can use it by selecting the model from the dropd
|
||||
]}
|
||||
zoom={15}
|
||||
/>
|
||||
<PlaceDetails place={place} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -675,20 +674,81 @@ GPT-4o has been re-enabled! You can use it by selecting the model from the dropd
|
||||
if (toolInvocation.toolName === 'nearby_search') {
|
||||
if (!result) {
|
||||
return (
|
||||
<div key={index}>
|
||||
<MapSkeleton />
|
||||
<p>Loading nearby places...</p>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-5 w-5 text-neutral-700 dark:text-neutral-300 animate-pulse" />
|
||||
<span className="text-neutral-700 dark:text-neutral-300 text-lg">
|
||||
Finding nearby {args.type}s...
|
||||
</span>
|
||||
</div>
|
||||
<motion.div className="flex space-x-1">
|
||||
{[0, 1, 2].map((index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="w-2 h-2 bg-neutral-400 dark:bg-neutral-600 rounded-full"
|
||||
initial={{ opacity: 0.3 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
duration: 0.8,
|
||||
delay: index * 0.2,
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<MapComponent
|
||||
<div key={index} className="my-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Nearby {args.type}s
|
||||
</h2>
|
||||
<Badge variant="secondary" className="bg-neutral-200 dark:bg-neutral-700">
|
||||
Found {result.results.length} places
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<MapView
|
||||
center={result.center}
|
||||
places={result.results}
|
||||
zoom={14}
|
||||
view={viewMode}
|
||||
onViewChange={(newView) => setViewMode(newView)}
|
||||
/>
|
||||
|
||||
{viewMode === 'list' && (
|
||||
<div className="mt-4 space-y-4">
|
||||
{result.results.map((place: Place, placeIndex: number) => (
|
||||
<PlaceDetails
|
||||
key={placeIndex}
|
||||
{...place}
|
||||
onDirectionsClick={() => {
|
||||
const url = `https://www.google.com/maps/dir/?api=1&destination=${place.location.lat},${place.location.lng}`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
onWebsiteClick={() => {
|
||||
if (place.place_id) {
|
||||
window.open(`https://www.tripadvisor.com/Attraction_Review-g-d${place.place_id}`, '_blank');
|
||||
} else {
|
||||
toast.info("Website not available");
|
||||
}
|
||||
}}
|
||||
onCallClick={() => {
|
||||
if (place.phone) {
|
||||
window.open(`tel:${place.phone}`);
|
||||
} else {
|
||||
toast.info("Phone number not available");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Star } from 'lucide-react';
|
||||
import { Star, MapPin, Globe, Phone } from 'lucide-react';
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
|
||||
|
||||
@ -12,12 +15,19 @@ interface Location {
|
||||
lng: number;
|
||||
}
|
||||
|
||||
interface Place {
|
||||
export interface Place {
|
||||
name: string;
|
||||
location: Location;
|
||||
vicinity?: string;
|
||||
rating?: number;
|
||||
user_ratings_total?: number;
|
||||
place_id?: string; // TripAdvisor location_id
|
||||
distance?: string; // Distance from search center
|
||||
bearing?: string; // Direction from search center (e.g., "north", "southeast")
|
||||
type?: string; // Type of place (e.g., "restaurant", "hotel")
|
||||
phone?: string; // Phone number if available
|
||||
website?: string; // Website URL if available
|
||||
photos?: string[]; // Array of photo URLs
|
||||
}
|
||||
|
||||
interface MapProps {
|
||||
@ -26,7 +36,7 @@ interface MapProps {
|
||||
zoom?: number;
|
||||
}
|
||||
|
||||
const MapComponent = ({ center, places = [], zoom = 14 }: MapProps) => {
|
||||
const MapComponent = ({ center, places = [], zoom = 14, onMarkerClick }: MapProps & { onMarkerClick?: (place: Place) => void }) => {
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstance = useRef<mapboxgl.Map | null>(null);
|
||||
const markersRef = useRef<mapboxgl.Marker[]>([]);
|
||||
@ -83,9 +93,15 @@ const MapComponent = ({ center, places = [], zoom = 14 }: MapProps) => {
|
||||
)
|
||||
.addTo(mapInstance.current!);
|
||||
|
||||
marker.getElement().addEventListener('click', () => {
|
||||
if (onMarkerClick) {
|
||||
onMarkerClick(place);
|
||||
}
|
||||
});
|
||||
|
||||
markersRef.current.push(marker);
|
||||
});
|
||||
}, [places]);
|
||||
}, [places, onMarkerClick]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-64 rounded-lg overflow-hidden shadow-lg mt-6">
|
||||
@ -107,22 +123,67 @@ const MapSkeleton = () => (
|
||||
<Skeleton className="w-full h-64 bg-neutral-200 dark:bg-neutral-700" />
|
||||
);
|
||||
|
||||
const PlaceDetails = ({ place }: { place: Place }) => (
|
||||
<div className="flex justify-between items-start py-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{place.name}</h2>
|
||||
{place.vicinity && <p className="text-sm text-gray-600">{place.vicinity}</p>}
|
||||
interface PlaceDetailsProps extends Place {
|
||||
onDirectionsClick?: () => void;
|
||||
onWebsiteClick?: () => void;
|
||||
onCallClick?: () => void;
|
||||
}
|
||||
|
||||
const PlaceDetails = ({
|
||||
name,
|
||||
vicinity,
|
||||
rating,
|
||||
user_ratings_total,
|
||||
onDirectionsClick,
|
||||
onWebsiteClick,
|
||||
onCallClick
|
||||
}: PlaceDetailsProps) => (
|
||||
<Card className="w-full bg-white dark:bg-neutral-800 shadow-lg">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{name}</h3>
|
||||
{vicinity && <p className="text-sm text-neutral-500 dark:text-neutral-400">{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"
|
||||
>
|
||||
<Star className="h-3 w-3 mr-1 text-yellow-400" />
|
||||
{place.rating} ({place.user_ratings_total})
|
||||
{rating && (
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
<Star className="h-3 w-3 text-yellow-400" />
|
||||
<span>{rating}</span>
|
||||
{user_ratings_total && <span className="text-xs">({user_ratings_total})</span>}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onDirectionsClick}
|
||||
className="flex-1"
|
||||
>
|
||||
<MapPin className="h-4 w-4 mr-2" />
|
||||
Directions
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onWebsiteClick}
|
||||
className="flex-1"
|
||||
>
|
||||
<Globe className="h-4 w-4 mr-2" />
|
||||
Website
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCallClick}
|
||||
className="flex-1"
|
||||
>
|
||||
<Phone className="h-4 w-4 mr-2" />
|
||||
Call
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
interface MapContainerProps {
|
||||
@ -152,10 +213,63 @@ const MapContainer: React.FC<MapContainerProps> = ({
|
||||
<h2 className="text-xl font-semibold mb-2">{title}</h2>
|
||||
<MapComponent center={center} places={places} />
|
||||
{places.map((place, index) => (
|
||||
<PlaceDetails key={index} place={place} />
|
||||
<PlaceDetails key={index} {...place} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { MapComponent, MapSkeleton, MapContainer, PlaceDetails };
|
||||
interface MapViewProps extends MapProps {
|
||||
view: 'map' | 'list';
|
||||
onViewChange: (view: 'map' | 'list') => void;
|
||||
}
|
||||
|
||||
const MapView: React.FC<MapViewProps> = ({
|
||||
center,
|
||||
places = [],
|
||||
zoom = 14,
|
||||
view,
|
||||
onViewChange
|
||||
}) => {
|
||||
const [selectedPlace, setSelectedPlace] = useState<Place | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Nearby Places
|
||||
</h2>
|
||||
<Tabs value={view} onValueChange={(v) => onViewChange(v as 'map' | 'list')}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="map">Map</TabsTrigger>
|
||||
<TabsTrigger value="list">List</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{view === 'map' ? (
|
||||
<div className="relative">
|
||||
<MapComponent
|
||||
center={center}
|
||||
places={places}
|
||||
zoom={zoom}
|
||||
onMarkerClick={setSelectedPlace}
|
||||
/>
|
||||
{selectedPlace && (
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<PlaceDetails {...selectedPlace} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{places.map((place, index) => (
|
||||
<PlaceDetails key={index} {...place} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { MapComponent, MapSkeleton, MapContainer, PlaceDetails, MapView };
|
||||
Loading…
Reference in New Issue
Block a user