/* eslint-disable @next/next/no-img-element */ "use client"; import React, { useRef, useCallback, useState, useEffect, useMemo } from 'react'; import ReactMarkdown, { Components } from 'react-markdown'; 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, ChevronDown, FastForward, Sparkles, ArrowRight, Globe, AlignLeft, Newspaper, Copy, TrendingUp, Cloud, Code, Check, Loader2, User2, } from 'lucide-react'; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; 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 StockChart from '@/components/stock-chart'; 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"; export default function Home() { 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 [isModelSelectorOpen, setIsModelSelectorOpen] = useState(false); const [selectedModel, setSelectedModel] = useState('Speed'); const [showExamples, setShowExamples] = useState(false) const [showConfirmModal, setShowConfirmModal] = useState(false); const [newSelectedModel, setNewSelectedModel] = useState(''); const { isLoading, input, messages, setInput, append, reload, handleSubmit, setMessages } = useChat({ api: '/api/chat', body: { model: selectedModel === 'Speed' ? 'gpt-4o-mini' : selectedModel === 'Quality (GPT)' ? 'gpt-4o' : 'claude-3-5-sonnet-20240620', }, maxToolRoundtrips: 2, 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); }, onToolCall({ toolCall }) { if (toolCall.toolName === 'stock_chart_ui') { return 'Stock chart was shown to the user.'; } }, onError: (error) => { console.error("Chat error:", error); toast.error("An error occurred. Please try again."); }, }); const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); toast.success("Copied to clipboard"); return true; } catch (error) { console.error('Failed to copy:', error); toast.error("Failed to copy"); return false; } }; const CopyButton = ({ text }: { text: string }) => { const [isCopied, setIsCopied] = useState(false); const handleCopy = async () => { const success = await copyToClipboard(text); if (success) { setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } }; return ( ); }; const models = [ { name: 'Speed', description: 'High speed, but lower quality.', details: '(OpenAI/GPT-4o-mini)', icon: FastForward }, { name: 'Quality (GPT)', description: 'Speed and quality, balanced.', details: '(OpenAI/GPT | Optimized)', icon: Sparkles }, { name: 'Quality (Claude)', description: 'High quality generation.', details: '(Anthropic/Claude-3.5-Sonnet)', icon: Sparkles }, ]; const handleModelChange = (value: string) => { if (value !== selectedModel) { if (hasSubmitted) { setNewSelectedModel(value); setShowConfirmModal(true); } else { setSelectedModel(value); reload({ body: { model: value === 'Speed' ? 'gpt-4o-mini' : value === 'Quality (GPT)' ? 'gpt-4o' : 'claude-3-5-sonnet-20240620', }, }); } } setIsModelSelectorOpen(false); }; const handleConfirmModelChange = () => { if (newSelectedModel !== selectedModel) { setSelectedModel(newSelectedModel); setShowConfirmModal(false); setSuggestedQuestions([]); reload({ body: { model: newSelectedModel === 'Speed' ? 'gpt-4o-mini' : newSelectedModel === 'Quality (GPT)' ? 'gpt-4o' : 'claude-3-5-sonnet-20240620', }, }); } else { setShowConfirmModal(false); } }; interface ModelSelectorProps { selectedModel: string; onModelSelect: (model: string) => void; isDisabled: boolean; } function ModelSelector({ selectedModel, onModelSelect, isDisabled }: ModelSelectorProps) { const [isOpen, setIsOpen] = useState(false); const handleToggle = () => { if (!isDisabled) { setIsOpen(!isOpen); } }; return ( {models.map((model) => ( onModelSelect(model.name)} className={`flex items-start p-3 !font-sans rounded-md ${selectedModel === model.name ? 'bg-muted' : ''}`} >
{model.name} {selectedModel === model.name && ( Active )}
{model.description}
{model.details}
))}
); } 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'; 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 === 'stock_chart_ui') { return (

{args.symbol}

); } 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
{result ? ( Run Complete ) : ( Running )}
{args?.code && (

Code

{args.code}
Python
)}

Result

{result && }
{result ? (
                    {result}
                  
) : (
Executing code...
{[0, 1, 2].map((index) => ( ))}
)}
); } 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, children, index }) => { const citationText = Array.isArray(children) ? children[0] : children; 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 exampleQueries = [ "Weather in Doha", "Latest on Paris Olympics", "Count the number of r's in strawberry", "OpenAI GPT-4o mini" ]; return (

MiniPerplx

{!hasSubmitted &&

A minimalistic AI-powered search engine that helps you find information on the internet.

}
{!hasSubmitted && (
{isModelSelectorOpen && (
{models.map((model) => ( ))}
)}
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)} >

{lastSubmittedQuery}

{lastSubmittedQuery}

)}
{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" />
)}
Confirm Model Change Are you sure you want to change the model? This will change the quality of the responses and cannot be undone.
); }