|
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 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', |
|
}; |
|
|
|
|
|
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) => { |
|
|
|
const [activeTab, setActiveTab] = useState<TabType | null>(null); |
|
const [loadingTab, setLoadingTab] = useState<TabType | null>(null); |
|
const [showTabManagement, setShowTabManagement] = useState(false); |
|
|
|
|
|
const tabConfiguration = useStore(tabConfigurationStore); |
|
const developerMode = useStore(developerModeStore); |
|
const profile = useStore(profileStore) as Profile; |
|
|
|
|
|
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(); |
|
|
|
|
|
const baseTabConfig = useMemo(() => { |
|
return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab])); |
|
}, []); |
|
|
|
|
|
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; |
|
|
|
|
|
if (developerMode) { |
|
const seenTabs = new Set<TabType>(); |
|
const devTabs: ExtendedTabConfig[] = []; |
|
|
|
|
|
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, |
|
}); |
|
} |
|
}; |
|
|
|
|
|
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)); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
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]); |
|
|
|
|
|
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, |
|
}, |
|
}, |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
if (!open) { |
|
|
|
setActiveTab(null); |
|
setLoadingTab(null); |
|
setShowTabManagement(false); |
|
} else { |
|
|
|
setActiveTab(null); |
|
} |
|
}, [open]); |
|
|
|
|
|
const handleClose = () => { |
|
setActiveTab(null); |
|
setLoadingTab(null); |
|
setShowTabManagement(false); |
|
onClose(); |
|
}; |
|
|
|
|
|
const handleBack = () => { |
|
if (showTabManagement) { |
|
setShowTabManagement(false); |
|
} else if (activeTab) { |
|
setActiveTab(null); |
|
} |
|
}; |
|
|
|
const handleDeveloperModeChange = (checked: boolean) => { |
|
console.log('Developer mode changed:', checked); |
|
setDeveloperMode(checked); |
|
}; |
|
|
|
|
|
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); |
|
|
|
|
|
switch (tabId) { |
|
case 'update': |
|
acknowledgeUpdate(); |
|
break; |
|
case 'features': |
|
acknowledgeAllFeatures(); |
|
break; |
|
case 'notifications': |
|
markAllAsRead(); |
|
break; |
|
case 'connection': |
|
acknowledgeIssue(); |
|
break; |
|
case 'debug': |
|
acknowledgeAllIssues(); |
|
break; |
|
} |
|
|
|
|
|
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> |
|
); |
|
}; |
|
|