import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { DialogState, Memori as IMemori, Tenant, Integration, } from '@memori.ai/memori-api-client/dist/types'; import memoriApiClient from '@memori.ai/memori-api-client'; import MemoriWidget, { Props as WidgetProps, } from './components/MemoriWidget/MemoriWidget'; import { VisemeProvider } from './context/visemeContext'; import { Toaster } from 'react-hot-toast'; import toast from 'react-hot-toast'; import { safeParseJSON } from './helpers/utils'; import i18n from './i18n'; import { useTranslation } from 'react-i18next'; import I18nWrapper from './I18nWrapper'; import { ArtifactProvider } from './components/MemoriArtifactSystem/context/ArtifactContext'; import { version } from './version'; export interface Props { memoriName?: string | null; memoriID?: string | null; ownerUserName?: string | null; ownerUserID?: string | null; integration?: Integration; integrationID?: string; tenantID: string; secretToken?: string; sessionID?: string; layout?: WidgetProps['layout']; customLayout?: WidgetProps['customLayout']; showShare?: boolean; showChatHistory?: boolean; showCopyButton?: boolean; showTranslationOriginal?: boolean; showInputs?: boolean; showDates?: boolean; showContextPerLine?: boolean; showMessageConsumption?: boolean; showSettings?: boolean; __WEBCOMPONENT__?: boolean; showClear?: boolean; showOnlyLastMessages?: boolean; showTypingText?: boolean; showLogin?: boolean; showUpload?: boolean; showReasoning?: boolean; /** When true and layout is WEBSITE_ASSISTANT, hide the 3D avatar in the expanded panel. */ avatar3dHidden?: boolean; height?: number | string; baseURL?: string; apiURL?: string; engineURL?: string; tag?: string; pin?: string; context?: { [key: string]: string }; initialQuestion?: string; uiLang?: 'en' | 'it' | 'fr' | 'es' | 'de' | 'IT' | 'EN' | 'FR' | 'ES' | 'DE'; spokenLang?: string; multilingual?: boolean; authToken?: string; enableAudio?: boolean; defaultSpeakerActive?: boolean; disableTextEnteredEvents?: boolean; onStateChange?: (state?: DialogState) => void; additionalInfo?: WidgetProps['additionalInfo']; customMediaRenderer?: WidgetProps['customMediaRenderer']; additionalSettings?: WidgetProps['additionalSettings']; userAvatar?: WidgetProps['userAvatar']; useMathFormatting?: boolean; autoStart?: boolean; applyVarsToRoot?: boolean; /** Max per-document content length (character count). Kept name for backward compatibility. Default: 300000. */ maxTotalMessagePayload?: number; /** Max characters allowed in the chat textarea. When set, shows a counter (e.g. "0 / 500") and enforces that pasted content + existing text does not exceed this limit. */ maxTextareaCharacters?: number; } const getPreferredLanguages = () => { const browserLanguage = navigator.language; if (browserLanguage) { let lng = browserLanguage.split('-')[0]; if (['en', 'it'].includes(lng)) { return { lng, fallbackLng: lng === 'en' ? 'it' : 'en', }; } } return { lng: 'en', fallbackLng: 'it', }; }; const getParsedContext = (context?: string | null) => { if (!context) return {}; const parsedContext = context?.split(',')?.reduce((acc, cur) => { const [key, value] = cur.split(':').map(t => t.trim()); return { ...acc, [key]: value }; }, {}); return parsedContext; }; const Memori: React.FC = ({ ownerUserName, ownerUserID, memoriName, memoriID, integration, integrationID, tenantID, secretToken, sessionID, layout, customLayout, showShare, showCopyButton = true, showTranslationOriginal = false, showSettings, showTypingText = false, showClear = false, showOnlyLastMessages, showInputs = true, showDates = false, showContextPerLine = false, showMessageConsumption = false, showUpload, showLogin, showReasoning, avatar3dHidden, height = '100%', baseURL, apiURL = 'https://backend.memori.ai', engineURL = 'https://engine.memori.ai', tag, pin, context, initialQuestion, showChatHistory = true, uiLang, spokenLang, multilingual, authToken, enableAudio, defaultSpeakerActive = true, disableTextEnteredEvents = false, onStateChange, additionalInfo, customMediaRenderer, additionalSettings, userAvatar, useMathFormatting = false, autoStart, applyVarsToRoot = false, __WEBCOMPONENT__ = false, maxTotalMessagePayload, maxTextareaCharacters, }) => { const [memori, setMemori] = useState(); const [tenant, setTenant] = useState(); const [provider, setProvider] = useState(); const [sessionId, setSessionId] = useState(); const { t } = useTranslation(); if (!((memoriID && ownerUserID) || (memoriName && ownerUserName))) { throw new Error( 'Identifier pair required: please provide either memoriID and ownerUserID or memoriName and ownerUserName' ); } const client = memoriApiClient(apiURL, engineURL); const fetchSpeechKey = useCallback(async () => { const url = baseURL || (tenantID.startsWith('https://') ? tenantID : `https://${tenantID}`); try { const result = await fetch(`${url}/api/speechkey?tenant=${tenantID}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); const data = await result.json(); if (data.provider) { setProvider(data.provider); } else { console.warn('Provider not found in speech key response'); } } catch (error) { console.error('Error fetching speech key:', error); } }, [baseURL, tenantID]); useEffect(() => { fetchSpeechKey(); }, []); /** * Fetches the Memori data from the backend */ const fetchMemori = useCallback(async () => { if (memoriID && ownerUserID) { const { memori, ...resp } = await client.backend.getMemoriByUserAndId( tenantID, ownerUserID, memoriID ); if (resp.resultCode === 0 && !!memori) { setMemori(memori); } else { console.error('[MEMORI]', resp, memori); } } else if (memoriName && ownerUserName) { const { memori, ...resp } = await client.backend.getMemori( tenantID, ownerUserName, memoriName ); if (resp.resultCode === 0 && !!memori) { if (!memori.ownerUserID && ownerUserID) { memori.ownerUserID = ownerUserID; } setMemori(memori); } else { console.error('[MEMORI]', resp, memori); } } }, [memoriID, ownerUserID, memoriName, ownerUserName, tenantID]); useEffect(() => { fetchMemori(); }, [fetchMemori, tenantID]); /** * Fetches the Tenant data from the backend */ const fetchTenant = useCallback(async () => { const { tenant, ...resp } = await client.backend.tenant.getTenant(tenantID); if (tenant && resp.resultCode === 0) setTenant(tenant); else console.debug('[TENANT]', resp, tenant); }, [tenantID, apiURL]); useEffect(() => { fetchTenant(); }, [fetchTenant]); useEffect(() => { const targetNode = document.querySelector(`memori-client[memoriname="${memori?.name}"]`) || document.querySelector(`memori-client[memoriid="${memori?.memoriID}"]`) || document.querySelector('memori-client'); if (!targetNode) { return; } const config = { attributes: true, childList: false, subtree: false }; const callback: MutationCallback = (mutationList, _observer) => { for (const mutation of mutationList) { if (mutation.type === 'attributes') { const target = mutation.target.nodeName === 'MEMORI-CLIENT' ? mutation.target : mutation.target.parentElement; // @ts-ignore const targetSessionId = target.getAttribute('sessionid') || undefined; if (target && targetSessionId) { setSessionId(targetSessionId); } } } }; const observer = new MutationObserver(callback); observer.observe(targetNode, config); return () => { observer.disconnect(); }; }, []); /** * Sets the language in the i18n instance */ useEffect(() => { if (uiLang) { // @ts-ignore i18n.changeLanguage(uiLang.toLowerCase()); } else { const { lng, fallbackLng } = getPreferredLanguages(); // @ts-ignore i18n.changeLanguage(lng).catch(() => { // @ts-ignore i18n.changeLanguage(fallbackLng); }); } }, [uiLang]); const layoutIntegration = integration ?? memori?.integrations?.find(i => integrationID ? i.integrationID === integrationID : !!i.publish && i.type === 'LANDING_EXPERIENCE' ); const layoutIntegrationConfig = safeParseJSON( layoutIntegration?.customData ?? '{}' ); const whiteListedDomains = [ tenant?.name, ...(tenant?.aliases || []), ...(layoutIntegrationConfig.whiteListedDomains || []), ]; if (layoutIntegrationConfig?.whiteListedDomains?.length) { // check if we are client side if (typeof window !== 'undefined') { // check if the current domain is in the whiteListedDomains with Regex if ( !whiteListedDomains.some((domain: string) => new RegExp(domain).test(window.location.hostname) ) ) { return null; } } } const ignoreClientAttributes = layoutIntegrationConfig.ignoreClientAttributes ?? false; const clientAttributes = ignoreClientAttributes ? { initialContextVars: getParsedContext( layoutIntegrationConfig.contextVars ), initialQuestion: layoutIntegrationConfig.initialQuestion as | string | undefined, showLogin: memori?.enableDeepThought, memoriLang: memori?.culture?.split('-')?.[0], uiLang, spokenLang, autoStart: layout === 'HIDDEN_CHAT' ? true : layout === 'WEBSITE_ASSISTANT' ? false : autoStart, } : { ...(tag && pin ? { personification: { tag, pin } } : {}), multilingual, showCopyButton, showTranslationOriginal, showSettings, showChatHistory, showShare, showTypingText, showClear, showLogin: showLogin ?? memori?.enableDeepThought, showUpload, showReasoning, showContextPerLine, showMessageConsumption, initialContextVars: context ?? getParsedContext(layoutIntegrationConfig.contextVars), initialQuestion: initialQuestion ?? (layoutIntegrationConfig.initialQuestion as string | undefined), autoStart: layout === 'WEBSITE_ASSISTANT' ? false : autoStart !== undefined ? autoStart : layout === 'HIDDEN_CHAT' ? true : autoStart, enableAudio, defaultSpeakerActive, useMathFormatting, memoriLang: memori?.culture?.split('-')?.[0], uiLang, spokenLang, }; const [pulseSent, setPulseSent] = useState(false); const sendPulse = useCallback(() => { if ((memori?.memoriID || memoriID) && !pulseSent) { let origin = window?.location.origin; if ( !origin || origin.includes('localhost') || origin.includes('memori-ai.github.io') ) { setPulseSent(true); return; } setPulseSent(true); fetch('https://pulse.aisuru.com/post', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ pulse: { clientType: __WEBCOMPONENT__ ? 'memori-webcomponent' : 'memori-react', clientVersion: `v${version}`, userAgent: navigator.userAgent, language: navigator.language, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, memoriID: memori?.memoriID ?? memoriID, tenant: tenantID, referrer: origin, }, }), }); } }, [memori?.memoriID, memoriID, tenantID, __WEBCOMPONENT__, pulseSent]); useEffect(() => { sendPulse(); }, [sendPulse]); return ( {memori ? ( ) : (

{t('loading') || 'Loading'}...

)}
); }; Memori.propTypes = { memoriName: PropTypes.string, memoriID: PropTypes.string, ownerUserName: PropTypes.string, ownerUserID: PropTypes.string, integrationID: PropTypes.string, integration: PropTypes.any, tenantID: PropTypes.string.isRequired, secretToken: PropTypes.string, sessionID: PropTypes.string, layout: PropTypes.oneOf([ 'DEFAULT', 'FULLPAGE', 'TOTEM', 'WEBSITE_ASSISTANT', 'CHAT', 'HIDDEN_CHAT', 'ZOOMED_FULL_BODY', ]), customLayout: PropTypes.any, showShare: PropTypes.bool, showCopyButton: PropTypes.bool, showTranslationOriginal: PropTypes.bool, showInputs: PropTypes.bool, showDates: PropTypes.bool, showContextPerLine: PropTypes.bool, showMessageConsumption: PropTypes.bool, showSettings: PropTypes.bool, showClear: PropTypes.bool, showOnlyLastMessages: PropTypes.bool, showTypingText: PropTypes.bool, showLogin: PropTypes.bool, showUpload: PropTypes.bool, showReasoning: PropTypes.bool, avatar3dHidden: PropTypes.bool, height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), baseURL: PropTypes.string, apiURL: PropTypes.string, engineURL: PropTypes.string, tag: PropTypes.string, pin: PropTypes.string, context: PropTypes.objectOf(PropTypes.any), initialQuestion: PropTypes.string, uiLang: PropTypes.oneOf([ 'en', 'it', 'fr', 'es', 'de', 'EN', 'IT', 'FR', 'ES', 'DE', ]), spokenLang: PropTypes.string, multilingual: PropTypes.bool, authToken: PropTypes.string, enableAudio: PropTypes.bool, defaultSpeakerActive: PropTypes.bool, disableTextEnteredEvents: PropTypes.bool, onStateChange: PropTypes.func, additionalInfo: PropTypes.objectOf(PropTypes.any), customMediaRenderer: PropTypes.func, additionalSettings: PropTypes.any, userAvatar: PropTypes.oneOfType([PropTypes.string, PropTypes.any]), useMathFormatting: PropTypes.bool, autoStart: PropTypes.bool, applyVarsToRoot: PropTypes.bool, maxTotalMessagePayload: PropTypes.number, maxTextareaCharacters: PropTypes.number, }; export default Memori;