mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-05-01 00:32:38 +00:00
Made optimization mode persist between page refreshes. Added mode switcher to chat so it can be changed while researching.
213 lines
8.0 KiB
TypeScript
213 lines
8.0 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,
|
|
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,
|
|
isCompact,
|
|
sendMessage,
|
|
}: {
|
|
message: Message;
|
|
messageIndex: number;
|
|
history: Message[];
|
|
loading: boolean;
|
|
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
|
isLast: boolean;
|
|
rewrite: (messageId: string) => void;
|
|
isCompact: boolean;
|
|
sendMessage: (
|
|
message: string,
|
|
messageId?: string,
|
|
options?: { isCompact?: boolean },
|
|
) => 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 });
|
|
|
|
const handleSuggestionClick = (suggestion: string) => {
|
|
sendMessage(suggestion, undefined, { isCompact });
|
|
};
|
|
|
|
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={() => {
|
|
handleSuggestionClick(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;
|