Spaces:
Running
Running
| 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]"> | |
| <RadixDialog.Overlay asChild> | |
| <motion.div | |
| className="absolute inset-0 bg-black/50 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> | |
| ); | |
| }; | |