146 lines
5.3 KiB
TypeScript
146 lines
5.3 KiB
TypeScript
'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, Share, Volume2, StopCircle } 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,
|
|
}: {
|
|
message: Message;
|
|
messageIndex: number;
|
|
history: Message[];
|
|
loading: boolean;
|
|
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
|
isLast: boolean;
|
|
rewrite: (messageId: 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-[#1C1C1C] px-1 rounded ml-1 no-underline text-xs 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-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-white" size={20} />
|
|
<h3 className="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-white',
|
|
isLast && loading ? 'animate-spin' : 'animate-none',
|
|
)}
|
|
size={20}
|
|
/>
|
|
<h3 className="text-white font-medium text-xl">Answer</h3>
|
|
</div>
|
|
<Markdown className="prose max-w-none break-words prose-invert prose-p:leading-relaxed prose-pre:p-0 text-white text-sm md:text-base font-medium">
|
|
{parsedMessage}
|
|
</Markdown>
|
|
{loading && isLast ? null : (
|
|
<div className="flex flex-row items-center justify-between w-full text-white py-4 -mx-2">
|
|
<div className="flex flex-row items-center space-x-1">
|
|
<button className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white">
|
|
<Share size={18} />
|
|
</button>
|
|
<Rewrite rewrite={rewrite} messageId={message.id} />
|
|
</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-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
|
|
>
|
|
{speechStatus === 'started' ? (
|
|
<StopCircle size={18} />
|
|
) : (
|
|
<Volume2 size={18} />
|
|
)}
|
|
</button>
|
|
</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}
|
|
chat_history={history.slice(0, messageIndex - 1)}
|
|
/>
|
|
<SearchVideos
|
|
chat_history={history.slice(0, messageIndex - 1)}
|
|
query={history[messageIndex - 1].content}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MessageBox;
|