/* eslint-disable @next/next/no-img-element */ "use client"; import React, { useRef, useCallback, useState, useEffect, useMemo, memo } from 'react'; import ReactMarkdown, { Components } from 'react-markdown'; import { useRouter } from 'next/navigation'; import remarkGfm from 'remark-gfm'; import { useChat } from 'ai/react'; import { ToolInvocation } from 'ai'; import { toast } from 'sonner'; import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; import { suggestQuestions, Message } from './actions'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { SearchIcon, Sparkles, ArrowRight, Globe, AlignLeft, Newspaper, Copy, Cloud, Code, Check, Loader2, User2, Edit2, Heart, X, MapPin, Star, Plus, Terminal, ImageIcon, Download, } from 'lucide-react'; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Line, LineChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; import { GitHubLogoIcon } from '@radix-ui/react-icons'; import { Skeleton } from '@/components/ui/skeleton'; import Link from 'next/link'; import { cn } from '@/lib/utils'; export const maxDuration = 60; declare global { interface Window { google: any; initMap: () => void; } } export default function Home() { const router = useRouter(); const inputRef = useRef(null); const [lastSubmittedQuery, setLastSubmittedQuery] = useState(""); const [hasSubmitted, setHasSubmitted] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const bottomRef = useRef(null); const [suggestedQuestions, setSuggestedQuestions] = useState([]); const [showExamples, setShowExamples] = useState(false) const [isEditingQuery, setIsEditingQuery] = useState(false); const { isLoading, input, messages, setInput, append, handleSubmit, setMessages } = useChat({ api: '/api/chat', maxToolRoundtrips: 1, onFinish: async (message, { finishReason }) => { if (finishReason === 'stop') { const newHistory: Message[] = [{ role: "user", content: lastSubmittedQuery, }, { role: "assistant", content: message.content }]; const { questions } = await suggestQuestions(newHistory); setSuggestedQuestions(questions); } setIsAnimating(false); }, onError: (error) => { console.error("Chat error:", error); toast.error("An error occurred.", { description: "We must have ran out of credits. Sponsor us on GitHub to keep this service running.", action: { label: "Sponsor", onClick: () => window.open("https://git.new/mplx", "_blank"), }, }); }, }); const CopyButton = ({ text }: { text: string }) => { const [isCopied, setIsCopied] = useState(false); return ( ); }; // Weather chart components interface WeatherDataPoint { date: string; minTemp: number; maxTemp: number; } const WeatherChart: React.FC<{ result: any }> = React.memo(({ result }) => { const { chartData, minTemp, maxTemp } = useMemo(() => { const weatherData: WeatherDataPoint[] = result.list.map((item: any) => ({ date: new Date(item.dt * 1000).toLocaleDateString(), minTemp: Number((item.main.temp_min - 273.15).toFixed(1)), maxTemp: Number((item.main.temp_max - 273.15).toFixed(1)), })); // Group data by date and calculate min and max temperatures const groupedData: { [key: string]: WeatherDataPoint } = weatherData.reduce((acc, curr) => { if (!acc[curr.date]) { acc[curr.date] = { ...curr }; } else { acc[curr.date].minTemp = Math.min(acc[curr.date].minTemp, curr.minTemp); acc[curr.date].maxTemp = Math.max(acc[curr.date].maxTemp, curr.maxTemp); } return acc; }, {} as { [key: string]: WeatherDataPoint }); const chartData = Object.values(groupedData); // Calculate overall min and max temperatures const minTemp = Math.min(...chartData.map(d => d.minTemp)); const maxTemp = Math.max(...chartData.map(d => d.maxTemp)); return { chartData, minTemp, maxTemp }; }, [result]); const chartConfig: ChartConfig = useMemo(() => ({ minTemp: { label: "Min Temp.", color: "hsl(var(--chart-1))", }, maxTemp: { label: "Max Temp.", color: "hsl(var(--chart-2))", }, }), []); return ( Weather Forecast for {result.city.name} Showing min and max temperatures for the next 5 days new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} /> `${value}°C`} /> } />
{result.city.name}, {result.city.country}
Next 5 days forecast
); }); WeatherChart.displayName = 'WeatherChart'; // Google Maps components const isValidCoordinate = (coord: number) => { return typeof coord === 'number' && !isNaN(coord) && isFinite(coord); }; const loadGoogleMapsScript = (callback: () => void) => { if (window.google && window.google.maps) { callback(); return; } const existingScript = document.getElementById('googleMapsScript'); if (existingScript) { existingScript.remove(); } window.initMap = callback; const script = document.createElement('script'); script.id = 'googleMapsScript'; script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places,marker&callback=initMap`; script.async = true; script.defer = true; document.head.appendChild(script); }; const MapComponent = React.memo(({ center, places }: { center: { lat: number; lng: number }, places: any[] }) => { const mapRef = useRef(null); const [mapError, setMapError] = useState(null); const googleMapRef = useRef(null); const markersRef = useRef([]); const memoizedCenter = useMemo(() => center, [center]); const memoizedPlaces = useMemo(() => places, [places]); const initializeMap = useCallback(async () => { if (mapRef.current && isValidCoordinate(memoizedCenter.lat) && isValidCoordinate(memoizedCenter.lng)) { const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary; const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; if (!googleMapRef.current) { googleMapRef.current = new Map(mapRef.current, { center: memoizedCenter, zoom: 14, mapId: "347ff92e0c7225cf", }); } else { googleMapRef.current.setCenter(memoizedCenter); } // Clear existing markers markersRef.current.forEach(marker => marker.map = null); markersRef.current = []; memoizedPlaces.forEach((place) => { if (isValidCoordinate(place.location.lat) && isValidCoordinate(place.location.lng)) { const marker = new AdvancedMarkerElement({ map: googleMapRef.current, position: place.location, title: place.name, }); markersRef.current.push(marker); } }); } else { setMapError('Invalid coordinates provided'); } }, [memoizedCenter, memoizedPlaces]); useEffect(() => { loadGoogleMapsScript(() => { try { initializeMap(); } catch (error) { console.error('Error initializing map:', error); setMapError('Failed to initialize Google Maps'); } }); return () => { // Clean up markers when component unmounts markersRef.current.forEach(marker => marker.map = null); }; }, [initializeMap]); if (mapError) { return
{mapError}
; } return
; }); MapComponent.displayName = 'MapComponent'; const MapSkeleton = () => ( ); const PlaceDetails = ({ place }: { place: any }) => (

{place.name}

{place.vicinity}

{place.rating && ( {place.rating} ({place.user_ratings_total}) )}
); const MapEmbed = memo(({ location, zoom = 15 }: { location: string, zoom?: number }) => { const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(location)}&zoom=${zoom}`; return (
); }); MapEmbed.displayName = 'MapEmbed'; const FindPlaceResult = memo(({ result }: { result: any }) => { const place = result.candidates[0]; const location = `${place.geometry.location.lat},${place.geometry.location.lng}`; return ( {place.name}

Address: {place.formatted_address}

{place.rating && (
Rating: {place.rating}
)} {place.opening_hours && (

Open now: {place.opening_hours.open_now ? 'Yes' : 'No'}

)}
); }); FindPlaceResult.displayName = 'FindPlaceResult'; const TextSearchResult = memo(({ result }: { result: any }) => { const centerLocation = result.results[0]?.geometry?.location; const mapLocation = centerLocation ? `${centerLocation.lat},${centerLocation.lng}` : ''; return ( Text Search Results {mapLocation && } Place Details
{result.results.map((place: any, index: number) => (

{place.name}

{place.formatted_address}

{place.rating && ( {place.rating} ({place.user_ratings_total}) )}
))}
); }); TextSearchResult.displayName = 'TextSearchResult'; const renderToolInvocation = (toolInvocation: ToolInvocation, index: number) => { const args = JSON.parse(JSON.stringify(toolInvocation.args)); const result = 'result' in toolInvocation ? JSON.parse(JSON.stringify(toolInvocation.result)) : null; if (toolInvocation.toolName === 'nearby_search') { if (!result) { return (
Searching nearby places...
{[0, 1, 2].map((index) => ( ))}
); } if (isLoading) { return ( ); } return ( Nearby {args.type ? args.type.charAt(0).toUpperCase() + args.type.slice(1) + 's' : 'Places'} {args.keyword && {args.keyword}} Place Details
{result.results.map((place: any, placeIndex: number) => ( ))}
); } if (toolInvocation.toolName === 'find_place') { if (!result) { return (
Finding place...
{[0, 1, 2].map((index) => ( ))}
); } return ; } if (toolInvocation.toolName === 'text_search') { if (!result) { return (
Searching places...
{[0, 1, 2].map((index) => ( ))}
); } return ; } if (toolInvocation.toolName === 'get_weather_data') { if (!result) { return (
Fetching weather data...
{[0, 1, 2].map((index) => ( ))}
); } if (isLoading) { return (
); } return ; } if (toolInvocation.toolName === 'programming') { return (
Programming
Code Output {result?.images && result.images.length > 0 && ( Images )}
{args.code}
{result ? ( <>
                      {result.message}
                    
) : (
Executing code...
)}
{result?.images && result.images.length > 0 && (
{result.images.map((img: { format: 'png' | 'jpeg' | 'svg', data: string }, imgIndex: number) => (

Image {imgIndex + 1}

{`Generated
))}
)}
); } if (toolInvocation.toolName === 'nearby_search') { if (!result) { return (
Searching nearby places...
{[0, 1, 2].map((index) => ( ))}
); } const mapUrl = `https://www.google.com/maps/embed/v1/search?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&q=${encodeURIComponent(args.type)}¢er=${result.results[0].geometry.location.lat},${result.results[0].geometry.location.lng}&zoom=14`; return ( Nearby {args.type.charAt(0).toUpperCase() + args.type.slice(1)}s
{result.results.map((place: any, placeIndex: number) => (

{place.name}

{place.vicinity}

{place.rating} ★ ({place.user_ratings_total})
))}
); } return (
{!result ? (
Running a search...
{[0, 1, 2].map((index) => ( ))}
) :

Sources Found

{result && ( {result.results.length} results )}
{args?.query && ( {args.query} )} {result && (
{result.results.map((item: any, itemIndex: number) => ( Favicon {item.title}

{item.content}

))}
)}
}
); }; interface CitationComponentProps { href: string; children: React.ReactNode; index: number; } const CitationComponent: React.FC = React.memo(({ href, index }) => { const faviconUrl = `https://www.google.com/s2/favicons?sz=128&domain=${new URL(href).hostname}`; return ( {index + 1} Favicon {href} ); }); CitationComponent.displayName = "CitationComponent"; interface MarkdownRendererProps { content: string; } const MarkdownRenderer: React.FC = React.memo(({ content }) => { const citationLinks = useMemo(() => { return [...content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map(([_, text, link]) => ({ text, link, })); }, [content]); const components: Partial = useMemo(() => ({ a: ({ href, children }) => { if (!href) return null; const index = citationLinks.findIndex((link) => link.link === href); return index !== -1 ? ( {children} ) : ( {children} ); }, }), [citationLinks]); return ( {content} ); }); MarkdownRenderer.displayName = "MarkdownRenderer"; useEffect(() => { if (bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: "smooth" }); } }, [messages, suggestedQuestions]); const handleExampleClick = useCallback(async (query: string) => { setLastSubmittedQuery(query.trim()); setHasSubmitted(true); setSuggestedQuestions([]); setIsAnimating(true); await append({ content: query.trim(), role: 'user' }); }, [append]); const handleFormSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { setMessages([]); setLastSubmittedQuery(input.trim()); setHasSubmitted(true); setIsAnimating(true); setSuggestedQuestions([]); handleSubmit(e); } else { toast.error("Please enter a search query."); } }, [input, setMessages, handleSubmit]); const handleSuggestedQuestionClick = useCallback(async (question: string) => { setMessages([]); setLastSubmittedQuery(question.trim()); setHasSubmitted(true); setSuggestedQuestions([]); setIsAnimating(true); await append({ content: question.trim(), role: 'user' }); }, [append, setMessages]); const handleQueryEdit = useCallback(() => { setIsAnimating(true) setIsEditingQuery(true); setInput(lastSubmittedQuery); }, [lastSubmittedQuery, setInput]); const handleQuerySubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { setLastSubmittedQuery(input.trim()); setIsEditingQuery(false); setMessages([]); setHasSubmitted(true); setIsAnimating(true); setSuggestedQuestions([]); handleSubmit(e); } else { toast.error("Please enter a search query."); } }, [input, setMessages, handleSubmit]); const exampleQueries = [ "Weather in Doha", "What is new with Grok 2.0?", "Count the number of r's in strawberry", "Explain Claude 3.5 Sonnet" ]; const Navbar = () => (

Sponsor this project on GitHub

); return (
{!hasSubmitted && (

MiniPerplx

In search for minimalism and simplicity

MiniPerplx - A minimalistic AI-powered search engine. | Product Hunt Peerlist
)} {!hasSubmitted && (
setInput(e.target.value)} disabled={isLoading} className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" onFocus={() => setShowExamples(true)} onBlur={() => setShowExamples(false)} />
{exampleQueries.map((message, index) => ( ))}
)}
{hasSubmitted && ( setIsAnimating(false)} >
{isEditingQuery ? (
setInput(e.target.value)} className="flex-grow" />
) : (

{lastSubmittedQuery}

{lastSubmittedQuery}

)}
{!isEditingQuery && ( )}
)}
{messages.map((message, index) => (
{message.role === 'assistant' && message.content && (

Answer

)} {message.toolInvocations?.map((toolInvocation: ToolInvocation, toolIndex: number) => (
{renderToolInvocation(toolInvocation, toolIndex)}
))}
))} {suggestedQuestions.length > 0 && (

Suggested questions

{suggestedQuestions.map((question, index) => ( ))}
)}
{hasSubmitted && !isAnimating && (
setInput(e.target.value)} disabled={isLoading} className="w-full min-h-12 py-3 px-4 bg-muted border border-input rounded-full pr-12 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-200 focus-visible:ring-offset-2 text-sm sm:text-base" />
)}
); }