feat: fetch & memoize popular queries and display them as suggestions to the user instead of static cards

This commit is contained in:
Waleed Latif 2024-12-18 15:09:32 -08:00
parent 00dc2c11bb
commit 7ce1cb394a
2 changed files with 238 additions and 40 deletions

121
app/api/trending/route.ts Normal file
View File

@ -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<TrendingQuery[]> {
const fetchTrends = async (geo: string): Promise<TrendingQuery[]> => {
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(/<title>(?!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();
}
}

View File

@ -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 }) => {
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-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
<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>