feat: fetch & memoize popular queries and display them as suggestions to the user instead of static cards
This commit is contained in:
parent
00dc2c11bb
commit
7ce1cb394a
121
app/api/trending/route.ts
Normal file
121
app/api/trending/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user