From a05ccaf31123a6a57be32efd0fdc033500386231 Mon Sep 17 00:00:00 2001 From: zaidmukaddam Date: Wed, 28 Aug 2024 18:51:48 +0530 Subject: [PATCH] feat: Landing Page, shifted main app to /search and Image support on web search --- app/api/chat/route.ts | 36 +- app/globals.css | 15 + app/layout.tsx | 1 + app/new/page.tsx | 2 +- app/page.tsx | 2409 +++++++++-------------------- app/search/page.tsx | 1744 +++++++++++++++++++++ components/ui/carousel.tsx | 262 ++++ components/ui/navigation-menu.tsx | 128 ++ components/ui/popover.tsx | 33 + package.json | 5 + pnpm-lock.yaml | 148 +- 11 files changed, 3142 insertions(+), 1641 deletions(-) create mode 100644 app/search/page.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/popover.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index dbd4a93..88f703f 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -15,6 +15,18 @@ const azure = createAzure({ apiKey: process.env.AZURE_API_KEY, }); +function sanitizeUrl(url: string): string { + return url.replace(/\s+/g, '%20') +} + +type SearchResultImage = + | string + | { + url: string + description: string + number_of_results?: number + } + const provider = process.env.OPENAI_PROVIDER; export async function POST(req: Request) { @@ -144,6 +156,7 @@ When asked a "What is" question, maintain the same format as the question and an exclude_domains?: string[]; }) => { const apiKey = process.env.TAVILY_API_KEY; + const includeImageDescriptions = true let body = JSON.stringify({ api_key: apiKey, @@ -152,6 +165,8 @@ When asked a "What is" question, maintain the same format as the question and an max_results: maxResults < 5 ? 5 : maxResults, search_depth: searchDepth, include_answers: true, + include_images: true, + include_image_descriptions: includeImageDescriptions, exclude_domains: exclude_domains, }); @@ -164,6 +179,8 @@ When asked a "What is" question, maintain the same format as the question and an max_results: maxResults < 5 ? 5 : maxResults, search_depth: searchDepth, include_answers: true, + include_images: true, + include_image_descriptions: includeImageDescriptions, exclude_domains: exclude_domains, }); } @@ -179,7 +196,7 @@ When asked a "What is" question, maintain the same format as the question and an const data = await response.json(); let context = data.results.map( - (obj: { url: any; content: any; title: any; raw_content: any, published_date: any }) => { + (obj: { url: any; content: any; title: any; raw_content: any, published_date: any }, index: number) => { if (topic === "news") { return { url: obj.url, @@ -198,8 +215,25 @@ When asked a "What is" question, maintain the same format as the question and an }, ); + const processedImages = includeImageDescriptions + ? data.images + .map(({ url, description }: { url: string; description: string }) => ({ + url: sanitizeUrl(url), + description + })) + .filter( + ( + image: SearchResultImage + ): image is { url: string; description: string } => + typeof image === 'object' && + image.description !== undefined && + image.description !== '' + ) + : data.images.map((url: string) => sanitizeUrl(url)) + return { results: context, + images: processedImages, }; }, }), diff --git a/app/globals.css b/app/globals.css index 3630c51..7d982a8 100644 --- a/app/globals.css +++ b/app/globals.css @@ -13,6 +13,21 @@ body { font-family: var(--font-sans), sans-serif; } +.homeBtn { + box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, + rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset, + inset 0 1px 0 0 #ffffff52; +} + +.tweet-container { + display: flex; + flex-direction: column; +} + +.tweet-container > div { + flex: 1; +} + h1 { font-family: var(--font-serif); } diff --git a/app/layout.tsx b/app/layout.tsx index 09a3513..89406e9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -36,6 +36,7 @@ const plexMono = IBM_Plex_Mono({ const instrumentSerif = Instrument_Serif({ weight: "400", subsets: ["latin"], + variable: "--font-serif" }) export default function RootLayout({ diff --git a/app/new/page.tsx b/app/new/page.tsx index 5852151..ac8602b 100644 --- a/app/new/page.tsx +++ b/app/new/page.tsx @@ -1,5 +1,5 @@ import { redirect } from 'next/navigation' export default async function NewPage() { - redirect('/') + redirect('/search') } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 06c69ce..72ef869 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,1465 +1,661 @@ -/* 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 remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; -import rehypeKatex from 'rehype-katex'; -import 'katex/dist/katex.min.css'; -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 { generateSpeech, suggestQuestions } from './actions'; -import { Wave } from "@foobar404/wave"; -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, - Download, - Flame, - Sun, - Terminal, - Pause, - Play, - TrendingUpIcon, - Calendar, - Calculator -} 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 React, { useState, useEffect, useRef } from 'react' +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" 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"; + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from "@/components/ui/navigation-menu" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" 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'; - -export const maxDuration = 60; - -declare global { - interface Window { - google: any; - initMap: () => void; - } -} - -export default function Home() { - const inputRef = useRef(null); - const [lastSubmittedQuery, setLastSubmittedQuery] = useState(""); - const [hasSubmitted, setHasSubmitted] = useState(false); - const bottomRef = useRef(null); - const [suggestedQuestions, setSuggestedQuestions] = useState([]); - const [isEditingMessage, setIsEditingMessage] = useState(false); - const [editingMessageIndex, setEditingMessageIndex] = useState(-1); - - const { isLoading, input, messages, setInput, append, handleSubmit, setMessages } = useChat({ - api: '/api/chat', - maxToolRoundtrips: 1, - onFinish: async (message, { finishReason }) => { - console.log("[finish reason]:", finishReason); - if (message.content && finishReason === 'stop' || finishReason === 'length') { - const newHistory = [...messages, { role: "user", content: lastSubmittedQuery }, { role: "assistant", content: message.content }]; - const { questions } = await suggestQuestions(newHistory); - setSuggestedQuestions(questions); - } - }, - 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 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} -
-
-
-
- {audioUrl && ( -
- ); - }; - - 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

-
- {!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 - - )} - - -
- - {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 -
- )} -
-
- ))} -
-
- )} -
-
-
-
-
- ); - } - - 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}) - -
- ))} -
-
-
- ); - } - - if (toolInvocation.toolName === 'web_search') { - 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}

-
-
- - {item.url} - -
- ))} -
- )} -
-
-
- )} -
- ); - } - - 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 ; - } - - 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; - citationText: string; - } - - const CitationComponent: React.FC = React.memo(({ href, index, citationText }) => { - const { hostname } = new URL(href); - const faviconUrl = `https://www.google.com/s2/favicons?sz=128&domain=${hostname}`; - - return ( - - - - - {index + 1} - - - - - -

{citationText}

-
-
- ); - }); - - CitationComponent.displayName = "CitationComponent"; - - interface MarkdownRendererProps { - content: string; - } - - const MarkdownRenderer: React.FC = React.memo(({ content }) => { - // Escape dollar signs that are likely to be currency - const escapedContent = content.replace(/\$(\d+(\.\d{1,2})?)/g, '\\$$1'); - - const citationLinks = useMemo(() => { - return [...escapedContent.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map(([_, text, link]) => ({ - text, - link, - })); - }, [escapedContent]); - - 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 ( - - {escapedContent} - - ); - }); - - MarkdownRenderer.displayName = "MarkdownRenderer"; - - 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 = useCallback(async (query: string) => { - setLastSubmittedQuery(query.trim()); - setHasSubmitted(true); - setSuggestedQuestions([]); - await append({ - content: query.trim(), - role: 'user' - }); - }, [append]); - - const handleSuggestedQuestionClick = useCallback((question: string) => { - setHasSubmitted(true); - setSuggestedQuestions([]); - setInput(question.trim()); - handleSubmit(new Event('submit') as any); - }, [setInput, handleSubmit]); - - const handleFormSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (input.trim()) { - setHasSubmitted(true); - setSuggestedQuestions([]); - handleSubmit(e); - } else { - toast.error("Please enter a search query."); - } - }, [input, handleSubmit]); - - 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: "What's new with XAI's Grok?" }, - { icon: , text: "Latest updates on OpenAI" }, - { icon: , text: "Weather in Doha" }, - { icon: , text: "Count the no. of r's in strawberry?" }, + Search, + Zap, + Code, + Cloud, + Link, + MapPin, + Globe, + Mic, + ArrowRight, + Github, + LucideIcon, + Server, + Palette, + Cpu, + ChevronDown, + Check, + Menu, + X +} from "lucide-react" +import NextLink from "next/link" +import { + motion, + useScroll, + useTransform, + useSpring, + useInView, + AnimatePresence, + useAnimation +} from "framer-motion" +import { cn } from '@/lib/utils'; +import { Tweet } from 'react-tweet' +import Image from 'next/image'; + +const TestimonialSection: React.FC = () => { + const tweetIds = [ + "1825543755748782500", + "1825876424755941787", + "1827580223606669661", + "1825574082345136506", + "1825973306924872143", + "1825821083817103852" ]; - const Navbar = () => ( -
- - - -
- - - - - - - -

Sponsor this project on GitHub

-
-
-
-
-
- ); + const [isSmallScreen, setIsSmallScreen] = useState(false); + const controls = useAnimation(); + + useEffect(() => { + const checkScreenSize = () => setIsSmallScreen(window.innerWidth < 768); + checkScreenSize(); + window.addEventListener('resize', checkScreenSize); + return () => window.removeEventListener('resize', checkScreenSize); + }, []); + + useEffect(() => { + if (isSmallScreen) { + controls.start({ + x: [0, -200 + '%'], + transition: { + x: { + repeat: Infinity, + repeatType: "loop", + duration: 30, + ease: "linear", + }, + }, + }); + } else { + controls.stop(); + } + }, [isSmallScreen, controls]); return ( -
- +
+
+

+ What People Are Saying +

+
+ + {[...tweetIds, ...tweetIds].map((id, index) => ( +
+ +
+ ))} +
+
+
+ {tweetIds.map((id) => ( +
+ +
+ ))} +
+
+
+ ); +}; -
- {!hasSubmitted && ( -
-

MiniPerplx

-

- In search for minimalism and simplicity -

-
- +const AboutUsSection: React.FC = () => { + const aboutPoints = [ + { + icon: Search, + title: "Minimalistic Search", + description: "We strip away the clutter to focus on what matters most - delivering accurate and relevant results." + }, + { + icon: Code, + title: "AI-Powered", + description: "Leveraging cutting-edge AI technology to understand and respond to your queries with precision." + }, + { + icon: Zap, + title: "Lightning Fast", + description: "Designed for speed, MiniPerplx provides instant answers to keep up with your pace of work." + } + ]; + + return ( +
+
+ +

+ About MiniPerplx +

+

+ MiniPerplx is reimagining the way you search and interact with information online. +

+
+
+ {aboutPoints.map((point, index) => ( + + + + +

{point.title}

+

{point.description}

+
+
+
+ ))} +
+
+
+ ); +}; + + +const MarqueeTestimonials: React.FC = () => { + const testimonials = [ + "Absolutely love MiniPerplx! 🚀", + "Game-changer for my workflow. 💼", + "Simplicity at its finest. ✨", + "Can't imagine working without it now. 🙌", + "MiniPerplx is a must-have tool! 🛠️", + ]; + + return ( +
+ + {testimonials.concat(testimonials).map((text, index) => ( + + {text} + + ))} + +
+ ) +} + +interface FeatureCardProps { + icon: LucideIcon; + title: string; + description: string; +} + +const FeatureCard: React.FC = ({ icon: Icon, title, description }) => ( + + + + + + {title} + + +

{description}

+
+
+) + +interface Star { + x: number; + y: number; + size: number; + name: string; + category: string; +} + +const TechConstellation: React.FC = () => { + const [stars, setStars] = useState([]) + const [hoveredCategory, setHoveredCategory] = useState(null) + const constellationRef = useRef(null) + + const techStack = [ + { + category: "Core Technologies", + icon: Server, + items: ["Next.js", "React", "TypeScript", "Vercel AI SDK", "Tailwind CSS"] + }, + { + category: "UI & Styling", + icon: Palette, + items: ["shadcn/ui", "Framer Motion", "Lucide Icons"] + }, + { + category: "AI Services & APIs", + icon: Cpu, + items: ["Azure OpenAI", "Tavily AI", "e2b.dev", "OpenWeatherMap", "Google Maps API", "Firecrawl"] + } + ]; + + useEffect(() => { + if (constellationRef.current) { + const { width, height } = constellationRef.current.getBoundingClientRect() + const newStars: Star[] = [] + const centerX = width / 2 + const centerY = height / 2 + const maxRadius = Math.min(width, height) * 0.4 // 40% of the smaller dimension + + techStack.forEach((category, categoryIndex) => { + const categoryAngle = (categoryIndex / techStack.length) * Math.PI * 2 + const categoryRadius = maxRadius * 0.8 // 80% of maxRadius for category centers + + const categoryCenterX = centerX + Math.cos(categoryAngle) * categoryRadius + const categoryCenterY = centerY + Math.sin(categoryAngle) * categoryRadius + + category.items.forEach((item, index) => { + const itemAngle = categoryAngle + (index / category.items.length - 0.5) * Math.PI * 0.5 + const itemRadius = Math.random() * maxRadius * 0.3 + maxRadius * 0.1 // Between 10% and 40% of maxRadius + + const x = categoryCenterX + Math.cos(itemAngle) * itemRadius + const y = categoryCenterY + Math.sin(itemAngle) * itemRadius + + newStars.push({ + x, + y, + size: Math.random() * 2 + 2, + name: item, + category: category.category + }) + }) + }) + + setStars(newStars) + } + }, []) + + const getStarColor = (category: string) => { + switch (category) { + case "Core Technologies": + return "#FFD700" + case "UI & Styling": + return "#00CED1" + case "AI Services & APIs": + return "#FF69B4" + default: + return "#FFFFFF" + } + } + + return ( + +
+ + {stars.map((star, index) => ( + + + + + +
{star.name}
+
{star.category}
+
+
+ ))} +
+ {hoveredCategory && ( + + {stars + .filter((star) => star.category === hoveredCategory) + .map((star, index, filteredStars) => { + const nextStar = filteredStars[(index + 1) % filteredStars.length] + return ( + + ) + })} + + )} +
+ {techStack.map((category, index) => ( + setHoveredCategory(category.category)} + onMouseLeave={() => setHoveredCategory(null)} + > +
+ {category.category} + + ))} +
+
+ + ) +} + +interface AnimatedSectionProps { + children: React.ReactNode; + className?: string; + delay?: number; +} + +const AnimatedSection: React.FC = ({ children, className, delay = 0 }) => { + const ref = useRef(null) + const isInView = useInView(ref, { once: true, margin: "-100px" }) + + return ( + + {children} + + ) +} + +const TryButton: React.FC = () => { + return ( + + Try MiniPerplx + + + ) +} + +const ScrollProgress: React.FC = () => { + const { scrollYProgress } = useScroll() + const scaleX = useSpring(scrollYProgress, { + stiffness: 100, + damping: 30, + restDelta: 0.001 + }) + return ( + + ) +} + +const FloatingIcon: React.FC<{ Icon: LucideIcon }> = ({ Icon }) => ( + + + +) + +const FloatingIcons: React.FC = () => { + const icons = [Search, Zap, Code, Cloud, Link, MapPin, Globe, Mic]; + + return ( +
+
+ {icons.map((Icon, index) => ( + + ))} +
+
+ {icons.slice(0, 4).map((Icon, index) => ( + + ))} +
+
+ ) +} + + +const NavItem: React.FC<{ href: string; children: React.ReactNode }> = ({ href, children }) => { + return ( +
  • + + + {children} + + +
  • + ) +} + + +const MobileNavItem: React.FC<{ href: string; children: React.ReactNode; onClick: () => void }> = ({ href, children, onClick }) => { + return ( +
  • + + {children} + +
  • + ) +} + +const EnhancedLandingPage: React.FC = () => { + const { scrollYProgress } = useScroll() + const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0]) + const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.95]) + const y = useTransform(scrollYProgress, [0, 0.2], [0, -50]) + + const [isMenuOpen, setIsMenuOpen] = useState(false) + + const toggleMenu = () => setIsMenuOpen(!isMenuOpen) + + + const [mounted, setMounted] = useState(false) + useEffect(() => setMounted(true), []) + React.useEffect(() => { + if (isMenuOpen) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = 'unset' + } + + return () => { + document.body.style.overflow = 'unset' + } + }, [isMenuOpen]) + + useEffect(() => { + document.documentElement.style.scrollBehavior = 'smooth'; + + return () => { + document.documentElement.style.scrollBehavior = ''; + }; + }, []); + + if (!mounted) return null + + const features = [ + { icon: Globe, title: "Web Search", description: "Powered by Tavily AI for comprehensive web results." }, + { icon: Code, title: "Code Interpreter", description: "Utilize e2b.dev for advanced code interpretation and execution." }, + { icon: Cloud, title: "Weather Forecast", description: "Get accurate weather information via OpenWeatherMap." }, + { icon: Link, title: "URL Summary", description: "Summarize web content quickly with Jina AI Reader." }, + { icon: MapPin, title: "Location Search", description: "Find places and nearby locations using Google Maps API." }, + { icon: Mic, title: "Translation & TTS", description: "Translate text and convert to speech with OpenAI TTS." }, + ] + + + return ( +
    + +
    + + MiniPerplx + + + + + Explore + +
      + +
      About Us
      +

      Learn more about MiniPerplx and our mission.

      +
      + +
      Features
      +

      Discover the powerful capabilities of MiniPerplx.

      +
      + +
      Tech Stack
      +

      Explore the technologies powering MiniPerplx.

      +
      + +
      Testimonials
      +

      See what others are saying about MiniPerplx.

      +
      +
    +
    +
    + + + + Try It + + + +
    +
    + +
    + + + {isMenuOpen && ( + + + + )} + + +
    +
    + + +
    + +

    + Introducing MiniPerplx +

    +
    + +

    + A minimalistic AI search engine designed to deliver answers in the simplest and most elegant way possible.✨ +

    +
    + + + +
    +
    +
    + + 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" - /> - -
    -
    - -
    - {suggestionCards.map((card, index) => ( - - ))} -
    +
    - )} -
    - - -
    - {messages.map((message, index) => ( -
    - {message.role === 'user' && ( - - -
    - {isEditingMessage && editingMessageIndex === index ? ( -
    - setInput(e.target.value)} - className="flex-grow" - /> - - -
    - ) : ( -

    - {message.content} -

    - )} -
    - {!isEditingMessage && index === lastUserMessageIndex && ( - - )} -
    - )} - {message.role === 'assistant' && message.content && ( -
    -
    -
    - -

    Answer

    -
    - -
    -
    - -
    -
    - )} - {message.toolInvocations?.map((toolInvocation: ToolInvocation, toolIndex: number) => ( -
    - {renderToolInvocation(toolInvocation, toolIndex)} -
    +
    + + +
    +
    +

    + Powerful Features +

    +
    + {features.map((feature, index) => ( + ))}
    - ))} - {suggestedQuestions.length > 0 && ( - -
    - -

    Suggested questions

    -
    -
    - {suggestedQuestions.map((question, index) => ( - - ))} -
    -
    - )} -
    -
    -
    +
    + - - {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" - /> - -
    -
    -
    - )} -
    + + +
    +
    + + + +
    + +
    + +

    + MiniPerplx +

    +
    + +

    © {new Date().getFullYear()} MiniPerplx. All rights reserved.

    +
    +
    +
    + {[...Array(20)].map((_, i) => ( + + ))} +
    +
    +
    - ); -} \ No newline at end of file + ) +} + +export default EnhancedLandingPage \ No newline at end of file diff --git a/app/search/page.tsx b/app/search/page.tsx new file mode 100644 index 0000000..49654f7 --- /dev/null +++ b/app/search/page.tsx @@ -0,0 +1,1744 @@ +/* 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 remarkGfm from 'remark-gfm'; +import remarkMath from 'remark-math'; +import rehypeKatex from 'rehype-katex'; +import 'katex/dist/katex.min.css'; +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 { generateSpeech, suggestQuestions } from '../actions'; +import { Wave } from "@foobar404/wave"; +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, + Download, + Flame, + Sun, + Terminal, + Pause, + Play, + TrendingUpIcon, + Calendar, + Calculator, + PlusCircle, + ImageIcon +} 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, PlusCircledIcon } from '@radix-ui/react-icons'; +import { Skeleton } from '@/components/ui/skeleton'; +import Link from 'next/link'; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel"; + +export const maxDuration = 60; + +declare global { + interface Window { + google: any; + initMap: () => void; + } +} + +export default function Home() { + const inputRef = useRef(null); + const [lastSubmittedQuery, setLastSubmittedQuery] = useState(""); + const [hasSubmitted, setHasSubmitted] = useState(false); + const bottomRef = useRef(null); + const [suggestedQuestions, setSuggestedQuestions] = useState([]); + const [isEditingMessage, setIsEditingMessage] = useState(false); + const [editingMessageIndex, setEditingMessageIndex] = useState(-1); + + const { isLoading, input, messages, setInput, append, handleSubmit, setMessages } = useChat({ + api: '/api/chat', + maxToolRoundtrips: 1, + onFinish: async (message, { finishReason }) => { + console.log("[finish reason]:", finishReason); + if (message.content && finishReason === 'stop' || finishReason === 'length') { + const newHistory = [...messages, { role: "user", content: lastSubmittedQuery }, { role: "assistant", content: message.content }]; + const { questions } = await suggestQuestions(newHistory); + setSuggestedQuestions(questions); + } + }, + 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 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} +
    +
    +
    +
    + {audioUrl && ( +
    + ); + }; + + interface SearchImage { + url: string; + description: string; + } + + const ImageCarousel = ({ images, onClose }: { images: SearchImage[], onClose: () => void }) => { + return ( + + + + + + {images.map((image, index) => ( + + {image.description} +

    {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) => ( +
    +
    + Favicon +
    +

    {item.title}

    +

    {item.content}

    +
    +
    + + {item.url} + +
    + ))} +
    + )} +
    +
    +
    + {result && result.images && result.images.length > 0 && ( +
    +
    + +

    Images

    +
    +
    + {result.images.slice(0, 4).map((image: SearchImage, itemIndex: number) => ( +
    handleImageClick(itemIndex)} + > + {image.description} + {itemIndex === 3 && result.images.length > 4 && ( +
    + +
    + )} +
    + ))} +
    +
    + )} + {openDialog && result.images && ( + + )} +
    + ); + }; + + 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

    +
    + {!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 + + )} + + +
    + + {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 +
    + )} +
    +
    + ))} +
    +
    + )} +
    +
    +
    +
    +
    + ); + } + + 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}) + +
    + ))} +
    +
    +
    + ); + } + + 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 ; + } + + 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; + citationText: string; + } + + const CitationComponent: React.FC = React.memo(({ href, index, citationText }) => { + const { hostname } = new URL(href); + const faviconUrl = `https://www.google.com/s2/favicons?sz=128&domain=${hostname}`; + + return ( + + + + + {index + 1} + + + + + +

    {citationText}

    +
    +
    + ); + }); + + CitationComponent.displayName = "CitationComponent"; + + interface MarkdownRendererProps { + content: string; + } + + const MarkdownRenderer: React.FC = React.memo(({ content }) => { + // Escape dollar signs that are likely to be currency + const escapedContent = content.replace(/\$(\d+(\.\d{1,2})?)/g, '\\$$1'); + + const citationLinks = useMemo(() => { + return [...escapedContent.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map(([_, text, link]) => ({ + text, + link, + })); + }, [escapedContent]); + + 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 ( + + {escapedContent} + + ); + }); + + MarkdownRenderer.displayName = "MarkdownRenderer"; + + 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 = useCallback(async (query: string) => { + setLastSubmittedQuery(query.trim()); + setHasSubmitted(true); + setSuggestedQuestions([]); + await append({ + content: query.trim(), + role: 'user' + }); + }, [append]); + + const handleSuggestedQuestionClick = useCallback((question: string) => { + setHasSubmitted(true); + setSuggestedQuestions([]); + setInput(question.trim()); + handleSubmit(new Event('submit') as any); + }, [setInput, handleSubmit]); + + const handleFormSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + if (input.trim()) { + setHasSubmitted(true); + setSuggestedQuestions([]); + handleSubmit(e); + } else { + toast.error("Please enter a search query."); + } + }, [input, handleSubmit]); + + 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: "What's new with XAI's Grok?" }, + { icon: , text: "Latest updates on OpenAI" }, + { icon: , text: "Weather in Doha" }, + { icon: , text: "Count the no. of r's in strawberry?" }, + ]; + + const Navbar = () => ( +
    + + + +
    + + + + + + + +

    Sponsor this project on GitHub

    +
    +
    +
    +
    +
    + ); + + return ( +
    + + +
    + {!hasSubmitted && ( +
    +

    MiniPerplx

    +

    + In search for minimalism and simplicity +

    +
    + )} + + {!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" + /> + +
    +
    + +
    + {suggestionCards.map((card, index) => ( + + ))} +
    +
    + )} +
    + + +
    + {messages.map((message, index) => ( +
    + {message.role === 'user' && ( + + +
    + {isEditingMessage && editingMessageIndex === index ? ( +
    + setInput(e.target.value)} + className="flex-grow" + /> + + +
    + ) : ( +

    + {message.content} +

    + )} +
    + {!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 && ( + +
    +
    + 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" + /> + +
    +
    +
    + )} +
    +
    + ); +} \ No newline at end of file diff --git a/components/ui/carousel.tsx b/components/ui/carousel.tsx new file mode 100644 index 0000000..f9b6840 --- /dev/null +++ b/components/ui/carousel.tsx @@ -0,0 +1,262 @@ +"use client" + +import * as React from "react" +import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons" +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
    + {children} +
    +
    + ) + } +) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
    +
    +
    + ) +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
    + ) +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +}) +CarouselNext.displayName = "CarouselNext" + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..5841fb3 --- /dev/null +++ b/components/ui/navigation-menu.tsx @@ -0,0 +1,128 @@ +import * as React from "react" +import { ChevronDownIcon } from "@radix-ui/react-icons" +import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" +import { cva } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const NavigationMenu = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)) +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName + +const NavigationMenuItem = NavigationMenuPrimitive.Item + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" +) + +const NavigationMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children}{" "} + +)) +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName + +const NavigationMenuLink = NavigationMenuPrimitive.Link + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
    + +
    +)) +NavigationMenuViewport.displayName = + NavigationMenuPrimitive.Viewport.displayName + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +
    + +)) +NavigationMenuIndicator.displayName = + NavigationMenuPrimitive.Indicator.displayName + +export { + navigationMenuTriggerStyle, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, +} diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx new file mode 100644 index 0000000..29c7bd2 --- /dev/null +++ b/components/ui/popover.tsx @@ -0,0 +1,33 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/package.json b/package.json index a025ff7..e890426 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@ai-sdk/azure": "^0.0.31", "@ai-sdk/cohere": "latest", + "@ai-sdk/google": "^0.0.46", "@ai-sdk/openai": "latest", "@e2b/code-interpreter": "^0.0.8", "@foobar404/wave": "^2.0.5", @@ -20,6 +21,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", @@ -32,6 +35,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "embla-carousel-react": "^8.2.0", "framer-motion": "^11.3.19", "katex": "^0.16.11", "lucide-react": "^0.424.0", @@ -41,6 +45,7 @@ "react-dom": "^18", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", + "react-tweet": "^3.2.1", "recharts": "^2.12.7", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8847c7..9391c62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,10 @@ dependencies: version: 0.0.31(zod@3.23.8) '@ai-sdk/cohere': specifier: latest - version: 0.0.21(zod@3.23.8) + version: 0.0.22(zod@3.23.8) + '@ai-sdk/google': + specifier: ^0.0.46 + version: 0.0.46(zod@3.23.8) '@ai-sdk/openai': specifier: latest version: 0.0.54(zod@3.23.8) @@ -38,6 +41,12 @@ dependencies: '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) + '@radix-ui/react-navigation-menu': + specifier: ^1.2.0 + version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) @@ -74,6 +83,9 @@ dependencies: date-fns: specifier: ^3.6.0 version: 3.6.0 + embla-carousel-react: + specifier: ^8.2.0 + version: 8.2.0(react@18.3.1) framer-motion: specifier: ^11.3.19 version: 11.3.20(react-dom@18.3.1)(react@18.3.1) @@ -101,6 +113,9 @@ dependencies: react-syntax-highlighter: specifier: ^15.5.0 version: 15.5.0(react@18.3.1) + react-tweet: + specifier: ^3.2.1 + version: 3.2.1(react-dom@18.3.1)(react@18.3.1) recharts: specifier: ^2.12.7 version: 2.12.7(react-dom@18.3.1)(react@18.3.1) @@ -175,8 +190,8 @@ packages: zod: 3.23.8 dev: false - /@ai-sdk/cohere@0.0.21(zod@3.23.8): - resolution: {integrity: sha512-WmhCuxJp5VqVhb47ILAQ6RPXS9Ophm1loMnG44PL/YjO2m1qfu3UWM+FLuRI57WFS0eTIjP/VUAhjQq45isAjw==} + /@ai-sdk/cohere@0.0.22(zod@3.23.8): + resolution: {integrity: sha512-UMUOsbSf1uBOOZT76+r28DJKestluGjfHc31kgeJkQx8Pve6acgrIvH0A7mvCAO3H8TDWIO3SmOWos2+XlMBRA==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -186,6 +201,18 @@ packages: zod: 3.23.8 dev: false + /@ai-sdk/google@0.0.46(zod@3.23.8): + resolution: {integrity: sha512-rsc3Wh54EfSt3l/7IqPdTeuxA7xvFk2p8/HxxyoHfcwvQYmQ/bpgxmadId862sVsK79L8k3iRxvVwGVkkaEeaA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + dependencies: + '@ai-sdk/provider': 0.0.22 + '@ai-sdk/provider-utils': 1.0.17(zod@3.23.8) + json-schema: 0.4.0 + zod: 3.23.8 + dev: false + /@ai-sdk/openai@0.0.53(zod@3.23.8): resolution: {integrity: sha512-Wm4+EYG2Zl5WmhvZJrLhrBY+C46FEQmDjQ9ZB5h2DvRoJZNKtNiVNFMEQuyBK7QwivvlCSMJkPRBfFcbJgNLMQ==} engines: {node: '>=18'} @@ -1011,6 +1038,73 @@ packages: react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1) dev: false + /@radix-ui/react-navigation-menu@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-OQ8tcwAOR0DhPlSY3e4VMXeHiol7la4PPdJWhhwJiJA+NLX0SaCaonOkRnI3gCDHoZ7Fo7bb/G6q25fRM2Y+3Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-popover@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1) + dev: false + /@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} peerDependencies: @@ -1284,6 +1378,19 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-use-previous@1.1.0(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + /@radix-ui/react-use-rect@1.1.0(@types/react@18.3.3)(react@18.3.1): resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -2446,6 +2553,28 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + /embla-carousel-react@8.2.0(react@18.3.1): + resolution: {integrity: sha512-dWqbmaEBQjeAcy/EKrcAX37beVr0ubXuHPuLZkx27z58V1FIvRbbMb4/c3cLZx0PAv/ofngX2QFrwUB+62SPnw==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 + dependencies: + embla-carousel: 8.2.0 + embla-carousel-reactive-utils: 8.2.0(embla-carousel@8.2.0) + react: 18.3.1 + dev: false + + /embla-carousel-reactive-utils@8.2.0(embla-carousel@8.2.0): + resolution: {integrity: sha512-ZdaPNgMydkPBiDRUv+wRIz3hpZJ3LKrTyz+XWi286qlwPyZFJDjbzPBiXnC3czF9N/nsabSc7LTRvGauUzwKEg==} + peerDependencies: + embla-carousel: 8.2.0 + dependencies: + embla-carousel: 8.2.0 + dev: false + + /embla-carousel@8.2.0: + resolution: {integrity: sha512-rf2GIX8rab9E6ZZN0Uhz05746qu2KrDje9IfFyHzjwxLwhvGjUt6y9+uaY1Sf+B0OPSa3sgas7BE2hWZCtopTA==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4957,6 +5086,19 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /react-tweet@3.2.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==} + peerDependencies: + react: '>= 18.0.0' + react-dom: '>= 18.0.0' + dependencies: + '@swc/helpers': 0.5.5 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + swr: 2.2.5(react@18.3.1) + dev: false + /react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'}