import React, { useCallback, useEffect, useMemo, useState } from 'react'; import cx from 'classnames'; import { Memori, Message, Tenant, Venue, User, } from '@memori.ai/memori-api-client/dist/types'; import Button from '../ui/Button'; import Dropdown from '../ui/Dropdown'; import MapMarker from '../icons/MapMarker'; import SoundDeactivated from '../icons/SoundDeactivated'; import Sound from '../icons/Sound'; import { useTranslation } from 'react-i18next'; import Setting from '../icons/Setting'; import ShareButton from '../ShareButton/ShareButton'; import FullscreenExit from '../icons/FullscreenExit'; import Fullscreen from '../icons/Fullscreen'; import Refresh from '../icons/Refresh'; import Clear from '../icons/Clear'; import DeepThought from '../icons/DeepThought'; import Group from '../icons/Group'; import UserIcon from '../icons/User'; import MessageIcon from '../icons/Message'; import GasStation from '../icons/GasStation'; import Logout from '../icons/Logout'; import { getErrori18nKey } from '../../helpers/error'; import toast from 'react-hot-toast'; import memoriApiClient from '@memori.ai/memori-api-client'; import { Props as WidgetProps } from '../MemoriWidget/MemoriWidget'; import { BADGE_EMOJI } from '../../helpers/llmUsage'; const imgMimeTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/gif']; export interface Props { className?: string; memori: Memori; tenant?: Tenant; history: Message[]; position?: Venue; setShowPositionDrawer: (show: boolean) => void; setShowSettingsDrawer: (show: boolean) => void; setShowChatHistoryDrawer: (show: boolean) => void; setShowKnownFactsDrawer: (show: boolean) => void; setShowExpertsDrawer: (show: boolean) => void; enableAudio?: boolean; speakerMuted: boolean; setSpeakerMuted: (mute: boolean) => void; hasUserActivatedSpeak?: boolean; showShare?: boolean; showSettings?: boolean; showChatHistory?: boolean; showReload?: boolean; showClear?: boolean; showLogin?: boolean; setShowLoginDrawer: (show: boolean) => void; clearHistory: () => void; loginToken?: string; user?: User; sessionID?: string; baseUrl?: string; fullScreenHandler?: (e: React.MouseEvent) => void; onLogout?: () => void; apiClient: ReturnType; layout?: WidgetProps['layout']; additionalSettings?: WidgetProps['additionalSettings']; showMessageConsumption?: boolean; } const Header: React.FC = ({ className, memori, tenant, history, position, setShowPositionDrawer, setShowSettingsDrawer, setShowChatHistoryDrawer, setShowKnownFactsDrawer, setShowExpertsDrawer, enableAudio = true, speakerMuted, setSpeakerMuted, hasUserActivatedSpeak = false, showShare = true, showSettings = true, showReload = false, showClear = false, showLogin = true, setShowLoginDrawer, clearHistory, loginToken, user, sessionID, showChatHistory = true, fullScreenHandler, baseUrl, onLogout, apiClient, layout, additionalSettings, showMessageConsumption = false, }) => { const { t, i18n } = useTranslation(); const { uploadAsset, pwlUpdateUser } = apiClient.backend; const [fullScreenAvailable, setFullScreenAvailable] = useState(false); const [fullScreen, setFullScreen] = useState(false); type ImpactMetricType = 'energy' | 'co2' | 'water'; type LlmUsageEnergyImpact = { energy?: number | { source?: string; parsedValue?: number }; gwp?: number | { source?: string; parsedValue?: number }; wcf?: number | { source?: string; parsedValue?: number }; }; const getMetricValue = ( metric?: number | { source?: string; parsedValue?: number }, ): number | undefined => { if (typeof metric === 'number' && Number.isFinite(metric)) return metric; if (!metric || typeof metric !== 'object') return undefined; if ( typeof metric.parsedValue === 'number' && Number.isFinite(metric.parsedValue) ) { return metric.parsedValue; } if (typeof metric.source === 'string') { const parsed = Number(metric.source); if (Number.isFinite(parsed)) return parsed; } return undefined; }; const formatMetricValue = (value: number, locale: string): string => new Intl.NumberFormat(locale, { minimumFractionDigits: 0, maximumFractionDigits: Math.abs(value) >= 1 ? 3 : 4, }).format(value); const formatImpactInReadableUnit = ( value: number, metricType: ImpactMetricType, locale: string, ): string => { const absValue = Math.abs(value); if (metricType === 'energy') { if (absValue >= 1) return `${formatMetricValue(value, locale)} kWh`; const wh = value * 1000; if (Math.abs(wh) >= 1) return `${formatMetricValue(wh, locale)} Wh`; return `${formatMetricValue(wh * 1000, locale)} mWh`; } if (metricType === 'co2') { if (absValue >= 1) return `${formatMetricValue(value, locale)} kg`; const g = value * 1000; if (Math.abs(g) >= 1) return `${formatMetricValue(g, locale)} g`; return `${formatMetricValue(g * 1000, locale)} mg`; } if (absValue >= 1) return `${formatMetricValue(value, locale)} L`; const ml = value * 1000; if (Math.abs(ml) >= 1) return `${formatMetricValue(ml, locale)} mL`; return `${formatMetricValue(ml * 1000, locale)} μL`; }; const currentLocale = i18n.language || navigator.language || 'en'; const chatLog = useMemo(() => ({ lines: history }), [history]); const sustainabilityTotals = useMemo(() => { const totals = { energy: 0, gwp: 0, wcf: 0 }; (chatLog?.lines ?? []).forEach(line => { const energyImpact = (line as Message & { llmUsage?: { energyImpact?: LlmUsageEnergyImpact }; }).llmUsage?.energyImpact; if (!energyImpact) return; totals.energy += getMetricValue(energyImpact.energy) ?? 0; totals.gwp += getMetricValue(energyImpact.gwp) ?? 0; totals.wcf += getMetricValue(energyImpact.wcf) ?? 0; }); return totals; }, [chatLog]); const hasSustainabilityData = useMemo( () => (chatLog?.lines ?? []).some( line => !!(line as Message & { llmUsage?: { energyImpact?: LlmUsageEnergyImpact } }) .llmUsage?.energyImpact ), [chatLog] ); useEffect(() => { if (document.fullscreenEnabled) { setFullScreenAvailable(true); } }, []); // Helper function to determine if settings drawer has content const hasSettingsContent = useCallback(( layout?: WidgetProps['layout'], additionalSettings?: WidgetProps['additionalSettings'] ): boolean => { return ( layout === 'TOTEM' || (additionalSettings && Object.keys(additionalSettings).length > 0) || false ); }, [layout, additionalSettings]); const hasSpacedButtons = layout === 'FULLPAGE' || layout === 'CHAT' || layout === 'ZOOMED_FULL_BODY'; const updateAvatar = async (avatar: any) => { if (avatar && loginToken) { const reader = new FileReader(); reader.onload = async e => { try { const { asset: avatarAsset, ...resp } = await uploadAsset( avatar.name ?? 'avatar', e.target?.result as string, loginToken ?? '' ); if (resp.resultCode !== 0) { console.error('[updateAvatar] Upload failed:', resp); toast.error(t(getErrori18nKey(resp.resultCode))); } else if (avatarAsset) { let newUser: Partial = { userID: user?.userID, avatarURL: avatarAsset.assetURL, }; const { user: patchedUser, ...resp } = await pwlUpdateUser( loginToken ?? '', user?.userID ?? '', newUser ); } } catch (e) { let err = e as Error; console.error('[updateAvatar] Error:', err); if (err?.message) toast.error(err.message); } }; reader.readAsDataURL(avatar as Blob); } else { console.error('[updateAvatar] Missing avatar or login token', { avatar, loginToken, }); toast.error(t('login.avatarUploadError')); } }; return (
{memori.needsPosition && position && (
{position.latitude !== 0 && position.longitude !== 0 && ( {position.placeName} )}
)} {showReload && (
) : (