/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useContext, useEffect, useState, useRef } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { UserContext } from '../../context/User/index.js'; import { useSetTheme, useTheme } from '../../context/Theme/index.js'; import { useTranslation } from 'react-i18next'; import { API, getLogo, getSystemName, showSuccess, stringToColor } from '../../helpers/index.js'; import fireworks from 'react-fireworks'; import { CN, GB } from 'country-flag-icons/react/3x2'; import NoticeModal from './NoticeModal.js'; import { IconClose, IconMenu, IconLanguage, IconChevronDown, IconSun, IconMoon, IconExit, IconUserSetting, IconCreditCard, IconKey, IconBell, } from '@douyinfe/semi-icons'; import { Avatar, Button, Dropdown, Tag, Typography, Skeleton, Badge, } from '@douyinfe/semi-ui'; import { StatusContext } from '../../context/Status/index.js'; import { useIsMobile } from '../../hooks/common/useIsMobile.js'; import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed.js'; import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime.js'; const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { const { t, i18n } = useTranslation(); const [userState, userDispatch] = useContext(UserContext); const [statusState, statusDispatch] = useContext(StatusContext); const isMobile = useIsMobile(); const [collapsed, toggleCollapsed] = useSidebarCollapsed(); const [logoLoaded, setLogoLoaded] = useState(false); let navigate = useNavigate(); const [currentLang, setCurrentLang] = useState(i18n.language); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const location = useLocation(); const [noticeVisible, setNoticeVisible] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const loading = statusState?.status === undefined; const isLoading = useMinimumLoadingTime(loading); const systemName = getSystemName(); const logo = getLogo(); const currentDate = new Date(); const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1; const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false; const docsLink = statusState?.status?.docs_link || ''; const isDemoSiteMode = statusState?.status?.demo_site_enabled || false; const isConsoleRoute = location.pathname.startsWith('/console'); const theme = useTheme(); const setTheme = useSetTheme(); const announcements = statusState?.status?.announcements || []; const getAnnouncementKey = (a) => `${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`; const calculateUnreadCount = () => { if (!announcements.length) return 0; let readKeys = []; try { readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || []; } catch (_) { readKeys = []; } const readSet = new Set(readKeys); return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).length; }; const getUnreadKeys = () => { if (!announcements.length) return []; let readKeys = []; try { readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || []; } catch (_) { readKeys = []; } const readSet = new Set(readKeys); return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).map(getAnnouncementKey); }; useEffect(() => { setUnreadCount(calculateUnreadCount()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [announcements]); const mainNavLinks = [ { text: t('首页'), itemKey: 'home', to: '/', }, { text: t('控制台'), itemKey: 'console', to: '/console', }, { text: t('模型广场'), itemKey: 'pricing', to: '/pricing', }, ...(docsLink ? [ { text: t('文档'), itemKey: 'docs', isExternal: true, externalLink: docsLink, }, ] : []), { text: t('关于'), itemKey: 'about', to: '/about', }, ]; async function logout() { await API.get('/api/user/logout'); showSuccess(t('注销成功!')); userDispatch({ type: 'logout' }); localStorage.removeItem('user'); navigate('/login'); setMobileMenuOpen(false); } const handleNewYearClick = () => { fireworks.init('root', {}); fireworks.start(); setTimeout(() => { fireworks.stop(); }, 3000); }; const handleNoticeOpen = () => { setNoticeVisible(true); }; const handleNoticeClose = () => { setNoticeVisible(false); if (announcements.length) { let readKeys = []; try { readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || []; } catch (_) { readKeys = []; } const mergedKeys = Array.from(new Set([...readKeys, ...announcements.map(getAnnouncementKey)])); localStorage.setItem('notice_read_keys', JSON.stringify(mergedKeys)); } setUnreadCount(0); }; useEffect(() => { if (theme === 'dark') { document.body.setAttribute('theme-mode', 'dark'); document.documentElement.classList.add('dark'); } else { document.body.removeAttribute('theme-mode'); document.documentElement.classList.remove('dark'); } const iframe = document.querySelector('iframe'); if (iframe) { iframe.contentWindow.postMessage({ themeMode: theme }, '*'); } }, [theme, isNewYear]); useEffect(() => { const handleLanguageChanged = (lng) => { setCurrentLang(lng); const iframe = document.querySelector('iframe'); if (iframe) { iframe.contentWindow.postMessage({ lang: lng }, '*'); } }; i18n.on('languageChanged', handleLanguageChanged); return () => { i18n.off('languageChanged', handleLanguageChanged); }; }, [i18n]); useEffect(() => { setLogoLoaded(false); if (!logo) return; const img = new Image(); img.src = logo; img.onload = () => setLogoLoaded(true); }, [logo]); const handleLanguageChange = (lang) => { i18n.changeLanguage(lang); setMobileMenuOpen(false); }; const handleNavLinkClick = (itemKey) => { if (itemKey === 'home') { // styleDispatch(styleActions.setSider(false)); // This line is removed } setMobileMenuOpen(false); }; const renderNavLinks = (isMobileView = false, isLoading = false) => { if (isLoading) { const skeletonLinkClasses = isMobileView ? 'flex items-center gap-1 p-3 w-full rounded-md' : 'flex items-center gap-1 p-2 rounded-md'; return Array(4) .fill(null) .map((_, index) => (
} />
)); } return mainNavLinks.map((link) => { const commonLinkClasses = isMobileView ? 'flex items-center gap-1 p-3 w-full text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors font-semibold' : 'flex items-center gap-1 p-2 text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors rounded-md font-semibold'; const linkContent = ( {link.text} ); if (link.isExternal) { return ( handleNavLinkClick(link.itemKey)} > {linkContent} ); } let targetPath = link.to; if (link.itemKey === 'console' && !userState.user) { targetPath = '/login'; } return ( handleNavLinkClick(link.itemKey)} > {linkContent} ); }); }; const renderUserArea = () => { if (isLoading) { return (
} />
} />
); } if (userState.user) { return ( { navigate('/console/personal'); setMobileMenuOpen(false); }} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white" >
{t('个人设置')}
{ navigate('/console/token'); setMobileMenuOpen(false); }} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white" >
{t('令牌管理')}
{ navigate('/console/topup'); setMobileMenuOpen(false); }} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white" >
{t('钱包管理')}
{t('退出')}
} >
); } else { const showRegisterButton = !isSelfUseMode; const commonSizingAndLayoutClass = "flex items-center justify-center !py-[10px] !px-1.5"; const loginButtonSpecificStyling = "!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors"; let loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`; let registerButtonClasses = `${commonSizingAndLayoutClass}`; const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5"; const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5"; if (showRegisterButton) { if (isMobile) { loginButtonClasses += " !rounded-full"; } else { loginButtonClasses += " !rounded-l-full !rounded-r-none"; } registerButtonClasses += " !rounded-r-full !rounded-l-none"; } else { loginButtonClasses += " !rounded-full"; } return (
handleNavLinkClick('login')} className="flex"> {showRegisterButton && (
handleNavLinkClick('register')} className="flex -ml-px">
)}
); } }; return (
0 ? 'system' : 'inApp'} unreadKeys={getUnreadKeys()} />
handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
{(isLoading || !logoLoaded) && ( )} logo
} > {systemName} {(isSelfUseMode || isDemoSiteMode) && !isLoading && ( {isSelfUseMode ? t('自用模式') : t('演示站点')} )}
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
{isSelfUseMode ? t('自用模式') : t('演示站点')}
)}
{isNewYear && ( Happy New Year!!! 🎉 } >
); }; export default HeaderBar;