'use client'; import styles from './HomeSidebar.module.css'; import { useEffect, useState, Suspense } from 'react'; import { SidebarChild, SidebarChildVO, } from '@/app/home/components/home-sidebar/HomeSidebarChild'; import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList'; import langbotIcon from '@/app/assets/langbot-logo.webp'; import { systemInfo, httpClient } from '@/app/infra/http/HttpClient'; import { getCloudServiceClientSync } from '@/app/infra/http'; import { useTranslation } from 'react-i18next'; import { Moon, Sun, Monitor, CircleHelp, Lightbulb, LogOut, KeyRound, Loader2, } from 'lucide-react'; import { useTheme } from 'next-themes'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { LanguageSelector } from '@/components/ui/language-selector'; import { Badge } from '@/components/ui/badge'; import AccountSettingsDialog from '@/app/home/components/account-settings-dialog/AccountSettingsDialog'; import ApiIntegrationDialog from '@/app/home/components/api-integration-dialog/ApiIntegrationDialog'; import NewVersionDialog from '@/app/home/components/new-version-dialog/NewVersionDialog'; import ModelsDialog from '@/app/home/components/models-dialog/ModelsDialog'; import { GitHubRelease } from '@/app/infra/http/CloudServiceClient'; // Compare two version strings, returns true if v1 > v2 function compareVersions(v1: string, v2: string): boolean { // Remove 'v' prefix if present const clean1 = v1.replace(/^v/, ''); const clean2 = v2.replace(/^v/, ''); const parts1 = clean1.split('.').map((p) => parseInt(p, 10) || 0); const parts2 = clean2.split('.').map((p) => parseInt(p, 10) || 0); const maxLen = Math.max(parts1.length, parts2.length); for (let i = 0; i < maxLen; i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 > p2) return true; if (p1 < p2) return false; } return false; } // TODO 侧边导航栏要加动画 function HomeSidebarContent({ onSelectedChangeAction, }: { onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void; }) { // 路由相关 const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); // 路由被动变化时处理 useEffect(() => { handleRouteChange(pathname); }, [pathname]); // 检查 URL 参数,自动打开模型对话框 useEffect(() => { if (searchParams.get('action') === 'showModelSettings') { setModelsDialogOpen(true); } if (searchParams.get('action') === 'showAccountSettings') { setAccountSettingsOpen(true); } if (searchParams.get('action') === 'showApiIntegrationSettings') { setApiKeyDialogOpen(true); } }, [searchParams]); const [selectedChild, setSelectedChild] = useState(); const { theme, setTheme } = useTheme(); const { t } = useTranslation(); const [popoverOpen, setPopoverOpen] = useState(false); const [accountSettingsOpen, setAccountSettingsOpen] = useState(false); const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false); const [languageSelectorOpen, setLanguageSelectorOpen] = useState(false); const [starCount, setStarCount] = useState(null); const [latestRelease, setLatestRelease] = useState( null, ); const [hasNewVersion, setHasNewVersion] = useState(false); const [versionDialogOpen, setVersionDialogOpen] = useState(false); const [modelsDialogOpen, setModelsDialogOpen] = useState(false); const [userEmail, setUserEmail] = useState(''); // 处理模型对话框的打开和关闭,同时更新 URL function handleModelsDialogChange(open: boolean) { setModelsDialogOpen(open); if (open) { const params = new URLSearchParams(searchParams.toString()); params.set('action', 'showModelSettings'); router.replace(`${pathname}?${params.toString()}`, { scroll: false }); } else { const params = new URLSearchParams(searchParams.toString()); params.delete('action'); const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname; router.replace(newUrl, { scroll: false }); } } // 处理账户设置对话框的打开和关闭,同时更新 URL function handleAccountSettingsChange(open: boolean) { setAccountSettingsOpen(open); if (open) { const params = new URLSearchParams(searchParams.toString()); params.set('action', 'showAccountSettings'); router.replace(`${pathname}?${params.toString()}`, { scroll: false }); } else { const params = new URLSearchParams(searchParams.toString()); params.delete('action'); const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname; router.replace(newUrl, { scroll: false }); } } useEffect(() => { initSelect(); if (!localStorage.getItem('token')) { localStorage.setItem('token', 'test-token'); localStorage.setItem('userEmail', 'test@example.com'); } // Load user email const storedEmail = localStorage.getItem('userEmail'); if (storedEmail) { setUserEmail(storedEmail); } else { // Fetch from API if not in localStorage httpClient .getUserInfo() .then((info) => { setUserEmail(info.user); localStorage.setItem('userEmail', info.user); }) .catch(() => {}); } getCloudServiceClientSync() .get('/api/v1/dist/info/repo') .then((response) => { const data = response as { repo: { stargazers_count: number } }; setStarCount(data.repo.stargazers_count); }) .catch((error) => { console.error('Failed to fetch GitHub star count:', error); }); // Fetch releases to check for new version getCloudServiceClientSync() .getLangBotReleases() .then((releases) => { if (releases && releases.length > 0) { // Find the latest non-prerelease, non-draft release const latestStable = releases.find((r) => !r.prerelease && !r.draft); const latest = latestStable || releases[0]; setLatestRelease(latest); // Compare versions const currentVersion = systemInfo?.version; if (currentVersion && latest.tag_name) { const isNewer = compareVersions(latest.tag_name, currentVersion); setHasNewVersion(isNewer); } } }) .catch((error) => { console.error('Failed to fetch releases:', error); }); }, []); function handleChildClick(child: SidebarChildVO) { setSelectedChild(child); handleRoute(child); onSelectedChangeAction(child); } function initSelect() { // 根据当前路径选择对应的菜单项 const currentPath = pathname; const matchedChild = sidebarConfigList.find( (childConfig) => childConfig.route === currentPath, ); if (matchedChild) { handleChildClick(matchedChild); } else { // 如果没有匹配的路径,则默认选择第一个 handleChildClick(sidebarConfigList[0]); } } function handleRoute(child: SidebarChildVO) { router.push(`${child.route}`); } function handleRouteChange(pathname: string) { // TODO 这段逻辑并不好,未来router封装好后改掉 // 判断在home下,并且路由更改的是自己的路由子组件则更新UI const routeList = pathname.split('/'); if ( routeList[1] === 'home' && sidebarConfigList.find((childConfig) => childConfig.route === pathname) ) { const routeSelectChild = sidebarConfigList.find( (childConfig) => childConfig.route === pathname, ); if (routeSelectChild) { setSelectedChild(routeSelectChild); } } } function handleLogout() { localStorage.removeItem('token'); localStorage.removeItem('userEmail'); window.location.href = '/login'; } return (
{/* LangBot、ICON区域 */}
{/* icon */} langbot-icon {/* 文字 */}
LangBot
{systemInfo?.version}
{hasNewVersion && ( setVersionDialogOpen(true)} className="bg-red-500 hover:bg-red-600 text-white text-[0.6rem] px-1.5 py-0 h-4 cursor-pointer" > {t('plugins.new')} )}
{/* 菜单列表,后期可升级成配置驱动 */}
{sidebarConfigList.map((config) => { return (
{ handleChildClick(config); }} > {}} isSelected={ selectedChild !== undefined && selectedChild.id === config.id } icon={config.icon} name={config.name} />
); })}
{starCount !== null && (
{ window.open('https://github.com/langbot-app/LangBot', '_blank'); }} className="flex justify-center cursor-pointer p-2 rounded-lg transition-colors" >
{starCount.toLocaleString()}
)} handleModelsDialogChange(true)} isSelected={false} icon={ } name={t('models.title')} /> { // 防止语言选择器打开时关闭popover if (!open && languageSelectorOpen) return; setPopoverOpen(open); }} > {}} isSelected={false} icon={ } name={t('common.accountOptions')} />
{ handleAccountSettingsChange(true); setPopoverOpen(false); }} >
{userEmail ? userEmail.charAt(0).toUpperCase() : 'U'}
{userEmail || t('account.settings')}
); } function SidebarLoadingFallback() { return (
); } export default function HomeSidebar({ onSelectedChangeAction, }: { onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void; }) { return ( }> ); }