/* 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 { useSearchParams } from 'next/navigation';
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 {
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';
export const maxDuration = 60;
interface Attachment {
name: string;
contentType: string;
url: string;
size: number;
}
interface SearchImage {
url: string;
description: string;
}
const ImageCarousel = ({ images, onClose }: { images: SearchImage[], onClose: () => void }) => {
return (
Close
{images.map((image, index) => (
{image.description}
))}
);
};
const WebSearchResults = ({ result, args }: { result: any, args: any }) => {
const [openDialog, setOpenDialog] = useState(false);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const handleImageClick = (index: number) => {
setSelectedImageIndex(index);
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
};
return (
Sources Found
{result && (
{result.results.length} results
)}
{args?.query && (
{args.query}
)}
{result && (
{result.results.map((item: any, itemIndex: number) => (
))}
)}
{result && result.images && result.images.length > 0 && (
Images
{result.images.slice(0, 4).map((image: SearchImage, itemIndex: number) => (
handleImageClick(itemIndex)}
>
{itemIndex === 3 && result.images.length > 4 && (
)}
))}
)}
{openDialog && result.images && (
)}
);
};
const HomeContent = () => {
const searchParams = useSearchParams();
const initialQuery = searchParams.get('query') || '';
const initialModel = searchParams.get('model') || 'azure:gpt4o-mini';
const lastSubmittedQueryRef = useRef(initialQuery);
const [hasSubmitted, setHasSubmitted] = useState(!!initialQuery);
const [selectedModel, setSelectedModel] = useState(initialModel);
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 { 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"),
},
});
},
});
const ThemeToggle: React.FC = () => {
const { theme, setTheme } = useTheme();
return (
setTheme(theme === 'dark' ? 'light' : 'dark')}
className="bg-transparent hover:bg-neutral-100 dark:hover:bg-neutral-800"
>
Toggle theme
);
};
const CopyButton = ({ text }: { text: string }) => {
const [isCopied, setIsCopied] = useState(false);
return (
{
if (!navigator.clipboard) {
return;
}
await navigator.clipboard.writeText(text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
toast.success("Copied to clipboard");
}}
className="h-8 px-2 text-xs rounded-full"
>
{isCopied ? (
) : (
)}
);
};
type Changelog = {
id: string;
images: string[];
content: string;
title: string;
};
const changelogs: Changelog[] = [
{
id: "1",
title: "Dark mode is here!",
images: [
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-dark-mode-promo.png",
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-new-input-bar-promo.png",
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-gpt-4o-back-Lwzx44RD4XofYLAmrEsLD3Fngnn33K.png"
],
content:
`## **Dark Mode**
The most requested feature is finally here! You can now toggle between light and dark mode. Default is set to your system preference.
## **New Input Bar Design**
The input bar has been redesigned to make it more focused, user-friendly and accessible. The model selection dropdown has been moved to the bottom left corner inside the input bar.
## **GPT-4o is back!**
GPT-4o has been re-enabled! You can use it by selecting the model from the dropdown.`,
}
];
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}
,
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 (
{isGeneratingAudio ? (
"Generating..."
) : isPlaying ? (
<> Pause>
) : (
<> Play>
)}
The phrase {toolInvocation.args.text} translates from {result.detectedLanguage} to {toolInvocation.args.to} as {result.translatedText} in {toolInvocation.args.to} .
{audioUrl && (
setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onEnded={() => { setIsPlaying(false); handleReset(); }}
/>
)}
);
};
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.description}
{visibleData.map((item, index) => (
{item.title}
{item.content}
))}
{result.table_data.length > 3 && (
setShowAll(!showAll)}
>
{showAll ? (
<>
Show Less
>
) : (
<>
Show More
>
)}
)}
);
});
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}s...
{[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
)}
{result ? (
<>
{result.message}
>
) : (
)}
{result?.images && result.images.length > 0 && (
{result.images.map((img: { format: string, url: string }, imgIndex: number) => (
Image {imgIndex + 1}
{img.url && img.url.trim() !== '' && (
{
window.open(img.url + "?download=1", '_blank');
}}
>
)}
{img.url && img.url.trim() !== '' ? (
) : (
Image upload failed or URL is empty
)}
))}
)}
{result?.chart && (
)}
);
}
if (toolInvocation.toolName === 'web_search') {
return (
{!result ? (
Running a search...
{[0, 1, 2].map((index) => (
))}
) : (
)}
);
}
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 (
);
}
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}
{isCopied ? : }
);
};
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 (
{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 (
New
window.open("https://git.new/mplx", "_blank")}
className="flex items-center space-x-2 bg-neutral-100 dark:bg-neutral-800 shadow-none"
>
GitHub
window.open("https://github.com/sponsors/zaidmukaddam", "_blank")}
className="flex items-center space-x-2 bg-red-100 dark:bg-red-900 shadow-none hover:bg-red-200 dark:hover:bg-red-800"
>
Sponsor
Sponsor this project on GitHub
);
};
const SuggestionCards: React.FC<{ selectedModel: string }> = ({ selectedModel }) => {
return (
{suggestionCards.map((card, index) => (
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"
>
{card.icon}
{card.text}
))}
);
};
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 ? (
) : (
{message.content}
{message.experimental_attachments?.map((attachment, attachmentIndex) => (
{attachment.contentType!.startsWith('image/') && (
)}
))}
)}
{!isEditingMessage && index === lastUserMessageIndex && (
handleMessageEdit(index)}
className="ml-2 text-neutral-500 dark:text-neutral-400"
disabled={isLoading}
>
)}
)}
{message.role === 'assistant' && message.content && (
)}
{message.toolInvocations?.map((toolInvocation: ToolInvocation, toolIndex: number) => (
{renderToolInvocation(toolInvocation, toolIndex)}
))}
))}
{suggestedQuestions.length > 0 && (
{suggestedQuestions.map((question, index) => (
handleSuggestedQuestionClick(question)}
>
{question}
))}
)}
{hasSubmitted && (
)}
);
}
const LoadingFallback = () => (
MiniPerplx
Loading your minimalist AI experience...
);
const Home = () => {
return (
}>
);
};
export default Home;