Perplexica/src/components/MessageBox.tsx

331 lines
12 KiB
TypeScript
Raw Normal View History

'use client';
2024-04-09 16:21:05 +05:30
/* eslint-disable @next/next/no-img-element */
import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils';
import { getSuggestions } from '@/lib/actions';
2024-05-18 13:11:15 +05:30
import {
BookCopy,
Disc3,
Volume2,
StopCircle,
Layers3,
Plus,
Sparkles,
2024-05-18 13:11:15 +05:30
} from 'lucide-react';
2025-03-20 10:56:03 +05:30
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
2024-04-09 16:21:05 +05:30
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
import ModelInfoButton from './MessageActions/ModelInfo';
2024-04-09 16:21:05 +05:30
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
2024-04-30 14:31:32 +05:30
import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech';
2025-03-20 10:56:03 +05:30
import ThinkBox from './ThinkBox';
const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => {
return <ThinkBox content={children as string} />;
};
2024-04-09 16:21:05 +05:30
const MessageBox = ({
message,
messageIndex,
history,
loading,
dividerRef,
isLast,
rewrite,
2024-05-18 13:11:15 +05:30
sendMessage,
2024-04-09 16:21:05 +05:30
}: {
message: Message;
messageIndex: number;
history: Message[];
loading: boolean;
dividerRef?: MutableRefObject<HTMLDivElement | null>;
isLast: boolean;
rewrite: (messageId: string) => void;
sendMessage: (
message: string,
options?: {
messageId?: string;
rewriteIndex?: number;
suggestions?: string[];
},
) => void;
2024-04-09 16:21:05 +05:30
}) => {
const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
const [autoSuggestions, setAutoSuggestions] = useState(
localStorage.getItem('autoSuggestions'),
);
const handleLoadSuggestions = async () => {
if (
loadingSuggestions ||
(message?.suggestions && message.suggestions.length > 0)
)
return;
setLoadingSuggestions(true);
try {
const suggestions = await getSuggestions([...history]);
// We need to update the message.suggestions property through parent component
sendMessage('', { messageId: message.messageId, suggestions });
} catch (error) {
console.error('Error loading suggestions:', error);
} finally {
setLoadingSuggestions(false);
}
};
2024-04-09 16:21:05 +05:30
useEffect(() => {
const citationRegex = /\[([^\]]+)\]/g;
2024-05-04 10:48:42 +05:30
const regex = /\[(\d+)\]/g;
2025-03-20 10:56:03 +05:30
let processedMessage = message.content;
if (message.role === 'assistant' && message.content.includes('<think>')) {
const openThinkTag = processedMessage.match(/<think>/g)?.length || 0;
const closeThinkTag = processedMessage.match(/<\/think>/g)?.length || 0;
if (openThinkTag > closeThinkTag) {
processedMessage += '</think> <a> </a>'; // The extra <a> </a> is to prevent the the think component from looking bad
}
}
2024-05-04 10:48:42 +05:30
2024-04-09 16:21:05 +05:30
if (
message.role === 'assistant' &&
message?.sources &&
message.sources.length > 0
) {
2025-03-20 10:56:03 +05:30
setParsedMessage(
processedMessage.replace(
citationRegex,
(_, capturedContent: string) => {
const numbers = capturedContent
.split(',')
2025-04-06 13:48:58 +05:30
.map((numStr) => numStr.trim());
const linksHtml = numbers
.map((numStr) => {
const number = parseInt(numStr);
if (isNaN(number) || number <= 0) {
return `[${numStr}]`;
}
const source = message.sources?.[number - 1];
const url = source?.metadata?.url;
if (url) {
return `<a href="${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">${numStr}</a>`;
} else {
return `[${numStr}]`;
}
})
.join('');
return linksHtml;
},
2024-04-09 16:21:05 +05:30
),
);
2025-04-08 22:16:27 -07:00
setSpeechMessage(message.content.replace(regex, ''));
2025-03-20 10:56:03 +05:30
return;
2024-04-09 16:21:05 +05:30
}
2024-05-04 10:48:42 +05:30
setSpeechMessage(message.content.replace(regex, ''));
2025-03-20 10:56:03 +05:30
setParsedMessage(processedMessage);
2024-04-09 16:21:05 +05:30
}, [message.content, message.sources, message.role]);
useEffect(() => {
const handleStorageChange = () => {
setAutoSuggestions(localStorage.getItem('autoSuggestions'));
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
2025-03-20 10:56:03 +05:30
const markdownOverrides: MarkdownToJSX.Options = {
overrides: {
think: {
component: ThinkTagProcessor,
},
},
};
2024-04-09 16:21:05 +05:30
return (
<div>
{message.role === 'user' && (
2025-03-14 22:05:07 +05:30
<div
className={cn(
'w-full',
messageIndex === 0 ? 'pt-16' : 'pt-8',
'break-words',
)}
>
2024-05-24 20:29:49 +08:00
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
2024-04-09 16:21:05 +05:30
{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">
2024-05-24 20:29:49 +08:00
<BookCopy className="text-black dark:text-white" size={20} />
<h3 className="text-black dark:text-white font-medium text-xl">
Sources
</h3>
2024-04-09 16:21:05 +05:30
</div>
<MessageSources sources={message.sources} />
</div>
)}
<div className="flex flex-col space-y-2">
{' '}
2024-04-09 16:21:05 +05:30
<div className="flex flex-row items-center space-x-2">
<Disc3
className={cn(
2024-05-24 20:29:49 +08:00
'text-black dark:text-white',
2024-04-09 16:21:05 +05:30
isLast && loading ? 'animate-spin' : 'animate-none',
)}
size={20}
/>
2024-05-24 20:29:49 +08:00
<h3 className="text-black dark:text-white font-medium text-xl">
Answer
</h3>
{message.modelStats && (
<ModelInfoButton modelStats={message.modelStats} />
)}
2024-04-09 16:21:05 +05:30
</div>
<Markdown
className={cn(
2024-12-05 20:19:41 +05:30
'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',
)}
2025-03-20 10:56:03 +05:30
options={markdownOverrides}
>
2024-04-09 16:21:05 +05:30
{parsedMessage}
</Markdown>
2024-05-04 10:48:42 +05:30
{loading && isLast ? null : (
2024-05-24 20:29:49 +08:00
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
2024-04-09 16:21:05 +05:30
<div className="flex flex-row items-center space-x-1">
2024-05-27 11:49:09 +08:00
{/* <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">
2024-04-09 16:21:05 +05:30
<Share size={18} />
2024-05-18 13:11:15 +05:30
</button> */}
2024-06-29 11:09:51 +05:30
<Rewrite rewrite={rewrite} messageId={message.messageId} />
2024-04-09 16:21:05 +05:30
</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();
}
}}
2024-05-27 11:49:09 +08:00
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} />
)}
2024-04-09 16:21:05 +05:30
</button>
</div>
</div>
)}
{isLast && 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>{' '}
{(!autoSuggestions || autoSuggestions === 'false') &&
(!message.suggestions ||
message.suggestions.length === 0) ? (
<div className="bg-light-secondary dark:bg-dark-secondary">
<button
onClick={handleLoadSuggestions}
disabled={loadingSuggestions}
className="px-4 py-2 flex flex-row items-center justify-center space-x-2 rounded-lg bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white"
>
{loadingSuggestions ? (
<div className="w-4 h-4 border-2 border-t-transparent border-gray-400 dark:border-gray-500 rounded-full animate-spin" />
) : (
<Sparkles size={16} />
)}
<span>
{loadingSuggestions
? 'Loading suggestions...'
: 'Load suggestions'}
</span>
</button>
</div>
) : null}
</div>
{message.suggestions && message.suggestions.length > 0 ? (
2024-05-18 13:11:15 +05:30
<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}
>
2024-05-27 11:49:09 +08:00
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
2024-05-18 13:11:15 +05:30
<div
onClick={() => {
sendMessage(suggestion);
2024-05-18 13:11:15 +05:30
}}
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"
/>
2024-05-18 13:11:15 +05:30
</div>
</div>
))}
</div>
) : null}
</div>
</>
)}
2024-04-09 16:21:05 +05:30
</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)}
2025-03-19 13:42:28 +05:30
messageId={message.messageId}
/>
2024-04-30 14:31:32 +05:30
<SearchVideos
chatHistory={history.slice(0, messageIndex - 1)}
2024-04-30 14:31:32 +05:30
query={history[messageIndex - 1].content}
2025-03-19 13:42:28 +05:30
messageId={message.messageId}
2024-04-30 14:31:32 +05:30
/>
2024-04-09 16:21:05 +05:30
</div>
</div>
)}
</div>
);
};
export default MessageBox;