Merge pull request #1826 from xKevIsDev/error-fix
fix: enhanced error handling for llm api, general cleanup
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,15 @@
|
|||||||
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 { 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,61 +20,19 @@ 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 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';
|
|
||||||
|
|
||||||
interface ControlPanelProps {
|
interface ControlPanelProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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', '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">
|
||||||
@@ -92,66 +40,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);
|
||||||
@@ -160,15 +48,12 @@ 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
|
||||||
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
|
||||||
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(() => {
|
||||||
@@ -186,41 +71,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) => {
|
||||||
@@ -235,33 +85,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(() => {
|
||||||
@@ -293,21 +117,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 />;
|
||||||
@@ -325,14 +135,8 @@ 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':
|
|
||||||
return <UpdateTab />;
|
|
||||||
case 'task-manager':
|
|
||||||
return <TaskManagerTab />;
|
|
||||||
case 'service-status':
|
case 'service-status':
|
||||||
return <ServiceStatusTab />;
|
return <ServiceStatusTab />;
|
||||||
default:
|
default:
|
||||||
@@ -342,16 +146,12 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
|
|
||||||
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
case 'update':
|
|
||||||
return hasUpdate;
|
|
||||||
case 'features':
|
case 'features':
|
||||||
return hasNewFeatures;
|
return hasNewFeatures;
|
||||||
case 'notifications':
|
case 'notifications':
|
||||||
return hasUnreadNotifications;
|
return hasUnreadNotifications;
|
||||||
case 'connection':
|
case 'connection':
|
||||||
return hasConnectionIssues;
|
return hasConnectionIssues;
|
||||||
case 'debug':
|
|
||||||
return hasActiveWarnings;
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -359,8 +159,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
|
|
||||||
const getStatusMessage = (tabId: TabType): string => {
|
const getStatusMessage = (tabId: TabType): string => {
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
case 'update':
|
|
||||||
return `New update available (v${currentVersion})`;
|
|
||||||
case 'features':
|
case 'features':
|
||||||
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
||||||
case 'notifications':
|
case 'notifications':
|
||||||
@@ -371,12 +169,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 '';
|
||||||
}
|
}
|
||||||
@@ -389,9 +181,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
|
|
||||||
// Acknowledge notifications based on tab
|
// Acknowledge notifications based on tab
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
case 'update':
|
|
||||||
acknowledgeUpdate();
|
|
||||||
break;
|
|
||||||
case 'features':
|
case 'features':
|
||||||
acknowledgeAllFeatures();
|
acknowledgeAllFeatures();
|
||||||
break;
|
break;
|
||||||
@@ -401,9 +190,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
|
||||||
@@ -414,15 +200,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}
|
||||||
@@ -430,19 +208,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 />
|
||||||
@@ -454,7 +230,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>
|
||||||
@@ -465,18 +241,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>
|
||||||
|
|
||||||
@@ -504,49 +270,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>
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import type { TabType } from './types';
|
import type { TabType } from './types';
|
||||||
|
|
||||||
export const TAB_ICONS: Record<TabType, string> = {
|
export const TAB_ICONS: Record<TabType, string> = {
|
||||||
profile: 'i-ph:user-circle-fill',
|
profile: 'i-ph:user-circle',
|
||||||
settings: 'i-ph:gear-six-fill',
|
settings: 'i-ph:gear-six',
|
||||||
notifications: 'i-ph:bell-fill',
|
notifications: 'i-ph:bell',
|
||||||
features: 'i-ph:star-fill',
|
features: 'i-ph:star',
|
||||||
data: 'i-ph:database-fill',
|
data: 'i-ph:database',
|
||||||
'cloud-providers': 'i-ph:cloud-fill',
|
'cloud-providers': 'i-ph:cloud',
|
||||||
'local-providers': 'i-ph:desktop-fill',
|
'local-providers': 'i-ph:laptop',
|
||||||
'service-status': 'i-ph:activity-bold',
|
'service-status': 'i-ph:activity-bold',
|
||||||
connection: 'i-ph:wifi-high-fill',
|
connection: 'i-ph:wifi-high',
|
||||||
debug: 'i-ph:bug-fill',
|
'event-logs': 'i-ph:list-bullets',
|
||||||
'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',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TAB_LABELS: Record<TabType, string> = {
|
export const TAB_LABELS: Record<TabType, string> = {
|
||||||
@@ -27,11 +23,7 @@ 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',
|
|
||||||
'task-manager': 'Task Manager',
|
|
||||||
'tab-management': 'Tab Management',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
export const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||||
@@ -44,11 +36,7 @@ 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',
|
|
||||||
'task-manager': 'Monitor system resources and processes',
|
|
||||||
'tab-management': 'Configure visible tabs and their order',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_TAB_CONFIG = [
|
export const DEFAULT_TAB_CONFIG = [
|
||||||
@@ -62,27 +50,7 @@ export const DEFAULT_TAB_CONFIG = [
|
|||||||
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
|
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
|
||||||
|
|
||||||
// User Window Tabs (In dropdown, initially hidden)
|
// User Window Tabs (In dropdown, initially hidden)
|
||||||
{ id: 'profile', visible: false, window: 'user' as const, order: 7 },
|
{ id: 'profile', visible: true, window: 'user' as const, order: 7 },
|
||||||
{ id: 'settings', visible: false, window: 'user' as const, order: 8 },
|
{ id: 'service-status', visible: true, window: 'user' as const, order: 8 },
|
||||||
{ id: 'task-manager', visible: false, window: 'user' as const, order: 9 },
|
{ id: 'settings', visible: true, window: 'user' as const, order: 9 },
|
||||||
{ id: 'service-status', visible: false, window: 'user' as const, order: 10 },
|
|
||||||
|
|
||||||
// User Window Tabs (Hidden, controlled by TaskManagerTab)
|
|
||||||
{ id: 'debug', visible: false, window: 'user' as const, order: 11 },
|
|
||||||
{ id: 'update', visible: false, window: 'user' as const, order: 12 },
|
|
||||||
|
|
||||||
// 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,7 @@ export type TabType =
|
|||||||
| 'local-providers'
|
| 'local-providers'
|
||||||
| 'service-status'
|
| 'service-status'
|
||||||
| 'connection'
|
| 'connection'
|
||||||
| 'debug'
|
| 'event-logs';
|
||||||
| 'event-logs'
|
|
||||||
| 'update'
|
|
||||||
| 'task-manager'
|
|
||||||
| 'tab-management';
|
|
||||||
|
|
||||||
export type WindowType = 'user' | 'developer';
|
export type WindowType = 'user' | 'developer';
|
||||||
|
|
||||||
@@ -63,7 +59,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> = {
|
||||||
@@ -76,11 +71,7 @@ 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',
|
|
||||||
'task-manager': 'Task Manager',
|
|
||||||
'tab-management': 'Tab Management',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const categoryLabels: Record<SettingCategory, string> = {
|
export const categoryLabels: Record<SettingCategory, string> = {
|
||||||
|
|||||||
@@ -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,380 +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',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define which tabs are default in user mode
|
|
||||||
const DEFAULT_USER_TABS: TabType[] = [
|
|
||||||
'features',
|
|
||||||
'data',
|
|
||||||
'cloud-providers',
|
|
||||||
'local-providers',
|
|
||||||
'connection',
|
|
||||||
'notifications',
|
|
||||||
'event-logs',
|
|
||||||
];
|
|
||||||
|
|
||||||
// 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,628 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useSettings } from '~/lib/hooks/useSettings';
|
|
||||||
import { logStore } from '~/lib/stores/logs';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog';
|
|
||||||
import { classNames } from '~/utils/classNames';
|
|
||||||
import { Markdown } from '~/components/chat/Markdown';
|
|
||||||
|
|
||||||
interface UpdateProgress {
|
|
||||||
stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
|
|
||||||
message: string;
|
|
||||||
progress?: number;
|
|
||||||
error?: string;
|
|
||||||
details?: {
|
|
||||||
changedFiles?: string[];
|
|
||||||
additions?: number;
|
|
||||||
deletions?: number;
|
|
||||||
commitMessages?: string[];
|
|
||||||
totalSize?: string;
|
|
||||||
currentCommit?: string;
|
|
||||||
remoteCommit?: string;
|
|
||||||
updateReady?: boolean;
|
|
||||||
changelog?: string;
|
|
||||||
compareUrl?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateSettings {
|
|
||||||
autoUpdate: boolean;
|
|
||||||
notifyInApp: boolean;
|
|
||||||
checkInterval: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProgressBar = ({ progress }: { progress: number }) => (
|
|
||||||
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
||||||
<motion.div
|
|
||||||
className="h-full bg-blue-500"
|
|
||||||
initial={{ width: 0 }}
|
|
||||||
animate={{ width: `${progress}%` }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const UpdateProgressDisplay = ({ progress }: { progress: UpdateProgress }) => (
|
|
||||||
<div className="mt-4 space-y-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm font-medium">{progress.message}</span>
|
|
||||||
<span className="text-sm text-gray-500">{progress.progress}%</span>
|
|
||||||
</div>
|
|
||||||
<ProgressBar progress={progress.progress || 0} />
|
|
||||||
{progress.details && (
|
|
||||||
<div className="mt-2 text-sm text-gray-600">
|
|
||||||
{progress.details.changedFiles && progress.details.changedFiles.length > 0 && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="font-medium mb-2">Changed Files:</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/* Group files by type */}
|
|
||||||
{['Modified', 'Added', 'Deleted'].map((type) => {
|
|
||||||
const filesOfType = progress.details?.changedFiles?.filter((file) => file.startsWith(type)) || [];
|
|
||||||
|
|
||||||
if (filesOfType.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={type} className="space-y-1">
|
|
||||||
<div
|
|
||||||
className={classNames('text-sm font-medium', {
|
|
||||||
'text-blue-500': type === 'Modified',
|
|
||||||
'text-green-500': type === 'Added',
|
|
||||||
'text-red-500': type === 'Deleted',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{type} ({filesOfType.length})
|
|
||||||
</div>
|
|
||||||
<div className="pl-4 space-y-1">
|
|
||||||
{filesOfType.map((file, index) => {
|
|
||||||
const fileName = file.split(': ')[1];
|
|
||||||
return (
|
|
||||||
<div key={index} className="text-sm text-bolt-elements-textSecondary flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={classNames('w-4 h-4', {
|
|
||||||
'i-ph:pencil-simple': type === 'Modified',
|
|
||||||
'i-ph:plus': type === 'Added',
|
|
||||||
'i-ph:trash': type === 'Deleted',
|
|
||||||
'text-blue-500': type === 'Modified',
|
|
||||||
'text-green-500': type === 'Added',
|
|
||||||
'text-red-500': type === 'Deleted',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<span className="font-mono text-xs">{fileName}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{progress.details.totalSize && <div className="mt-1">Total size: {progress.details.totalSize}</div>}
|
|
||||||
{progress.details.additions !== undefined && progress.details.deletions !== undefined && (
|
|
||||||
<div className="mt-1">
|
|
||||||
Changes: <span className="text-green-600">+{progress.details.additions}</span>{' '}
|
|
||||||
<span className="text-red-600">-{progress.details.deletions}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{progress.details.currentCommit && progress.details.remoteCommit && (
|
|
||||||
<div className="mt-1">
|
|
||||||
Updating from {progress.details.currentCommit} to {progress.details.remoteCommit}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const UpdateTab = () => {
|
|
||||||
const { isLatestBranch } = useSettings();
|
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [updateSettings, setUpdateSettings] = useState<UpdateSettings>(() => {
|
|
||||||
const stored = localStorage.getItem('update_settings');
|
|
||||||
return stored
|
|
||||||
? JSON.parse(stored)
|
|
||||||
: {
|
|
||||||
autoUpdate: false,
|
|
||||||
notifyInApp: true,
|
|
||||||
checkInterval: 24,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
|
||||||
const [updateProgress, setUpdateProgress] = useState<UpdateProgress | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
|
||||||
}, [updateSettings]);
|
|
||||||
|
|
||||||
const checkForUpdates = async () => {
|
|
||||||
console.log('Starting update check...');
|
|
||||||
setIsChecking(true);
|
|
||||||
setError(null);
|
|
||||||
setUpdateProgress(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
|
||||||
|
|
||||||
// Start the update check with streaming progress
|
|
||||||
const response = await fetch('/api/update', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
branch: branchToCheck,
|
|
||||||
autoUpdate: updateSettings.autoUpdate,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Update check failed: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
|
|
||||||
if (!reader) {
|
|
||||||
throw new Error('No response stream available');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the stream
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the chunk to text and parse the JSON
|
|
||||||
const chunk = new TextDecoder().decode(value);
|
|
||||||
const lines = chunk.split('\n').filter(Boolean);
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
try {
|
|
||||||
const progress = JSON.parse(line) as UpdateProgress;
|
|
||||||
setUpdateProgress(progress);
|
|
||||||
|
|
||||||
if (progress.error) {
|
|
||||||
setError(progress.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're done, update the UI accordingly
|
|
||||||
if (progress.stage === 'complete') {
|
|
||||||
setIsChecking(false);
|
|
||||||
|
|
||||||
if (!progress.error) {
|
|
||||||
// Update check completed
|
|
||||||
toast.success('Update check completed');
|
|
||||||
|
|
||||||
// Show update dialog only if there are changes and auto-update is disabled
|
|
||||||
if (progress.details?.changedFiles?.length && progress.details.updateReady) {
|
|
||||||
setShowUpdateDialog(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing progress update:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setError(error instanceof Error ? error.message : 'Unknown error occurred');
|
|
||||||
logStore.logWarning('Update Check Failed', {
|
|
||||||
type: 'update',
|
|
||||||
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsChecking(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
|
||||||
setShowUpdateDialog(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
|
||||||
|
|
||||||
// Start the update with autoUpdate set to true to force the update
|
|
||||||
const response = await fetch('/api/update', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
branch: branchToCheck,
|
|
||||||
autoUpdate: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Update failed: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the update progress stream
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
|
|
||||||
if (!reader) {
|
|
||||||
throw new Error('No response stream available');
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunk = new TextDecoder().decode(value);
|
|
||||||
const lines = chunk.split('\n').filter(Boolean);
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
try {
|
|
||||||
const progress = JSON.parse(line) as UpdateProgress;
|
|
||||||
setUpdateProgress(progress);
|
|
||||||
|
|
||||||
if (progress.error) {
|
|
||||||
setError(progress.error);
|
|
||||||
toast.error('Update failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress.stage === 'complete' && !progress.error) {
|
|
||||||
toast.success('Update completed successfully');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing update progress:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setError(error instanceof Error ? error.message : 'Unknown error occurred');
|
|
||||||
toast.error('Update failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<motion.div
|
|
||||||
className="flex items-center gap-3"
|
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<div className="i-ph:arrow-circle-up text-xl text-purple-500" />
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Updates</h3>
|
|
||||||
<p className="text-sm text-bolt-elements-textSecondary">Check for and manage application updates</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Update Settings Card */}
|
|
||||||
<motion.div
|
|
||||||
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3, delay: 0.1 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<div className="i-ph:gear text-purple-500 w-5 h-5" />
|
|
||||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Update Settings</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-bolt-elements-textPrimary">Automatic Updates</span>
|
|
||||||
<p className="text-xs text-bolt-elements-textSecondary">
|
|
||||||
Automatically check and apply updates when available
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setUpdateSettings((prev) => ({ ...prev, autoUpdate: !prev.autoUpdate }))}
|
|
||||||
className={classNames(
|
|
||||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
|
||||||
updateSettings.autoUpdate ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
|
|
||||||
updateSettings.autoUpdate ? 'translate-x-6' : 'translate-x-1',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-bolt-elements-textPrimary">In-App Notifications</span>
|
|
||||||
<p className="text-xs text-bolt-elements-textSecondary">Show notifications when updates are available</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setUpdateSettings((prev) => ({ ...prev, notifyInApp: !prev.notifyInApp }))}
|
|
||||||
className={classNames(
|
|
||||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
|
||||||
updateSettings.notifyInApp ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
|
|
||||||
updateSettings.notifyInApp ? 'translate-x-6' : 'translate-x-1',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-bolt-elements-textPrimary">Check Interval</span>
|
|
||||||
<p className="text-xs text-bolt-elements-textSecondary">How often to check for updates</p>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
value={updateSettings.checkInterval}
|
|
||||||
onChange={(e) => setUpdateSettings((prev) => ({ ...prev, checkInterval: Number(e.target.value) }))}
|
|
||||||
className={classNames(
|
|
||||||
'px-3 py-2 rounded-lg text-sm',
|
|
||||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
|
||||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
|
||||||
'text-bolt-elements-textPrimary',
|
|
||||||
'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
|
||||||
'transition-colors duration-200',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<option value="6">6 hours</option>
|
|
||||||
<option value="12">12 hours</option>
|
|
||||||
<option value="24">24 hours</option>
|
|
||||||
<option value="48">48 hours</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Update Status Card */}
|
|
||||||
<motion.div
|
|
||||||
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3, delay: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="i-ph:arrows-clockwise text-purple-500 w-5 h-5" />
|
|
||||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Update Status</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{updateProgress?.details?.updateReady && !updateSettings.autoUpdate && (
|
|
||||||
<button
|
|
||||||
onClick={handleUpdate}
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
|
|
||||||
'bg-purple-500 text-white',
|
|
||||||
'hover:bg-purple-600',
|
|
||||||
'transition-colors duration-200',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="i-ph:arrow-circle-up w-4 h-4" />
|
|
||||||
Update Now
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setError(null);
|
|
||||||
checkForUpdates();
|
|
||||||
}}
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
|
|
||||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
|
||||||
'hover:bg-purple-500/10 hover:text-purple-500',
|
|
||||||
'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
|
|
||||||
'text-bolt-elements-textPrimary',
|
|
||||||
'transition-colors duration-200',
|
|
||||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
||||||
)}
|
|
||||||
disabled={isChecking}
|
|
||||||
>
|
|
||||||
{isChecking ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<motion.div
|
|
||||||
animate={{ rotate: 360 }}
|
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
|
||||||
className="i-ph:arrows-clockwise w-4 h-4"
|
|
||||||
/>
|
|
||||||
Checking...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="i-ph:arrows-clockwise w-4 h-4" />
|
|
||||||
Check for Updates
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Show progress information */}
|
|
||||||
{updateProgress && <UpdateProgressDisplay progress={updateProgress} />}
|
|
||||||
|
|
||||||
{error && <div className="mt-4 p-4 bg-red-100 text-red-700 rounded">{error}</div>}
|
|
||||||
|
|
||||||
{/* Show update source information */}
|
|
||||||
{updateProgress?.details?.currentCommit && updateProgress?.details?.remoteCommit && (
|
|
||||||
<div className="mt-4 text-sm text-bolt-elements-textSecondary">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
Updates are fetched from: <span className="font-mono">stackblitz-labs/bolt.diy</span> (
|
|
||||||
{isLatestBranch ? 'main' : 'stable'} branch)
|
|
||||||
</p>
|
|
||||||
<p className="mt-1">
|
|
||||||
Current version: <span className="font-mono">{updateProgress.details.currentCommit}</span>
|
|
||||||
<span className="mx-2">→</span>
|
|
||||||
Latest version: <span className="font-mono">{updateProgress.details.remoteCommit}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{updateProgress?.details?.compareUrl && (
|
|
||||||
<a
|
|
||||||
href={updateProgress.details.compareUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
|
|
||||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
|
||||||
'hover:bg-purple-500/10 hover:text-purple-500',
|
|
||||||
'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
|
|
||||||
'text-bolt-elements-textPrimary',
|
|
||||||
'transition-colors duration-200',
|
|
||||||
'w-fit',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="i-ph:github-logo w-4 h-4" />
|
|
||||||
View Changes on GitHub
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{updateProgress?.details?.additions !== undefined && updateProgress?.details?.deletions !== undefined && (
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<div className="i-ph:git-diff text-purple-500 w-4 h-4" />
|
|
||||||
Changes: <span className="text-green-600">+{updateProgress.details.additions}</span>{' '}
|
|
||||||
<span className="text-red-600">-{updateProgress.details.deletions}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add this before the changed files section */}
|
|
||||||
{updateProgress?.details?.changelog && (
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<div className="i-ph:scroll text-purple-500 w-5 h-5" />
|
|
||||||
<p className="font-medium">Changelog</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-4 overflow-auto max-h-[300px]">
|
|
||||||
<div className="prose dark:prose-invert prose-sm max-w-none">
|
|
||||||
<Markdown>{updateProgress.details.changelog}</Markdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add this in the update status card, after the commit info */}
|
|
||||||
{updateProgress?.details?.compareUrl && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<a
|
|
||||||
href={updateProgress.details.compareUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
|
|
||||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
|
||||||
'hover:bg-purple-500/10 hover:text-purple-500',
|
|
||||||
'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
|
|
||||||
'text-bolt-elements-textPrimary',
|
|
||||||
'transition-colors duration-200',
|
|
||||||
'w-fit',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="i-ph:github-logo w-4 h-4" />
|
|
||||||
View Changes on GitHub
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updateProgress?.details?.commitMessages && updateProgress.details.commitMessages.length > 0 && (
|
|
||||||
<div className="mb-6">
|
|
||||||
<p className="font-medium mb-2">Changes in this Update:</p>
|
|
||||||
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-4 overflow-auto max-h-[400px]">
|
|
||||||
<div className="prose dark:prose-invert prose-sm max-w-none">
|
|
||||||
{updateProgress.details.commitMessages.map((section, index) => (
|
|
||||||
<Markdown key={index}>{section}</Markdown>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Update dialog */}
|
|
||||||
<DialogRoot open={showUpdateDialog} onOpenChange={setShowUpdateDialog}>
|
|
||||||
<Dialog>
|
|
||||||
<DialogTitle>Update Available</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<div className="mt-4">
|
|
||||||
<p className="text-sm text-bolt-elements-textSecondary mb-4">
|
|
||||||
A new version is available from <span className="font-mono">stackblitz-labs/bolt.diy</span> (
|
|
||||||
{isLatestBranch ? 'main' : 'stable'} branch)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{updateProgress?.details?.compareUrl && (
|
|
||||||
<div className="mb-6">
|
|
||||||
<a
|
|
||||||
href={updateProgress.details.compareUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
|
|
||||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
|
||||||
'hover:bg-purple-500/10 hover:text-purple-500',
|
|
||||||
'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
|
|
||||||
'text-bolt-elements-textPrimary',
|
|
||||||
'transition-colors duration-200',
|
|
||||||
'w-fit',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="i-ph:github-logo w-4 h-4" />
|
|
||||||
View Changes on GitHub
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updateProgress?.details?.commitMessages && updateProgress.details.commitMessages.length > 0 && (
|
|
||||||
<div className="mb-6">
|
|
||||||
<p className="font-medium mb-2">Commit Messages:</p>
|
|
||||||
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 space-y-2">
|
|
||||||
{updateProgress.details.commitMessages.map((msg, index) => (
|
|
||||||
<div key={index} className="text-sm text-bolt-elements-textSecondary flex items-start gap-2">
|
|
||||||
<div className="i-ph:git-commit text-purple-500 w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
||||||
<span>{msg}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updateProgress?.details?.totalSize && (
|
|
||||||
<div className="flex items-center gap-4 text-sm text-bolt-elements-textSecondary">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="i-ph:file text-purple-500 w-4 h-4" />
|
|
||||||
Total size: {updateProgress.details.totalSize}
|
|
||||||
</div>
|
|
||||||
{updateProgress?.details?.additions !== undefined &&
|
|
||||||
updateProgress?.details?.deletions !== undefined && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="i-ph:git-diff text-purple-500 w-4 h-4" />
|
|
||||||
Changes: <span className="text-green-600">+{updateProgress.details.additions}</span>{' '}
|
|
||||||
<span className="text-red-600">-{updateProgress.details.deletions}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogDescription>
|
|
||||||
<div className="flex justify-end gap-2 mt-6">
|
|
||||||
<DialogButton type="secondary" onClick={() => setShowUpdateDialog(false)}>
|
|
||||||
Cancel
|
|
||||||
</DialogButton>
|
|
||||||
<DialogButton type="primary" onClick={handleUpdate}>
|
|
||||||
Update Now
|
|
||||||
</DialogButton>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</DialogRoot>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateTab;
|
|
||||||
@@ -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';
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
|||||||
import GitCloneButton from './GitCloneButton';
|
import GitCloneButton from './GitCloneButton';
|
||||||
import type { ProviderInfo } from '~/types/model';
|
import type { ProviderInfo } from '~/types/model';
|
||||||
import StarterTemplates from './StarterTemplates';
|
import StarterTemplates from './StarterTemplates';
|
||||||
import type { ActionAlert, SupabaseAlert, DeployAlert } from '~/types/actions';
|
import type { ActionAlert, SupabaseAlert, DeployAlert, LlmErrorAlertType } from '~/types/actions';
|
||||||
import DeployChatAlert from '~/components/deploy/DeployAlert';
|
import DeployChatAlert from '~/components/deploy/DeployAlert';
|
||||||
import ChatAlert from './ChatAlert';
|
import ChatAlert from './ChatAlert';
|
||||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||||
@@ -32,6 +32,7 @@ import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
|
|||||||
import { ChatBox } from './ChatBox';
|
import { ChatBox } from './ChatBox';
|
||||||
import type { DesignScheme } from '~/types/design-scheme';
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
import type { ElementInfo } from '~/components/workbench/Inspector';
|
import type { ElementInfo } from '~/components/workbench/Inspector';
|
||||||
|
import LlmErrorAlert from './LLMApiAlert';
|
||||||
|
|
||||||
const TEXTAREA_MIN_HEIGHT = 76;
|
const TEXTAREA_MIN_HEIGHT = 76;
|
||||||
|
|
||||||
@@ -69,6 +70,8 @@ interface BaseChatProps {
|
|||||||
clearSupabaseAlert?: () => void;
|
clearSupabaseAlert?: () => void;
|
||||||
deployAlert?: DeployAlert;
|
deployAlert?: DeployAlert;
|
||||||
clearDeployAlert?: () => void;
|
clearDeployAlert?: () => void;
|
||||||
|
llmErrorAlert?: LlmErrorAlertType;
|
||||||
|
clearLlmErrorAlert?: () => void;
|
||||||
data?: JSONValue[] | undefined;
|
data?: JSONValue[] | undefined;
|
||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||||
@@ -113,6 +116,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
clearDeployAlert,
|
clearDeployAlert,
|
||||||
supabaseAlert,
|
supabaseAlert,
|
||||||
clearSupabaseAlert,
|
clearSupabaseAlert,
|
||||||
|
llmErrorAlert,
|
||||||
|
clearLlmErrorAlert,
|
||||||
data,
|
data,
|
||||||
chatMode,
|
chatMode,
|
||||||
setChatMode,
|
setChatMode,
|
||||||
@@ -411,6 +416,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{llmErrorAlert && <LlmErrorAlert alert={llmErrorAlert} clearAlert={() => clearLlmErrorAlert?.()} />}
|
||||||
</div>
|
</div>
|
||||||
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
|
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
|
||||||
<ChatBox
|
<ChatBox
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { filesToArtifacts } from '~/utils/fileUtils';
|
|||||||
import { supabaseConnection } from '~/lib/stores/supabase';
|
import { supabaseConnection } from '~/lib/stores/supabase';
|
||||||
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
|
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
|
||||||
import type { ElementInfo } from '~/components/workbench/Inspector';
|
import type { ElementInfo } from '~/components/workbench/Inspector';
|
||||||
|
import type { LlmErrorAlertType } from '~/types/actions';
|
||||||
|
|
||||||
const toastAnimation = cssTransition({
|
const toastAnimation = cssTransition({
|
||||||
enter: 'animated fadeInRight',
|
enter: 'animated fadeInRight',
|
||||||
@@ -129,12 +130,13 @@ export const ChatImpl = memo(
|
|||||||
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
|
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
|
||||||
const actionAlert = useStore(workbenchStore.alert);
|
const actionAlert = useStore(workbenchStore.alert);
|
||||||
const deployAlert = useStore(workbenchStore.deployAlert);
|
const deployAlert = useStore(workbenchStore.deployAlert);
|
||||||
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection
|
const supabaseConn = useStore(supabaseConnection);
|
||||||
const selectedProject = supabaseConn.stats?.projects?.find(
|
const selectedProject = supabaseConn.stats?.projects?.find(
|
||||||
(project) => project.id === supabaseConn.selectedProjectId,
|
(project) => project.id === supabaseConn.selectedProjectId,
|
||||||
);
|
);
|
||||||
const supabaseAlert = useStore(workbenchStore.supabaseAlert);
|
const supabaseAlert = useStore(workbenchStore.supabaseAlert);
|
||||||
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
|
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
|
||||||
|
const [llmErrorAlert, setLlmErrorAlert] = useState<LlmErrorAlertType | undefined>(undefined);
|
||||||
const [model, setModel] = useState(() => {
|
const [model, setModel] = useState(() => {
|
||||||
const savedModel = Cookies.get('selectedModel');
|
const savedModel = Cookies.get('selectedModel');
|
||||||
return savedModel || DEFAULT_MODEL;
|
return savedModel || DEFAULT_MODEL;
|
||||||
@@ -181,15 +183,8 @@ export const ChatImpl = memo(
|
|||||||
},
|
},
|
||||||
sendExtraMessageFields: true,
|
sendExtraMessageFields: true,
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
logger.error('Request failed\n\n', e, error);
|
setFakeLoading(false);
|
||||||
logStore.logError('Chat request failed', e, {
|
handleError(e, 'chat');
|
||||||
component: 'Chat',
|
|
||||||
action: 'request',
|
|
||||||
error: e.message,
|
|
||||||
});
|
|
||||||
toast.error(
|
|
||||||
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onFinish: (message, response) => {
|
onFinish: (message, response) => {
|
||||||
const usage = response.usage;
|
const usage = response.usage;
|
||||||
@@ -272,6 +267,80 @@ export const ChatImpl = memo(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleError = useCallback(
|
||||||
|
(error: any, context: 'chat' | 'template' | 'llmcall' = 'chat') => {
|
||||||
|
logger.error(`${context} request failed`, error);
|
||||||
|
|
||||||
|
stop();
|
||||||
|
setFakeLoading(false);
|
||||||
|
|
||||||
|
let errorInfo = {
|
||||||
|
message: 'An unexpected error occurred',
|
||||||
|
isRetryable: true,
|
||||||
|
statusCode: 500,
|
||||||
|
provider: provider.name,
|
||||||
|
type: 'unknown' as const,
|
||||||
|
retryDelay: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error.message) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(error.message);
|
||||||
|
|
||||||
|
if (parsed.error || parsed.message) {
|
||||||
|
errorInfo = { ...errorInfo, ...parsed };
|
||||||
|
} else {
|
||||||
|
errorInfo.message = error.message;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorInfo.message = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorType: LlmErrorAlertType['errorType'] = 'unknown';
|
||||||
|
let title = 'Request Failed';
|
||||||
|
|
||||||
|
if (errorInfo.statusCode === 401 || errorInfo.message.toLowerCase().includes('api key')) {
|
||||||
|
errorType = 'authentication';
|
||||||
|
title = 'Authentication Error';
|
||||||
|
} else if (errorInfo.statusCode === 429 || errorInfo.message.toLowerCase().includes('rate limit')) {
|
||||||
|
errorType = 'rate_limit';
|
||||||
|
title = 'Rate Limit Exceeded';
|
||||||
|
} else if (errorInfo.message.toLowerCase().includes('quota')) {
|
||||||
|
errorType = 'quota';
|
||||||
|
title = 'Quota Exceeded';
|
||||||
|
} else if (errorInfo.statusCode >= 500) {
|
||||||
|
errorType = 'network';
|
||||||
|
title = 'Server Error';
|
||||||
|
}
|
||||||
|
|
||||||
|
logStore.logError(`${context} request failed`, error, {
|
||||||
|
component: 'Chat',
|
||||||
|
action: 'request',
|
||||||
|
error: errorInfo.message,
|
||||||
|
context,
|
||||||
|
retryable: errorInfo.isRetryable,
|
||||||
|
errorType,
|
||||||
|
provider: provider.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create API error alert
|
||||||
|
setLlmErrorAlert({
|
||||||
|
type: 'error',
|
||||||
|
title,
|
||||||
|
description: errorInfo.message,
|
||||||
|
provider: provider.name,
|
||||||
|
errorType,
|
||||||
|
});
|
||||||
|
setData([]);
|
||||||
|
},
|
||||||
|
[provider.name, stop],
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearApiErrorAlert = useCallback(() => {
|
||||||
|
setLlmErrorAlert(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
|
|
||||||
@@ -571,6 +640,8 @@ export const ChatImpl = memo(
|
|||||||
clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()}
|
clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()}
|
||||||
deployAlert={deployAlert}
|
deployAlert={deployAlert}
|
||||||
clearDeployAlert={() => workbenchStore.clearDeployAlert()}
|
clearDeployAlert={() => workbenchStore.clearDeployAlert()}
|
||||||
|
llmErrorAlert={llmErrorAlert}
|
||||||
|
clearLlmErrorAlert={clearApiErrorAlert}
|
||||||
data={chatData}
|
data={chatData}
|
||||||
chatMode={chatMode}
|
chatMode={chatMode}
|
||||||
setChatMode={setChatMode}
|
setChatMode={setChatMode}
|
||||||
|
|||||||
109
app/components/chat/LLMApiAlert.tsx
Normal file
109
app/components/chat/LLMApiAlert.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import type { LlmErrorAlertType } from '~/types/actions';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
alert: LlmErrorAlertType;
|
||||||
|
clearAlert: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LlmErrorAlert({ alert, clearAlert }: Props) {
|
||||||
|
const { title, description, provider, errorType } = alert;
|
||||||
|
|
||||||
|
const getErrorIcon = () => {
|
||||||
|
switch (errorType) {
|
||||||
|
case 'authentication':
|
||||||
|
return 'i-ph:key-duotone';
|
||||||
|
case 'rate_limit':
|
||||||
|
return 'i-ph:clock-duotone';
|
||||||
|
case 'quota':
|
||||||
|
return 'i-ph:warning-circle-duotone';
|
||||||
|
default:
|
||||||
|
return 'i-ph:warning-duotone';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getErrorMessage = () => {
|
||||||
|
switch (errorType) {
|
||||||
|
case 'authentication':
|
||||||
|
return `Authentication failed with ${provider}. Please check your API key.`;
|
||||||
|
case 'rate_limit':
|
||||||
|
return `Rate limit exceeded for ${provider}. Please wait before retrying.`;
|
||||||
|
case 'quota':
|
||||||
|
return `Quota exceeded for ${provider}. Please check your account limits.`;
|
||||||
|
default:
|
||||||
|
return 'An error occurred while processing your request.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4 mb-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-start">
|
||||||
|
<motion.div
|
||||||
|
className="flex-shrink-0"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className={`${getErrorIcon()} text-xl text-bolt-elements-button-danger-text`}></div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
<motion.h3
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="text-sm font-medium text-bolt-elements-textPrimary"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="mt-2 text-sm text-bolt-elements-textSecondary"
|
||||||
|
>
|
||||||
|
<p>{getErrorMessage()}</p>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
|
||||||
|
Error Details: {description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="mt-4"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={clearAlert}
|
||||||
|
className={classNames(
|
||||||
|
'px-2 py-1.5 rounded-md text-sm font-medium',
|
||||||
|
'bg-bolt-elements-button-secondary-background',
|
||||||
|
'hover:bg-bolt-elements-button-secondary-backgroundHover',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
|
||||||
|
'text-bolt-elements-button-secondary-text',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
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 };
|
||||||
@@ -4,8 +4,6 @@ export * from './useShortcuts';
|
|||||||
export * from './StickToBottom';
|
export * from './StickToBottom';
|
||||||
export * from './useEditChatDescription';
|
export * from './useEditChatDescription';
|
||||||
export { default } from './useViewport';
|
export { default } from './useViewport';
|
||||||
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,58 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { checkForUpdates, acknowledgeUpdate } from '~/lib/api/updates';
|
|
||||||
|
|
||||||
const LAST_ACKNOWLEDGED_VERSION_KEY = 'bolt_last_acknowledged_version';
|
|
||||||
|
|
||||||
export const useUpdateCheck = () => {
|
|
||||||
const [hasUpdate, setHasUpdate] = useState(false);
|
|
||||||
const [currentVersion, setCurrentVersion] = useState<string>('');
|
|
||||||
const [lastAcknowledgedVersion, setLastAcknowledgedVersion] = useState<string | null>(() => {
|
|
||||||
try {
|
|
||||||
return localStorage.getItem(LAST_ACKNOWLEDGED_VERSION_KEY);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkUpdate = async () => {
|
|
||||||
try {
|
|
||||||
const { available, version } = await checkForUpdates();
|
|
||||||
setCurrentVersion(version);
|
|
||||||
|
|
||||||
// Only show update if it's a new version and hasn't been acknowledged
|
|
||||||
setHasUpdate(available && version !== lastAcknowledgedVersion);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to check for updates:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check immediately and then every 30 minutes
|
|
||||||
checkUpdate();
|
|
||||||
|
|
||||||
const interval = setInterval(checkUpdate, 30 * 60 * 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [lastAcknowledgedVersion]);
|
|
||||||
|
|
||||||
const handleAcknowledgeUpdate = async () => {
|
|
||||||
try {
|
|
||||||
const { version } = await checkForUpdates();
|
|
||||||
await acknowledgeUpdate(version);
|
|
||||||
|
|
||||||
// Store in localStorage
|
|
||||||
try {
|
|
||||||
localStorage.setItem(LAST_ACKNOWLEDGED_VERSION_KEY, version);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to persist acknowledged version:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastAcknowledgedVersion(version);
|
|
||||||
setHasUpdate(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to acknowledge update:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { hasUpdate, currentVersion, acknowledgeUpdate: handleAcknowledgeUpdate };
|
|
||||||
};
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -361,16 +361,34 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|
||||||
|
const errorResponse = {
|
||||||
|
error: true,
|
||||||
|
message: error.message || 'An unexpected error occurred',
|
||||||
|
statusCode: error.statusCode || 500,
|
||||||
|
isRetryable: error.isRetryable !== false, // Default to retryable unless explicitly false
|
||||||
|
provider: error.provider || 'unknown',
|
||||||
|
};
|
||||||
|
|
||||||
if (error.message?.includes('API key')) {
|
if (error.message?.includes('API key')) {
|
||||||
throw new Response('Invalid or missing API key', {
|
return new Response(
|
||||||
status: 401,
|
JSON.stringify({
|
||||||
statusText: 'Unauthorized',
|
...errorResponse,
|
||||||
});
|
message: 'Invalid or missing API key',
|
||||||
|
statusCode: 401,
|
||||||
|
isRetryable: false,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Response(null, {
|
return new Response(JSON.stringify(errorResponse), {
|
||||||
status: 500,
|
status: errorResponse.statusCode,
|
||||||
statusText: 'Internal Server Error',
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
statusText: 'Error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,16 +139,34 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
||||||
|
const errorResponse = {
|
||||||
|
error: true,
|
||||||
|
message: error instanceof Error ? error.message : 'An unexpected error occurred',
|
||||||
|
statusCode: (error as any).statusCode || 500,
|
||||||
|
isRetryable: (error as any).isRetryable !== false,
|
||||||
|
provider: (error as any).provider || 'unknown',
|
||||||
|
};
|
||||||
|
|
||||||
if (error instanceof Error && error.message?.includes('API key')) {
|
if (error instanceof Error && error.message?.includes('API key')) {
|
||||||
throw new Response('Invalid or missing API key', {
|
return new Response(
|
||||||
status: 401,
|
JSON.stringify({
|
||||||
statusText: 'Unauthorized',
|
...errorResponse,
|
||||||
});
|
message: 'Invalid or missing API key',
|
||||||
|
statusCode: 401,
|
||||||
|
isRetryable: false,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Response(null, {
|
return new Response(JSON.stringify(errorResponse), {
|
||||||
status: 500,
|
status: errorResponse.statusCode,
|
||||||
statusText: 'Internal Server Error',
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
statusText: 'Error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -62,6 +62,15 @@ export interface DeployAlert {
|
|||||||
source?: 'vercel' | 'netlify' | 'github';
|
source?: 'vercel' | 'netlify' | 'github';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LlmErrorAlertType {
|
||||||
|
type: 'error' | 'warning';
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
content?: string;
|
||||||
|
provider?: string;
|
||||||
|
errorType?: 'authentication' | 'rate_limit' | 'quota' | 'network' | 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileHistory {
|
export interface FileHistory {
|
||||||
originalContent: string;
|
originalContent: string;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
|
|||||||
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;
|
||||||
|
|||||||
@@ -5,90 +5,12 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
|||||||
import { optimizeCssModules } from 'vite-plugin-optimize-css-modules';
|
import { optimizeCssModules } from 'vite-plugin-optimize-css-modules';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// Get detailed git info with fallbacks
|
|
||||||
const getGitInfo = () => {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
commitHash: execSync('git rev-parse --short HEAD').toString().trim(),
|
|
||||||
branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
|
|
||||||
commitTime: execSync('git log -1 --format=%cd').toString().trim(),
|
|
||||||
author: execSync('git log -1 --format=%an').toString().trim(),
|
|
||||||
email: execSync('git log -1 --format=%ae').toString().trim(),
|
|
||||||
remoteUrl: execSync('git config --get remote.origin.url').toString().trim(),
|
|
||||||
repoName: execSync('git config --get remote.origin.url')
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.replace(/^.*github.com[:/]/, '')
|
|
||||||
.replace(/\.git$/, ''),
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
commitHash: 'no-git-info',
|
|
||||||
branch: 'unknown',
|
|
||||||
commitTime: 'unknown',
|
|
||||||
author: 'unknown',
|
|
||||||
email: 'unknown',
|
|
||||||
remoteUrl: 'unknown',
|
|
||||||
repoName: 'unknown',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read package.json with detailed dependency info
|
|
||||||
const getPackageJson = () => {
|
|
||||||
try {
|
|
||||||
const pkgPath = join(process.cwd(), 'package.json');
|
|
||||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: pkg.name,
|
|
||||||
description: pkg.description,
|
|
||||||
license: pkg.license,
|
|
||||||
dependencies: pkg.dependencies || {},
|
|
||||||
devDependencies: pkg.devDependencies || {},
|
|
||||||
peerDependencies: pkg.peerDependencies || {},
|
|
||||||
optionalDependencies: pkg.optionalDependencies || {},
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
name: 'bolt.diy',
|
|
||||||
description: 'A DIY LLM interface',
|
|
||||||
license: 'MIT',
|
|
||||||
dependencies: {},
|
|
||||||
devDependencies: {},
|
|
||||||
peerDependencies: {},
|
|
||||||
optionalDependencies: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pkg = getPackageJson();
|
|
||||||
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