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 86c90c5..d4c643e 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -68,7 +68,10 @@ import { Book, Eye, ExternalLink, - Building + Building, + Users, + Brain, + TrendingUp } from 'lucide-react'; import { HoverCard, @@ -124,6 +127,7 @@ 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'; export const maxDuration = 60; @@ -585,6 +589,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 +629,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 +1139,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 +1776,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> @@ -2042,15 +2058,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 +2100,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 +2150,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 +2258,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 +2313,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>