import { useState, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useStore } from '@nanostores/react'; import { Switch } from '@radix-ui/react-switch'; import * as RadixDialog from '@radix-ui/react-dialog'; import { classNames } from '~/utils/classNames'; import { TabManagement } from '~/components/@settings/shared/components/TabManagement'; import { TabTile } from '~/components/@settings/shared/components/TabTile'; import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck'; import { useFeatures } from '~/lib/hooks/useFeatures'; import { useNotifications } from '~/lib/hooks/useNotifications'; import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus'; import { useDebugStatus } from '~/lib/hooks/useDebugStatus'; import { tabConfigurationStore, developerModeStore, setDeveloperMode, resetTabConfiguration, } from '~/lib/stores/settings'; import { profileStore } from '~/lib/stores/profile'; import type { TabType, TabVisibilityConfig, Profile } from './types'; import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants'; import { DialogTitle } from '~/components/ui/Dialog'; import { AvatarDropdown } from './AvatarDropdown'; import BackgroundRays from '~/components/ui/BackgroundRays'; // Import all tab components import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab'; import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab'; import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab'; import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab'; import { DataTab } from '~/components/@settings/tabs/data/DataTab'; import DebugTab from '~/components/@settings/tabs/debug/DebugTab'; import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab'; import UpdateTab from '~/components/@settings/tabs/update/UpdateTab'; import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab'; import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab'; import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab'; import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab'; import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab'; interface ControlPanelProps { open: boolean; onClose: () => void; } interface TabWithDevType extends TabVisibilityConfig { isExtraDevTab?: boolean; } interface ExtendedTabConfig extends TabVisibilityConfig { isExtraDevTab?: boolean; } interface BaseTabConfig { id: TabType; visible: boolean; window: 'user' | 'developer'; order: number; } interface AnimatedSwitchProps { checked: boolean; onCheckedChange: (checked: boolean) => void; id: string; label: string; } const TAB_DESCRIPTIONS: Record<TabType, string> = { profile: 'Manage your profile and account settings', settings: 'Configure application preferences', notifications: 'View and manage your notifications', features: 'Explore new and upcoming features', data: 'Manage your data and storage', 'cloud-providers': 'Configure cloud AI providers and models', 'local-providers': 'Configure local AI providers and models', 'service-status': 'Monitor cloud LLM service status', connection: 'Check connection status and settings', debug: 'Debug tools and system information', 'event-logs': 'View system events and logs', update: 'Check for updates and release notes', 'task-manager': 'Monitor system resources and processes', 'tab-management': 'Configure visible tabs and their order', }; // Beta status for experimental features const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']); const BetaLabel = () => ( <div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20"> <span className="text-[10px] font-medium text-purple-600 dark:text-purple-400">BETA</span> </div> ); const AnimatedSwitch = ({ checked, onCheckedChange, id, label }: AnimatedSwitchProps) => { return ( <div className="flex items-center gap-2"> <Switch id={id} checked={checked} onCheckedChange={onCheckedChange} className={classNames( 'relative inline-flex h-6 w-11 items-center rounded-full', 'transition-all duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)]', 'bg-gray-200 dark:bg-gray-700', 'data-[state=checked]:bg-purple-500', 'focus:outline-none focus:ring-2 focus:ring-purple-500/20', 'cursor-pointer', 'group', )} > <motion.span className={classNames( 'absolute left-[2px] top-[2px]', 'inline-block h-5 w-5 rounded-full', 'bg-white shadow-lg', 'transition-shadow duration-300', 'group-hover:shadow-md group-active:shadow-sm', 'group-hover:scale-95 group-active:scale-90', )} initial={false} transition={{ type: 'spring', stiffness: 500, damping: 30, duration: 0.2, }} animate={{ x: checked ? '1.25rem' : '0rem', }} > <motion.div className="absolute inset-0 rounded-full bg-white" initial={false} animate={{ scale: checked ? 1 : 0.8, }} transition={{ duration: 0.2 }} /> </motion.span> <span className="sr-only">Toggle {label}</span> </Switch> <div className="flex items-center gap-2"> <label htmlFor={id} className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]" > {label} </label> </div> </div> ); }; export const ControlPanel = ({ open, onClose }: ControlPanelProps) => { // State const [activeTab, setActiveTab] = useState<TabType | null>(null); const [loadingTab, setLoadingTab] = useState<TabType | null>(null); const [showTabManagement, setShowTabManagement] = useState(false); // Store values const tabConfiguration = useStore(tabConfigurationStore); const developerMode = useStore(developerModeStore); const profile = useStore(profileStore) as Profile; // Status hooks const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck(); const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures(); const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications(); const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus(); const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus(); // Memoize the base tab configurations to avoid recalculation const baseTabConfig = useMemo(() => { return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab])); }, []); // Add visibleTabs logic using useMemo with optimized calculations const visibleTabs = useMemo(() => { if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) { console.warn('Invalid tab configuration, resetting to defaults'); resetTabConfiguration(); return []; } const notificationsDisabled = profile?.preferences?.notifications === false; // In developer mode, show ALL tabs without restrictions if (developerMode) { const seenTabs = new Set<TabType>(); const devTabs: ExtendedTabConfig[] = []; // Process tabs in order of priority: developer, user, default const processTab = (tab: BaseTabConfig) => { if (!seenTabs.has(tab.id)) { seenTabs.add(tab.id); devTabs.push({ id: tab.id, visible: true, window: 'developer', order: tab.order || devTabs.length, }); } }; // Process tabs in priority order tabConfiguration.developerTabs?.forEach((tab) => processTab(tab as BaseTabConfig)); tabConfiguration.userTabs.forEach((tab) => processTab(tab as BaseTabConfig)); DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig)); // Add Tab Management tile devTabs.push({ id: 'tab-management' as TabType, visible: true, window: 'developer', order: devTabs.length, isExtraDevTab: true, }); return devTabs.sort((a, b) => a.order - b.order); } // Optimize user mode tab filtering return tabConfiguration.userTabs .filter((tab) => { if (!tab?.id) { return false; } if (tab.id === 'notifications' && notificationsDisabled) { return false; } return tab.visible && tab.window === 'user'; }) .sort((a, b) => a.order - b.order); }, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]); // Optimize animation performance with layout animations const gridLayoutVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.05, delayChildren: 0.1, }, }, }; const itemVariants = { hidden: { opacity: 0, scale: 0.8 }, visible: { opacity: 1, scale: 1, transition: { type: 'spring', stiffness: 200, damping: 20, mass: 0.6, }, }, }; // Reset to default view when modal opens/closes useEffect(() => { if (!open) { // Reset when closing setActiveTab(null); setLoadingTab(null); setShowTabManagement(false); } else { // When opening, set to null to show the main view setActiveTab(null); } }, [open]); // Handle closing const handleClose = () => { setActiveTab(null); setLoadingTab(null); setShowTabManagement(false); onClose(); }; // Handlers const handleBack = () => { if (showTabManagement) { setShowTabManagement(false); } else if (activeTab) { setActiveTab(null); } }; const handleDeveloperModeChange = (checked: boolean) => { console.log('Developer mode changed:', checked); setDeveloperMode(checked); }; // Add effect to log developer mode changes useEffect(() => { console.log('Current developer mode:', developerMode); }, [developerMode]); const getTabComponent = (tabId: TabType | 'tab-management') => { if (tabId === 'tab-management') { return <TabManagement />; } switch (tabId) { case 'profile': return <ProfileTab />; case 'settings': return <SettingsTab />; case 'notifications': return <NotificationsTab />; case 'features': return <FeaturesTab />; case 'data': return <DataTab />; case 'cloud-providers': return <CloudProvidersTab />; case 'local-providers': return <LocalProvidersTab />; case 'connection': return <ConnectionsTab />; case 'debug': return <DebugTab />; case 'event-logs': return <EventLogsTab />; case 'update': return <UpdateTab />; case 'task-manager': return <TaskManagerTab />; case 'service-status': return <ServiceStatusTab />; default: return null; } }; const getTabUpdateStatus = (tabId: TabType): boolean => { switch (tabId) { case 'update': return hasUpdate; case 'features': return hasNewFeatures; case 'notifications': return hasUnreadNotifications; case 'connection': return hasConnectionIssues; case 'debug': return hasActiveWarnings; default: return false; } }; const getStatusMessage = (tabId: TabType): string => { switch (tabId) { case 'update': return `New update available (v${currentVersion})`; case 'features': return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`; case 'notifications': return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`; case 'connection': return currentIssue === 'disconnected' ? 'Connection lost' : currentIssue === 'high-latency' ? 'High latency detected' : 'Connection issues detected'; case 'debug': { const warnings = activeIssues.filter((i) => i.type === 'warning').length; const errors = activeIssues.filter((i) => i.type === 'error').length; return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`; } default: return ''; } }; const handleTabClick = (tabId: TabType) => { setLoadingTab(tabId); setActiveTab(tabId); setShowTabManagement(false); // Acknowledge notifications based on tab switch (tabId) { case 'update': acknowledgeUpdate(); break; case 'features': acknowledgeAllFeatures(); break; case 'notifications': markAllAsRead(); break; case 'connection': acknowledgeIssue(); break; case 'debug': acknowledgeAllIssues(); break; } // Clear loading state after a delay setTimeout(() => setLoadingTab(null), 500); }; return ( <RadixDialog.Root open={open}> <RadixDialog.Portal> <div className="fixed inset-0 flex items-center justify-center z-[100] modern-scrollbar"> <RadixDialog.Overlay asChild> <motion.div className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} /> </RadixDialog.Overlay> <RadixDialog.Content aria-describedby={undefined} onEscapeKeyDown={handleClose} onPointerDownOutside={handleClose} className="relative z-[101]" > <motion.div className={classNames( 'w-[1200px] h-[90vh]', 'bg-[#FAFAFA] dark:bg-[#0A0A0A]', 'rounded-2xl shadow-2xl', 'border border-[#E5E5E5] dark:border-[#1A1A1A]', 'flex flex-col overflow-hidden', 'relative', )} initial={{ opacity: 0, scale: 0.95, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 20 }} transition={{ duration: 0.2 }} > <div className="absolute inset-0 overflow-hidden rounded-2xl"> <BackgroundRays /> </div> <div className="relative z-10 flex flex-col h-full"> {/* Header */} <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center space-x-4"> {(activeTab || showTabManagement) && ( <button onClick={handleBack} className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200" > <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" /> </button> )} <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white"> {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'} </DialogTitle> </div> <div className="flex items-center gap-6"> {/* Mode Toggle */} <div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6"> <AnimatedSwitch id="developer-mode" checked={developerMode} onCheckedChange={handleDeveloperModeChange} label={developerMode ? 'Developer Mode' : 'User Mode'} /> </div> {/* Avatar and Dropdown */} <div className="border-l border-gray-200 dark:border-gray-800 pl-6"> <AvatarDropdown onSelectTab={handleTabClick} /> </div> {/* Close Button */} <button onClick={handleClose} className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200" > <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" /> </button> </div> </div> {/* Content */} <div className={classNames( 'flex-1', 'overflow-y-auto', 'hover:overflow-y-auto', 'scrollbar scrollbar-w-2', 'scrollbar-track-transparent', 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]', 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]', 'will-change-scroll', 'touch-auto', )} > <motion.div key={activeTab || 'home'} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} className="p-6" > {showTabManagement ? ( <TabManagement /> ) : activeTab ? ( getTabComponent(activeTab) ) : ( <motion.div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative" variants={gridLayoutVariants} initial="hidden" animate="visible" > <AnimatePresence mode="popLayout"> {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => ( <motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]"> <TabTile tab={tab} onClick={() => handleTabClick(tab.id as TabType)} isActive={activeTab === tab.id} hasUpdate={getTabUpdateStatus(tab.id)} statusMessage={getStatusMessage(tab.id)} description={TAB_DESCRIPTIONS[tab.id]} isLoading={loadingTab === tab.id} className="h-full relative" > {BETA_TABS.has(tab.id) && <BetaLabel />} </TabTile> </motion.div> ))} </AnimatePresence> </motion.div> )} </motion.div> </div> </div> </motion.div> </RadixDialog.Content> </div> </RadixDialog.Portal> </RadixDialog.Root> ); };