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="i-ph:question w-6 h-6" />
|
||||
<div className="i-ph:user w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
</motion.button>
|
||||
@@ -72,7 +72,7 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500 font-medium text-lg">
|
||||
<span className="relative -top-0.5">?</span>
|
||||
<div className="i-ph:user w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -117,24 +117,6 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
|
||||
|
||||
<DropdownMenu.Item
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-4 py-2.5',
|
||||
'text-sm text-gray-700 dark:text-gray-200',
|
||||
'hover:bg-purple-50 dark:hover:bg-purple-500/10',
|
||||
'hover:text-purple-500 dark:hover:text-purple-400',
|
||||
'cursor-pointer transition-all duration-200',
|
||||
'outline-none',
|
||||
'group',
|
||||
)}
|
||||
onClick={() => onSelectTab('task-manager')}
|
||||
>
|
||||
<div className="i-ph:activity w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
||||
Task Manager
|
||||
<BetaLabel />
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-4 py-2.5',
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Switch } from '@radix-ui/react-switch';
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { TabManagement } from '~/components/@settings/shared/components/TabManagement';
|
||||
import { TabTile } from '~/components/@settings/shared/components/TabTile';
|
||||
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
||||
import { useFeatures } from '~/lib/hooks/useFeatures';
|
||||
import { useNotifications } from '~/lib/hooks/useNotifications';
|
||||
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
||||
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
||||
import {
|
||||
tabConfigurationStore,
|
||||
developerModeStore,
|
||||
setDeveloperMode,
|
||||
resetTabConfiguration,
|
||||
} from '~/lib/stores/settings';
|
||||
import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
|
||||
import { profileStore } from '~/lib/stores/profile';
|
||||
import type { TabType, TabVisibilityConfig, Profile } from './types';
|
||||
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
|
||||
import type { TabType, Profile } from './types';
|
||||
import { TAB_LABELS, DEFAULT_TAB_CONFIG, TAB_DESCRIPTIONS } from './constants';
|
||||
import { DialogTitle } from '~/components/ui/Dialog';
|
||||
import { AvatarDropdown } from './AvatarDropdown';
|
||||
import BackgroundRays from '~/components/ui/BackgroundRays';
|
||||
@@ -30,61 +20,19 @@ import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
|
||||
import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
|
||||
import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
|
||||
import { DataTab } from '~/components/@settings/tabs/data/DataTab';
|
||||
import DebugTab from '~/components/@settings/tabs/debug/DebugTab';
|
||||
import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
|
||||
import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
|
||||
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
|
||||
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
|
||||
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
|
||||
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
|
||||
import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab';
|
||||
|
||||
interface ControlPanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface TabWithDevType extends TabVisibilityConfig {
|
||||
isExtraDevTab?: boolean;
|
||||
}
|
||||
|
||||
interface ExtendedTabConfig extends TabVisibilityConfig {
|
||||
isExtraDevTab?: boolean;
|
||||
}
|
||||
|
||||
interface BaseTabConfig {
|
||||
id: TabType;
|
||||
visible: boolean;
|
||||
window: 'user' | 'developer';
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface AnimatedSwitchProps {
|
||||
checked: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
profile: 'Manage your profile and account settings',
|
||||
settings: 'Configure application preferences',
|
||||
notifications: 'View and manage your notifications',
|
||||
features: 'Explore new and upcoming features',
|
||||
data: 'Manage your data and storage',
|
||||
'cloud-providers': 'Configure cloud AI providers and models',
|
||||
'local-providers': 'Configure local AI providers and models',
|
||||
'service-status': 'Monitor cloud LLM service status',
|
||||
connection: 'Check connection status and settings',
|
||||
debug: 'Debug tools and system information',
|
||||
'event-logs': 'View system events and logs',
|
||||
update: 'Check for updates and release notes',
|
||||
'task-manager': 'Monitor system resources and processes',
|
||||
'tab-management': 'Configure visible tabs and their order',
|
||||
};
|
||||
|
||||
// Beta status for experimental features
|
||||
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
|
||||
const BETA_TABS = new Set<TabType>(['service-status', 'local-providers']);
|
||||
|
||||
const BetaLabel = () => (
|
||||
<div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
|
||||
@@ -92,66 +40,6 @@ const BetaLabel = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const AnimatedSwitch = ({ checked, onCheckedChange, id, label }: AnimatedSwitchProps) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
className={classNames(
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full',
|
||||
'transition-all duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)]',
|
||||
'bg-gray-200 dark:bg-gray-700',
|
||||
'data-[state=checked]:bg-purple-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
|
||||
'cursor-pointer',
|
||||
'group',
|
||||
)}
|
||||
>
|
||||
<motion.span
|
||||
className={classNames(
|
||||
'absolute left-[2px] top-[2px]',
|
||||
'inline-block h-5 w-5 rounded-full',
|
||||
'bg-white shadow-lg',
|
||||
'transition-shadow duration-300',
|
||||
'group-hover:shadow-md group-active:shadow-sm',
|
||||
'group-hover:scale-95 group-active:scale-90',
|
||||
)}
|
||||
initial={false}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
duration: 0.2,
|
||||
}}
|
||||
animate={{
|
||||
x: checked ? '1.25rem' : '0rem',
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full bg-white"
|
||||
initial={false}
|
||||
animate={{
|
||||
scale: checked ? 1 : 0.8,
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</motion.span>
|
||||
<span className="sr-only">Toggle {label}</span>
|
||||
</Switch>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
// State
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
@@ -160,15 +48,12 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
|
||||
// Store values
|
||||
const tabConfiguration = useStore(tabConfigurationStore);
|
||||
const developerMode = useStore(developerModeStore);
|
||||
const profile = useStore(profileStore) as Profile;
|
||||
|
||||
// Status hooks
|
||||
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
||||
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
||||
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
||||
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
||||
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
||||
|
||||
// Memoize the base tab configurations to avoid recalculation
|
||||
const baseTabConfig = useMemo(() => {
|
||||
@@ -186,41 +71,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
|
||||
const notificationsDisabled = profile?.preferences?.notifications === false;
|
||||
|
||||
// In developer mode, show ALL tabs without restrictions
|
||||
if (developerMode) {
|
||||
const seenTabs = new Set<TabType>();
|
||||
const devTabs: ExtendedTabConfig[] = [];
|
||||
|
||||
// Process tabs in order of priority: developer, user, default
|
||||
const processTab = (tab: BaseTabConfig) => {
|
||||
if (!seenTabs.has(tab.id)) {
|
||||
seenTabs.add(tab.id);
|
||||
devTabs.push({
|
||||
id: tab.id,
|
||||
visible: true,
|
||||
window: 'developer',
|
||||
order: tab.order || devTabs.length,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Process tabs in priority order
|
||||
tabConfiguration.developerTabs?.forEach((tab) => processTab(tab as BaseTabConfig));
|
||||
tabConfiguration.userTabs.forEach((tab) => processTab(tab as BaseTabConfig));
|
||||
DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig));
|
||||
|
||||
// Add Tab Management tile
|
||||
devTabs.push({
|
||||
id: 'tab-management' as TabType,
|
||||
visible: true,
|
||||
window: 'developer',
|
||||
order: devTabs.length,
|
||||
isExtraDevTab: true,
|
||||
});
|
||||
|
||||
return devTabs.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
// Optimize user mode tab filtering
|
||||
return tabConfiguration.userTabs
|
||||
.filter((tab) => {
|
||||
@@ -235,33 +85,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
return tab.visible && tab.window === 'user';
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]);
|
||||
|
||||
// Optimize animation performance with layout animations
|
||||
const gridLayoutVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, scale: 0.8 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 20,
|
||||
mass: 0.6,
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [tabConfiguration, profile?.preferences?.notifications, baseTabConfig]);
|
||||
|
||||
// Reset to default view when modal opens/closes
|
||||
useEffect(() => {
|
||||
@@ -293,21 +117,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeveloperModeChange = (checked: boolean) => {
|
||||
console.log('Developer mode changed:', checked);
|
||||
setDeveloperMode(checked);
|
||||
};
|
||||
|
||||
// Add effect to log developer mode changes
|
||||
useEffect(() => {
|
||||
console.log('Current developer mode:', developerMode);
|
||||
}, [developerMode]);
|
||||
|
||||
const getTabComponent = (tabId: TabType | 'tab-management') => {
|
||||
if (tabId === 'tab-management') {
|
||||
return <TabManagement />;
|
||||
}
|
||||
|
||||
const getTabComponent = (tabId: TabType) => {
|
||||
switch (tabId) {
|
||||
case 'profile':
|
||||
return <ProfileTab />;
|
||||
@@ -325,14 +135,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
return <LocalProvidersTab />;
|
||||
case 'connection':
|
||||
return <ConnectionsTab />;
|
||||
case 'debug':
|
||||
return <DebugTab />;
|
||||
case 'event-logs':
|
||||
return <EventLogsTab />;
|
||||
case 'update':
|
||||
return <UpdateTab />;
|
||||
case 'task-manager':
|
||||
return <TaskManagerTab />;
|
||||
case 'service-status':
|
||||
return <ServiceStatusTab />;
|
||||
default:
|
||||
@@ -342,16 +146,12 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
|
||||
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
||||
switch (tabId) {
|
||||
case 'update':
|
||||
return hasUpdate;
|
||||
case 'features':
|
||||
return hasNewFeatures;
|
||||
case 'notifications':
|
||||
return hasUnreadNotifications;
|
||||
case 'connection':
|
||||
return hasConnectionIssues;
|
||||
case 'debug':
|
||||
return hasActiveWarnings;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -359,8 +159,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
|
||||
const getStatusMessage = (tabId: TabType): string => {
|
||||
switch (tabId) {
|
||||
case 'update':
|
||||
return `New update available (v${currentVersion})`;
|
||||
case 'features':
|
||||
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
||||
case 'notifications':
|
||||
@@ -371,12 +169,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
: currentIssue === 'high-latency'
|
||||
? 'High latency detected'
|
||||
: 'Connection issues detected';
|
||||
case 'debug': {
|
||||
const warnings = activeIssues.filter((i) => i.type === 'warning').length;
|
||||
const errors = activeIssues.filter((i) => i.type === 'error').length;
|
||||
|
||||
return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -389,9 +181,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
|
||||
// Acknowledge notifications based on tab
|
||||
switch (tabId) {
|
||||
case 'update':
|
||||
acknowledgeUpdate();
|
||||
break;
|
||||
case 'features':
|
||||
acknowledgeAllFeatures();
|
||||
break;
|
||||
@@ -401,9 +190,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
case 'connection':
|
||||
acknowledgeIssue();
|
||||
break;
|
||||
case 'debug':
|
||||
acknowledgeAllIssues();
|
||||
break;
|
||||
}
|
||||
|
||||
// Clear loading state after a delay
|
||||
@@ -414,15 +200,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Portal>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[100] modern-scrollbar">
|
||||
<RadixDialog.Overlay asChild>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
<RadixDialog.Overlay className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm transition-opacity duration-200" />
|
||||
|
||||
<RadixDialog.Content
|
||||
aria-describedby={undefined}
|
||||
@@ -430,19 +208,17 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
onPointerDownOutside={handleClose}
|
||||
className="relative z-[101]"
|
||||
>
|
||||
<motion.div
|
||||
<div
|
||||
className={classNames(
|
||||
'w-[1200px] h-[90vh]',
|
||||
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
||||
'bg-bolt-elements-background-depth-1',
|
||||
'rounded-2xl shadow-2xl',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'border border-bolt-elements-borderColor',
|
||||
'flex flex-col overflow-hidden',
|
||||
'relative',
|
||||
'transform transition-all duration-200 ease-out',
|
||||
open ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-95 translate-y-4',
|
||||
)}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="absolute inset-0 overflow-hidden rounded-2xl">
|
||||
<BackgroundRays />
|
||||
@@ -454,7 +230,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
{(activeTab || showTabManagement) && (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-colors duration-150"
|
||||
>
|
||||
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||
</button>
|
||||
@@ -465,18 +241,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Mode Toggle */}
|
||||
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
|
||||
<AnimatedSwitch
|
||||
id="developer-mode"
|
||||
checked={developerMode}
|
||||
onCheckedChange={handleDeveloperModeChange}
|
||||
label={developerMode ? 'Developer Mode' : 'User Mode'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Avatar and Dropdown */}
|
||||
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
|
||||
<div className="pl-6">
|
||||
<AvatarDropdown onSelectTab={handleTabClick} />
|
||||
</div>
|
||||
|
||||
@@ -504,49 +270,48 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
'touch-auto',
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
key={activeTab || 'home'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="p-6"
|
||||
<div
|
||||
className={classNames(
|
||||
'p-6 transition-opacity duration-150',
|
||||
activeTab || showTabManagement ? 'opacity-100' : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{showTabManagement ? (
|
||||
<TabManagement />
|
||||
) : activeTab ? (
|
||||
{activeTab ? (
|
||||
getTabComponent(activeTab)
|
||||
) : (
|
||||
<motion.div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
|
||||
variants={gridLayoutVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
|
||||
<motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
|
||||
<TabTile
|
||||
tab={tab}
|
||||
onClick={() => handleTabClick(tab.id as TabType)}
|
||||
isActive={activeTab === tab.id}
|
||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||
statusMessage={getStatusMessage(tab.id)}
|
||||
description={TAB_DESCRIPTIONS[tab.id]}
|
||||
isLoading={loadingTab === tab.id}
|
||||
className="h-full relative"
|
||||
>
|
||||
{BETA_TABS.has(tab.id) && <BetaLabel />}
|
||||
</TabTile>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
|
||||
{visibleTabs.map((tab, index) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={classNames(
|
||||
'aspect-[1.5/1] transition-transform duration-100 ease-out',
|
||||
'hover:scale-[1.01]',
|
||||
)}
|
||||
style={{
|
||||
animationDelay: `${index * 30}ms`,
|
||||
animation: open ? 'fadeInUp 200ms ease-out forwards' : 'none',
|
||||
}}
|
||||
>
|
||||
<TabTile
|
||||
tab={tab}
|
||||
onClick={() => handleTabClick(tab.id as TabType)}
|
||||
isActive={activeTab === tab.id}
|
||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||
statusMessage={getStatusMessage(tab.id)}
|
||||
description={TAB_DESCRIPTIONS[tab.id]}
|
||||
isLoading={loadingTab === tab.id}
|
||||
className="h-full relative"
|
||||
>
|
||||
{BETA_TABS.has(tab.id) && <BetaLabel />}
|
||||
</TabTile>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</RadixDialog.Content>
|
||||
</div>
|
||||
</RadixDialog.Portal>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { TabType } from './types';
|
||||
|
||||
export 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',
|
||||
profile: 'i-ph:user-circle',
|
||||
settings: 'i-ph:gear-six',
|
||||
notifications: 'i-ph:bell',
|
||||
features: 'i-ph:star',
|
||||
data: 'i-ph:database',
|
||||
'cloud-providers': 'i-ph:cloud',
|
||||
'local-providers': 'i-ph:laptop',
|
||||
'service-status': 'i-ph:activity-bold',
|
||||
connection: 'i-ph:wifi-high-fill',
|
||||
debug: 'i-ph:bug-fill',
|
||||
'event-logs': 'i-ph:list-bullets-fill',
|
||||
update: 'i-ph:arrow-clockwise-fill',
|
||||
'task-manager': 'i-ph:chart-line-fill',
|
||||
'tab-management': 'i-ph:squares-four-fill',
|
||||
connection: 'i-ph:wifi-high',
|
||||
'event-logs': 'i-ph:list-bullets',
|
||||
};
|
||||
|
||||
export const TAB_LABELS: Record<TabType, string> = {
|
||||
@@ -27,11 +23,7 @@ export const TAB_LABELS: Record<TabType, string> = {
|
||||
'local-providers': 'Local Providers',
|
||||
'service-status': 'Service Status',
|
||||
connection: 'Connection',
|
||||
debug: 'Debug',
|
||||
'event-logs': 'Event Logs',
|
||||
update: 'Updates',
|
||||
'task-manager': 'Task Manager',
|
||||
'tab-management': 'Tab Management',
|
||||
};
|
||||
|
||||
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',
|
||||
'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',
|
||||
};
|
||||
|
||||
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 },
|
||||
|
||||
// User Window Tabs (In dropdown, initially hidden)
|
||||
{ id: 'profile', visible: false, window: 'user' as const, order: 7 },
|
||||
{ id: 'settings', visible: false, window: 'user' as const, order: 8 },
|
||||
{ id: 'task-manager', visible: false, 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 },
|
||||
{ id: 'profile', visible: true, window: 'user' as const, order: 7 },
|
||||
{ id: 'service-status', visible: true, window: 'user' as const, order: 8 },
|
||||
{ id: 'settings', visible: true, window: 'user' as const, order: 9 },
|
||||
];
|
||||
|
||||
@@ -12,11 +12,7 @@ export type TabType =
|
||||
| 'local-providers'
|
||||
| 'service-status'
|
||||
| 'connection'
|
||||
| 'debug'
|
||||
| 'event-logs'
|
||||
| 'update'
|
||||
| 'task-manager'
|
||||
| 'tab-management';
|
||||
| 'event-logs';
|
||||
|
||||
export type WindowType = 'user' | 'developer';
|
||||
|
||||
@@ -63,7 +59,6 @@ export interface UserTabConfig extends TabVisibilityConfig {
|
||||
|
||||
export interface TabWindowConfig {
|
||||
userTabs: UserTabConfig[];
|
||||
developerTabs: DevTabConfig[];
|
||||
}
|
||||
|
||||
export const TAB_LABELS: Record<TabType, string> = {
|
||||
@@ -76,11 +71,7 @@ export const TAB_LABELS: Record<TabType, string> = {
|
||||
'local-providers': 'Local Providers',
|
||||
'service-status': 'Service Status',
|
||||
connection: 'Connections',
|
||||
debug: 'Debug',
|
||||
'event-logs': 'Event Logs',
|
||||
update: 'Updates',
|
||||
'task-manager': 'Task Manager',
|
||||
'tab-management': 'Tab Management',
|
||||
};
|
||||
|
||||
export const categoryLabels: Record<SettingCategory, string> = {
|
||||
|
||||
@@ -7,8 +7,6 @@ export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constan
|
||||
|
||||
// Shared components
|
||||
export { TabTile } from './shared/components/TabTile';
|
||||
export { TabManagement } from './shared/components/TabManagement';
|
||||
|
||||
// Utils
|
||||
export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
|
||||
export * from './utils/animations';
|
||||
|
||||
@@ -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 { classNames } from '~/utils/classNames';
|
||||
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
|
||||
import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
|
||||
import { GlowingEffect } from '~/components/ui/GlowingEffect';
|
||||
|
||||
interface TabTileProps {
|
||||
tab: TabVisibilityConfig;
|
||||
@@ -28,106 +28,118 @@ export const TabTile: React.FC<TabTileProps> = ({
|
||||
children,
|
||||
}: TabTileProps) => {
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<Tooltip.Provider delayDuration={0}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<motion.div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'relative flex flex-col items-center p-6 rounded-xl',
|
||||
'w-full h-full min-h-[160px]',
|
||||
'bg-white dark:bg-[#141414]',
|
||||
'border border-[#E5E5E5] dark:border-[#333333]',
|
||||
'group',
|
||||
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
||||
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
||||
isActive ? 'border-purple-500 dark:border-purple-500/50 bg-purple-500/5 dark:bg-purple-500/10' : '',
|
||||
isLoading ? 'cursor-wait opacity-70' : '',
|
||||
className || '',
|
||||
)}
|
||||
>
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-col items-center justify-center flex-1 w-full">
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
<div className={classNames('min-h-[160px] list-none', className || '')}>
|
||||
<div className="relative h-full rounded-xl border border-[#E5E5E5] dark:border-[#333333] p-0.5">
|
||||
<GlowingEffect
|
||||
blur={0}
|
||||
borderWidth={1}
|
||||
spread={20}
|
||||
glow={true}
|
||||
disabled={false}
|
||||
proximity={40}
|
||||
inactiveZone={0.3}
|
||||
movementDuration={0.4}
|
||||
/>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'relative',
|
||||
'w-14 h-14',
|
||||
'flex items-center justify-center',
|
||||
'rounded-xl',
|
||||
'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',
|
||||
'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' : '',
|
||||
'relative flex flex-col items-center justify-center h-full p-4 rounded-lg',
|
||||
'bg-white dark:bg-[#141414]',
|
||||
'group cursor-pointer',
|
||||
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
||||
'transition-colors duration-100 ease-out',
|
||||
isActive ? 'bg-purple-500/5 dark:bg-purple-500/10' : '',
|
||||
isLoading ? 'cursor-wait opacity-70 pointer-events-none' : '',
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={classNames(
|
||||
TAB_ICONS[tab.id],
|
||||
'w-8 h-8',
|
||||
'text-gray-600 dark:text-gray-300',
|
||||
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
|
||||
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Label and Description */}
|
||||
<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' : '',
|
||||
'relative',
|
||||
'w-14 h-14',
|
||||
'flex items-center justify-center',
|
||||
'rounded-xl',
|
||||
'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',
|
||||
'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
|
||||
'transition-all duration-100 ease-out',
|
||||
isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
|
||||
)}
|
||||
>
|
||||
{TAB_LABELS[tab.id]}
|
||||
</h3>
|
||||
{description && (
|
||||
<p
|
||||
<div
|
||||
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',
|
||||
isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
|
||||
TAB_ICONS[tab.id],
|
||||
'w-8 h-8',
|
||||
'text-gray-600 dark:text-gray-300',
|
||||
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
|
||||
'transition-colors duration-100 ease-out',
|
||||
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
|
||||
)}
|
||||
/>
|
||||
</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}
|
||||
</p>
|
||||
{TAB_LABELS[tab.id]}
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
|
||||
export const getVisibleTabs = (
|
||||
tabConfiguration: { userTabs: TabVisibilityConfig[]; developerTabs?: TabVisibilityConfig[] },
|
||||
isDeveloperMode: boolean,
|
||||
tabConfiguration: { userTabs: TabVisibilityConfig[] },
|
||||
notificationsEnabled: boolean,
|
||||
): TabVisibilityConfig[] => {
|
||||
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
||||
@@ -11,35 +10,6 @@ export const getVisibleTabs = (
|
||||
return DEFAULT_TAB_CONFIG as TabVisibilityConfig[];
|
||||
}
|
||||
|
||||
// In developer mode, show ALL tabs without restrictions
|
||||
if (isDeveloperMode) {
|
||||
// Combine all unique tabs from both user and developer configurations
|
||||
const allTabs = new Set([
|
||||
...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
|
||||
...tabConfiguration.userTabs.map((tab) => tab.id),
|
||||
...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
|
||||
'task-manager' as TabType, // Always include task-manager in developer mode
|
||||
]);
|
||||
|
||||
// Create a complete tab list with all tabs visible
|
||||
const devTabs = Array.from(allTabs).map((tabId) => {
|
||||
// Try to find existing configuration for this tab
|
||||
const existingTab =
|
||||
tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
|
||||
tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
|
||||
DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
|
||||
|
||||
return {
|
||||
id: tabId as TabType,
|
||||
visible: true,
|
||||
window: 'developer' as const,
|
||||
order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
|
||||
} as TabVisibilityConfig;
|
||||
});
|
||||
|
||||
return devTabs.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
// In user mode, only show visible user tabs
|
||||
return tabConfiguration.userTabs
|
||||
.filter((tab) => {
|
||||
@@ -53,11 +23,6 @@ export const getVisibleTabs = (
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always show task-manager in user mode if it's configured as visible
|
||||
if (tab.id === 'task-manager') {
|
||||
return tab.visible;
|
||||
}
|
||||
|
||||
// Only show tabs that are explicitly visible and assigned to the user window
|
||||
return tab.visible && tab.window === 'user';
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
||||
import GitCloneButton from './GitCloneButton';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
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 ChatAlert from './ChatAlert';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
@@ -32,6 +32,7 @@ import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
|
||||
import { ChatBox } from './ChatBox';
|
||||
import type { DesignScheme } from '~/types/design-scheme';
|
||||
import type { ElementInfo } from '~/components/workbench/Inspector';
|
||||
import LlmErrorAlert from './LLMApiAlert';
|
||||
|
||||
const TEXTAREA_MIN_HEIGHT = 76;
|
||||
|
||||
@@ -69,6 +70,8 @@ interface BaseChatProps {
|
||||
clearSupabaseAlert?: () => void;
|
||||
deployAlert?: DeployAlert;
|
||||
clearDeployAlert?: () => void;
|
||||
llmErrorAlert?: LlmErrorAlertType;
|
||||
clearLlmErrorAlert?: () => void;
|
||||
data?: JSONValue[] | undefined;
|
||||
chatMode?: 'discuss' | 'build';
|
||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||
@@ -113,6 +116,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
clearDeployAlert,
|
||||
supabaseAlert,
|
||||
clearSupabaseAlert,
|
||||
llmErrorAlert,
|
||||
clearLlmErrorAlert,
|
||||
data,
|
||||
chatMode,
|
||||
setChatMode,
|
||||
@@ -411,6 +416,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{llmErrorAlert && <LlmErrorAlert alert={llmErrorAlert} clearAlert={() => clearLlmErrorAlert?.()} />}
|
||||
</div>
|
||||
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
|
||||
<ChatBox
|
||||
|
||||
@@ -29,6 +29,7 @@ import { filesToArtifacts } from '~/utils/fileUtils';
|
||||
import { supabaseConnection } from '~/lib/stores/supabase';
|
||||
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
|
||||
import type { ElementInfo } from '~/components/workbench/Inspector';
|
||||
import type { LlmErrorAlertType } from '~/types/actions';
|
||||
|
||||
const toastAnimation = cssTransition({
|
||||
enter: 'animated fadeInRight',
|
||||
@@ -129,12 +130,13 @@ export const ChatImpl = memo(
|
||||
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
|
||||
const actionAlert = useStore(workbenchStore.alert);
|
||||
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(
|
||||
(project) => project.id === supabaseConn.selectedProjectId,
|
||||
);
|
||||
const supabaseAlert = useStore(workbenchStore.supabaseAlert);
|
||||
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
|
||||
const [llmErrorAlert, setLlmErrorAlert] = useState<LlmErrorAlertType | undefined>(undefined);
|
||||
const [model, setModel] = useState(() => {
|
||||
const savedModel = Cookies.get('selectedModel');
|
||||
return savedModel || DEFAULT_MODEL;
|
||||
@@ -181,15 +183,8 @@ export const ChatImpl = memo(
|
||||
},
|
||||
sendExtraMessageFields: true,
|
||||
onError: (e) => {
|
||||
logger.error('Request failed\n\n', e, error);
|
||||
logStore.logError('Chat request failed', e, {
|
||||
component: 'Chat',
|
||||
action: 'request',
|
||||
error: e.message,
|
||||
});
|
||||
toast.error(
|
||||
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
||||
);
|
||||
setFakeLoading(false);
|
||||
handleError(e, 'chat');
|
||||
},
|
||||
onFinish: (message, response) => {
|
||||
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(() => {
|
||||
const textarea = textareaRef.current;
|
||||
|
||||
@@ -571,6 +640,8 @@ export const ChatImpl = memo(
|
||||
clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()}
|
||||
deployAlert={deployAlert}
|
||||
clearDeployAlert={() => workbenchStore.clearDeployAlert()}
|
||||
llmErrorAlert={llmErrorAlert}
|
||||
clearLlmErrorAlert={clearApiErrorAlert}
|
||||
data={chatData}
|
||||
chatMode={chatMode}
|
||||
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 './useEditChatDescription';
|
||||
export { default } from './useViewport';
|
||||
export { useUpdateCheck } from './useUpdateCheck';
|
||||
export { useFeatures } from './useFeatures';
|
||||
export { useNotifications } from './useNotifications';
|
||||
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,
|
||||
enableContextOptimizationStore,
|
||||
tabConfigurationStore,
|
||||
updateTabConfiguration as updateTabConfig,
|
||||
resetTabConfiguration as resetTabConfig,
|
||||
updateProviderSettings as updateProviderSettingsStore,
|
||||
updateLatestBranch,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
|
||||
import type { TabWindowConfig, TabVisibilityConfig } from '~/components/@settings/core/types';
|
||||
import type { TabWindowConfig } from '~/components/@settings/core/types';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
import { getLocalStorage, setLocalStorage } from '~/lib/persistence';
|
||||
|
||||
@@ -62,7 +61,6 @@ export interface UseSettingsReturn {
|
||||
|
||||
// Tab configuration
|
||||
tabConfiguration: TabWindowConfig;
|
||||
updateTabConfiguration: (config: TabVisibilityConfig) => void;
|
||||
resetTabConfiguration: () => void;
|
||||
}
|
||||
|
||||
@@ -205,7 +203,6 @@ export function useSettings(): UseSettingsReturn {
|
||||
setTimezone,
|
||||
settings,
|
||||
tabConfiguration,
|
||||
updateTabConfiguration: updateTabConfig,
|
||||
resetTabConfiguration: resetTabConfig,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 { PROVIDER_LIST } from '~/utils/constants';
|
||||
import type { IProviderConfig } from '~/types/model';
|
||||
import type {
|
||||
TabVisibilityConfig,
|
||||
TabWindowConfig,
|
||||
UserTabConfig,
|
||||
DevTabConfig,
|
||||
} from '~/components/@settings/core/types';
|
||||
import type { TabVisibilityConfig, TabWindowConfig, UserTabConfig } from '~/components/@settings/core/types';
|
||||
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
|
||||
import Cookies from 'js-cookie';
|
||||
import { toggleTheme } from './theme';
|
||||
import { create } from 'zustand';
|
||||
|
||||
@@ -202,7 +196,6 @@ export const updatePromptId = (id: string) => {
|
||||
const getInitialTabConfiguration = (): TabWindowConfig => {
|
||||
const defaultConfig: TabWindowConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is UserTabConfig => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is DevTabConfig => tab.window === 'developer'),
|
||||
};
|
||||
|
||||
if (!isBrowser) {
|
||||
@@ -218,16 +211,13 @@ const getInitialTabConfiguration = (): TabWindowConfig => {
|
||||
|
||||
const parsed = JSON.parse(saved);
|
||||
|
||||
if (!parsed?.userTabs || !parsed?.developerTabs) {
|
||||
if (!parsed?.userTabs) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
// Ensure proper typing of loaded configuration
|
||||
return {
|
||||
userTabs: parsed.userTabs.filter((tab: TabVisibilityConfig): tab is UserTabConfig => tab.window === 'user'),
|
||||
developerTabs: parsed.developerTabs.filter(
|
||||
(tab: TabVisibilityConfig): tab is DevTabConfig => tab.window === 'developer',
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse tab configuration:', error);
|
||||
@@ -239,60 +229,16 @@ const getInitialTabConfiguration = (): TabWindowConfig => {
|
||||
|
||||
export const tabConfigurationStore = map<TabWindowConfig>(getInitialTabConfiguration());
|
||||
|
||||
// Helper function to update tab configuration
|
||||
export const updateTabConfiguration = (config: TabVisibilityConfig) => {
|
||||
const currentConfig = tabConfigurationStore.get();
|
||||
console.log('Current tab configuration before update:', currentConfig);
|
||||
|
||||
const isUserTab = config.window === 'user';
|
||||
const targetArray = isUserTab ? 'userTabs' : 'developerTabs';
|
||||
|
||||
// Only update the tab in its respective window
|
||||
const updatedTabs = currentConfig[targetArray].map((tab) => (tab.id === config.id ? { ...config } : tab));
|
||||
|
||||
// If tab doesn't exist in this window yet, add it
|
||||
if (!updatedTabs.find((tab) => tab.id === config.id)) {
|
||||
updatedTabs.push(config);
|
||||
}
|
||||
|
||||
// Create new config, only updating the target window's tabs
|
||||
const newConfig: TabWindowConfig = {
|
||||
...currentConfig,
|
||||
[targetArray]: updatedTabs,
|
||||
};
|
||||
|
||||
console.log('New tab configuration after update:', newConfig);
|
||||
|
||||
tabConfigurationStore.set(newConfig);
|
||||
Cookies.set('tabConfiguration', JSON.stringify(newConfig), {
|
||||
expires: 365, // Set cookie to expire in 1 year
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to reset tab configuration
|
||||
export const resetTabConfiguration = () => {
|
||||
const defaultConfig: TabWindowConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is UserTabConfig => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab): tab is DevTabConfig => tab.window === 'developer'),
|
||||
};
|
||||
|
||||
tabConfigurationStore.set(defaultConfig);
|
||||
localStorage.setItem('bolt_tab_configuration', JSON.stringify(defaultConfig));
|
||||
};
|
||||
|
||||
// Developer mode store with persistence
|
||||
export const developerModeStore = atom<boolean>(initialSettings.developerMode);
|
||||
|
||||
export const setDeveloperMode = (value: boolean) => {
|
||||
developerModeStore.set(value);
|
||||
|
||||
if (isBrowser) {
|
||||
localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
// First, let's define the SettingsStore interface
|
||||
interface SettingsStore {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -10,22 +10,19 @@ export interface TabConfig {
|
||||
|
||||
interface TabConfigurationStore {
|
||||
userTabs: TabConfig[];
|
||||
developerTabs: TabConfig[];
|
||||
get: () => { userTabs: TabConfig[]; developerTabs: TabConfig[] };
|
||||
set: (config: { userTabs: TabConfig[]; developerTabs: TabConfig[] }) => void;
|
||||
get: () => { userTabs: TabConfig[] };
|
||||
set: (config: { userTabs: TabConfig[] }) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
userTabs: [],
|
||||
developerTabs: [],
|
||||
};
|
||||
|
||||
export const tabConfigurationStore = create<TabConfigurationStore>((set, get) => ({
|
||||
...DEFAULT_CONFIG,
|
||||
get: () => ({
|
||||
userTabs: get().userTabs,
|
||||
developerTabs: get().developerTabs,
|
||||
}),
|
||||
set: (config) => set(config),
|
||||
reset: () => set(DEFAULT_CONFIG),
|
||||
|
||||
@@ -361,16 +361,34 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
} catch (error: any) {
|
||||
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')) {
|
||||
throw new Response('Invalid or missing API key', {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
...errorResponse,
|
||||
message: 'Invalid or missing API key',
|
||||
statusCode: 401,
|
||||
isRetryable: false,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
return new Response(JSON.stringify(errorResponse), {
|
||||
status: errorResponse.statusCode,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
statusText: 'Error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,16 +139,34 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||
} catch (error: unknown) {
|
||||
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')) {
|
||||
throw new Response('Invalid or missing API key', {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
...errorResponse,
|
||||
message: 'Invalid or missing API key',
|
||||
statusCode: 401,
|
||||
isRetryable: false,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
return new Response(JSON.stringify(errorResponse), {
|
||||
status: errorResponse.statusCode,
|
||||
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';
|
||||
}
|
||||
|
||||
export interface LlmErrorAlertType {
|
||||
type: 'error' | 'warning';
|
||||
title: string;
|
||||
description: string;
|
||||
content?: string;
|
||||
provider?: string;
|
||||
errorType?: 'authentication' | 'rate_limit' | 'quota' | 'network' | 'unknown';
|
||||
}
|
||||
|
||||
export interface FileHistory {
|
||||
originalContent: string;
|
||||
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';
|
||||
|
||||
const chalk = new Chalk({ level: 3 });
|
||||
@@ -14,7 +14,7 @@ interface Logger {
|
||||
setLevel: (level: DebugLevel) => void;
|
||||
}
|
||||
|
||||
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
|
||||
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL || (import.meta.env.DEV ? 'debug' : 'info');
|
||||
|
||||
export const logger: Logger = {
|
||||
trace: (...messages: any[]) => log('trace', undefined, messages),
|
||||
@@ -45,12 +45,17 @@ function setLevel(level: DebugLevel) {
|
||||
}
|
||||
|
||||
function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
|
||||
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];
|
||||
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'none'];
|
||||
|
||||
if (levelOrder.indexOf(level) < levelOrder.indexOf(currentLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If current level is 'none', don't log anything
|
||||
if (currentLevel === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
const allMessages = messages.reduce((acc, current) => {
|
||||
if (acc.endsWith('\n')) {
|
||||
return acc + current;
|
||||
|
||||
Reference in New Issue
Block a user