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:
@@ -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="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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</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">
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -117,24 +117,6 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
|
<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
|
<DropdownMenu.Item
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center gap-2 px-4 py-2.5',
|
'flex items-center gap-2 px-4 py-2.5',
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { Switch } from '@radix-ui/react-switch';
|
|
||||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { TabManagement } from '~/components/@settings/shared/components/TabManagement';
|
|
||||||
import { TabTile } from '~/components/@settings/shared/components/TabTile';
|
import { TabTile } from '~/components/@settings/shared/components/TabTile';
|
||||||
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
||||||
import { useFeatures } from '~/lib/hooks/useFeatures';
|
import { useFeatures } from '~/lib/hooks/useFeatures';
|
||||||
import { useNotifications } from '~/lib/hooks/useNotifications';
|
import { useNotifications } from '~/lib/hooks/useNotifications';
|
||||||
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
||||||
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
|
||||||
import {
|
|
||||||
tabConfigurationStore,
|
|
||||||
developerModeStore,
|
|
||||||
setDeveloperMode,
|
|
||||||
resetTabConfiguration,
|
|
||||||
} from '~/lib/stores/settings';
|
|
||||||
import { profileStore } from '~/lib/stores/profile';
|
import { profileStore } from '~/lib/stores/profile';
|
||||||
import type { TabType, TabVisibilityConfig, Profile } from './types';
|
import type { TabType, Profile } from './types';
|
||||||
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
|
import { TAB_LABELS, DEFAULT_TAB_CONFIG, TAB_DESCRIPTIONS } from './constants';
|
||||||
import { DialogTitle } from '~/components/ui/Dialog';
|
import { DialogTitle } from '~/components/ui/Dialog';
|
||||||
import { AvatarDropdown } from './AvatarDropdown';
|
import { AvatarDropdown } from './AvatarDropdown';
|
||||||
import BackgroundRays from '~/components/ui/BackgroundRays';
|
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 NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
|
||||||
import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
|
import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
|
||||||
import { DataTab } from '~/components/@settings/tabs/data/DataTab';
|
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 { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
|
||||||
import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
|
import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
|
||||||
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
|
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
|
||||||
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
|
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
|
||||||
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
|
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
|
||||||
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
|
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';
|
import McpTab from '~/components/@settings/tabs/mcp/McpTab';
|
||||||
|
|
||||||
interface ControlPanelProps {
|
interface ControlPanelProps {
|
||||||
@@ -45,48 +34,8 @@ interface ControlPanelProps {
|
|||||||
onClose: () => void;
|
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
|
// 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 = () => (
|
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">
|
<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>
|
</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) => {
|
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||||
// State
|
// State
|
||||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||||
@@ -162,7 +51,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
|
|
||||||
// Store values
|
// Store values
|
||||||
const tabConfiguration = useStore(tabConfigurationStore);
|
const tabConfiguration = useStore(tabConfigurationStore);
|
||||||
const developerMode = useStore(developerModeStore);
|
|
||||||
const profile = useStore(profileStore) as Profile;
|
const profile = useStore(profileStore) as Profile;
|
||||||
|
|
||||||
// Status hooks
|
// Status hooks
|
||||||
@@ -170,7 +58,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
||||||
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
||||||
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
||||||
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
|
||||||
|
|
||||||
// Memoize the base tab configurations to avoid recalculation
|
// Memoize the base tab configurations to avoid recalculation
|
||||||
const baseTabConfig = useMemo(() => {
|
const baseTabConfig = useMemo(() => {
|
||||||
@@ -188,41 +75,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
|
|
||||||
const notificationsDisabled = profile?.preferences?.notifications === false;
|
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
|
// Optimize user mode tab filtering
|
||||||
return tabConfiguration.userTabs
|
return tabConfiguration.userTabs
|
||||||
.filter((tab) => {
|
.filter((tab) => {
|
||||||
@@ -237,33 +89,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
return tab.visible && tab.window === 'user';
|
return tab.visible && tab.window === 'user';
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
}, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]);
|
}, [tabConfiguration, profile?.preferences?.notifications, baseTabConfig]);
|
||||||
|
|
||||||
// Optimize animation performance with layout animations
|
|
||||||
const gridLayoutVariants = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.05,
|
|
||||||
delayChildren: 0.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants = {
|
|
||||||
hidden: { opacity: 0, scale: 0.8 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
transition: {
|
|
||||||
type: 'spring',
|
|
||||||
stiffness: 200,
|
|
||||||
damping: 20,
|
|
||||||
mass: 0.6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset to default view when modal opens/closes
|
// Reset to default view when modal opens/closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -295,21 +121,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeveloperModeChange = (checked: boolean) => {
|
const getTabComponent = (tabId: TabType) => {
|
||||||
console.log('Developer mode changed:', checked);
|
|
||||||
setDeveloperMode(checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add effect to log developer mode changes
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('Current developer mode:', developerMode);
|
|
||||||
}, [developerMode]);
|
|
||||||
|
|
||||||
const getTabComponent = (tabId: TabType | 'tab-management') => {
|
|
||||||
if (tabId === 'tab-management') {
|
|
||||||
return <TabManagement />;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
case 'profile':
|
case 'profile':
|
||||||
return <ProfileTab />;
|
return <ProfileTab />;
|
||||||
@@ -327,14 +139,10 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
return <LocalProvidersTab />;
|
return <LocalProvidersTab />;
|
||||||
case 'connection':
|
case 'connection':
|
||||||
return <ConnectionsTab />;
|
return <ConnectionsTab />;
|
||||||
case 'debug':
|
|
||||||
return <DebugTab />;
|
|
||||||
case 'event-logs':
|
case 'event-logs':
|
||||||
return <EventLogsTab />;
|
return <EventLogsTab />;
|
||||||
case 'update':
|
case 'update':
|
||||||
return <UpdateTab />;
|
return <UpdateTab />;
|
||||||
case 'task-manager':
|
|
||||||
return <TaskManagerTab />;
|
|
||||||
case 'service-status':
|
case 'service-status':
|
||||||
return <ServiceStatusTab />;
|
return <ServiceStatusTab />;
|
||||||
case 'mcp':
|
case 'mcp':
|
||||||
@@ -354,8 +162,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
return hasUnreadNotifications;
|
return hasUnreadNotifications;
|
||||||
case 'connection':
|
case 'connection':
|
||||||
return hasConnectionIssues;
|
return hasConnectionIssues;
|
||||||
case 'debug':
|
|
||||||
return hasActiveWarnings;
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -375,12 +181,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
: currentIssue === 'high-latency'
|
: currentIssue === 'high-latency'
|
||||||
? 'High latency detected'
|
? 'High latency detected'
|
||||||
: 'Connection issues 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:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -405,9 +205,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
case 'connection':
|
case 'connection':
|
||||||
acknowledgeIssue();
|
acknowledgeIssue();
|
||||||
break;
|
break;
|
||||||
case 'debug':
|
|
||||||
acknowledgeAllIssues();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear loading state after a delay
|
// Clear loading state after a delay
|
||||||
@@ -418,15 +215,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
<RadixDialog.Root open={open}>
|
<RadixDialog.Root open={open}>
|
||||||
<RadixDialog.Portal>
|
<RadixDialog.Portal>
|
||||||
<div className="fixed inset-0 flex items-center justify-center z-[100] modern-scrollbar">
|
<div className="fixed inset-0 flex items-center justify-center z-[100] modern-scrollbar">
|
||||||
<RadixDialog.Overlay asChild>
|
<RadixDialog.Overlay className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm transition-opacity duration-200" />
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
/>
|
|
||||||
</RadixDialog.Overlay>
|
|
||||||
|
|
||||||
<RadixDialog.Content
|
<RadixDialog.Content
|
||||||
aria-describedby={undefined}
|
aria-describedby={undefined}
|
||||||
@@ -434,19 +223,17 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
onPointerDownOutside={handleClose}
|
onPointerDownOutside={handleClose}
|
||||||
className="relative z-[101]"
|
className="relative z-[101]"
|
||||||
>
|
>
|
||||||
<motion.div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-[1200px] h-[90vh]',
|
'w-[1200px] h-[90vh]',
|
||||||
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
'bg-bolt-elements-background-depth-1',
|
||||||
'rounded-2xl shadow-2xl',
|
'rounded-2xl shadow-2xl',
|
||||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
'border border-bolt-elements-borderColor',
|
||||||
'flex flex-col overflow-hidden',
|
'flex flex-col overflow-hidden',
|
||||||
'relative',
|
'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">
|
<div className="absolute inset-0 overflow-hidden rounded-2xl">
|
||||||
<BackgroundRays />
|
<BackgroundRays />
|
||||||
@@ -458,7 +245,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
{(activeTab || showTabManagement) && (
|
{(activeTab || showTabManagement) && (
|
||||||
<button
|
<button
|
||||||
onClick={handleBack}
|
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" />
|
<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>
|
</button>
|
||||||
@@ -469,18 +256,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-6">
|
<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 */}
|
{/* Avatar and Dropdown */}
|
||||||
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
|
<div className="pl-6">
|
||||||
<AvatarDropdown onSelectTab={handleTabClick} />
|
<AvatarDropdown onSelectTab={handleTabClick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -508,49 +285,48 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
'touch-auto',
|
'touch-auto',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<motion.div
|
<div
|
||||||
key={activeTab || 'home'}
|
className={classNames(
|
||||||
initial={{ opacity: 0 }}
|
'p-6 transition-opacity duration-150',
|
||||||
animate={{ opacity: 1 }}
|
activeTab || showTabManagement ? 'opacity-100' : 'opacity-100',
|
||||||
exit={{ opacity: 0 }}
|
)}
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="p-6"
|
|
||||||
>
|
>
|
||||||
{showTabManagement ? (
|
{activeTab ? (
|
||||||
<TabManagement />
|
|
||||||
) : activeTab ? (
|
|
||||||
getTabComponent(activeTab)
|
getTabComponent(activeTab)
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
|
||||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
|
{visibleTabs.map((tab, index) => (
|
||||||
variants={gridLayoutVariants}
|
<div
|
||||||
initial="hidden"
|
key={tab.id}
|
||||||
animate="visible"
|
className={classNames(
|
||||||
>
|
'aspect-[1.5/1] transition-transform duration-100 ease-out',
|
||||||
<AnimatePresence mode="popLayout">
|
'hover:scale-[1.01]',
|
||||||
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
|
)}
|
||||||
<motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
|
style={{
|
||||||
<TabTile
|
animationDelay: `${index * 30}ms`,
|
||||||
tab={tab}
|
animation: open ? 'fadeInUp 200ms ease-out forwards' : 'none',
|
||||||
onClick={() => handleTabClick(tab.id as TabType)}
|
}}
|
||||||
isActive={activeTab === tab.id}
|
>
|
||||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
<TabTile
|
||||||
statusMessage={getStatusMessage(tab.id)}
|
tab={tab}
|
||||||
description={TAB_DESCRIPTIONS[tab.id]}
|
onClick={() => handleTabClick(tab.id as TabType)}
|
||||||
isLoading={loadingTab === tab.id}
|
isActive={activeTab === tab.id}
|
||||||
className="h-full relative"
|
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||||
>
|
statusMessage={getStatusMessage(tab.id)}
|
||||||
{BETA_TABS.has(tab.id) && <BetaLabel />}
|
description={TAB_DESCRIPTIONS[tab.id]}
|
||||||
</TabTile>
|
isLoading={loadingTab === tab.id}
|
||||||
</motion.div>
|
className="h-full relative"
|
||||||
))}
|
>
|
||||||
</AnimatePresence>
|
{BETA_TABS.has(tab.id) && <BetaLabel />}
|
||||||
</motion.div>
|
</TabTile>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</RadixDialog.Content>
|
</RadixDialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</RadixDialog.Portal>
|
</RadixDialog.Portal>
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ export const TAB_ICONS: Record<TabType, string> = {
|
|||||||
'local-providers': 'i-ph:desktop-fill',
|
'local-providers': 'i-ph:desktop-fill',
|
||||||
'service-status': 'i-ph:activity-bold',
|
'service-status': 'i-ph:activity-bold',
|
||||||
connection: 'i-ph:wifi-high-fill',
|
connection: 'i-ph:wifi-high-fill',
|
||||||
debug: 'i-ph:bug-fill',
|
|
||||||
'event-logs': 'i-ph:list-bullets-fill',
|
'event-logs': 'i-ph:list-bullets-fill',
|
||||||
update: 'i-ph:arrow-clockwise-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',
|
mcp: 'i-ph:hard-drives-bold',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,11 +25,8 @@ export const TAB_LABELS: Record<TabType, string> = {
|
|||||||
'local-providers': 'Local Providers',
|
'local-providers': 'Local Providers',
|
||||||
'service-status': 'Service Status',
|
'service-status': 'Service Status',
|
||||||
connection: 'Connection',
|
connection: 'Connection',
|
||||||
debug: 'Debug',
|
|
||||||
'event-logs': 'Event Logs',
|
'event-logs': 'Event Logs',
|
||||||
update: 'Updates',
|
update: 'Updates',
|
||||||
'task-manager': 'Task Manager',
|
|
||||||
'tab-management': 'Tab Management',
|
|
||||||
mcp: 'MCP Servers',
|
mcp: 'MCP Servers',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,11 +40,8 @@ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
|||||||
'local-providers': 'Configure local AI providers and models',
|
'local-providers': 'Configure local AI providers and models',
|
||||||
'service-status': 'Monitor cloud LLM service status',
|
'service-status': 'Monitor cloud LLM service status',
|
||||||
connection: 'Check connection status and settings',
|
connection: 'Check connection status and settings',
|
||||||
debug: 'Debug tools and system information',
|
|
||||||
'event-logs': 'View system events and logs',
|
'event-logs': 'View system events and logs',
|
||||||
update: 'Check for updates and release notes',
|
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',
|
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: 'cloud-providers', visible: true, window: 'user' as const, order: 2 },
|
||||||
{ id: 'local-providers', visible: true, window: 'user' as const, order: 3 },
|
{ 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: 4 },
|
||||||
{ id: 'connection', visible: true, window: 'user' as const, order: 5 },
|
{ id: 'notifications', visible: true, window: 'user' as const, order: 5 },
|
||||||
{ id: 'notifications', visible: true, window: 'user' as const, order: 6 },
|
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
|
||||||
{ id: 'mcp', visible: true, window: 'user' as const, order: 7 },
|
{ 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)
|
// 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 },
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ export type TabType =
|
|||||||
| 'local-providers'
|
| 'local-providers'
|
||||||
| 'service-status'
|
| 'service-status'
|
||||||
| 'connection'
|
| 'connection'
|
||||||
| 'debug'
|
|
||||||
| 'event-logs'
|
| 'event-logs'
|
||||||
| 'update'
|
| 'update'
|
||||||
| 'task-manager'
|
|
||||||
| 'tab-management'
|
|
||||||
| 'mcp';
|
| 'mcp';
|
||||||
|
|
||||||
export type WindowType = 'user' | 'developer';
|
export type WindowType = 'user' | 'developer';
|
||||||
@@ -64,7 +61,6 @@ export interface UserTabConfig extends TabVisibilityConfig {
|
|||||||
|
|
||||||
export interface TabWindowConfig {
|
export interface TabWindowConfig {
|
||||||
userTabs: UserTabConfig[];
|
userTabs: UserTabConfig[];
|
||||||
developerTabs: DevTabConfig[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TAB_LABELS: Record<TabType, string> = {
|
export const TAB_LABELS: Record<TabType, string> = {
|
||||||
@@ -77,11 +73,8 @@ export const TAB_LABELS: Record<TabType, string> = {
|
|||||||
'local-providers': 'Local Providers',
|
'local-providers': 'Local Providers',
|
||||||
'service-status': 'Service Status',
|
'service-status': 'Service Status',
|
||||||
connection: 'Connections',
|
connection: 'Connections',
|
||||||
debug: 'Debug',
|
|
||||||
'event-logs': 'Event Logs',
|
'event-logs': 'Event Logs',
|
||||||
update: 'Updates',
|
update: 'Updates',
|
||||||
'task-manager': 'Task Manager',
|
|
||||||
'tab-management': 'Tab Management',
|
|
||||||
mcp: 'MCP Servers',
|
mcp: 'MCP Servers',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constan
|
|||||||
|
|
||||||
// Shared components
|
// Shared components
|
||||||
export { TabTile } from './shared/components/TabTile';
|
export { TabTile } from './shared/components/TabTile';
|
||||||
export { TabManagement } from './shared/components/TabManagement';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
|
export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
|
||||||
export * from './utils/animations';
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { motion } from 'framer-motion';
|
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
|
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
|
||||||
import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
|
import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
|
||||||
|
import { GlowingEffect } from '~/components/ui/GlowingEffect';
|
||||||
|
|
||||||
interface TabTileProps {
|
interface TabTileProps {
|
||||||
tab: TabVisibilityConfig;
|
tab: TabVisibilityConfig;
|
||||||
@@ -28,106 +28,118 @@ export const TabTile: React.FC<TabTileProps> = ({
|
|||||||
children,
|
children,
|
||||||
}: TabTileProps) => {
|
}: TabTileProps) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip.Provider delayDuration={200}>
|
<Tooltip.Provider delayDuration={0}>
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<motion.div
|
<div className={classNames('min-h-[160px] list-none', className || '')}>
|
||||||
onClick={onClick}
|
<div className="relative h-full rounded-xl border border-[#E5E5E5] dark:border-[#333333] p-0.5">
|
||||||
className={classNames(
|
<GlowingEffect
|
||||||
'relative flex flex-col items-center p-6 rounded-xl',
|
blur={0}
|
||||||
'w-full h-full min-h-[160px]',
|
borderWidth={1}
|
||||||
'bg-white dark:bg-[#141414]',
|
spread={20}
|
||||||
'border border-[#E5E5E5] dark:border-[#333333]',
|
glow={true}
|
||||||
'group',
|
disabled={false}
|
||||||
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
proximity={40}
|
||||||
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
inactiveZone={0.3}
|
||||||
isActive ? 'border-purple-500 dark:border-purple-500/50 bg-purple-500/5 dark:bg-purple-500/10' : '',
|
movementDuration={0.4}
|
||||||
isLoading ? 'cursor-wait opacity-70' : '',
|
/>
|
||||||
className || '',
|
<div
|
||||||
)}
|
onClick={onClick}
|
||||||
>
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="flex flex-col items-center justify-center flex-1 w-full">
|
|
||||||
{/* Icon */}
|
|
||||||
<motion.div
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'relative',
|
'relative flex flex-col items-center justify-center h-full p-4 rounded-lg',
|
||||||
'w-14 h-14',
|
'bg-white dark:bg-[#141414]',
|
||||||
'flex items-center justify-center',
|
'group cursor-pointer',
|
||||||
'rounded-xl',
|
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
||||||
'bg-gray-100 dark:bg-gray-800',
|
'transition-colors duration-100 ease-out',
|
||||||
'ring-1 ring-gray-200 dark:ring-gray-700',
|
isActive ? 'bg-purple-500/5 dark:bg-purple-500/10' : '',
|
||||||
'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
|
isLoading ? 'cursor-wait opacity-70 pointer-events-none' : '',
|
||||||
'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
|
|
||||||
isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<motion.div
|
{/* Icon */}
|
||||||
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
TAB_ICONS[tab.id],
|
'relative',
|
||||||
'w-8 h-8',
|
'w-14 h-14',
|
||||||
'text-gray-600 dark:text-gray-300',
|
'flex items-center justify-center',
|
||||||
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
|
'rounded-xl',
|
||||||
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
|
'bg-gray-100 dark:bg-gray-800',
|
||||||
)}
|
'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||||
/>
|
'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
|
||||||
</motion.div>
|
'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
|
||||||
|
'transition-all duration-100 ease-out',
|
||||||
{/* Label and Description */}
|
isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
|
||||||
<div className="flex flex-col items-center mt-5 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',
|
|
||||||
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{TAB_LABELS[tab.id]}
|
<div
|
||||||
</h3>
|
|
||||||
{description && (
|
|
||||||
<p
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'text-[13px] leading-relaxed',
|
TAB_ICONS[tab.id],
|
||||||
'text-gray-500 dark:text-gray-400',
|
'w-8 h-8',
|
||||||
'max-w-[85%]',
|
'text-gray-600 dark:text-gray-300',
|
||||||
'text-center',
|
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
|
||||||
'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' : '',
|
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Label and Description */}
|
||||||
|
<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' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{description}
|
{TAB_LABELS[tab.id]}
|
||||||
</p>
|
</h3>
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
className={classNames(
|
||||||
|
'text-[13px] leading-relaxed',
|
||||||
|
'text-gray-500 dark:text-gray-400',
|
||||||
|
'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' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Update Indicator with Tooltip */}
|
||||||
|
{hasUpdate && (
|
||||||
|
<>
|
||||||
|
<div className="absolute top-4 right-4 w-2 h-2 rounded-full bg-purple-500 dark:bg-purple-400 animate-pulse" />
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content
|
||||||
|
className={classNames(
|
||||||
|
'px-3 py-1.5 rounded-lg',
|
||||||
|
'bg-[#18181B] text-white',
|
||||||
|
'text-sm font-medium',
|
||||||
|
'select-none',
|
||||||
|
'z-[100]',
|
||||||
|
)}
|
||||||
|
side="top"
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
{statusMessage}
|
||||||
|
<Tooltip.Arrow className="fill-[#18181B]" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Children (e.g. Beta Label) */}
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Update Indicator with Tooltip */}
|
|
||||||
{hasUpdate && (
|
|
||||||
<>
|
|
||||||
<div className="absolute top-4 right-4 w-2 h-2 rounded-full bg-purple-500 dark:bg-purple-400 animate-pulse" />
|
|
||||||
<Tooltip.Portal>
|
|
||||||
<Tooltip.Content
|
|
||||||
className={classNames(
|
|
||||||
'px-3 py-1.5 rounded-lg',
|
|
||||||
'bg-[#18181B] text-white',
|
|
||||||
'text-sm font-medium',
|
|
||||||
'select-none',
|
|
||||||
'z-[100]',
|
|
||||||
)}
|
|
||||||
side="top"
|
|
||||||
sideOffset={5}
|
|
||||||
>
|
|
||||||
{statusMessage}
|
|
||||||
<Tooltip.Arrow className="fill-[#18181B]" />
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Portal>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Children (e.g. Beta Label) */}
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
</Tooltip.Provider>
|
</Tooltip.Provider>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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';
|
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
|
||||||
|
|
||||||
export const getVisibleTabs = (
|
export const getVisibleTabs = (
|
||||||
tabConfiguration: { userTabs: TabVisibilityConfig[]; developerTabs?: TabVisibilityConfig[] },
|
tabConfiguration: { userTabs: TabVisibilityConfig[] },
|
||||||
isDeveloperMode: boolean,
|
|
||||||
notificationsEnabled: boolean,
|
notificationsEnabled: boolean,
|
||||||
): TabVisibilityConfig[] => {
|
): TabVisibilityConfig[] => {
|
||||||
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
||||||
@@ -11,35 +10,6 @@ export const getVisibleTabs = (
|
|||||||
return DEFAULT_TAB_CONFIG as TabVisibilityConfig[];
|
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
|
// In user mode, only show visible user tabs
|
||||||
return tabConfiguration.userTabs
|
return tabConfiguration.userTabs
|
||||||
.filter((tab) => {
|
.filter((tab) => {
|
||||||
@@ -53,11 +23,6 @@ export const getVisibleTabs = (
|
|||||||
return false;
|
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
|
// Only show tabs that are explicitly visible and assigned to the user window
|
||||||
return tab.visible && tab.window === 'user';
|
return tab.visible && tab.window === 'user';
|
||||||
})
|
})
|
||||||
|
|||||||
192
app/components/ui/GlowingEffect.tsx
Normal file
192
app/components/ui/GlowingEffect.tsx
Normal 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 };
|
||||||
@@ -8,4 +8,3 @@ export { useUpdateCheck } from './useUpdateCheck';
|
|||||||
export { useFeatures } from './useFeatures';
|
export { useFeatures } from './useFeatures';
|
||||||
export { useNotifications } from './useNotifications';
|
export { useNotifications } from './useNotifications';
|
||||||
export { useConnectionStatus } from './useConnectionStatus';
|
export { useConnectionStatus } from './useConnectionStatus';
|
||||||
export { useDebugStatus } from './useDebugStatus';
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
autoSelectStarterTemplate,
|
autoSelectStarterTemplate,
|
||||||
enableContextOptimizationStore,
|
enableContextOptimizationStore,
|
||||||
tabConfigurationStore,
|
tabConfigurationStore,
|
||||||
updateTabConfiguration as updateTabConfig,
|
|
||||||
resetTabConfiguration as resetTabConfig,
|
resetTabConfiguration as resetTabConfig,
|
||||||
updateProviderSettings as updateProviderSettingsStore,
|
updateProviderSettings as updateProviderSettingsStore,
|
||||||
updateLatestBranch,
|
updateLatestBranch,
|
||||||
@@ -20,7 +19,7 @@ import {
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
|
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 { logStore } from '~/lib/stores/logs';
|
||||||
import { getLocalStorage, setLocalStorage } from '~/lib/persistence';
|
import { getLocalStorage, setLocalStorage } from '~/lib/persistence';
|
||||||
|
|
||||||
@@ -62,7 +61,6 @@ export interface UseSettingsReturn {
|
|||||||
|
|
||||||
// Tab configuration
|
// Tab configuration
|
||||||
tabConfiguration: TabWindowConfig;
|
tabConfiguration: TabWindowConfig;
|
||||||
updateTabConfiguration: (config: TabVisibilityConfig) => void;
|
|
||||||
resetTabConfiguration: () => void;
|
resetTabConfiguration: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +203,6 @@ export function useSettings(): UseSettingsReturn {
|
|||||||
setTimezone,
|
setTimezone,
|
||||||
settings,
|
settings,
|
||||||
tabConfiguration,
|
tabConfiguration,
|
||||||
updateTabConfiguration: updateTabConfig,
|
|
||||||
resetTabConfiguration: resetTabConfig,
|
resetTabConfiguration: resetTabConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { atom, map } from 'nanostores';
|
import { atom, map } from 'nanostores';
|
||||||
import { PROVIDER_LIST } from '~/utils/constants';
|
import { PROVIDER_LIST } from '~/utils/constants';
|
||||||
import type { IProviderConfig } from '~/types/model';
|
import type { IProviderConfig } from '~/types/model';
|
||||||
import type {
|
import type { TabVisibilityConfig, TabWindowConfig, UserTabConfig } from '~/components/@settings/core/types';
|
||||||
TabVisibilityConfig,
|
|
||||||
TabWindowConfig,
|
|
||||||
UserTabConfig,
|
|
||||||
DevTabConfig,
|
|
||||||
} from '~/components/@settings/core/types';
|
|
||||||
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
|
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import { toggleTheme } from './theme';
|
import { toggleTheme } from './theme';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
@@ -202,7 +196,6 @@ export const updatePromptId = (id: string) => {
|
|||||||
const getInitialTabConfiguration = (): TabWindowConfig => {
|
const getInitialTabConfiguration = (): TabWindowConfig => {
|
||||||
const defaultConfig: TabWindowConfig = {
|
const defaultConfig: TabWindowConfig = {
|
||||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is UserTabConfig => tab.window === 'user'),
|
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) {
|
if (!isBrowser) {
|
||||||
@@ -218,16 +211,13 @@ const getInitialTabConfiguration = (): TabWindowConfig => {
|
|||||||
|
|
||||||
const parsed = JSON.parse(saved);
|
const parsed = JSON.parse(saved);
|
||||||
|
|
||||||
if (!parsed?.userTabs || !parsed?.developerTabs) {
|
if (!parsed?.userTabs) {
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure proper typing of loaded configuration
|
// Ensure proper typing of loaded configuration
|
||||||
return {
|
return {
|
||||||
userTabs: parsed.userTabs.filter((tab: TabVisibilityConfig): tab is UserTabConfig => tab.window === 'user'),
|
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) {
|
} catch (error) {
|
||||||
console.warn('Failed to parse tab configuration:', error);
|
console.warn('Failed to parse tab configuration:', error);
|
||||||
@@ -239,60 +229,16 @@ const getInitialTabConfiguration = (): TabWindowConfig => {
|
|||||||
|
|
||||||
export const tabConfigurationStore = map<TabWindowConfig>(getInitialTabConfiguration());
|
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
|
// Helper function to reset tab configuration
|
||||||
export const resetTabConfiguration = () => {
|
export const resetTabConfiguration = () => {
|
||||||
const defaultConfig: TabWindowConfig = {
|
const defaultConfig: TabWindowConfig = {
|
||||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is UserTabConfig => tab.window === 'user'),
|
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);
|
tabConfigurationStore.set(defaultConfig);
|
||||||
localStorage.setItem('bolt_tab_configuration', JSON.stringify(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
|
// First, let's define the SettingsStore interface
|
||||||
interface SettingsStore {
|
interface SettingsStore {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -10,22 +10,19 @@ export interface TabConfig {
|
|||||||
|
|
||||||
interface TabConfigurationStore {
|
interface TabConfigurationStore {
|
||||||
userTabs: TabConfig[];
|
userTabs: TabConfig[];
|
||||||
developerTabs: TabConfig[];
|
get: () => { userTabs: TabConfig[] };
|
||||||
get: () => { userTabs: TabConfig[]; developerTabs: TabConfig[] };
|
set: (config: { userTabs: TabConfig[] }) => void;
|
||||||
set: (config: { userTabs: TabConfig[]; developerTabs: TabConfig[] }) => void;
|
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
userTabs: [],
|
userTabs: [],
|
||||||
developerTabs: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tabConfigurationStore = create<TabConfigurationStore>((set, get) => ({
|
export const tabConfigurationStore = create<TabConfigurationStore>((set, get) => ({
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
get: () => ({
|
get: () => ({
|
||||||
userTabs: get().userTabs,
|
userTabs: get().userTabs,
|
||||||
developerTabs: get().developerTabs,
|
|
||||||
}),
|
}),
|
||||||
set: (config) => set(config),
|
set: (config) => set(config),
|
||||||
reset: () => set(DEFAULT_CONFIG),
|
reset: () => set(DEFAULT_CONFIG),
|
||||||
|
|||||||
@@ -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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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
6
app/utils/cn.ts
Normal 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));
|
||||||
|
}
|
||||||
@@ -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';
|
import { Chalk } from 'chalk';
|
||||||
|
|
||||||
const chalk = new Chalk({ level: 3 });
|
const chalk = new Chalk({ level: 3 });
|
||||||
@@ -14,7 +14,7 @@ interface Logger {
|
|||||||
setLevel: (level: DebugLevel) => void;
|
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 = {
|
export const logger: Logger = {
|
||||||
trace: (...messages: any[]) => log('trace', undefined, messages),
|
trace: (...messages: any[]) => log('trace', undefined, messages),
|
||||||
@@ -45,12 +45,17 @@ function setLevel(level: DebugLevel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
|
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)) {
|
if (levelOrder.indexOf(level) < levelOrder.indexOf(currentLevel)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If current level is 'none', don't log anything
|
||||||
|
if (currentLevel === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const allMessages = messages.reduce((acc, current) => {
|
const allMessages = messages.reduce((acc, current) => {
|
||||||
if (acc.endsWith('\n')) {
|
if (acc.endsWith('\n')) {
|
||||||
return acc + current;
|
return acc + current;
|
||||||
|
|||||||
@@ -74,21 +74,6 @@ const gitInfo = getGitInfo();
|
|||||||
export default defineConfig((config) => {
|
export default defineConfig((config) => {
|
||||||
return {
|
return {
|
||||||
define: {
|
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),
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user