Merge pull request #1826 from xKevIsDev/error-fix

fix: enhanced error handling for llm api, general cleanup
This commit is contained in:
KevIsDev
2025-07-08 02:11:37 +01:00
committed by GitHub
31 changed files with 633 additions and 6405 deletions

View File

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

View File

@@ -1,25 +1,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>

View File

@@ -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 },
];

View File

@@ -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> = {

View File

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

View File

@@ -1,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>
);
};

View File

@@ -1,8 +1,8 @@
import { motion } from 'framer-motion';
import * as Tooltip from '@radix-ui/react-tooltip';
import { classNames } from '~/utils/classNames';
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
import { GlowingEffect } from '~/components/ui/GlowingEffect';
interface TabTileProps {
tab: TabVisibilityConfig;
@@ -28,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

View File

@@ -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;

View File

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

View File

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

View File

@@ -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

View File

@@ -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}

View 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>
);
}

View File

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

View File

@@ -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';

View File

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

View File

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

View File

@@ -1,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 };
};

View File

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

View File

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

View File

@@ -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',
});
}
}

View File

@@ -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',
});
}
}

View File

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

View File

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

View File

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

View File

@@ -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
View File

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

View File

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