refactor: remove developer mode and related components

add glowing effect component for tab tiles
improve tab tile appearance with new glow effect
add 'none' log level and simplify log level handling
simplify tab configuration store by removing developer tabs
remove useDebugStatus hook and related debug functionality
remove system info endpoints no longer needed
This commit is contained in:
KevIsDev
2025-07-01 14:26:42 +01:00
committed by Roamin
parent 22cb5977be
commit 590363cf6e
23 changed files with 370 additions and 5608 deletions

View File

@@ -36,7 +36,7 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
/>
) : (
<div className="w-full h-full rounded-full flex items-center justify-center bg-white dark:bg-gray-800 text-gray-400 dark:text-gray-500">
<div className="i-ph:question w-6 h-6" />
<div className="i-ph:user w-6 h-6" />
</div>
)}
</motion.button>
@@ -72,7 +72,7 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500 font-medium text-lg">
<span className="relative -top-0.5">?</span>
<div className="i-ph:user w-6 h-6" />
</div>
)}
</div>
@@ -117,24 +117,6 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
</DropdownMenu.Item>
<div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
<DropdownMenu.Item
className={classNames(
'flex items-center gap-2 px-4 py-2.5',
'text-sm text-gray-700 dark:text-gray-200',
'hover:bg-purple-50 dark:hover:bg-purple-500/10',
'hover:text-purple-500 dark:hover:text-purple-400',
'cursor-pointer transition-all duration-200',
'outline-none',
'group',
)}
onClick={() => onSelectTab('task-manager')}
>
<div className="i-ph:activity w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
Task Manager
<BetaLabel />
</DropdownMenu.Item>
<DropdownMenu.Item
className={classNames(
'flex items-center gap-2 px-4 py-2.5',

View File

@@ -1,25 +1,16 @@
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 { tabConfigurationStore, 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 type { TabType, Profile } from './types';
import { TAB_LABELS, DEFAULT_TAB_CONFIG, TAB_DESCRIPTIONS } from './constants';
import { DialogTitle } from '~/components/ui/Dialog';
import { AvatarDropdown } from './AvatarDropdown';
import BackgroundRays from '~/components/ui/BackgroundRays';
@@ -30,14 +21,12 @@ 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';
import McpTab from '~/components/@settings/tabs/mcp/McpTab';
interface ControlPanelProps {
@@ -45,48 +34,8 @@ interface ControlPanelProps {
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',
mcp: 'Configure MCP (Model Context Protocol) servers',
};
// Beta status for experimental features
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
const BETA_TABS = new Set<TabType>(['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">
@@ -94,66 +43,6 @@ const BetaLabel = () => (
</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);
@@ -162,7 +51,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
// Store values
const tabConfiguration = useStore(tabConfigurationStore);
const developerMode = useStore(developerModeStore);
const profile = useStore(profileStore) as Profile;
// Status hooks
@@ -170,7 +58,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
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(() => {
@@ -188,41 +75,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
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) => {
@@ -237,33 +89,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
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,
},
},
};
}, [tabConfiguration, profile?.preferences?.notifications, baseTabConfig]);
// Reset to default view when modal opens/closes
useEffect(() => {
@@ -295,21 +121,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
}
};
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 />;
}
const getTabComponent = (tabId: TabType) => {
switch (tabId) {
case 'profile':
return <ProfileTab />;
@@ -327,14 +139,10 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
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 />;
case 'mcp':
@@ -354,8 +162,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
return hasUnreadNotifications;
case 'connection':
return hasConnectionIssues;
case 'debug':
return hasActiveWarnings;
default:
return false;
}
@@ -375,12 +181,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
: 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 '';
}
@@ -405,9 +205,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
case 'connection':
acknowledgeIssue();
break;
case 'debug':
acknowledgeAllIssues();
break;
}
// Clear loading state after a delay
@@ -418,15 +215,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
<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.Overlay className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm transition-opacity duration-200" />
<RadixDialog.Content
aria-describedby={undefined}
@@ -434,19 +223,17 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
onPointerDownOutside={handleClose}
className="relative z-[101]"
>
<motion.div
<div
className={classNames(
'w-[1200px] h-[90vh]',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
'bg-bolt-elements-background-depth-1',
'rounded-2xl shadow-2xl',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'border border-bolt-elements-borderColor',
'flex flex-col overflow-hidden',
'relative',
'transform transition-all duration-200 ease-out',
open ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-95 translate-y-4',
)}
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 />
@@ -458,7 +245,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
{(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"
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-colors duration-150"
>
<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>
@@ -469,18 +256,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
</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">
<div className="pl-6">
<AvatarDropdown onSelectTab={handleTabClick} />
</div>
@@ -508,28 +285,28 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
'touch-auto',
)}
>
<motion.div
key={activeTab || 'home'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="p-6"
<div
className={classNames(
'p-6 transition-opacity duration-150',
activeTab || showTabManagement ? 'opacity-100' : 'opacity-100',
)}
>
{showTabManagement ? (
<TabManagement />
) : activeTab ? (
{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"
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
{visibleTabs.map((tab, index) => (
<div
key={tab.id}
className={classNames(
'aspect-[1.5/1] transition-transform duration-100 ease-out',
'hover:scale-[1.01]',
)}
style={{
animationDelay: `${index * 30}ms`,
animation: open ? 'fadeInUp 200ms ease-out forwards' : 'none',
}}
>
<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)}
@@ -542,15 +319,14 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</TabTile>
</motion.div>
</div>
))}
</AnimatePresence>
</motion.div>
</div>
)}
</motion.div>
</div>
</div>
</motion.div>
</div>
</div>
</RadixDialog.Content>
</div>
</RadixDialog.Portal>

View File

@@ -10,11 +10,8 @@ export const TAB_ICONS: Record<TabType, string> = {
'local-providers': 'i-ph:desktop-fill',
'service-status': 'i-ph:activity-bold',
connection: 'i-ph:wifi-high-fill',
debug: 'i-ph:bug-fill',
'event-logs': 'i-ph:list-bullets-fill',
update: 'i-ph:arrow-clockwise-fill',
'task-manager': 'i-ph:chart-line-fill',
'tab-management': 'i-ph:squares-four-fill',
mcp: 'i-ph:hard-drives-bold',
};
@@ -28,11 +25,8 @@ export const TAB_LABELS: Record<TabType, string> = {
'local-providers': 'Local Providers',
'service-status': 'Service Status',
connection: 'Connection',
debug: 'Debug',
'event-logs': 'Event Logs',
update: 'Updates',
'task-manager': 'Task Manager',
'tab-management': 'Tab Management',
mcp: 'MCP Servers',
};
@@ -46,11 +40,8 @@ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
'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',
mcp: 'Configure MCP (Model Context Protocol) servers',
};
@@ -61,32 +52,13 @@ export const DEFAULT_TAB_CONFIG = [
{ id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 },
{ id: 'local-providers', visible: true, window: 'user' as const, order: 3 },
{ id: 'connection', visible: true, window: 'user' as const, order: 4 },
{ id: 'connection', visible: true, window: 'user' as const, order: 5 },
{ id: 'notifications', visible: true, window: 'user' as const, order: 6 },
{ id: 'notifications', visible: true, window: 'user' as const, order: 5 },
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
{ id: 'mcp', visible: true, window: 'user' as const, order: 7 },
{ id: 'profile', visible: true, window: 'user' as const, order: 8 },
{ id: 'settings', visible: true, window: 'user' as const, order: 9 },
{ id: 'service-status', visible: true, window: 'user' as const, order: 10 },
{ id: 'update', visible: true, window: 'user' as const, order: 11 },
// User Window Tabs (In dropdown, initially hidden)
{ id: 'profile', visible: false, window: 'user' as const, order: 8 },
{ id: 'settings', visible: false, window: 'user' as const, order: 9 },
{ id: 'task-manager', visible: false, window: 'user' as const, order: 10 },
{ id: 'service-status', visible: false, window: 'user' as const, order: 11 },
// User Window Tabs (Hidden, controlled by TaskManagerTab)
{ id: 'debug', visible: false, window: 'user' as const, order: 12 },
{ id: 'update', visible: false, window: 'user' as const, order: 13 },
// Developer Window Tabs (All visible by default)
{ id: 'features', visible: true, window: 'developer' as const, order: 0 },
{ id: 'data', visible: true, window: 'developer' as const, order: 1 },
{ id: 'cloud-providers', visible: true, window: 'developer' as const, order: 2 },
{ id: 'local-providers', visible: true, window: 'developer' as const, order: 3 },
{ id: 'connection', visible: true, window: 'developer' as const, order: 4 },
{ id: 'notifications', visible: true, window: 'developer' as const, order: 5 },
{ id: 'event-logs', visible: true, window: 'developer' as const, order: 6 },
{ id: 'profile', visible: true, window: 'developer' as const, order: 7 },
{ id: 'settings', visible: true, window: 'developer' as const, order: 8 },
{ id: 'task-manager', visible: true, window: 'developer' as const, order: 9 },
{ id: 'service-status', visible: true, window: 'developer' as const, order: 10 },
{ id: 'debug', visible: true, window: 'developer' as const, order: 11 },
{ id: 'update', visible: true, window: 'developer' as const, order: 12 },
];

View File

@@ -12,11 +12,8 @@ export type TabType =
| 'local-providers'
| 'service-status'
| 'connection'
| 'debug'
| 'event-logs'
| 'update'
| 'task-manager'
| 'tab-management'
| 'mcp';
export type WindowType = 'user' | 'developer';
@@ -64,7 +61,6 @@ export interface UserTabConfig extends TabVisibilityConfig {
export interface TabWindowConfig {
userTabs: UserTabConfig[];
developerTabs: DevTabConfig[];
}
export const TAB_LABELS: Record<TabType, string> = {
@@ -77,11 +73,8 @@ export const TAB_LABELS: Record<TabType, string> = {
'local-providers': 'Local Providers',
'service-status': 'Service Status',
connection: 'Connections',
debug: 'Debug',
'event-logs': 'Event Logs',
update: 'Updates',
'task-manager': 'Task Manager',
'tab-management': 'Tab Management',
mcp: 'MCP Servers',
};

View File

@@ -7,8 +7,6 @@ export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constan
// Shared components
export { TabTile } from './shared/components/TabTile';
export { TabManagement } from './shared/components/TabManagement';
// Utils
export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
export * from './utils/animations';

View File

@@ -1,382 +0,0 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { useStore } from '@nanostores/react';
import { Switch } from '~/components/ui/Switch';
import { classNames } from '~/utils/classNames';
import { tabConfigurationStore } from '~/lib/stores/settings';
import { TAB_LABELS } from '~/components/@settings/core/constants';
import type { TabType } from '~/components/@settings/core/types';
import { toast } from 'react-toastify';
import { TbLayoutGrid } from 'react-icons/tb';
import { useSettingsStore } from '~/lib/stores/settings';
// Define tab icons mapping
const TAB_ICONS: Record<TabType, string> = {
profile: 'i-ph:user-circle-fill',
settings: 'i-ph:gear-six-fill',
notifications: 'i-ph:bell-fill',
features: 'i-ph:star-fill',
data: 'i-ph:database-fill',
'cloud-providers': 'i-ph:cloud-fill',
'local-providers': 'i-ph:desktop-fill',
'service-status': 'i-ph:activity-fill',
connection: 'i-ph:wifi-high-fill',
debug: 'i-ph:bug-fill',
'event-logs': 'i-ph:list-bullets-fill',
update: 'i-ph:arrow-clockwise-fill',
'task-manager': 'i-ph:chart-line-fill',
'tab-management': 'i-ph:squares-four-fill',
mcp: 'i-ph:hard-drives-bold',
};
// Define which tabs are default in user mode
const DEFAULT_USER_TABS: TabType[] = [
'features',
'data',
'cloud-providers',
'local-providers',
'connection',
'notifications',
'event-logs',
'mcp',
];
// Define which tabs can be added to user mode
const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 'service-status', 'debug', 'update'];
// All available tabs for user mode
const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS];
// Define which tabs are beta
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
// Beta label component
const BetaLabel = () => (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-purple-500/10 text-purple-500 font-medium">BETA</span>
);
export const TabManagement = () => {
const [searchQuery, setSearchQuery] = useState('');
const tabConfiguration = useStore(tabConfigurationStore);
const { setSelectedTab } = useSettingsStore();
const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
// Get current tab configuration
const currentTab = tabConfiguration.userTabs.find((tab) => tab.id === tabId);
// If tab doesn't exist in configuration, create it
if (!currentTab) {
const newTab = {
id: tabId,
visible: checked,
window: 'user' as const,
order: tabConfiguration.userTabs.length,
};
const updatedTabs = [...tabConfiguration.userTabs, newTab];
tabConfigurationStore.set({
...tabConfiguration,
userTabs: updatedTabs,
});
toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
return;
}
// Check if tab can be enabled in user mode
const canBeEnabled = DEFAULT_USER_TABS.includes(tabId) || OPTIONAL_USER_TABS.includes(tabId);
if (!canBeEnabled && checked) {
toast.error('This tab cannot be enabled in user mode');
return;
}
// Update tab visibility
const updatedTabs = tabConfiguration.userTabs.map((tab) => {
if (tab.id === tabId) {
return { ...tab, visible: checked };
}
return tab;
});
// Update store
tabConfigurationStore.set({
...tabConfiguration,
userTabs: updatedTabs,
});
// Show success message
toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
};
// Create a map of existing tab configurations
const tabConfigMap = new Map(tabConfiguration.userTabs.map((tab) => [tab.id, tab]));
// Generate the complete list of tabs, including those not in the configuration
const allTabs = ALL_USER_TABS.map((tabId) => {
return (
tabConfigMap.get(tabId) || {
id: tabId,
visible: false,
window: 'user' as const,
order: -1,
}
);
});
// Filter tabs based on search query
const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
useEffect(() => {
// Reset to first tab when component unmounts
return () => {
setSelectedTab('user'); // Reset to user tab when unmounting
};
}, [setSelectedTab]);
return (
<div className="space-y-6">
<motion.div
className="space-y-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* Header */}
<div className="flex items-center justify-between gap-4 mt-8 mb-4">
<div className="flex items-center gap-2">
<div
className={classNames(
'w-8 h-8 flex items-center justify-center rounded-lg',
'bg-bolt-elements-background-depth-3',
'text-purple-500',
)}
>
<TbLayoutGrid className="w-5 h-5" />
</div>
<div>
<h4 className="text-md font-medium text-bolt-elements-textPrimary">Tab Management</h4>
<p className="text-sm text-bolt-elements-textSecondary">Configure visible tabs and their order</p>
</div>
</div>
{/* Search */}
<div className="relative w-64">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<div className="i-ph:magnifying-glass w-4 h-4 text-gray-400" />
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search tabs..."
className={classNames(
'w-full pl-10 pr-4 py-2 rounded-lg',
'bg-bolt-elements-background-depth-2',
'border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary',
'placeholder-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'transition-all duration-200',
)}
/>
</div>
</div>
{/* Tab Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Default Section Header */}
{filteredTabs.some((tab) => DEFAULT_USER_TABS.includes(tab.id)) && (
<div className="col-span-full flex items-center gap-2 mt-4 mb-2">
<div className="i-ph:star-fill w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">Default Tabs</span>
</div>
)}
{/* Default Tabs */}
{filteredTabs
.filter((tab) => DEFAULT_USER_TABS.includes(tab.id))
.map((tab, index) => (
<motion.div
key={tab.id}
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-all duration-200',
'relative overflow-hidden group',
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
whileHover={{ scale: 1.02 }}
>
{/* Status Badges */}
<div className="absolute top-1 right-1.5 flex gap-1">
<span className="px-1.5 py-0.25 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium mr-2">
Default
</span>
</div>
<div className="flex items-start gap-4 p-4">
<motion.div
className={classNames(
'w-10 h-10 flex items-center justify-center rounded-xl',
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
'transition-all duration-200',
tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div
className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
>
<div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
</div>
</motion.div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
{TAB_LABELS[tab.id]}
</h4>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</div>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
{tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
</p>
</div>
<Switch
checked={tab.visible}
onCheckedChange={(checked) => {
const isDisabled =
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id);
if (!isDisabled) {
handleTabVisibilityChange(tab.id, checked);
}
}}
className={classNames('data-[state=checked]:bg-purple-500 ml-4', {
'opacity-50 pointer-events-none':
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
})}
/>
</div>
</div>
</div>
<motion.div
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
animate={{
borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
scale: tab.visible ? 1 : 0.98,
}}
transition={{ duration: 0.2 }}
/>
</motion.div>
))}
{/* Optional Section Header */}
{filteredTabs.some((tab) => OPTIONAL_USER_TABS.includes(tab.id)) && (
<div className="col-span-full flex items-center gap-2 mt-8 mb-2">
<div className="i-ph:plus-circle-fill w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">Optional Tabs</span>
</div>
)}
{/* Optional Tabs */}
{filteredTabs
.filter((tab) => OPTIONAL_USER_TABS.includes(tab.id))
.map((tab, index) => (
<motion.div
key={tab.id}
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-all duration-200',
'relative overflow-hidden group',
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
whileHover={{ scale: 1.02 }}
>
{/* Status Badges */}
<div className="absolute top-1 right-1.5 flex gap-1">
<span className="px-1.5 py-0.25 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium mr-2">
Optional
</span>
</div>
<div className="flex items-start gap-4 p-4">
<motion.div
className={classNames(
'w-10 h-10 flex items-center justify-center rounded-xl',
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
'transition-all duration-200',
tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div
className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
>
<div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
</div>
</motion.div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
{TAB_LABELS[tab.id]}
</h4>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</div>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
{tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
</p>
</div>
<Switch
checked={tab.visible}
onCheckedChange={(checked) => {
const isDisabled =
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id);
if (!isDisabled) {
handleTabVisibilityChange(tab.id, checked);
}
}}
className={classNames('data-[state=checked]:bg-purple-500 ml-4', {
'opacity-50 pointer-events-none':
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
})}
/>
</div>
</div>
</div>
<motion.div
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
animate={{
borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
scale: tab.visible ? 1 : 0.98,
}}
transition={{ duration: 0.2 }}
/>
</motion.div>
))}
</div>
</motion.div>
</div>
);
};

View File

@@ -1,8 +1,8 @@
import { motion } from 'framer-motion';
import * as Tooltip from '@radix-ui/react-tooltip';
import { classNames } from '~/utils/classNames';
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
import { GlowingEffect } from '~/components/ui/GlowingEffect';
interface TabTileProps {
tab: TabVisibilityConfig;
@@ -28,28 +28,35 @@ export const TabTile: React.FC<TabTileProps> = ({
children,
}: TabTileProps) => {
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Provider delayDuration={0}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<motion.div
<div className={classNames('min-h-[160px] list-none', className || '')}>
<div className="relative h-full rounded-xl border border-[#E5E5E5] dark:border-[#333333] p-0.5">
<GlowingEffect
blur={0}
borderWidth={1}
spread={20}
glow={true}
disabled={false}
proximity={40}
inactiveZone={0.3}
movementDuration={0.4}
/>
<div
onClick={onClick}
className={classNames(
'relative flex flex-col items-center p-6 rounded-xl',
'w-full h-full min-h-[160px]',
'relative flex flex-col items-center justify-center h-full p-4 rounded-lg',
'bg-white dark:bg-[#141414]',
'border border-[#E5E5E5] dark:border-[#333333]',
'group',
'group cursor-pointer',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'hover:border-purple-200 dark:hover:border-purple-900/30',
isActive ? 'border-purple-500 dark:border-purple-500/50 bg-purple-500/5 dark:bg-purple-500/10' : '',
isLoading ? 'cursor-wait opacity-70' : '',
className || '',
'transition-colors duration-100 ease-out',
isActive ? 'bg-purple-500/5 dark:bg-purple-500/10' : '',
isLoading ? 'cursor-wait opacity-70 pointer-events-none' : '',
)}
>
{/* Main Content */}
<div className="flex flex-col items-center justify-center flex-1 w-full">
{/* Icon */}
<motion.div
<div
className={classNames(
'relative',
'w-14 h-14',
@@ -59,27 +66,30 @@ export const TabTile: React.FC<TabTileProps> = ({
'ring-1 ring-gray-200 dark:ring-gray-700',
'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
'transition-all duration-100 ease-out',
isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
)}
>
<motion.div
<div
className={classNames(
TAB_ICONS[tab.id],
'w-8 h-8',
'text-gray-600 dark:text-gray-300',
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
'transition-colors duration-100 ease-out',
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
)}
/>
</motion.div>
</div>
{/* Label and Description */}
<div className="flex flex-col items-center mt-5 w-full">
<div className="flex flex-col items-center mt-4 w-full">
<h3
className={classNames(
'text-[15px] font-medium leading-snug mb-2',
'text-gray-700 dark:text-gray-200',
'group-hover:text-purple-600 dark:group-hover:text-purple-300/90',
'transition-colors duration-100 ease-out',
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
)}
>
@@ -93,6 +103,7 @@ export const TabTile: React.FC<TabTileProps> = ({
'max-w-[85%]',
'text-center',
'group-hover:text-purple-500 dark:group-hover:text-purple-400/70',
'transition-colors duration-100 ease-out',
isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
)}
>
@@ -100,7 +111,6 @@ export const TabTile: React.FC<TabTileProps> = ({
</p>
)}
</div>
</div>
{/* Update Indicator with Tooltip */}
{hasUpdate && (
@@ -127,7 +137,9 @@ export const TabTile: React.FC<TabTileProps> = ({
{/* Children (e.g. Beta Label) */}
{children}
</motion.div>
</div>
</div>
</div>
</Tooltip.Trigger>
</Tooltip.Root>
</Tooltip.Provider>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +0,0 @@
import type { Variants } from 'framer-motion';
export const fadeIn: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
export const slideIn: Variants = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
};
export const scaleIn: Variants = {
initial: { opacity: 0, scale: 0.8 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0.8 },
};
export const tabAnimation: Variants = {
initial: { opacity: 0, scale: 0.8, y: 20 },
animate: { opacity: 1, scale: 1, y: 0 },
exit: { opacity: 0, scale: 0.8, y: -20 },
};
export const overlayAnimation: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
export const modalAnimation: Variants = {
initial: { opacity: 0, scale: 0.95, y: 20 },
animate: { opacity: 1, scale: 1, y: 0 },
exit: { opacity: 0, scale: 0.95, y: 20 },
};
export const transition = {
duration: 0.2,
};

View File

@@ -1,9 +1,8 @@
import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types';
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
export const getVisibleTabs = (
tabConfiguration: { userTabs: TabVisibilityConfig[]; developerTabs?: TabVisibilityConfig[] },
isDeveloperMode: boolean,
tabConfiguration: { userTabs: TabVisibilityConfig[] },
notificationsEnabled: boolean,
): TabVisibilityConfig[] => {
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
@@ -11,35 +10,6 @@ export const getVisibleTabs = (
return DEFAULT_TAB_CONFIG as TabVisibilityConfig[];
}
// In developer mode, show ALL tabs without restrictions
if (isDeveloperMode) {
// Combine all unique tabs from both user and developer configurations
const allTabs = new Set([
...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
...tabConfiguration.userTabs.map((tab) => tab.id),
...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
'task-manager' as TabType, // Always include task-manager in developer mode
]);
// Create a complete tab list with all tabs visible
const devTabs = Array.from(allTabs).map((tabId) => {
// Try to find existing configuration for this tab
const existingTab =
tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
return {
id: tabId as TabType,
visible: true,
window: 'developer' as const,
order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
} as TabVisibilityConfig;
});
return devTabs.sort((a, b) => a.order - b.order);
}
// In user mode, only show visible user tabs
return tabConfiguration.userTabs
.filter((tab) => {
@@ -53,11 +23,6 @@ export const getVisibleTabs = (
return false;
}
// Always show task-manager in user mode if it's configured as visible
if (tab.id === 'task-manager') {
return tab.visible;
}
// Only show tabs that are explicitly visible and assigned to the user window
return tab.visible && tab.window === 'user';
})

View File

@@ -0,0 +1,192 @@
import { memo, useCallback, useEffect, useRef } from 'react';
import { cn } from '~/utils/cn';
import { animate } from 'framer-motion';
interface GlowingEffectProps {
blur?: number;
inactiveZone?: number;
proximity?: number;
spread?: number;
variant?: 'default' | 'white';
glow?: boolean;
className?: string;
disabled?: boolean;
movementDuration?: number;
borderWidth?: number;
}
const GlowingEffect = memo(
({
blur = 0,
inactiveZone = 0.7,
proximity = 0,
spread = 20,
variant = 'default',
glow = false,
className,
movementDuration = 2,
borderWidth = 1,
disabled = true,
}: GlowingEffectProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const lastPosition = useRef({ x: 0, y: 0 });
const animationFrameRef = useRef<number>(0);
const handleMove = useCallback(
(e?: MouseEvent | { x: number; y: number }) => {
if (!containerRef.current) {
return;
}
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
const element = containerRef.current;
if (!element) {
return;
}
const { left, top, width, height } = element.getBoundingClientRect();
const mouseX = e?.x ?? lastPosition.current.x;
const mouseY = e?.y ?? lastPosition.current.y;
if (e) {
lastPosition.current = { x: mouseX, y: mouseY };
}
const center = [left + width * 0.5, top + height * 0.5];
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1]);
const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone;
if (distanceFromCenter < inactiveRadius) {
element.style.setProperty('--active', '0');
return;
}
const isActive =
mouseX > left - proximity &&
mouseX < left + width + proximity &&
mouseY > top - proximity &&
mouseY < top + height + proximity;
element.style.setProperty('--active', isActive ? '1' : '0');
if (!isActive) {
return;
}
const currentAngle = parseFloat(element.style.getPropertyValue('--start')) || 0;
const targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90;
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
const newAngle = currentAngle + angleDiff;
animate(currentAngle, newAngle, {
duration: movementDuration,
ease: [0.16, 1, 0.3, 1],
onUpdate: (value) => {
element.style.setProperty('--start', String(value));
},
});
});
},
[inactiveZone, proximity, movementDuration],
);
useEffect(() => {
if (disabled) {
return undefined;
}
const handleScroll = () => handleMove();
const handlePointerMove = (e: PointerEvent) => handleMove(e);
window.addEventListener('scroll', handleScroll, { passive: true });
document.body.addEventListener('pointermove', handlePointerMove, {
passive: true,
});
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
window.removeEventListener('scroll', handleScroll);
document.body.removeEventListener('pointermove', handlePointerMove);
};
}, [handleMove, disabled]);
return (
<>
<div
className={cn(
'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',
glow && 'opacity-100',
variant === 'white' && 'border-white',
disabled && '!block',
)}
/>
<div
ref={containerRef}
style={
{
'--blur': `${blur}px`,
'--spread': spread,
'--start': '0',
'--active': '0',
'--glowingeffect-border-width': `${borderWidth}px`,
'--repeating-conic-gradient-times': '5',
'--gradient':
variant === 'white'
? `repeating-conic-gradient(
from 236.84deg at 50% 50%,
var(--black),
var(--black) calc(25% / var(--repeating-conic-gradient-times))
)`
: `radial-gradient(circle, #9333ea 10%, #9333ea00 20%),
radial-gradient(circle at 40% 40%, #a855f7 5%, #a855f700 15%),
radial-gradient(circle at 60% 60%, #8b5cf6 10%, #8b5cf600 20%),
radial-gradient(circle at 40% 60%, #f63bdd 10%, #3b82f600 20%),
repeating-conic-gradient(
from 236.84deg at 50% 50%,
#9333ea 0%,
#a855f7 calc(25% / var(--repeating-conic-gradient-times)),
#8b5cf6 calc(50% / var(--repeating-conic-gradient-times)),
#f63bdd calc(75% / var(--repeating-conic-gradient-times)),
#9333ea calc(100% / var(--repeating-conic-gradient-times))
)`,
} as React.CSSProperties
}
className={cn(
'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',
glow && 'opacity-100',
blur > 0 && 'blur-[var(--blur)] ',
className,
disabled && '!hidden',
)}
>
<div
className={cn(
'glow',
'rounded-[inherit]',
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]',
'after:[border:var(--glowingeffect-border-width)_solid_transparent]',
'after:[background:var(--gradient)] after:[background-attachment:fixed]',
'after:opacity-[var(--active)] after:transition-opacity after:duration-300',
'after:[mask-clip:padding-box,border-box]',
'after:[mask-composite:intersect]',
'after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',
)}
/>
</div>
</>
);
},
);
GlowingEffect.displayName = 'GlowingEffect';
export { GlowingEffect };

View File

@@ -8,4 +8,3 @@ export { useUpdateCheck } from './useUpdateCheck';
export { useFeatures } from './useFeatures';
export { useNotifications } from './useNotifications';
export { useConnectionStatus } from './useConnectionStatus';
export { useDebugStatus } from './useDebugStatus';

View File

@@ -1,89 +0,0 @@
import { useState, useEffect } from 'react';
import { getDebugStatus, acknowledgeWarning, acknowledgeError, type DebugIssue } from '~/lib/api/debug';
const ACKNOWLEDGED_DEBUG_ISSUES_KEY = 'bolt_acknowledged_debug_issues';
const getAcknowledgedIssues = (): string[] => {
try {
const stored = localStorage.getItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
};
const setAcknowledgedIssues = (issueIds: string[]) => {
try {
localStorage.setItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY, JSON.stringify(issueIds));
} catch (error) {
console.error('Failed to persist acknowledged debug issues:', error);
}
};
export const useDebugStatus = () => {
const [hasActiveWarnings, setHasActiveWarnings] = useState(false);
const [activeIssues, setActiveIssues] = useState<DebugIssue[]>([]);
const [acknowledgedIssueIds, setAcknowledgedIssueIds] = useState<string[]>(() => getAcknowledgedIssues());
const checkDebugStatus = async () => {
try {
const status = await getDebugStatus();
const issues: DebugIssue[] = [
...status.warnings.map((w) => ({ ...w, type: 'warning' as const })),
...status.errors.map((e) => ({ ...e, type: 'error' as const })),
].filter((issue) => !acknowledgedIssueIds.includes(issue.id));
setActiveIssues(issues);
setHasActiveWarnings(issues.length > 0);
} catch (error) {
console.error('Failed to check debug status:', error);
}
};
useEffect(() => {
// Check immediately and then every 5 seconds
checkDebugStatus();
const interval = setInterval(checkDebugStatus, 5 * 1000);
return () => clearInterval(interval);
}, [acknowledgedIssueIds]);
const acknowledgeIssue = async (issue: DebugIssue) => {
try {
if (issue.type === 'warning') {
await acknowledgeWarning(issue.id);
} else {
await acknowledgeError(issue.id);
}
const newAcknowledgedIds = [...acknowledgedIssueIds, issue.id];
setAcknowledgedIssueIds(newAcknowledgedIds);
setAcknowledgedIssues(newAcknowledgedIds);
setActiveIssues((prev) => prev.filter((i) => i.id !== issue.id));
setHasActiveWarnings(activeIssues.length > 1);
} catch (error) {
console.error('Failed to acknowledge issue:', error);
}
};
const acknowledgeAllIssues = async () => {
try {
await Promise.all(
activeIssues.map((issue) =>
issue.type === 'warning' ? acknowledgeWarning(issue.id) : acknowledgeError(issue.id),
),
);
const newAcknowledgedIds = [...acknowledgedIssueIds, ...activeIssues.map((i) => i.id)];
setAcknowledgedIssueIds(newAcknowledgedIds);
setAcknowledgedIssues(newAcknowledgedIds);
setActiveIssues([]);
setHasActiveWarnings(false);
} catch (error) {
console.error('Failed to acknowledge all issues:', error);
}
};
return { hasActiveWarnings, activeIssues, acknowledgeIssue, acknowledgeAllIssues };
};

View File

@@ -8,7 +8,6 @@ import {
autoSelectStarterTemplate,
enableContextOptimizationStore,
tabConfigurationStore,
updateTabConfiguration as updateTabConfig,
resetTabConfiguration as resetTabConfig,
updateProviderSettings as updateProviderSettingsStore,
updateLatestBranch,
@@ -20,7 +19,7 @@ import {
import { useCallback, useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
import type { TabWindowConfig, TabVisibilityConfig } from '~/components/@settings/core/types';
import type { TabWindowConfig } from '~/components/@settings/core/types';
import { logStore } from '~/lib/stores/logs';
import { getLocalStorage, setLocalStorage } from '~/lib/persistence';
@@ -62,7 +61,6 @@ export interface UseSettingsReturn {
// Tab configuration
tabConfiguration: TabWindowConfig;
updateTabConfiguration: (config: TabVisibilityConfig) => void;
resetTabConfiguration: () => void;
}
@@ -205,7 +203,6 @@ export function useSettings(): UseSettingsReturn {
setTimezone,
settings,
tabConfiguration,
updateTabConfiguration: updateTabConfig,
resetTabConfiguration: resetTabConfig,
};
}

View File

@@ -1,14 +1,8 @@
import { atom, map } from 'nanostores';
import { PROVIDER_LIST } from '~/utils/constants';
import type { IProviderConfig } from '~/types/model';
import type {
TabVisibilityConfig,
TabWindowConfig,
UserTabConfig,
DevTabConfig,
} from '~/components/@settings/core/types';
import type { TabVisibilityConfig, TabWindowConfig, UserTabConfig } from '~/components/@settings/core/types';
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
import Cookies from 'js-cookie';
import { toggleTheme } from './theme';
import { create } from 'zustand';
@@ -202,7 +196,6 @@ export const updatePromptId = (id: string) => {
const getInitialTabConfiguration = (): TabWindowConfig => {
const defaultConfig: TabWindowConfig = {
userTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is UserTabConfig => tab.window === 'user'),
developerTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is DevTabConfig => tab.window === 'developer'),
};
if (!isBrowser) {
@@ -218,16 +211,13 @@ const getInitialTabConfiguration = (): TabWindowConfig => {
const parsed = JSON.parse(saved);
if (!parsed?.userTabs || !parsed?.developerTabs) {
if (!parsed?.userTabs) {
return defaultConfig;
}
// Ensure proper typing of loaded configuration
return {
userTabs: parsed.userTabs.filter((tab: TabVisibilityConfig): tab is UserTabConfig => tab.window === 'user'),
developerTabs: parsed.developerTabs.filter(
(tab: TabVisibilityConfig): tab is DevTabConfig => tab.window === 'developer',
),
};
} catch (error) {
console.warn('Failed to parse tab configuration:', error);
@@ -239,60 +229,16 @@ const getInitialTabConfiguration = (): TabWindowConfig => {
export const tabConfigurationStore = map<TabWindowConfig>(getInitialTabConfiguration());
// Helper function to update tab configuration
export const updateTabConfiguration = (config: TabVisibilityConfig) => {
const currentConfig = tabConfigurationStore.get();
console.log('Current tab configuration before update:', currentConfig);
const isUserTab = config.window === 'user';
const targetArray = isUserTab ? 'userTabs' : 'developerTabs';
// Only update the tab in its respective window
const updatedTabs = currentConfig[targetArray].map((tab) => (tab.id === config.id ? { ...config } : tab));
// If tab doesn't exist in this window yet, add it
if (!updatedTabs.find((tab) => tab.id === config.id)) {
updatedTabs.push(config);
}
// Create new config, only updating the target window's tabs
const newConfig: TabWindowConfig = {
...currentConfig,
[targetArray]: updatedTabs,
};
console.log('New tab configuration after update:', newConfig);
tabConfigurationStore.set(newConfig);
Cookies.set('tabConfiguration', JSON.stringify(newConfig), {
expires: 365, // Set cookie to expire in 1 year
path: '/',
sameSite: 'strict',
});
};
// Helper function to reset tab configuration
export const resetTabConfiguration = () => {
const defaultConfig: TabWindowConfig = {
userTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is UserTabConfig => tab.window === 'user'),
developerTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is DevTabConfig => tab.window === 'developer'),
};
tabConfigurationStore.set(defaultConfig);
localStorage.setItem('bolt_tab_configuration', JSON.stringify(defaultConfig));
};
// Developer mode store with persistence
export const developerModeStore = atom<boolean>(initialSettings.developerMode);
export const setDeveloperMode = (value: boolean) => {
developerModeStore.set(value);
if (isBrowser) {
localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
}
};
// First, let's define the SettingsStore interface
interface SettingsStore {
isOpen: boolean;

View File

@@ -10,22 +10,19 @@ export interface TabConfig {
interface TabConfigurationStore {
userTabs: TabConfig[];
developerTabs: TabConfig[];
get: () => { userTabs: TabConfig[]; developerTabs: TabConfig[] };
set: (config: { userTabs: TabConfig[]; developerTabs: TabConfig[] }) => void;
get: () => { userTabs: TabConfig[] };
set: (config: { userTabs: TabConfig[] }) => void;
reset: () => void;
}
const DEFAULT_CONFIG = {
userTabs: [],
developerTabs: [],
};
export const tabConfigurationStore = create<TabConfigurationStore>((set, get) => ({
...DEFAULT_CONFIG,
get: () => ({
userTabs: get().userTabs,
developerTabs: get().developerTabs,
}),
set: (config) => set(config),
reset: () => set(DEFAULT_CONFIG),

View File

@@ -1,135 +0,0 @@
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
import { json } from '@remix-run/cloudflare';
// These are injected by Vite at build time
declare const __APP_VERSION: string;
declare const __PKG_NAME: string;
declare const __PKG_DESCRIPTION: string;
declare const __PKG_LICENSE: string;
declare const __PKG_DEPENDENCIES: Record<string, string>;
declare const __PKG_DEV_DEPENDENCIES: Record<string, string>;
declare const __PKG_PEER_DEPENDENCIES: Record<string, string>;
declare const __PKG_OPTIONAL_DEPENDENCIES: Record<string, string>;
declare const __COMMIT_HASH: string;
declare const __GIT_BRANCH: string;
declare const __GIT_COMMIT_TIME: string;
declare const __GIT_AUTHOR: string;
declare const __GIT_EMAIL: string;
declare const __GIT_REMOTE_URL: string;
declare const __GIT_REPO_NAME: string;
const getGitInfo = () => {
return {
commitHash: __COMMIT_HASH || 'unknown',
branch: __GIT_BRANCH || 'unknown',
commitTime: __GIT_COMMIT_TIME || 'unknown',
author: __GIT_AUTHOR || 'unknown',
email: __GIT_EMAIL || 'unknown',
remoteUrl: __GIT_REMOTE_URL || 'unknown',
repoName: __GIT_REPO_NAME || 'unknown',
};
};
const formatDependencies = (
deps: Record<string, string>,
type: 'production' | 'development' | 'peer' | 'optional',
): Array<{ name: string; version: string; type: string }> => {
return Object.entries(deps || {}).map(([name, version]) => ({
name,
version: version.replace(/^\^|~/, ''),
type,
}));
};
const getAppResponse = () => {
const gitInfo = getGitInfo();
return {
name: __PKG_NAME || 'bolt.diy',
version: __APP_VERSION || '0.1.0',
description: __PKG_DESCRIPTION || 'A DIY LLM interface',
license: __PKG_LICENSE || 'MIT',
environment: 'cloudflare',
gitInfo,
timestamp: new Date().toISOString(),
runtimeInfo: {
nodeVersion: 'cloudflare',
},
dependencies: {
production: formatDependencies(__PKG_DEPENDENCIES, 'production'),
development: formatDependencies(__PKG_DEV_DEPENDENCIES, 'development'),
peer: formatDependencies(__PKG_PEER_DEPENDENCIES, 'peer'),
optional: formatDependencies(__PKG_OPTIONAL_DEPENDENCIES, 'optional'),
},
};
};
export const loader: LoaderFunction = async ({ request: _request }) => {
try {
return json(getAppResponse());
} catch (error) {
console.error('Failed to get webapp info:', error);
return json(
{
name: 'bolt.diy',
version: '0.0.0',
description: 'Error fetching app info',
license: 'MIT',
environment: 'error',
gitInfo: {
commitHash: 'error',
branch: 'unknown',
commitTime: 'unknown',
author: 'unknown',
email: 'unknown',
remoteUrl: 'unknown',
repoName: 'unknown',
},
timestamp: new Date().toISOString(),
runtimeInfo: { nodeVersion: 'unknown' },
dependencies: {
production: [],
development: [],
peer: [],
optional: [],
},
},
{ status: 500 },
);
}
};
export const action = async ({ request: _request }: ActionFunctionArgs) => {
try {
return json(getAppResponse());
} catch (error) {
console.error('Failed to get webapp info:', error);
return json(
{
name: 'bolt.diy',
version: '0.0.0',
description: 'Error fetching app info',
license: 'MIT',
environment: 'error',
gitInfo: {
commitHash: 'error',
branch: 'unknown',
commitTime: 'unknown',
author: 'unknown',
email: 'unknown',
remoteUrl: 'unknown',
repoName: 'unknown',
},
timestamp: new Date().toISOString(),
runtimeInfo: { nodeVersion: 'unknown' },
dependencies: {
production: [],
development: [],
peer: [],
optional: [],
},
},
{ status: 500 },
);
}
};

View File

@@ -1,280 +0,0 @@
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
import { json } from '@remix-run/cloudflare';
// Only import child_process if we're not in a Cloudflare environment
let execSync: any;
try {
// Check if we're in a Node.js environment
if (typeof process !== 'undefined' && process.platform) {
// Using dynamic import to avoid require()
const childProcess = { execSync: null };
execSync = childProcess.execSync;
}
} catch {
// In Cloudflare environment, this will fail, which is expected
console.log('Running in Cloudflare environment, child_process not available');
}
// For development environments, we'll always provide mock data if real data isn't available
const isDevelopment = process.env.NODE_ENV === 'development';
interface SystemMemoryInfo {
total: number;
free: number;
used: number;
percentage: number;
swap?: {
total: number;
free: number;
used: number;
percentage: number;
};
timestamp: string;
error?: string;
}
const getSystemMemoryInfo = (): SystemMemoryInfo => {
try {
// Check if we're in a Cloudflare environment and not in development
if (!execSync && !isDevelopment) {
// Return error for Cloudflare production environment
return {
total: 0,
free: 0,
used: 0,
percentage: 0,
timestamp: new Date().toISOString(),
error: 'System memory information is not available in this environment',
};
}
// If we're in development but not in Node environment, return mock data
if (!execSync && isDevelopment) {
// Return mock data for development
const mockTotal = 16 * 1024 * 1024 * 1024; // 16GB
const mockPercentage = Math.floor(30 + Math.random() * 20); // Random between 30-50%
const mockUsed = Math.floor((mockTotal * mockPercentage) / 100);
const mockFree = mockTotal - mockUsed;
return {
total: mockTotal,
free: mockFree,
used: mockUsed,
percentage: mockPercentage,
swap: {
total: 8 * 1024 * 1024 * 1024, // 8GB
free: 6 * 1024 * 1024 * 1024, // 6GB
used: 2 * 1024 * 1024 * 1024, // 2GB
percentage: 25,
},
timestamp: new Date().toISOString(),
};
}
// Different commands for different operating systems
let memInfo: { total: number; free: number; used: number; percentage: number; swap?: any } = {
total: 0,
free: 0,
used: 0,
percentage: 0,
};
// Check the operating system
const platform = process.platform;
if (platform === 'darwin') {
// macOS
const totalMemory = parseInt(execSync('sysctl -n hw.memsize').toString().trim(), 10);
// Get memory usage using vm_stat
const vmStat = execSync('vm_stat').toString().trim();
const pageSize = 4096; // Default page size on macOS
// Parse vm_stat output
const matches = {
free: /Pages free:\s+(\d+)/.exec(vmStat),
active: /Pages active:\s+(\d+)/.exec(vmStat),
inactive: /Pages inactive:\s+(\d+)/.exec(vmStat),
speculative: /Pages speculative:\s+(\d+)/.exec(vmStat),
wired: /Pages wired down:\s+(\d+)/.exec(vmStat),
compressed: /Pages occupied by compressor:\s+(\d+)/.exec(vmStat),
};
const freePages = parseInt(matches.free?.[1] || '0', 10);
const activePages = parseInt(matches.active?.[1] || '0', 10);
const inactivePages = parseInt(matches.inactive?.[1] || '0', 10);
// Speculative pages are not currently used in calculations, but kept for future reference
const wiredPages = parseInt(matches.wired?.[1] || '0', 10);
const compressedPages = parseInt(matches.compressed?.[1] || '0', 10);
const freeMemory = freePages * pageSize;
const usedMemory = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
memInfo = {
total: totalMemory,
free: freeMemory,
used: usedMemory,
percentage: Math.round((usedMemory / totalMemory) * 100),
};
// Get swap information
try {
const swapInfo = execSync('sysctl -n vm.swapusage').toString().trim();
const swapMatches = {
total: /total = (\d+\.\d+)M/.exec(swapInfo),
used: /used = (\d+\.\d+)M/.exec(swapInfo),
free: /free = (\d+\.\d+)M/.exec(swapInfo),
};
const swapTotal = parseFloat(swapMatches.total?.[1] || '0') * 1024 * 1024;
const swapUsed = parseFloat(swapMatches.used?.[1] || '0') * 1024 * 1024;
const swapFree = parseFloat(swapMatches.free?.[1] || '0') * 1024 * 1024;
memInfo.swap = {
total: swapTotal,
used: swapUsed,
free: swapFree,
percentage: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0,
};
} catch (swapError) {
console.error('Failed to get swap info:', swapError);
}
} else if (platform === 'linux') {
// Linux
const meminfo = execSync('cat /proc/meminfo').toString().trim();
const memTotal = parseInt(/MemTotal:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
// We use memAvailable instead of memFree for more accurate free memory calculation
const memAvailable = parseInt(/MemAvailable:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
/*
* Buffers and cached memory are included in the available memory calculation by the kernel
* so we don't need to calculate them separately
*/
const usedMemory = memTotal - memAvailable;
memInfo = {
total: memTotal,
free: memAvailable,
used: usedMemory,
percentage: Math.round((usedMemory / memTotal) * 100),
};
// Get swap information
const swapTotal = parseInt(/SwapTotal:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
const swapFree = parseInt(/SwapFree:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
const swapUsed = swapTotal - swapFree;
memInfo.swap = {
total: swapTotal,
free: swapFree,
used: swapUsed,
percentage: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0,
};
} else if (platform === 'win32') {
/*
* Windows
* Using PowerShell to get memory information
*/
const memoryInfo = execSync(
'powershell "Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize, FreePhysicalMemory | ConvertTo-Json"',
)
.toString()
.trim();
const memData = JSON.parse(memoryInfo);
const totalMemory = parseInt(memData.TotalVisibleMemorySize, 10) * 1024;
const freeMemory = parseInt(memData.FreePhysicalMemory, 10) * 1024;
const usedMemory = totalMemory - freeMemory;
memInfo = {
total: totalMemory,
free: freeMemory,
used: usedMemory,
percentage: Math.round((usedMemory / totalMemory) * 100),
};
// Get swap (page file) information
try {
const swapInfo = execSync(
"powershell \"Get-CimInstance Win32_PageFileUsage | Measure-Object -Property CurrentUsage, AllocatedBaseSize -Sum | Select-Object @{Name='CurrentUsage';Expression={$_.Sum}}, @{Name='AllocatedBaseSize';Expression={$_.Sum}} | ConvertTo-Json\"",
)
.toString()
.trim();
const swapData = JSON.parse(swapInfo);
const swapTotal = parseInt(swapData.AllocatedBaseSize, 10) * 1024 * 1024;
const swapUsed = parseInt(swapData.CurrentUsage, 10) * 1024 * 1024;
const swapFree = swapTotal - swapUsed;
memInfo.swap = {
total: swapTotal,
free: swapFree,
used: swapUsed,
percentage: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0,
};
} catch (swapError) {
console.error('Failed to get swap info:', swapError);
}
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
return {
...memInfo,
timestamp: new Date().toISOString(),
};
} catch (error) {
console.error('Failed to get system memory info:', error);
return {
total: 0,
free: 0,
used: 0,
percentage: 0,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
export const loader: LoaderFunction = async ({ request: _request }) => {
try {
return json(getSystemMemoryInfo());
} catch (error) {
console.error('Failed to get system memory info:', error);
return json(
{
total: 0,
free: 0,
used: 0,
percentage: 0,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 },
);
}
};
export const action = async ({ request: _request }: ActionFunctionArgs) => {
try {
return json(getSystemMemoryInfo());
} catch (error) {
console.error('Failed to get system memory info:', error);
return json(
{
total: 0,
free: 0,
used: 0,
percentage: 0,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 },
);
}
};

View File

@@ -1,424 +0,0 @@
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
import { json } from '@remix-run/cloudflare';
// Only import child_process if we're not in a Cloudflare environment
let execSync: any;
try {
// Check if we're in a Node.js environment
if (typeof process !== 'undefined' && process.platform) {
// Using dynamic import to avoid require()
const childProcess = { execSync: null };
execSync = childProcess.execSync;
}
} catch {
// In Cloudflare environment, this will fail, which is expected
console.log('Running in Cloudflare environment, child_process not available');
}
// For development environments, we'll always provide mock data if real data isn't available
const isDevelopment = process.env.NODE_ENV === 'development';
interface ProcessInfo {
pid: number;
name: string;
cpu: number;
memory: number;
command?: string;
timestamp: string;
error?: string;
}
const getProcessInfo = (): ProcessInfo[] => {
try {
// If we're in a Cloudflare environment and not in development, return error
if (!execSync && !isDevelopment) {
return [
{
pid: 0,
name: 'N/A',
cpu: 0,
memory: 0,
timestamp: new Date().toISOString(),
error: 'Process information is not available in this environment',
},
];
}
// If we're in development but not in Node environment, return mock data
if (!execSync && isDevelopment) {
return getMockProcessInfo();
}
// Different commands for different operating systems
const platform = process.platform;
let processes: ProcessInfo[] = [];
// Get CPU count for normalizing CPU percentages
let cpuCount = 1;
try {
if (platform === 'darwin') {
const cpuInfo = execSync('sysctl -n hw.ncpu', { encoding: 'utf-8' }).toString().trim();
cpuCount = parseInt(cpuInfo, 10) || 1;
} else if (platform === 'linux') {
const cpuInfo = execSync('nproc', { encoding: 'utf-8' }).toString().trim();
cpuCount = parseInt(cpuInfo, 10) || 1;
} else if (platform === 'win32') {
const cpuInfo = execSync('wmic cpu get NumberOfCores', { encoding: 'utf-8' }).toString().trim();
const match = cpuInfo.match(/\d+/);
cpuCount = match ? parseInt(match[0], 10) : 1;
}
} catch (error) {
console.error('Failed to get CPU count:', error);
// Default to 1 if we can't get the count
cpuCount = 1;
}
if (platform === 'darwin') {
// macOS - use ps command to get process information
try {
const output = execSync('ps -eo pid,pcpu,pmem,comm -r | head -n 11', { encoding: 'utf-8' }).toString().trim();
// Skip the header line
const lines = output.split('\n').slice(1);
processes = lines.map((line: string) => {
const parts = line.trim().split(/\s+/);
const pid = parseInt(parts[0], 10);
/*
* Normalize CPU percentage by dividing by CPU count
* This converts from "% of all CPUs" to "% of one CPU"
*/
const cpu = parseFloat(parts[1]) / cpuCount;
const memory = parseFloat(parts[2]);
const command = parts.slice(3).join(' ');
return {
pid,
name: command.split('/').pop() || command,
cpu,
memory,
command,
timestamp: new Date().toISOString(),
};
});
} catch (error) {
console.error('Failed to get macOS process info:', error);
// Try alternative command
try {
const output = execSync('top -l 1 -stats pid,cpu,mem,command -n 10', { encoding: 'utf-8' }).toString().trim();
// Parse top output - skip the first few lines of header
const lines = output.split('\n').slice(6);
processes = lines.map((line: string) => {
const parts = line.trim().split(/\s+/);
const pid = parseInt(parts[0], 10);
const cpu = parseFloat(parts[1]);
const memory = parseFloat(parts[2]);
const command = parts.slice(3).join(' ');
return {
pid,
name: command.split('/').pop() || command,
cpu,
memory,
command,
timestamp: new Date().toISOString(),
};
});
} catch (fallbackError) {
console.error('Failed to get macOS process info with fallback:', fallbackError);
return [
{
pid: 0,
name: 'N/A',
cpu: 0,
memory: 0,
timestamp: new Date().toISOString(),
error: 'Process information is not available in this environment',
},
];
}
}
} else if (platform === 'linux') {
// Linux - use ps command to get process information
try {
const output = execSync('ps -eo pid,pcpu,pmem,comm --sort=-pmem | head -n 11', { encoding: 'utf-8' })
.toString()
.trim();
// Skip the header line
const lines = output.split('\n').slice(1);
processes = lines.map((line: string) => {
const parts = line.trim().split(/\s+/);
const pid = parseInt(parts[0], 10);
// Normalize CPU percentage by dividing by CPU count
const cpu = parseFloat(parts[1]) / cpuCount;
const memory = parseFloat(parts[2]);
const command = parts.slice(3).join(' ');
return {
pid,
name: command.split('/').pop() || command,
cpu,
memory,
command,
timestamp: new Date().toISOString(),
};
});
} catch (error) {
console.error('Failed to get Linux process info:', error);
// Try alternative command
try {
const output = execSync('top -b -n 1 | head -n 17', { encoding: 'utf-8' }).toString().trim();
// Parse top output - skip the first few lines of header
const lines = output.split('\n').slice(7);
processes = lines.map((line: string) => {
const parts = line.trim().split(/\s+/);
const pid = parseInt(parts[0], 10);
const cpu = parseFloat(parts[8]);
const memory = parseFloat(parts[9]);
const command = parts[11] || parts[parts.length - 1];
return {
pid,
name: command.split('/').pop() || command,
cpu,
memory,
command,
timestamp: new Date().toISOString(),
};
});
} catch (fallbackError) {
console.error('Failed to get Linux process info with fallback:', fallbackError);
return [
{
pid: 0,
name: 'N/A',
cpu: 0,
memory: 0,
timestamp: new Date().toISOString(),
error: 'Process information is not available in this environment',
},
];
}
}
} else if (platform === 'win32') {
// Windows - use PowerShell to get process information
try {
const output = execSync(
'powershell "Get-Process | Sort-Object -Property WorkingSet64 -Descending | Select-Object -First 10 Id, CPU, @{Name=\'Memory\';Expression={$_.WorkingSet64/1MB}}, ProcessName | ConvertTo-Json"',
{ encoding: 'utf-8' },
)
.toString()
.trim();
const processData = JSON.parse(output);
const processArray = Array.isArray(processData) ? processData : [processData];
processes = processArray.map((proc: any) => ({
pid: proc.Id,
name: proc.ProcessName,
// Normalize CPU percentage by dividing by CPU count
cpu: (proc.CPU || 0) / cpuCount,
memory: proc.Memory,
timestamp: new Date().toISOString(),
}));
} catch (error) {
console.error('Failed to get Windows process info:', error);
// Try alternative command using tasklist
try {
const output = execSync('tasklist /FO CSV', { encoding: 'utf-8' }).toString().trim();
// Parse CSV output - skip the header line
const lines = output.split('\n').slice(1);
processes = lines.slice(0, 10).map((line: string) => {
// Parse CSV format
const parts = line.split(',').map((part: string) => part.replace(/^"(.+)"$/, '$1'));
const pid = parseInt(parts[1], 10);
const memoryStr = parts[4].replace(/[^\d]/g, '');
const memory = parseInt(memoryStr, 10) / 1024; // Convert KB to MB
return {
pid,
name: parts[0],
cpu: 0, // tasklist doesn't provide CPU info
memory,
timestamp: new Date().toISOString(),
};
});
} catch (fallbackError) {
console.error('Failed to get Windows process info with fallback:', fallbackError);
return [
{
pid: 0,
name: 'N/A',
cpu: 0,
memory: 0,
timestamp: new Date().toISOString(),
error: 'Process information is not available in this environment',
},
];
}
}
} else {
console.warn(`Unsupported platform: ${platform}, using browser fallback`);
return [
{
pid: 0,
name: 'N/A',
cpu: 0,
memory: 0,
timestamp: new Date().toISOString(),
error: 'Process information is not available in this environment',
},
];
}
return processes;
} catch (error) {
console.error('Failed to get process info:', error);
if (isDevelopment) {
return getMockProcessInfo();
}
return [
{
pid: 0,
name: 'N/A',
cpu: 0,
memory: 0,
timestamp: new Date().toISOString(),
error: 'Process information is not available in this environment',
},
];
}
};
// Generate mock process information with realistic values
const getMockProcessInfo = (): ProcessInfo[] => {
const timestamp = new Date().toISOString();
// Create some random variation in CPU usage
const randomCPU = () => Math.floor(Math.random() * 15);
const randomHighCPU = () => 15 + Math.floor(Math.random() * 25);
// Create some random variation in memory usage
const randomMem = () => Math.floor(Math.random() * 5);
const randomHighMem = () => 5 + Math.floor(Math.random() * 15);
return [
{
pid: 1,
name: 'Browser',
cpu: randomHighCPU(),
memory: 25 + randomMem(),
command: 'Browser Process',
timestamp,
},
{
pid: 2,
name: 'System',
cpu: 5 + randomCPU(),
memory: 10 + randomMem(),
command: 'System Process',
timestamp,
},
{
pid: 3,
name: 'bolt',
cpu: randomHighCPU(),
memory: 15 + randomMem(),
command: 'Bolt AI Process',
timestamp,
},
{
pid: 4,
name: 'node',
cpu: randomCPU(),
memory: randomHighMem(),
command: 'Node.js Process',
timestamp,
},
{
pid: 5,
name: 'wrangler',
cpu: randomCPU(),
memory: randomMem(),
command: 'Wrangler Process',
timestamp,
},
{
pid: 6,
name: 'vscode',
cpu: randomCPU(),
memory: 12 + randomMem(),
command: 'VS Code Process',
timestamp,
},
{
pid: 7,
name: 'chrome',
cpu: randomHighCPU(),
memory: 20 + randomMem(),
command: 'Chrome Browser',
timestamp,
},
{
pid: 8,
name: 'finder',
cpu: 1 + randomCPU(),
memory: 3 + randomMem(),
command: 'Finder Process',
timestamp,
},
{
pid: 9,
name: 'terminal',
cpu: 2 + randomCPU(),
memory: 5 + randomMem(),
command: 'Terminal Process',
timestamp,
},
{
pid: 10,
name: 'cloudflared',
cpu: randomCPU(),
memory: randomMem(),
command: 'Cloudflare Tunnel',
timestamp,
},
];
};
export const loader: LoaderFunction = async ({ request: _request }) => {
try {
return json(getProcessInfo());
} catch (error) {
console.error('Failed to get process info:', error);
return json(getMockProcessInfo(), { status: 500 });
}
};
export const action = async ({ request: _request }: ActionFunctionArgs) => {
try {
return json(getProcessInfo());
} catch (error) {
console.error('Failed to get process info:', error);
return json(getMockProcessInfo(), { status: 500 });
}
};

6
app/utils/cn.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -1,4 +1,4 @@
export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'none';
import { Chalk } from 'chalk';
const chalk = new Chalk({ level: 3 });
@@ -14,7 +14,7 @@ interface Logger {
setLevel: (level: DebugLevel) => void;
}
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL || (import.meta.env.DEV ? 'debug' : 'info');
export const logger: Logger = {
trace: (...messages: any[]) => log('trace', undefined, messages),
@@ -45,12 +45,17 @@ function setLevel(level: DebugLevel) {
}
function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'none'];
if (levelOrder.indexOf(level) < levelOrder.indexOf(currentLevel)) {
return;
}
// If current level is 'none', don't log anything
if (currentLevel === 'none') {
return;
}
const allMessages = messages.reduce((acc, current) => {
if (acc.endsWith('\n')) {
return acc + current;

View File

@@ -74,21 +74,6 @@ const gitInfo = getGitInfo();
export default defineConfig((config) => {
return {
define: {
__COMMIT_HASH: JSON.stringify(gitInfo.commitHash),
__GIT_BRANCH: JSON.stringify(gitInfo.branch),
__GIT_COMMIT_TIME: JSON.stringify(gitInfo.commitTime),
__GIT_AUTHOR: JSON.stringify(gitInfo.author),
__GIT_EMAIL: JSON.stringify(gitInfo.email),
__GIT_REMOTE_URL: JSON.stringify(gitInfo.remoteUrl),
__GIT_REPO_NAME: JSON.stringify(gitInfo.repoName),
__APP_VERSION: JSON.stringify(process.env.npm_package_version),
__PKG_NAME: JSON.stringify(pkg.name),
__PKG_DESCRIPTION: JSON.stringify(pkg.description),
__PKG_LICENSE: JSON.stringify(pkg.license),
__PKG_DEPENDENCIES: JSON.stringify(pkg.dependencies),
__PKG_DEV_DEPENDENCIES: JSON.stringify(pkg.devDependencies),
__PKG_PEER_DEPENDENCIES: JSON.stringify(pkg.peerDependencies),
__PKG_OPTIONAL_DEPENDENCIES: JSON.stringify(pkg.optionalDependencies),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
build: {