import { createPortal } from 'react-dom'; import { useEffect, useMemo, useState } from 'react'; import { BadgeCheck, Files, History, KeyRound, LayoutDashboard, LogOut, MessageCircle, MoonStar, Rocket, Settings2, Shield, SunMedium, UserRound, Users, Waypoints, Wrench, } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { PasswordInput } from '../components/PasswordInput'; import { pickLocale } from '../i18n'; import { appZhCn } from '../i18n/app.zh-cn'; import { appEn } from '../i18n/app.en'; import { useAppStore } from '../store/appStore'; import type { SysAuthBootstrap, SysMenuItem } from '../types/sys'; const iconMap: Record = { 'layout-dashboard': LayoutDashboard, waypoints: Waypoints, wrench: Wrench, 'sliders-horizontal': Settings2, files: Files, rocket: Rocket, shield: Shield, users: Users, 'badge-check': BadgeCheck, 'layout-grid': LayoutDashboard, 'message-circle': MessageCircle, 'user-round': UserRound, history: History, 'key-round': KeyRound, }; export function ThemeLocaleSwitches() { const { theme, setTheme, locale, setLocale } = useAppStore(); const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn }); return (
); } export function CompactHeaderSwitches() { const { theme, setTheme, locale, setLocale } = useAppStore(); const isZh = locale === 'zh'; return (
); } type DashboardLoginProps = { username: string; password: string; submitting: boolean; error: string; onUsernameChange: (value: string) => void; onPasswordChange: (value: string) => void; onSubmit: () => void; defaultUsername: string; }; export function DashboardLogin({ username, password, submitting, error, onUsernameChange, onPasswordChange, onSubmit, defaultUsername, }: DashboardLoginProps) { const { theme, locale } = useAppStore(); const passwordToggleLabels = locale === 'zh' ? { show: '显示密码', hide: '隐藏密码' } : { show: 'Show password', hide: 'Hide password' }; return (
Nanobot

Nanobot

{locale === 'zh' ? '用户登录' : 'User Sign In'}

onUsernameChange(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') onSubmit(); }} placeholder={defaultUsername || (locale === 'zh' ? '用户名' : 'Username')} autoFocus /> onPasswordChange(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') onSubmit(); }} placeholder={locale === 'zh' ? '密码' : 'Password'} toggleLabels={passwordToggleLabels} /> {error ?
{error}
: null}
{locale === 'zh' ? `首次初始化默认账号:${defaultUsername || 'admin'}` : `Initial seeded account: ${defaultUsername || 'admin'}`}
); } type SidebarMenuProps = { menus: SysMenuItem[]; activeMenuKey: string; onNavigate: (path: string) => void; }; export function SidebarMenu({ menus, activeMenuKey, onNavigate }: SidebarMenuProps) { const { locale } = useAppStore(); return ( ); } type SidebarAccountPillProps = { authBootstrap: SysAuthBootstrap; isZh: boolean; onOpenProfile: () => void; onLogout: () => void; }; export function SidebarAccountPill({ authBootstrap, isZh, onOpenProfile, onLogout }: SidebarAccountPillProps) { const username = String(authBootstrap.user.display_name || authBootstrap.user.username || ''); const subtitle = authBootstrap.user.role?.name || (isZh ? '账户设置' : 'Account settings'); return (
); } export function isNormalUserRole(authBootstrap: SysAuthBootstrap) { return String(authBootstrap.user.role?.role_key || '').trim().toLowerCase() === 'normal_user'; } export function normalizeAssignedBotTone(enabled?: boolean, dockerStatus?: string) { if (enabled === false) return 'is-disabled'; return String(dockerStatus || '').toUpperCase() === 'RUNNING' ? 'is-running' : 'is-stopped'; } export function useResolvedAssignedBots(authBootstrap: SysAuthBootstrap) { const activeBots = useAppStore((state) => state.activeBots); return useMemo(() => { const assigned = Array.isArray(authBootstrap.assigned_bots) ? authBootstrap.assigned_bots : []; if (assigned.length === 0) return []; const liveBotIds = new Set(Object.keys(activeBots).filter((botId) => String(botId || '').trim())); const preferLiveIntersection = liveBotIds.size > 0; return assigned .filter((item) => !preferLiveIntersection || liveBotIds.has(String(item.id || '').trim())) .map((item) => { const live = activeBots[item.id]; return live ? { id: live.id, name: live.name || item.name, enabled: live.enabled, docker_status: live.docker_status, node_id: live.node_id || item.node_id, node_display_name: live.node_display_name || item.node_display_name || item.node_id, } : item; }) .filter((item) => String(item.id || '').trim()); }, [activeBots, authBootstrap.assigned_bots]); } type BotSwitcherTriggerProps = { authBootstrap: SysAuthBootstrap; isZh: boolean; selectedBotId: string; onSelectBot: (botId: string) => void; className?: string; }; export function BotSwitcherTrigger({ authBootstrap, isZh, selectedBotId, onSelectBot, className, }: BotSwitcherTriggerProps) { const bots = useResolvedAssignedBots(authBootstrap); const theme = useAppStore((state) => state.theme); const [open, setOpen] = useState(false); const selectedBot = bots.find((bot) => bot.id === selectedBotId) || bots[0]; const shortName = String(selectedBot?.name || selectedBot?.id || '').slice(0, 1).toUpperCase() || 'B'; const tone = normalizeAssignedBotTone(selectedBot?.enabled, selectedBot?.docker_status); const canRenderPortal = typeof document !== 'undefined'; useEffect(() => { if (bots.length <= 1) setOpen(false); }, [bots.length]); return (
{open && canRenderPortal ? createPortal(
setOpen(false)}>
event.stopPropagation()}>
{isZh ? '切换 Bot' : 'Switch Bot'} {isZh ? `${bots.length} 个` : `${bots.length}`}
{bots.map((bot) => { const active = bot.id === selectedBot?.id; const itemTone = normalizeAssignedBotTone(bot.enabled, bot.docker_status); return ( ); })}
, document.body, ) : null}
); }