'use client'; /* eslint-disable @next/next/no-img-element */ import React, { MutableRefObject, useEffect, useState } from 'react'; import { Message } from './ChatWindow'; import { cn } from '@/lib/utils'; import { BookCopy, Disc3, Volume2, StopCircle, Layers3, Plus, } from 'lucide-react'; import Markdown from 'markdown-to-jsx'; import Copy from './MessageActions/Copy'; import Rewrite from './MessageActions/Rewrite'; import MessageSources from './MessageSources'; import SearchImages from './SearchImages'; import SearchVideos from './SearchVideos'; import { useSpeech } from 'react-text-to-speech'; const MessageBox = ({ message, messageIndex, history, loading, dividerRef, isLast, rewrite, sendMessage, }: { message: Message; messageIndex: number; history: Message[]; loading: boolean; dividerRef?: MutableRefObject<HTMLDivElement | null>; isLast: boolean; rewrite: (messageId: string) => void; sendMessage: (message: string) => void; }) => { const [parsedMessage, setParsedMessage] = useState(message.content); const [speechMessage, setSpeechMessage] = useState(message.content); useEffect(() => { const regex = /\[(\d+)\]/g; if ( message.role === 'assistant' && message?.sources && message.sources.length > 0 ) { return setParsedMessage( message.content.replace( regex, (_, number) => `<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`, ), ); } setSpeechMessage(message.content.replace(regex, '')); setParsedMessage(message.content); }, [message.content, message.sources, message.role]); const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); return ( <div> {message.role === 'user' && ( <div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}> <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> {message.content} </h2> </div> )} {message.role === 'assistant' && ( <div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9"> <div ref={dividerRef} className="flex flex-col space-y-6 w-full lg:w-9/12" > {message.sources && message.sources.length > 0 && ( <div className="flex flex-col space-y-2"> <div className="flex flex-row items-center space-x-2"> <BookCopy className="text-black dark:text-white" size={20} /> <h3 className="text-black dark:text-white font-medium text-xl"> Sources </h3> </div> <MessageSources sources={message.sources} /> </div> )} <div className="flex flex-col space-y-2"> <div className="flex flex-row items-center space-x-2"> <Disc3 className={cn( 'text-black dark:text-white', isLast && loading ? 'animate-spin' : 'animate-none', )} size={20} /> <h3 className="text-black dark:text-white font-medium text-xl"> Answer </h3> </div> <Markdown className={cn( 'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]', 'max-w-none break-words text-black dark:text-white', )} > {parsedMessage} </Markdown> {loading && isLast ? null : ( <div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2"> <div className="flex flex-row items-center space-x-1"> {/* <button className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black text-black dark:hover:text-white"> <Share size={18} /> </button> */} <Rewrite rewrite={rewrite} messageId={message.messageId} /> </div> <div className="flex flex-row items-center space-x-1"> <Copy initialMessage={message.content} message={message} /> <button onClick={() => { if (speechStatus === 'started') { stop(); } else { start(); } }} className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white" > {speechStatus === 'started' ? ( <StopCircle size={18} /> ) : ( <Volume2 size={18} /> )} </button> </div> </div> )} {isLast && message.suggestions && message.suggestions.length > 0 && message.role === 'assistant' && !loading && ( <> <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> <div className="flex flex-col space-y-3 text-black dark:text-white"> <div className="flex flex-row items-center space-x-2 mt-4"> <Layers3 /> <h3 className="text-xl font-medium">Related</h3> </div> <div className="flex flex-col space-y-3"> {message.suggestions.map((suggestion, i) => ( <div className="flex flex-col space-y-3 text-sm" key={i} > <div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" /> <div onClick={() => { sendMessage(suggestion); }} className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center" > <p className="transition duration-200 hover:text-[#24A0ED]"> {suggestion} </p> <Plus size={20} className="text-[#24A0ED] flex-shrink-0" /> </div> </div> ))} </div> </div> </> )} </div> </div> <div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4"> <SearchImages query={history[messageIndex - 1].content} chatHistory={history.slice(0, messageIndex - 1)} /> <SearchVideos chatHistory={history.slice(0, messageIndex - 1)} query={history[messageIndex - 1].content} /> </div> </div> )} </div> ); }; export default MessageBox;