/* eslint-disable @next/next/no-img-element */ "use client"; import 'katex/dist/katex.min.css'; import React, { useRef, useCallback, useState, useEffect, useMemo, Suspense } from 'react'; import ReactMarkdown from 'react-markdown'; import { useTheme } from 'next-themes'; import Marked, { ReactRenderer } from 'marked-react'; import { track } from '@vercel/analytics'; import { useRouter, useSearchParams } from 'next/navigation'; import { useChat } from 'ai/react'; import { Message, ToolInvocation } from 'ai'; import { toast } from 'sonner'; import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; import { fetchMetadata, generateSpeech, suggestQuestions } from '../actions'; import { Wave } from "@foobar404/wave"; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneLight, oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { SearchIcon, Sparkles, ArrowRight, Globe, AlignLeft, Newspaper, Copy, Cloud, Code, Check, Loader2, User2, Heart, X, MapPin, Plus, Download, Flame, Sun, Terminal, Pause, Play, TrendingUpIcon, Calendar, Calculator, ImageIcon, ChevronDown, Edit2, ChevronUp, Moon } 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 { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { GitHubLogoIcon, PlusCircledIcon } from '@radix-ui/react-icons'; import Link from 'next/link'; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel"; import { cn } from '@/lib/utils'; import { Table, TableBody, TableCell, TableRow, } from "@/components/ui/table"; import Autoplay from 'embla-carousel-autoplay'; import FormComponent from '@/components/ui/form-component'; import WeatherChart from '@/components/weather-chart'; import InteractiveChart from '@/components/interactive-charts'; import NearbySearchMapView from '@/components/nearby-search-map-view'; import { MapComponent, MapContainer, MapSkeleton } from '@/components/map-components'; import MultiSearch from '@/components/multi-search'; export const maxDuration = 60; interface Attachment { name: string; contentType: string; url: string; size: number; } const HomeContent = () => { const router = useRouter(); const searchParams = useSearchParams(); const initialQuery = searchParams.get('query') || ''; const initialModel = searchParams.get('model') || 'azure:gpt4o-mini'; // Memoize initial values to prevent re-calculation const initialState = useMemo(() => ({ query: searchParams.get('query') || '', model: searchParams.get('model') || 'azure:gpt4o-mini' }), []); // Empty dependency array as we only want this on mount const lastSubmittedQueryRef = useRef(initialState.query); const [hasSubmitted, setHasSubmitted] = useState(() => !!initialState.query); const [selectedModel, setSelectedModel] = useState(initialState.model); const bottomRef = useRef(null); const [suggestedQuestions, setSuggestedQuestions] = useState([]); const [isEditingMessage, setIsEditingMessage] = useState(false); const [editingMessageIndex, setEditingMessageIndex] = useState(-1); const [attachments, setAttachments] = useState([]); const fileInputRef = useRef(null); const inputRef = useRef(null); const initializedRef = useRef(false); const { theme } = useTheme(); const [openChangelog, setOpenChangelog] = useState(false); const { isLoading, input, messages, setInput, handleInputChange, append, handleSubmit, setMessages, reload, stop } = useChat({ maxSteps: 10, body: { model: selectedModel, }, onFinish: async (message, { finishReason }) => { console.log("[finish reason]:", finishReason); if (message.content && finishReason === 'stop' || finishReason === 'length') { const newHistory = [...messages, { role: "user", content: lastSubmittedQueryRef.current }, { role: "assistant", content: message.content }]; const { questions } = await suggestQuestions(newHistory); setSuggestedQuestions(questions); } }, onError: (error) => { console.error("Chat error:", error.cause, error.message); 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"), }, }); }, }); useEffect(() => { if (!initializedRef.current && initialState.query && !messages.length) { initializedRef.current = true; setHasSubmitted(true); console.log("[initial query]:", initialState.query); append({ content: initialState.query, role: 'user' }); } }, [initialState.query, append, setInput, messages.length]); const ThemeToggle: React.FC = () => { const { theme, setTheme } = useTheme(); return ( ); }; const CopyButton = ({ text }: { text: string }) => { const [isCopied, setIsCopied] = useState(false); return ( ); }; type Changelog = { id: string; images: string[]; content: string; title: string; }; const changelogs: Changelog[] = [ { id: "1", title: "New Updates!", images: [ "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-new-claude-models.png", "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-nearby-search-maps-demo.png", "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-multi-search-demo.png" ], content: `## **Nearby Map Search Beta** The new Nearby Map Search tool is now available in beta! You can use it to find nearby places, restaurants, attractions, and more. Give it a try and let us know what you think! ## **Multi Search is here by default** The AI powered Multiple Query Search tool is now available by default. The LLM model will now automatically suggest multiple queries based on your input and run the searches in parallel. ## **Claude 3.5 Sonnet(New) and 3.5 Haiku are here!** The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now available on the platform. ` } ]; const ChangeLogs: React.FC<{ open: boolean; setOpen: (open: boolean) => void }> = ({ open, setOpen }) => { return (

What's new

{changelogs.map((changelog) => (
{changelog.images.map((image, index) => ( {changelog.title} ))}

{changelog.title}

(

), p: ({ node, className, ...props }) => (

), }} className="text-sm" > {changelog.content}

))}
); }; const TranslationTool: React.FC<{ toolInvocation: ToolInvocation; result: any }> = ({ toolInvocation, result }) => { const [isPlaying, setIsPlaying] = useState(false); const [audioUrl, setAudioUrl] = useState(null); const [isGeneratingAudio, setIsGeneratingAudio] = useState(false); const audioRef = useRef(null); const canvasRef = useRef(null); const waveRef = useRef(null); useEffect(() => { return () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current.src = ''; } }; }, []); useEffect(() => { if (audioUrl && audioRef.current && canvasRef.current) { waveRef.current = new Wave(audioRef.current, canvasRef.current); waveRef.current.addAnimation(new waveRef.current.animations.Lines({ lineColor: "rgb(203, 113, 93)", lineWidth: 2, mirroredY: true, count: 100, })); } }, [audioUrl]); const handlePlayPause = async () => { if (!audioUrl && !isGeneratingAudio) { setIsGeneratingAudio(true); try { const { audio } = await generateSpeech(result.translatedText, 'alloy'); setAudioUrl(audio); setIsGeneratingAudio(false); } catch (error) { console.error("Error generating speech:", error); setIsGeneratingAudio(false); } } else if (audioRef.current) { if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play(); } setIsPlaying(!isPlaying); } }; const handleReset = () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; setIsPlaying(false); } }; if (!result) { return (
); } return (
The phrase {toolInvocation.args.text} translates from {result.detectedLanguage} to {toolInvocation.args.to} as {result.translatedText} in {toolInvocation.args.to}.
{audioUrl && (
); }; interface TableData { title: string; content: string; } interface ResultsOverviewProps { result: { image: string; title: string; description: string; table_data: TableData[]; }; } const ResultsOverview: React.FC = React.memo(({ result }) => { const [showAll, setShowAll] = useState(false); const visibleData = useMemo(() => { return showAll ? result.table_data : result.table_data.slice(0, 3); }, [showAll, result.table_data]); return (
{result.image && (
{result.title}
)}
{result.title}

{result.description}

{visibleData.map((item, index) => ( {item.title} {item.content} ))}
{result.table_data.length > 3 && ( )}
); }); ResultsOverview.displayName = 'ResultsOverview'; const renderToolInvocation = useCallback( (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 === 'find_place') { if (!result) { return (

Loading place information...

); } const place = result.features[0]; if (!place) return null; return (
); } if (toolInvocation.toolName === 'nearby_search') { if (!result) { return (
Finding nearby {args.type}...
{[0, 1, 2].map((index) => ( ))}
); } console.log(result); return (
); } if (toolInvocation.toolName === 'text_search') { if (!result) { return (
Searching places...
{[0, 1, 2].map((index) => ( ))}
); } const centerLocation = result.results[0]?.geometry?.location; return ( ({ name: place.name, location: place.geometry.location, vicinity: place.formatted_address }))} /> ); } if (toolInvocation.toolName === 'get_weather_data') { if (!result) { return (
Fetching weather data...
{[0, 1, 2].map((index) => ( ))}
); } return ; } if (toolInvocation.toolName === 'programming') { return (

Programming

{!result ? ( Executing ) : ( Executed )}
{args.icon === 'stock' && } {args.icon === 'default' && } {args.icon === 'date' && } {args.icon === 'calculation' && } {args.title}
Code Output {result?.images && result.images.length > 0 && ( Images )} {result?.chart && ( Visualization )}
{args.code}
{result ? ( <>
                                                            {result.message}
                                                        
) : (
Executing code...
)}
{result?.images && result.images.length > 0 && (
{result.images.map((img: { format: string, url: string }, imgIndex: number) => (

Image {imgIndex + 1}

{img.url && img.url.trim() !== '' && ( )}
{img.url && img.url.trim() !== '' ? ( {`Generated ) : (
Image upload failed or URL is empty
)}
))}
)} {result?.chart && ( )}
); } if (toolInvocation.toolName === 'web_search') { return (
); } if (toolInvocation.toolName === 'retrieve') { if (!result) { return (
Retrieving content...
{[0, 1, 2].map((index) => ( ))}
); } return (

Retrieved Content

{result.results[0].title}

{result.results[0].description}

{result.results[0].language || 'Unknown language'} Source
View Content
{result.results[0].content}
); } if (toolInvocation.toolName === 'text_translate') { return ; } if (toolInvocation.toolName === 'results_overview') { if (!result) { return (
Generating overview...
); } return ; } return null; }, [ResultsOverview, theme] ); interface MarkdownRendererProps { content: string; } interface CitationLink { text: string; link: string; } interface LinkMetadata { title: string; description: string; } const isValidUrl = (str: string) => { try { new URL(str); return true; } catch { return false; } }; const MarkdownRenderer: React.FC = ({ content }) => { const [metadataCache, setMetadataCache] = useState>({}); const citationLinks = useMemo(() => { return Array.from(content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)).map(([_, text, link]) => ({ text, link, })); }, [content]); const fetchMetadataWithCache = useCallback(async (url: string) => { if (metadataCache[url]) { return metadataCache[url]; } const metadata = await fetchMetadata(url); if (metadata) { setMetadataCache(prev => ({ ...prev, [url]: metadata })); } return metadata; }, [metadataCache]); const CodeBlock = ({ language, children }: { language: string | undefined; children: string }) => { const [isCopied, setIsCopied] = useState(false); const handleCopy = async () => { await navigator.clipboard.writeText(children); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); }; return (
{children}
); }; const LinkPreview = ({ href }: { href: string }) => { const [metadata, setMetadata] = useState(null); const [isLoading, setIsLoading] = useState(false); React.useEffect(() => { setIsLoading(true); fetchMetadataWithCache(href).then((data) => { setMetadata(data); setIsLoading(false); }); }, [href]); if (isLoading) { return (
); } const domain = new URL(href).hostname; return (
Favicon {domain}

{metadata?.title || "Untitled"}

{metadata?.description && (

{metadata.description}

)}
); }; const renderHoverCard = (href: string, text: React.ReactNode, isCitation: boolean = false) => { return ( {text} ); }; const renderer: Partial = { paragraph(children) { return

{children}

; }, code(children, language) { return {String(children)}; }, link(href, text) { const citationIndex = citationLinks.findIndex(link => link.link === href); if (citationIndex !== -1) { return ( {renderHoverCard(href, citationIndex + 1, true)} ); } return isValidUrl(href) ? renderHoverCard(href, text) : {text}; }, heading(children, level) { const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; const className = `text-${4 - level}xl font-bold my-4 text-neutral-800 dark:text-neutral-100`; return {children}; }, list(children, ordered) { const ListTag = ordered ? 'ol' : 'ul'; return {children}; }, listItem(children) { return
  • {children}
  • ; }, blockquote(children) { return
    {children}
    ; }, }; return (
    {content}
    ); }; const lastUserMessageIndex = useMemo(() => { for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === 'user') { return i; } } return -1; }, [messages]); useEffect(() => { if (bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: "smooth" }); } }, [messages, suggestedQuestions]); const handleExampleClick = async (card: typeof suggestionCards[number]) => { 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', }); }; const handleSuggestedQuestionClick = useCallback(async (question: string) => { setHasSubmitted(true); setSuggestedQuestions([]); await append({ content: question.trim(), role: 'user' }); }, [append]); const handleMessageEdit = useCallback((index: number) => { setIsEditingMessage(true); setEditingMessageIndex(index); setInput(messages[index].content); }, [messages, setInput]); const handleMessageUpdate = useCallback((e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { const updatedMessages = [...messages]; updatedMessages[editingMessageIndex] = { ...updatedMessages[editingMessageIndex], content: input.trim() }; setMessages(updatedMessages); setIsEditingMessage(false); setEditingMessageIndex(-1); handleSubmit(e); } else { toast.error("Please enter a valid message."); } }, [input, messages, editingMessageIndex, setMessages, handleSubmit]); const suggestionCards = [ { icon: , text: "Shah Rukh Khan", }, { icon: , text: "Weather in Doha", }, { icon: , text: "Count the no. of r's in strawberry?", }, ]; interface NavbarProps { } const Navbar: React.FC = () => { return (

    Sponsor this project on GitHub

    ); }; const SuggestionCards: React.FC<{ selectedModel: string }> = ({ selectedModel }) => { return (
    {suggestionCards.map((card, index) => ( ))}
    ); }; const handleModelChange = useCallback((newModel: string) => { setSelectedModel(newModel); setSuggestedQuestions([]); reload({ body: { model: newModel } }); }, [reload]); const resetSuggestedQuestions = useCallback(() => { setSuggestedQuestions([]); }, []); // const memoizedMessages = useMemo(() => messages, [messages]); return (
    {!hasSubmitted && (
    setOpenChangelog(true)} className="cursor-pointer gap-1 mb-2 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200" variant="secondary" > What's new

    MiniPerplx

    In search for minimalism and simplicity

    )} {!hasSubmitted && ( )}
    {messages.map((message, index) => (
    {message.role === 'user' && (
    {isEditingMessage && editingMessageIndex === index ? (
    setInput(e.target.value)} className="flex-grow bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100" />
    ) : (

    {message.content}

    {message.experimental_attachments?.map((attachment, attachmentIndex) => (
    {attachment.contentType!.startsWith('image/') && ( {attachment.name )}
    ))}
    )}
    {!isEditingMessage && index === lastUserMessageIndex && (
    )}
    )} {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 && ( )}
    ); } const LoadingFallback = () => (

    MiniPerplx

    Loading your minimalist AI experience...

    ); const Home = () => { return ( }> ); }; export default Home;