/* 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 && (
)}
)}
{/* Images Tab */}
{activeTab === 'images' && (
)}
{/* Videos Tab */}
{activeTab === 'videos' && (
)}
);
};
export default MessageTabs;