diff --git a/app/actions.ts b/app/actions.ts index 6d2ff41..5600e1e 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -108,7 +108,7 @@ const groupTools = { 'get_weather_data', 'find_place', 'programming', 'web_search', 'text_translate', 'nearby_search', 'x_search', 'youtube_search', 'shopping_search', - 'academic_search' + 'academic_search', 'track_flight' ] as const, academic: ['academic_search', 'programming'] as const, shopping: ['shopping_search', 'programming'] as const, @@ -133,7 +133,7 @@ Always put citations at the end of each paragraph and in the end of sentences wh Here are the tools available to you: -web_search, retrieve, get_weather_data, programming, text_translate, find_place +web_search, retrieve, get_weather_data, programming, text_translate, find_place, track_flight ## Basic Guidelines: @@ -155,6 +155,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 in the code. This will help in running the code successfully. Always remember to install the libraries using !pip install 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 tracking a flight, use the track_flight tool. Provide the flight number in the parameters, 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. @@ -187,6 +188,10 @@ Follow the format and guidelines for each tool and provide the response accordin - 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. - Never call find_place tool before or after the nearby_search tool in the same response at all costs!! THIS IS NOT ALLOWED AT ALL COSTS!!! +## Flight Tracking Tool Guidelines: +- For queries related to flight tracking, always use the track_flight tool to find information about the flight. Provide the flight number in the parameters, then compose your response based on the information gathered. +- Calling track_flight tool in the same response is allowed, but do not call the same tool in a response at all costs!! + ## Programming Tool Guidelines: The programming tool is actually a Python-only Code interpreter, so you can run any Python code in it. - This tool should not be called more than once in a response. diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 00dda1c..ff75f80 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -129,7 +129,7 @@ Always put citations at the end of each paragraph and in the end of sentences wh Here are the tools available to you: -web_search, retrieve, get_weather_data, programming, text_translate, find_place +web_search, retrieve, get_weather_data, programming, text_translate, find_place, track_flight ## Basic Guidelines: @@ -1141,7 +1141,24 @@ export async function POST(req: Request) { throw error; } }, - }) + }), + track_flight: tool({ + description: "Track flight information and status", + parameters: z.object({ + flight_number: z.string().describe("The flight number to track"), + }), + execute: async ({ flight_number }: { flight_number: string }) => { + try { + const response = await fetch( + `https://api.aviationstack.com/v1/flights?access_key=${process.env.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}` + ); + return await response.json(); + } catch (error) { + console.error('Flight tracking error:', error); + throw error; + } + }, + }), }, toolChoice: "auto", onChunk(event) { diff --git a/app/api/cohere/route.ts b/app/api/cohere/route.ts index b456a6e..f2e93cf 100644 --- a/app/api/cohere/route.ts +++ b/app/api/cohere/route.ts @@ -396,6 +396,23 @@ Remember to always run the appropriate tool(s) first and compose your response b return data; }, }), + track_flight: tool({ + description: "Track flight information and status", + parameters: z.object({ + flight_number: z.string().describe("The flight number to track"), + }), + execute: async ({ flight_number }: { flight_number: string }) => { + try { + const response = await fetch( + `https://api.aviationstack.com/v1/flights?access_key=${process.env.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}` + ); + return await response.json(); + } catch (error) { + console.error('Flight tracking error:', error); + throw error; + } + }, + }), }, toolChoice: "auto", }); diff --git a/app/api/trending/route.ts b/app/api/trending/route.ts new file mode 100644 index 0000000..561285d --- /dev/null +++ b/app/api/trending/route.ts @@ -0,0 +1,121 @@ +import { NextResponse } from 'next/server'; + +export interface TrendingQuery { + icon: string; + text: string; + category: string; +} + +interface RedditPost { + data: { + title: string; + }; +} + +async function fetchGoogleTrends(): Promise { + const fetchTrends = async (geo: string): Promise => { + try { + const response = await fetch(`https://trends.google.com/trends/trendingsearches/daily/rss?geo=${geo}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch from Google Trends RSS for geo: ${geo}`); + } + + const xmlText = await response.text(); + const items = xmlText.match(/(?!Daily Search Trends)(.*?)<\/title>/g) || []; + + return items.map(item => ({ + icon: 'trending', + text: item.replace(/<\/?title>/g, ''), + category: 'trending' // TODO: add category based on the query results + })); + } catch (error) { + console.error(`Failed to fetch Google Trends for geo: ${geo}`, error); + return []; + } + }; + + const trendsIN = await fetchTrends('IN'); + const trendsUS = await fetchTrends('US'); + + return [...trendsIN, ...trendsUS]; +} + +async function fetchRedditQuestions(): Promise<TrendingQuery[]> { + try { + const response = await fetch( + 'https://www.reddit.com/r/askreddit/hot.json?limit=100', + { + headers: { + 'User-Agent': 'MiniPerplx/1.0' + } + } + ); + + const data = await response.json(); + const maxLength = 50; + + return data.data.children + .map((post: RedditPost) => ({ + icon: 'question', + text: post.data.title, + category: 'community' + })) + .filter((query: TrendingQuery) => query.text.length <= maxLength) + .slice(0, 15); + } catch (error) { + console.error('Failed to fetch Reddit questions:', error); + return []; + } +} + +async function fetchFromMultipleSources() { + const [googleTrends, + // redditQuestions +] = await Promise.all([ + fetchGoogleTrends(), + // fetchRedditQuestions(), + ]); + + const allQueries = [...googleTrends, + // ...redditQuestions +]; + return allQueries + .sort(() => Math.random() - 0.5); +} + +export async function GET() { + try { + const trends = await fetchFromMultipleSources(); + + if (trends.length === 0) { + // Fallback queries if both sources fail + return NextResponse.json([ + { + icon: 'sparkles', + text: "What causes the Northern Lights?", + category: 'science' + }, + { + icon: 'code', + text: "Explain quantum computing", + category: 'tech' + }, + { + icon: 'globe', + text: "Most beautiful places in Japan", + category: 'travel' + } + ]); + } + + return NextResponse.json(trends); + } catch (error) { + console.error('Failed to fetch trends:', error); + return NextResponse.error(); + } +} \ No newline at end of file diff --git a/app/search/page.tsx b/app/search/page.tsx index 2251dce..a640b13 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -68,7 +68,11 @@ import { Book, Eye, ExternalLink, - Building + Building, + Users, + Brain, + TrendingUp, + Plane } from 'lucide-react'; import { HoverCard, @@ -124,6 +128,8 @@ import NearbySearchMapView from '@/components/nearby-search-map-view'; import { Place } from '../../components/map-components'; import { Separator } from '@/components/ui/separator'; import { ChartTypes } from '@e2b/code-interpreter'; +import { TrendingQuery } from '../api/trending/route'; +import { FlightTracker } from '@/components/flight-tracker'; export const maxDuration = 60; @@ -585,6 +591,8 @@ const HomeContent = () => { const [openChangelog, setOpenChangelog] = useState(false); + const [trendingQueries, setTrendingQueries] = useState<TrendingQuery[]>([]); + const { isLoading, input, messages, setInput, append, handleSubmit, setMessages, reload, stop } = useChat({ maxSteps: 10, body: { @@ -623,6 +631,21 @@ const HomeContent = () => { } }, [initialState.query, append, setInput, messages.length]); + useEffect(() => { + const fetchTrending = async () => { + try { + const res = await fetch('/api/trending'); + if (!res.ok) throw new Error('Failed to fetch trending queries'); + const data = await res.json(); + setTrendingQueries(data); + } catch (error) { + console.error('Error fetching trending queries:', error); + setTrendingQueries([]); + } + }; + + fetchTrending(); + }, []); const ThemeToggle: React.FC = () => { const { theme, setTheme } = useTheme(); @@ -1118,7 +1141,7 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab </div> </CardHeader> <CardContent className="p-0 mt-1"> - <div className="flex overflow-x-auto pb-4 gap-4 px-4 no-scrollbar snap-x snap-mandatory"> + <div className="flex overflow-x-auto pb-3 gap-2 px-4 no-scrollbar snap-x snap-mandatory"> {result.map((product: ShoppingProduct) => ( <motion.div key={product.url} @@ -1755,12 +1778,7 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab 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", - }} + transition={{ duration: 0.8, delay: index * 0.2, repeatType: "reverse" }} /> ))} </div> @@ -1819,6 +1837,49 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab return <ResultsOverview result={result} />; } + if (toolInvocation.toolName === 'track_flight') { + if (!result) { + return ( + <div className="flex items-center justify-between w-full"> + <div className="flex items-center gap-2"> + <Plane 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">Tracking flight...</span> + </div> + <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", + }} + /> + ))} + </div> + </div> + ); + } + + if (result.error) { + return ( + <div className="text-red-500 dark:text-red-400"> + Error tracking flight: {result.error} + </div> + ); + } + + return ( + <div className="my-4"> + <FlightTracker data={result} /> + </div> + ); + } + return null; }, [ResultsOverview, theme] @@ -2042,15 +2103,12 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab return () => window.removeEventListener('scroll', handleScroll); }, [messages, suggestedQuestions]); - const handleExampleClick = async (card: typeof suggestionCards[number]) => { + const handleExampleClick = async (card: TrendingQuery) => { const exampleText = card.text; track("search example", { query: exampleText }); lastSubmittedQueryRef.current = exampleText; setHasSubmitted(true); setSuggestedQuestions([]); - console.log('exampleText', exampleText); - console.log('lastSubmittedQuery', lastSubmittedQueryRef.current); - await append({ content: exampleText.trim(), role: 'user', @@ -2087,21 +2145,6 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab } }, [input, messages, editingMessageIndex, setMessages, handleSubmit]); - const suggestionCards = [ - { - icon: <User2 className="w-5 h-5 text-neutral-400 dark:text-neutral-500" />, - text: "Shah Rukh Khan", - }, - { - icon: <Sun className="w-5 h-5 text-neutral-400 dark:text-neutral-500" />, - text: "Weather in Doha", - }, - { - icon: <Terminal className="w-5 h-5 text-neutral-400 dark:text-neutral-500" />, - text: "Count the no. of r's in strawberry?", - }, - ]; - interface NavbarProps { } const Navbar: React.FC<NavbarProps> = () => { @@ -2152,20 +2195,92 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab ); }; - const SuggestionCards: React.FC<{ selectedModel: string }> = ({ selectedModel }) => { - return ( - <div className="flex gap-3 mt-4"> - <div className="flex flex-grow sm:flex-row sm:mx-auto w-full gap-2 sm:gap-[21px]"> - {suggestionCards.map((card, index) => ( - <button + const SuggestionCards: React.FC<{ + selectedModel: string; + trendingQueries: TrendingQuery[]; + }> = ({ selectedModel, trendingQueries }) => { + const [isLoading, setIsLoading] = useState(true); + const scrollRef = useRef<HTMLDivElement>(null); + const [isPaused, setIsPaused] = useState(false); + const scrollIntervalRef = useRef<NodeJS.Timeout>(); + + useEffect(() => { + setIsLoading(false); + }, [trendingQueries]); + + useEffect(() => { + const startScrolling = () => { + if (!scrollRef.current || isPaused) return; + scrollRef.current.scrollLeft += 2; + }; + + scrollIntervalRef.current = setInterval(startScrolling, 20); + + return () => { + if (scrollIntervalRef.current) { + clearInterval(scrollIntervalRef.current); + } + }; + }, [isPaused]); + + const getCardWidth = (text: string) => { + const charWidth = 8; + const padding = 32; + const iconWidth = 28; + return Math.min( + padding + iconWidth + (text.length * charWidth), + 400 + ); + }; + + if (isLoading || trendingQueries.length === 0) { + return ( + <div className="flex gap-2 mt-4"> + {[1, 2, 3].map((_, index) => ( + <div key={index} - onClick={() => handleExampleClick(card)} - className="bg-neutral-100 dark:bg-neutral-800 rounded-xl p-2 sm:p-4 text-left hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors duration-200" + className="flex-shrink-0 w-[200px] bg-neutral-100 dark:bg-neutral-800 rounded-xl p-4 animate-pulse" > - <div className="flex items-center space-x-2 text-neutral-700 dark:text-neutral-300"> - <span>{card.icon}</span> - <span className="text-xs sm:text-sm font-medium"> - {card.text} + <div className="flex items-center space-x-2"> + <div className="w-5 h-5 bg-neutral-200 dark:bg-neutral-700 rounded-full" /> + <div className="h-4 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" /> + </div> + </div> + ))} + </div> + ); + } + + const getIconForCategory = (category: string) => { + const iconMap = { + trending: <TrendingUp className="w-5 h-5" />, + community: <Users className="w-5 h-5" />, + science: <Brain className="w-5 h-5" />, + tech: <Code className="w-5 h-5" />, + travel: <Globe className="w-5 h-5" />, + }; + return iconMap[category as keyof typeof iconMap] || <Sparkles className="w-5 h-5" />; + }; + + return ( + <div className="relative"> + <div + ref={scrollRef} + className="flex gap-2 mt-4 overflow-x-auto pb-3 relative scroll-smooth no-scrollbar" + onMouseEnter={() => setIsPaused(true)} + onMouseLeave={() => setIsPaused(false)} + > + {Array(20).fill(trendingQueries).flat().map((query, index) => ( + <button + key={`${index}-${query.text}`} + onClick={() => handleExampleClick(query)} + className="flex-shrink-0 bg-neutral-100 dark:bg-neutral-800 rounded-xl p-3 text-left hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors duration-200" + style={{ width: `${getCardWidth(query.text)}px` }} + > + <div className="flex items-center gap-2 text-neutral-700 dark:text-neutral-300"> + <span className="flex-shrink-0">{getIconForCategory(query.category)}</span> + <span className="text-sm font-medium whitespace-nowrap pr-1"> + {query.text} </span> </div> </button> @@ -2188,6 +2303,13 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab // const memoizedMessages = useMemo(() => messages, [messages]); + const memoizedSuggestionCards = useMemo(() => ( + <SuggestionCards + selectedModel={selectedModel} + trendingQueries={trendingQueries} + /> + ), [selectedModel, trendingQueries]); + return ( <div className="flex flex-col font-sans items-center justify-center p-2 sm:p-4 bg-background text-foreground transition-all duration-500"> <Navbar /> @@ -2236,7 +2358,7 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab selectedGroup={selectedGroup} setSelectedGroup={setSelectedGroup} /> - <SuggestionCards selectedModel={selectedModel} /> + {memoizedSuggestionCards} </motion.div> )} </AnimatePresence> diff --git a/components/flight-tracker.tsx b/components/flight-tracker.tsx new file mode 100644 index 0000000..ef8e60e --- /dev/null +++ b/components/flight-tracker.tsx @@ -0,0 +1,198 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Plane, Clock, Terminal } from "lucide-react"; +import { motion } from "framer-motion"; + +interface FlightApiResponse { + data: Array<{ + flight_date: string; + flight_status: string; + departure: { + airport: string; + timezone: string; + iata: string; + terminal: string | null; + gate: string | null; + delay: number | null; + scheduled: string; + }; + arrival: { + airport: string; + timezone: string; + iata: string; + terminal: string | null; + gate: string | null; + delay: number | null; + scheduled: string; + }; + airline: { + name: string; + iata: string; + }; + flight: { + number: string; + iata: string; + duration: number | null; + }; + }>; +} + +interface FlightTrackerProps { + data: FlightApiResponse; +} + +export function FlightTracker({ data }: FlightTrackerProps) { + if (!data?.data?.[0]) { + return null; + } + + const flight = data.data[0]; + + const formatTime = (timestamp: string) => { + const date = new Date(timestamp); + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: 'UTC' + }) + ' UTC'; + }; + + const formatDate = (timestamp: string) => { + return new Date(timestamp).toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + }; + + const mapStatus = (status: string): "LANDED" | "DEPARTING ON TIME" | "DELAYED" | "SCHEDULED" => { + switch (status.toLowerCase()) { + case 'landed': + return 'LANDED'; + case 'active': + return flight.departure.delay ? 'DELAYED' : 'DEPARTING ON TIME'; + case 'scheduled': + return 'SCHEDULED'; + default: + return 'SCHEDULED'; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "LANDED": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; + case "DEPARTING ON TIME": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; + case "DELAYED": + return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; + default: + return "bg-neutral-100 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"; + } + }; + + const flightInfo = { + flightNumber: flight.flight.iata, + status: mapStatus(flight.flight_status), + departure: { + airport: flight.departure.airport, + code: flight.departure.iata, + time: formatTime(flight.departure.scheduled), + date: formatDate(flight.departure.scheduled), + terminal: flight.departure.terminal || undefined, + gate: flight.departure.gate || undefined + }, + arrival: { + airport: flight.arrival.airport, + code: flight.arrival.iata, + time: formatTime(flight.arrival.scheduled), + date: formatDate(flight.arrival.scheduled), + terminal: flight.arrival.terminal || undefined, + gate: flight.arrival.gate || undefined + }, + duration: flight.flight.duration ? `${flight.flight.duration} minutes` : 'N/A', + lastUpdated: new Date().toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true + }) + }; + + return ( + <Card className="w-full max-w-2xl bg-card dark:bg-card"> + <CardContent className="p-6"> + <div className="flex justify-between items-center mb-6"> + <div> + <h2 className="text-2xl font-bold">{flightInfo.flightNumber}</h2> + <Badge className={getStatusColor(flightInfo.status)}> + {flightInfo.status} + </Badge> + </div> + </div> + + <div className="relative"> + <div className="flex justify-between items-center"> + <div className="text-4xl font-mono">{flightInfo.departure.code}</div> + <motion.div + className="text-primary" + initial={{ x: -50 }} + animate={{ x: 50 }} + transition={{ duration: 2, repeat: Infinity, ease: "linear" }} + > + <Plane className="h-6 w-6" /> + </motion.div> + <div className="text-4xl font-mono">{flightInfo.arrival.code}</div> + </div> + <div className="mt-8 grid grid-cols-2 gap-8"> + <div> + <p className="text-lg font-medium">{flightInfo.departure.airport}</p> + <p className="text-3xl font-bold mt-1">{flightInfo.departure.time}</p> + <p className="text-sm text-muted-foreground">{flightInfo.departure.date}</p> + {(flightInfo.departure.terminal || flightInfo.departure.gate) && ( + <div className="mt-2 flex items-center gap-4"> + {flightInfo.departure.terminal && ( + <div className="flex items-center gap-1"> + <Terminal className="h-4 w-4" /> + <span>Terminal {flightInfo.departure.terminal}</span> + </div> + )} + {flightInfo.departure.gate && ( + <div>Gate {flightInfo.departure.gate}</div> + )} + </div> + )} + </div> + + <div> + <p className="text-lg font-medium">{flightInfo.arrival.airport}</p> + <p className="text-3xl font-bold mt-1">{flightInfo.arrival.time}</p> + <p className="text-sm text-muted-foreground">{flightInfo.arrival.date}</p> + {(flightInfo.arrival.terminal || flightInfo.arrival.gate) && ( + <div className="mt-2 flex items-center gap-4"> + {flightInfo.arrival.terminal && ( + <div className="flex items-center gap-1"> + <Terminal className="h-4 w-4" /> + <span>Terminal {flightInfo.arrival.terminal}</span> + </div> + )} + {flightInfo.arrival.gate && ( + <div>Gate {flightInfo.arrival.gate}</div> + )} + </div> + )} + </div> + </div> + + <div className="mt-6 flex items-center gap-2 text-sm text-muted-foreground"> + <Clock className="h-4 w-4" /> + <span>Duration: {flightInfo.duration}</span> + <span className="mx-2">•</span> + <span>Last updated: {flightInfo.lastUpdated}</span> + </div> + </div> + </CardContent> + </Card> + ); +} \ No newline at end of file