import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import cx from 'classnames'; import { ExpertReference, Memori, Message, Tenant, User, } from '@memori.ai/memori-api-client/dist/types'; import { Props as MemoriProps } from '../MemoriWidget/MemoriWidget'; import { Transition } from '@headlessui/react'; import { getResourceUrl } from '../../helpers/media'; import UserIcon from '../icons/User'; import AI from '../icons/AI'; import Translation from '../icons/Translation'; import Tooltip from '../ui/Tooltip'; import FeedbackButtons from '../FeedbackButtons/FeedbackButtons'; import { useTranslation } from 'react-i18next'; import Button from '../ui/Button'; import QuestionHelp from '../icons/QuestionHelp'; import Copy from '../icons/Copy'; import Code from '../icons/Code'; import Bug from '../icons/Bug'; import WhyThisAnswer from '../WhyThisAnswer/WhyThisAnswer'; import { stripHTML, stripOutputTags } from '../../helpers/utils'; import { renderMsg, sanitizeMsg, truncateMessage } from '../../helpers/message'; import Expandable from '../ui/Expandable'; import Modal from '../ui/Modal'; import memoriApiClient from '@memori.ai/memori-api-client'; // Always import and load MathJax import { installMathJax } from '../../helpers/utils'; // Import MathJax types declare global { interface Window { MathJax?: { typesetPromise?: (elements: string[]) => Promise; }; } } export interface Props { message: Message; memori: Memori; sessionID: string; tenant?: Tenant; baseUrl?: string; apiUrl?: string; client?: ReturnType; showFeedback?: boolean; showWhyThisAnswer?: boolean; showCopyButton?: boolean; showTranslationOriginal?: boolean; simulateUserPrompt?: (msg: string) => void; showAIicon?: boolean; useMathFormatting?: boolean; isFirst?: boolean; userAvatar?: MemoriProps['userAvatar']; user?: User; experts?: ExpertReference[]; showFunctionCache?: boolean; showReasoning?: boolean; usageHtml?: string; } const ChatBubble: React.FC = ({ message, memori, tenant, baseUrl, apiUrl, client, sessionID, showFeedback, showWhyThisAnswer = true, showCopyButton = true, showTranslationOriginal = false, simulateUserPrompt, showAIicon = true, isFirst = false, useMathFormatting = false, user, userAvatar, experts, showFunctionCache = false, showReasoning = false, usageHtml = '', }) => { const { t, i18n } = useTranslation(); const lang = i18n.language || 'en'; const [showingWhyThisAnswer, setShowingWhyThisAnswer] = useState(false); const [openFunctionCache, setOpenFunctionCache] = useState(false); const [copyFeedback, setCopyFeedback] = useState({ plain: false, raw: false, }); const copyFeedbackTimers = useRef<{ plain: ReturnType | null; raw: ReturnType | null; }>({ plain: null, raw: null, }); // Initialize MathJax on component mount useEffect(() => { if (typeof window !== 'undefined' && !window.MathJax) { installMathJax(); } }, []); // Clean text by removing document_attachment tags before rendering const cleanText = (message.translatedText || message.text).replace( /([\s\S]*?)<\/document_attachment>/g, '' ); const { text: renderedText } = renderMsg( message.fromUser ? truncateMessage(cleanText) : cleanText, useMathFormatting, t('reasoning') || 'Reasoning...', showReasoning ); const plainText = message.fromUser ? sanitizeMsg(truncateMessage(cleanText)) : stripHTML(stripOutputTags(renderedText)); const copyText = message.fromUser ? cleanText : plainText; const shouldShowCopyButtons = showCopyButton && (!!plainText?.length || !!message.text?.length); const shouldShowCopyRawButton = shouldShowCopyButtons && !!message.text?.length && plainText !== message.text; const rawMessageText = sanitizeMsg( message.fromUser ? message.text || '' : (message.text || '').replaceAll(/(.*?)<\/think>/gs, '') ); const copiedLabel = t('copied') || 'Copied'; // Format function cache content const functionCacheData = message.media?.filter( m => m.properties?.functionCache === 'true' ); useLayoutEffect(() => { if (typeof window !== 'undefined' && !message.fromUser) { const timer = setTimeout(() => { if (window.MathJax && window.MathJax.typesetPromise) { try { const elements = document.querySelectorAll( '.memori-chat--bubble-content' ); if (elements.length > 0) { // Salva la posizione di scroll corrente const scrollContainer = document.querySelector( '.memori-chat--history' ); const currentScrollTop = scrollContainer?.scrollTop || 0; const currentScrollHeight = scrollContainer?.scrollHeight || 0; window.MathJax.typesetPromise(['.memori-chat--bubble-content']) .then(() => { // Ripristina la posizione di scroll dopo il rendering MathJax if (scrollContainer) { const newScrollHeight = scrollContainer.scrollHeight; const heightDifference = newScrollHeight - currentScrollHeight; scrollContainer.scrollTop = currentScrollTop + heightDifference; } }) .catch(err => console.error('MathJax typesetting failed:', err) ); } } catch (error) { console.error('Error during MathJax typesetting:', error); } } }, 100); return () => clearTimeout(timer); } }, [cleanText, message.fromUser, renderedText]); useEffect(() => { return () => { ( Object.keys(copyFeedbackTimers.current) as Array<'plain' | 'raw'> ).forEach(key => { const timer = copyFeedbackTimers.current[key]; if (timer) { clearTimeout(timer); copyFeedbackTimers.current[key] = null; } }); }; }, []); const triggerCopyFeedback = (type: 'plain' | 'raw') => { setCopyFeedback(prev => ({ ...prev, [type]: true })); if (copyFeedbackTimers.current[type]) { clearTimeout(copyFeedbackTimers.current[type]!); } copyFeedbackTimers.current[type] = setTimeout(() => { setCopyFeedback(prev => ({ ...prev, [type]: false })); copyFeedbackTimers.current[type] = null; }, 1500); }; const handleCopyClick = (type: 'plain' | 'raw', text: string) => { if (!text?.length) return; if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { navigator.clipboard .writeText(text) .then(() => triggerCopyFeedback(type)) .catch(err => { console.error('Copy failed', err); }); } else { triggerCopyFeedback(type); } }; // Check if initial is a string (status message) or boolean (legacy) const initialStatus = typeof message.initial === 'string' ? message.initial : null; const showInitialDivider = (message.initial === true || isFirst) && !initialStatus; // Check if this is a system error message const isSystemError = message.emitter === 'system'; if (initialStatus) { return (
{initialStatus}
); } // Render system error messages as red warning messages if (isSystemError) { return ( { e.name === message.emitter) ? `${ new URL(apiUrl ?? '/').origin }/api/v1/memoriai/memori/avatar/${ experts.find(e => e.name === message.emitter) ?.expertMemoriID }` : memori.avatarURL && memori.avatarURL.length > 0 ? getResourceUrl({ type: 'avatar', tenantID: tenant?.name, resourceURI: memori.avatarURL, baseURL: baseUrl, apiURL: apiUrl, }) : getResourceUrl({ tenantID: tenant?.name, type: 'avatar', baseURL: baseUrl || 'https://www.aisuru.com', apiURL: apiUrl, }) } onError={e => { // Fallback image handling if primary source fails e.currentTarget.src = memori.avatarURL && memori.avatarURL.length > 0 ? getResourceUrl({ type: 'avatar', tenantID: tenant?.name, resourceURI: memori.avatarURL, baseURL: baseUrl, }) : getResourceUrl({ tenantID: tenant?.name, type: 'avatar', baseURL: baseUrl, }); e.currentTarget.onerror = null; }} />
{sanitizeMsg(cleanText)}
); } return ( <> {showInitialDivider &&
} {!message.fromUser && ( { e.name === message.emitter) ? `${ new URL(apiUrl ?? '/').origin }/api/v1/memoriai/memori/avatar/${ experts.find(e => e.name === message.emitter) ?.expertMemoriID }` : memori.avatarURL && memori.avatarURL.length > 0 ? getResourceUrl({ type: 'avatar', tenantID: tenant?.name, resourceURI: memori.avatarURL, baseURL: baseUrl, apiURL: apiUrl, }) : getResourceUrl({ tenantID: tenant?.name, type: 'avatar', baseURL: baseUrl || 'https://www.aisuru.com', apiURL: apiUrl, }) } onError={e => { // Fallback image handling if primary source fails e.currentTarget.src = memori.avatarURL && memori.avatarURL.length > 0 ? getResourceUrl({ type: 'avatar', tenantID: tenant?.name, resourceURI: memori.avatarURL, baseURL: baseUrl, }) : getResourceUrl({ tenantID: tenant?.name, type: 'avatar', baseURL: baseUrl, }); e.currentTarget.onerror = null; }} /> )} {message.fromUser ? (
) : (
)} {!!usageHtml && (
)} {(shouldShowCopyButtons || (message.generatedByAI && showAIicon) || (message.generatedByAI && showFunctionCache) || (showFeedback && simulateUserPrompt)) && (
{shouldShowCopyButtons && (
)} {message.fromUser && ( <> {(!!userAvatar && typeof userAvatar === 'string') || (!userAvatar && !!user?.avatarURL?.length) ? ( {user?.userName ) : !!userAvatar ? ( {userAvatar} ) : ( )} )} {/* Document attachments are extracted and passed to Chat.tsx for rendering */} {showingWhyThisAnswer && client && ( setShowingWhyThisAnswer(false)} sessionID={sessionID} /> )} setOpenFunctionCache(false)} className="memori-chat--function-cache-modal" > {functionCacheData?.map((f, i) => (
0 ? { marginTop: '1.5rem', paddingTop: '1.5rem', borderTop: '1px solid #e0e0e0', } : { paddingTop: '1.5rem', } } >

{f.title}

              {f.content}
            
))}
); }; export default ChatBubble;