/* eslint-disable @next/next/no-img-element */ 'use client'; import { getSuggestions } from '@/lib/actions'; import { cn } from '@/lib/utils'; import { BookCopy, CheckCheck, Copy as CopyIcon, Disc3, ImagesIcon, Layers3, Plus, Sparkles, StopCircle, VideoIcon, Volume2, } from 'lucide-react'; import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'; import { useEffect, useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { useSpeech } from 'react-text-to-speech'; import { Message } from './ChatWindow'; import Copy from './MessageActions/Copy'; import ModelInfoButton from './MessageActions/ModelInfo'; import Rewrite from './MessageActions/Rewrite'; import MessageSources from './MessageSources'; import SearchImages from './SearchImages'; import SearchVideos from './SearchVideos'; import ThinkBox from './ThinkBox'; const ThinkTagProcessor = ({ children }: { children: React.ReactNode }) => { return ; }; const CodeBlock = ({ className, children, }: { className?: string; children: React.ReactNode; }) => { // Extract language from className (format could be "language-javascript" or "lang-javascript") let language = ''; if (className) { if (className.startsWith('language-')) { language = className.replace('language-', ''); } else if (className.startsWith('lang-')) { language = className.replace('lang-', ''); } } const content = children as string; const [isCopied, setIsCopied] = useState(false); const handleCopyCode = () => { navigator.clipboard.writeText(content); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); }; return (
{language}
1} useInlineStyles={true} PreTag="div" > {content}
); }; type TabType = 'text' | 'sources' | 'images' | 'videos'; interface SearchTabsProps { chatHistory: Message[]; query: string; messageId: string; message: Message; isLast: boolean; loading: boolean; rewrite: (messageId: string) => void; sendMessage: ( message: string, options?: { messageId?: string; rewriteIndex?: number; suggestions?: string[]; }, ) => void; } const MessageTabs = ({ chatHistory, query, messageId, message, isLast, loading, rewrite, sendMessage, }: SearchTabsProps) => { const [activeTab, setActiveTab] = useState('text'); const [imageCount, setImageCount] = useState(0); const [videoCount, setVideoCount] = useState(0); const [parsedMessage, setParsedMessage] = useState(message.content); const [speechMessage, setSpeechMessage] = useState(message.content); const [loadingSuggestions, setLoadingSuggestions] = useState(false); const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); // Callback functions to update counts const updateImageCount = (count: number) => { setImageCount(count); }; const updateVideoCount = (count: number) => { setVideoCount(count); }; // Load suggestions handling const handleLoadSuggestions = async () => { if ( loadingSuggestions || (message?.suggestions && message.suggestions.length > 0) ) return; setLoadingSuggestions(true); try { const suggestions = await getSuggestions([...chatHistory, message]); // Update the message.suggestions property through parent component sendMessage('', { messageId: message.messageId, suggestions }); } catch (error) { console.error('Error loading suggestions:', error); } finally { setLoadingSuggestions(false); } }; // Process message content useEffect(() => { const citationRegex = /\[([^\]]+)\]/g; const regex = /\[(\d+)\]/g; let processedMessage = message.content; if (message.role === 'assistant' && message.content.includes('')) { const openThinkTag = processedMessage.match(//g)?.length || 0; const closeThinkTag = processedMessage.match(/<\/think>/g)?.length || 0; if (openThinkTag > closeThinkTag) { processedMessage += ' '; // The extra is to prevent the think component from looking bad } } if ( message.role === 'assistant' && message?.sources && message.sources.length > 0 ) { setParsedMessage( processedMessage.replace( citationRegex, (_, capturedContent: string) => { const numbers = capturedContent .split(',') .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 `${numStr}`; } else { return `[${numStr}]`; } }) .join(''); return linksHtml; }, ), ); setSpeechMessage(message.content.replace(regex, '')); return; } setSpeechMessage(message.content.replace(regex, '')); setParsedMessage(processedMessage); }, [message.content, message.sources, message.role]); // Auto-suggest effect (similar to MessageBox) useEffect(() => { const autoSuggestions = localStorage.getItem('autoSuggestions'); if ( isLast && message.role === 'assistant' && !loading && autoSuggestions === 'true' ) { handleLoadSuggestions(); } }, [isLast, loading, message.role]); // Markdown formatting options const markdownOverrides: MarkdownToJSX.Options = { overrides: { think: { component: ThinkTagProcessor, }, code: { component: ({ className, children }) => { // Check if it's an inline code block or a fenced code block if (className) { // This is a fenced code block (```code```) return {children}; } // This is an inline code block (`code`) return ( {children} ); }, }, pre: { component: ({ children }) => children, }, }, }; return (
{/* Tabs */}
{message.sources && message.sources.length > 0 && ( )}
{/* Tab content */}
{/* Answer Tab */} {activeTab === 'text' && (
{parsedMessage} {loading && isLast ? null : (
{message.modelStats && ( )}
)} {isLast && message.role === 'assistant' && !loading && ( <>

Related

{(!message.suggestions || message.suggestions.length === 0) && ( )}
{message.suggestions && message.suggestions.length > 0 && (
{message.suggestions.map((suggestion, i) => (
{ sendMessage(suggestion); }} className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center" >

{suggestion}

))}
)}
)}
)} {/* Sources Tab */} {activeTab === 'sources' && message.sources && message.sources.length > 0 && (
{message.searchQuery && (
Search query: {' '} {message.searchUrl ? ( {message.searchQuery} ) : ( {message.searchQuery} )}
)}
)} {/* Images Tab */} {activeTab === 'images' && (
)} {/* Videos Tab */} {activeTab === 'videos' && (
)}
); }; export default MessageTabs;