perplexica/ui/components/MessageBox.tsx

192 lines
7.0 KiB
TypeScript
Raw Normal View History

'use client';
2024-04-09 10:51:05 +00:00
/* eslint-disable @next/next/no-img-element */
import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils';
2024-05-18 07:41:15 +00:00
import {
BookCopy,
Disc3,
Share,
Volume2,
StopCircle,
Layers3,
Plus,
} from 'lucide-react';
2024-04-09 10:51:05 +00:00
import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
2024-04-30 09:01:32 +00:00
import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech';
2024-04-09 10:51:05 +00:00
const MessageBox = ({
message,
messageIndex,
history,
loading,
dividerRef,
isLast,
rewrite,
2024-05-18 07:41:15 +00:00
sendMessage,
2024-04-09 10:51:05 +00:00
}: {
message: Message;
messageIndex: number;
history: Message[];
loading: boolean;
dividerRef?: MutableRefObject<HTMLDivElement | null>;
isLast: boolean;
rewrite: (messageId: string) => void;
2024-05-18 07:41:15 +00:00
sendMessage: (message: string) => void;
2024-04-09 10:51:05 +00:00
}) => {
const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
2024-04-09 10:51:05 +00:00
useEffect(() => {
2024-05-04 05:18:42 +00:00
const regex = /\[(\d+)\]/g;
2024-04-09 10:51:05 +00:00
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>`,
),
);
}
2024-05-04 05:18:42 +00:00
setSpeechMessage(message.content.replace(regex, ''));
2024-04-09 10:51:05 +00:00
setParsedMessage(message.content);
}, [message.content, message.sources, message.role]);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
2024-04-09 10:51:05 +00:00
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>
2024-05-04 05:18:42 +00:00
{loading && isLast ? null : (
2024-04-09 10:51:05 +00:00
<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">
2024-05-18 07:41:15 +00:00
{/* <button className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white">
2024-04-09 10:51:05 +00:00
<Share size={18} />
2024-05-18 07:41:15 +00:00
</button> */}
2024-04-09 10:51:05 +00:00
<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} />
)}
2024-04-09 10:51:05 +00:00
</button>
</div>
</div>
)}
2024-05-18 07:41:15 +00:00
{isLast &&
message.suggestions &&
message.suggestions.length > 0 &&
message.role === 'assistant' &&
!loading && (
<>
<div className="h-px w-full bg-[#1C1C1C]" />
<div className="flex flex-col space-y-3 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-[#1C1C1C]" />
<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]" />
</div>
</div>
))}
</div>
</div>
</>
)}
2024-04-09 10:51:05 +00:00
</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)}
/>
2024-04-30 09:01:32 +00:00
<SearchVideos
chat_history={history.slice(0, messageIndex - 1)}
query={history[messageIndex - 1].content}
/>
2024-04-09 10:51:05 +00:00
</div>
</div>
)}
</div>
);
};
export default MessageBox;