/* 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 { track } from '@vercel/analytics'; 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, ImageIcon, Paperclip } 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 } 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; } } const MAX_IMAGES = 3; interface Attachment { name: string; contentType: string; url: string; size: number; } export default function Home() { 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 [files, setFiles] = useState(undefined); const [attachments, setAttachments] = useState([]); const fileInputRef = useRef(null); const inputRef = useRef(null); const { isLoading, input, messages, setInput, handleInputChange, 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 (card: typeof suggestionCards[number]) => { track("search example", { query: card.text }); setLastSubmittedQuery(card.text.trim()); setHasSubmitted(true); setSuggestedQuestions([]); await append({ content: card.text.trim(), role: 'user', experimental_attachments: card.attachment ? [card.attachment] : undefined, }); }, [append, setLastSubmittedQuery, setHasSubmitted, setSuggestedQuestions]); const handleSuggestedQuestionClick = useCallback(async (question: string) => { setHasSubmitted(true); setSuggestedQuestions([]); setInput(question.trim()); await append({ content: question.trim(), role: 'user' }); }, [setInput, 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: "Where is this place?", attachment: { name: 'taj_mahal.jpg', contentType: 'image/jpeg', url: 'https://metwm7frkvew6tn1.public.blob.vercel-storage.com/taj-mahal.jpg', } }, { 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

); interface UploadingAttachment { file: File; progress: number; } interface AttachmentPreviewProps { attachment: Attachment | UploadingAttachment; onRemove: () => void; isUploading: boolean; } const AttachmentPreview: React.FC = React.memo(({ attachment, onRemove, isUploading }) => { const formatFileSize = (bytes: number): string => { if (bytes < 1024) return bytes + ' bytes'; else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; else return (bytes / 1048576).toFixed(1) + ' MB'; }; const isUploadingAttachment = (attachment: Attachment | UploadingAttachment): attachment is UploadingAttachment => { return 'progress' in attachment; }; return ( {isUploading ? (
) : isUploadingAttachment(attachment) ? (
{Math.round(attachment.progress * 100)}%
) : ( {`Preview )}
{isUploadingAttachment(attachment) ? null : (

{attachment.name}

)}

{isUploadingAttachment(attachment) ? 'Uploading...' : formatFileSize((attachment as Attachment).size)}

{ e.stopPropagation(); onRemove(); }} className="absolute -top-2 -right-2 p-0.5 m-0 rounded-full bg-background border border-input shadow-sm hover:bg-muted transition-colors z-20" >
{!isUploadingAttachment(attachment) && ( {`Full )}
); }); AttachmentPreview.displayName = 'AttachmentPreview'; interface FormComponentProps { input: string; setInput: (input: string) => void; attachments: Attachment[]; setAttachments: React.Dispatch>; hasSubmitted: boolean; setHasSubmitted: (value: boolean) => void; isLoading: boolean; handleSubmit: (event: React.FormEvent, options?: { experimental_attachments?: Attachment[] }) => void; fileInputRef: React.RefObject; inputRef: React.RefObject; } const FormComponent: React.FC = ({ input, setInput, attachments, setAttachments, hasSubmitted, setHasSubmitted, isLoading, handleSubmit, fileInputRef, inputRef, }) => { const [uploadingAttachments, setUploadingAttachments] = useState([]); const uploadFile = async (file: File): Promise => { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/upload', { method: 'POST', body: formData, }); if (!response.ok) { throw new Error('Failed to upload file'); } return await response.json(); }; const handleFileChange = async (event: React.ChangeEvent) => { const selectedFiles = event.target.files; if (selectedFiles) { const imageFiles = Array.from(selectedFiles).filter(file => file.type.startsWith('image/')); if (imageFiles.length > 0) { if (imageFiles.length + attachments.length + uploadingAttachments.length > MAX_IMAGES) { toast.error(`You can only attach up to ${MAX_IMAGES} images.`); return; } const newUploadingAttachments = imageFiles.map(file => ({ file, progress: 0 })); setUploadingAttachments(prev => [...prev, ...newUploadingAttachments]); for (const file of imageFiles) { try { const uploadedFile = await uploadFile(file); setAttachments(prev => [...prev, uploadedFile]); setUploadingAttachments(prev => prev.filter(ua => ua.file !== file)); } catch (error) { console.error("Error uploading file:", error); toast.error(`Failed to upload ${file.name}`); setUploadingAttachments(prev => prev.filter(ua => ua.file !== file)); } } } else { toast.error("Please select image files only."); } } }; const removeAttachment = (index: number) => { setAttachments(prev => prev.filter((_, i) => i !== index)); }; const removeUploadingAttachment = (index: number) => { setUploadingAttachments(prev => prev.filter((_, i) => i !== index)); }; const handleInputChange = useCallback((e: React.ChangeEvent) => { setInput(e.target.value); }, [setInput]); useEffect(() => { if (inputRef.current) { inputRef.current.focus(); } }, [inputRef]); const onSubmit = (event: React.FormEvent) => { event.preventDefault(); event.stopPropagation(); if (input.trim() || attachments.length > 0) { setHasSubmitted(true); handleSubmit(event, { experimental_attachments: attachments, }); setAttachments([]); setUploadingAttachments([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } } else { toast.error("Please enter a search query or attach an image."); } }; return ( { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSubmit(e); } }} className={` ${hasSubmitted ? 'fixed bottom-4 left-1/2 transform -translate-x-1/2 max-w-[90%] sm:max-w-2xl' : 'max-w-full'} ${attachments.length > 0 || uploadingAttachments.length > 0 ? 'rounded-2xl' : 'rounded-full'} w-full bg-background border border-input overflow-hidden mb-4 transition-all duration-300 ease-in-out z-50 `} >
0 || uploadingAttachments.length > 0 ? 'p-2' : 'p-0'}`}> {(attachments.length > 0 || uploadingAttachments.length > 0) && ( {uploadingAttachments.map((attachment, index) => ( removeUploadingAttachment(index)} isUploading={true} /> ))} {attachments.map((attachment, index) => ( removeAttachment(index)} isUploading={false} /> ))} )}
); }; const SuggestionCards: React.FC = () => { return (
{suggestionCards.slice(1).map((card, index) => ( ))}
); }; return (
{!hasSubmitted && (

MiniPerplx

In search for minimalism and simplicity

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

{message.content}

{message.experimental_attachments?.map((attachment, attachmentIndex) => (
{attachment.contentType!.startsWith('image/') && ( {attachment.name )}
))}
)}
{!isEditingMessage && index === lastUserMessageIndex && ( )}
)} {message.role === 'assistant' && message.content && (

Answer

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

Suggested questions

{suggestedQuestions.map((question, index) => ( ))}
)}
{hasSubmitted && ( )}
); }