Stijnus commited on
Commit
6d98aff
Β·
1 Parent(s): 293fdb7

Add new features

Browse files

Bolt DIY UI

## New User Interface Features

### 🎨 Redesigned Control Panel

The Bolt DIY interface has been completely redesigned with a modern, intuitive layout featuring two main components:

1. **Users Window** - Main control panel for regular users
2. **Developer Window** - Advanced settings and debugging tools

### πŸ’‘ Core Features

- **Drag & Drop Tab Management**: Customize tab order in both User and Developer windows
- **Dynamic Status Updates**: Real-time status indicators for updates, notifications, and system health
- **Responsive Design**: Beautiful transitions and animations using Framer Motion
- **Dark/Light Mode Support**: Full theme support with consistent styling
- **Improved Accessibility**: Using Radix UI primitives for better accessibility
- **Enhanced Provider Management**: Split view for local and cloud providers
- **Resource Monitoring**: New Task Manager for system performance tracking

### 🎯 Tab Overview

#### User Window Tabs

1. **Profile**

- Manage user profile and account settings
- Avatar customization
- Account preferences

2. **Settings**

- Configure application preferences
- Customize UI behavior
- Manage general settings

3. **Notifications**

- Real-time notification center
- Unread notification tracking
- Notification preferences

4. **Features**

- Explore new and upcoming features
- Feature preview toggles
- Early access options

5. **Data**

- Data management tools
- Storage settings
- Backup and restore options

6. **Cloud Providers**

- Configure cloud-based AI providers
- API key management
- Cloud model selection
- Provider-specific settings
- Status monitoring for each provider

7. **Local Providers**

- Manage local AI models
- Ollama integration and model updates
- LM Studio configuration
- Local inference settings
- Model download and updates

8. **Task Manager**

- System resource monitoring
- Process management
- Performance metrics
- Resource usage graphs
- Alert configurations

9. **Connection**

- Network status monitoring
- Connection health metrics
- Troubleshooting tools
- Latency tracking
- Auto-reconnect settings

10. **Debug**

- System diagnostics
- Performance monitoring
- Error tracking
- Provider status checks
- System information

11. **Event Logs**

- Comprehensive system logs
- Filtered log views
- Log management tools
- Error tracking
- Performance metrics

12. **Update**
- Version management
- Update notifications
- Release notes
- Auto-update configuration

#### Developer Window Enhancements

- **Advanced Tab Management**

- Fine-grained control over tab visibility
- Custom tab ordering
- Tab permission management
- Category-based organization

- **Developer Tools**
- Enhanced debugging capabilities
- System metrics and monitoring
- Performance optimization tools
- Advanced logging features

### πŸš€ UI Improvements

1. **Enhanced Navigation**

- Intuitive back navigation
- Breadcrumb-style header
- Context-aware menu system
- Improved tab organization

2. **Status Indicators**

- Dynamic update badges
- Real-time connection status
- System health monitoring
- Provider status tracking

3. **Profile Integration**

- Quick access profile menu
- Avatar support
- Fast settings access
- Personalization options

4. **Accessibility Features**
- Keyboard navigation
- Screen reader support
- Focus management
- ARIA attributes

### πŸ›  Technical Enhancements

- **State Management**

- Nano Stores for efficient state handling
- Persistent settings storage
- Real-time state synchronization
- Provider state management

- **Performance Optimizations**

- Lazy loading of tab contents
- Efficient DOM updates
- Optimized animations
- Resource monitoring

- **Developer Experience**
- Improved error handling
- Better debugging tools
- Enhanced logging system
- Performance profiling

### 🎯 Future Roadmap

- [ ] Additional customization options
- [ ] Enhanced theme support
- [ ] More developer tools
- [ ] Extended API integrations
- [ ] Advanced monitoring capabilities
- [ ] Custom provider plugins
- [ ] Enhanced resource management
- [ ] Advanced debugging features

## πŸ”§ Technical Details

### Dependencies

- Radix UI for accessible components
- Framer Motion for animations
- React DnD for drag and drop
- Nano Stores for state management

### Browser Support

- Modern browsers (Chrome, Firefox, Safari, Edge)
- Progressive enhancement for older browsers

### Performance

- Optimized bundle size
- Efficient state updates
- Minimal re-renders
- Resource-aware operations

## πŸ“ Contributing

We welcome contributions! Please see our contributing guidelines for more information.

## πŸ“„ License

MIT License - see LICENSE for details

.gitignore CHANGED
@@ -39,4 +39,5 @@ modelfiles
39
  site
40
 
41
  # commit file ignore
42
- app/commit.json
 
 
39
  site
40
 
41
  # commit file ignore
42
+ app/commit.json
43
+ changelogUI.md
app/components/settings/developer/DeveloperWindow.tsx CHANGED
@@ -13,7 +13,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
13
  import DebugTab from '~/components/settings/debug/DebugTab';
14
  import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
15
  import UpdateTab from '~/components/settings/update/UpdateTab';
16
- import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
17
  import DataTab from '~/components/settings/data/DataTab';
18
  import FeaturesTab from '~/components/settings/features/FeaturesTab';
19
  import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
@@ -22,6 +21,9 @@ import ProfileTab from '~/components/settings/profile/ProfileTab';
22
  import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
23
  import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
24
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
 
 
 
25
 
26
  interface DraggableTabTileProps {
27
  tab: TabVisibilityConfig;
@@ -41,11 +43,13 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
41
  notifications: 'View and manage your notifications',
42
  features: 'Explore new and upcoming features',
43
  data: 'Manage your data and storage',
44
- providers: 'Configure AI providers and models',
 
45
  connection: 'Check connection status and settings',
46
  debug: 'Debug tools and system information',
47
  'event-logs': 'View system events and logs',
48
  update: 'Check for updates and release notes',
 
49
  };
50
 
51
  const DraggableTabTile = ({
@@ -209,8 +213,10 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
209
  return <FeaturesTab />;
210
  case 'data':
211
  return <DataTab />;
212
- case 'providers':
213
- return <ProvidersTab />;
 
 
214
  case 'connection':
215
  return <ConnectionsTab />;
216
  case 'debug':
@@ -219,6 +225,8 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
219
  return <EventLogsTab />;
220
  case 'update':
221
  return <UpdateTab />;
 
 
222
  default:
223
  return null;
224
  }
@@ -412,6 +420,15 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
412
  <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
413
  </>
414
  )}
 
 
 
 
 
 
 
 
 
415
  <DropdownMenu.Item
416
  className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
417
  onSelect={onClose}
 
13
  import DebugTab from '~/components/settings/debug/DebugTab';
14
  import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
15
  import UpdateTab from '~/components/settings/update/UpdateTab';
 
16
  import DataTab from '~/components/settings/data/DataTab';
17
  import FeaturesTab from '~/components/settings/features/FeaturesTab';
18
  import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
 
21
  import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
22
  import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
23
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
24
+ import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
25
+ import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
26
+ import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
27
 
28
  interface DraggableTabTileProps {
29
  tab: TabVisibilityConfig;
 
43
  notifications: 'View and manage your notifications',
44
  features: 'Explore new and upcoming features',
45
  data: 'Manage your data and storage',
46
+ 'cloud-providers': 'Configure cloud AI providers and models',
47
+ 'local-providers': 'Configure local AI providers and models',
48
  connection: 'Check connection status and settings',
49
  debug: 'Debug tools and system information',
50
  'event-logs': 'View system events and logs',
51
  update: 'Check for updates and release notes',
52
+ 'task-manager': 'Monitor system resources and processes',
53
  };
54
 
55
  const DraggableTabTile = ({
 
213
  return <FeaturesTab />;
214
  case 'data':
215
  return <DataTab />;
216
+ case 'cloud-providers':
217
+ return <CloudProvidersTab />;
218
+ case 'local-providers':
219
+ return <LocalProvidersTab />;
220
  case 'connection':
221
  return <ConnectionsTab />;
222
  case 'debug':
 
225
  return <EventLogsTab />;
226
  case 'update':
227
  return <UpdateTab />;
228
+ case 'task-manager':
229
+ return <TaskManagerTab />;
230
  default:
231
  return null;
232
  }
 
420
  <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
421
  </>
422
  )}
423
+ <DropdownMenu.Item
424
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
425
+ onSelect={() => handleTabClick('task-manager')}
426
+ >
427
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
428
+ <div className="i-ph:activity w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
429
+ </div>
430
+ <span className="group-hover:text-purple-500 transition-colors">Task Manager</span>
431
+ </DropdownMenu.Item>
432
  <DropdownMenu.Item
433
  className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
434
  onSelect={onClose}
app/components/settings/developer/TabManagement.tsx CHANGED
@@ -13,11 +13,13 @@ const TAB_ICONS: Record<TabType, string> = {
13
  notifications: 'i-ph:bell-fill',
14
  features: 'i-ph:sparkle-fill',
15
  data: 'i-ph:database-fill',
16
- providers: 'i-ph:robot-fill',
 
17
  connection: 'i-ph:plug-fill',
18
  debug: 'i-ph:bug-fill',
19
  'event-logs': 'i-ph:list-bullets-fill',
20
  update: 'i-ph:arrow-clockwise-fill',
 
21
  };
22
 
23
  interface TabGroupProps {
@@ -174,14 +176,15 @@ export const TabManagement = () => {
174
  const [searchQuery, setSearchQuery] = useState('');
175
 
176
  // Define standard (visible by default) tabs for each window
177
- const standardUserTabs: TabType[] = ['features', 'data', 'providers', 'connection', 'debug'];
178
  const standardDeveloperTabs: TabType[] = [
179
  'profile',
180
  'settings',
181
  'notifications',
182
  'features',
183
  'data',
184
- 'providers',
 
185
  'connection',
186
  'debug',
187
  'event-logs',
@@ -217,12 +220,12 @@ export const TabManagement = () => {
217
  };
218
 
219
  // Filter tabs based on search and window
220
- const userTabs = config.userTabs.filter((tab) =>
221
- TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
222
  );
223
 
224
- const developerTabs = config.developerTabs.filter((tab) =>
225
- TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
226
  );
227
 
228
  return (
 
13
  notifications: 'i-ph:bell-fill',
14
  features: 'i-ph:sparkle-fill',
15
  data: 'i-ph:database-fill',
16
+ 'cloud-providers': 'i-ph:cloud-fill',
17
+ 'local-providers': 'i-ph:desktop-fill',
18
  connection: 'i-ph:plug-fill',
19
  debug: 'i-ph:bug-fill',
20
  'event-logs': 'i-ph:list-bullets-fill',
21
  update: 'i-ph:arrow-clockwise-fill',
22
+ 'task-manager': 'i-ph:gauge-fill',
23
  };
24
 
25
  interface TabGroupProps {
 
176
  const [searchQuery, setSearchQuery] = useState('');
177
 
178
  // Define standard (visible by default) tabs for each window
179
+ const standardUserTabs: TabType[] = ['features', 'data', 'local-providers', 'cloud-providers', 'connection', 'debug'];
180
  const standardDeveloperTabs: TabType[] = [
181
  'profile',
182
  'settings',
183
  'notifications',
184
  'features',
185
  'data',
186
+ 'local-providers',
187
+ 'cloud-providers',
188
  'connection',
189
  'debug',
190
  'event-logs',
 
220
  };
221
 
222
  // Filter tabs based on search and window
223
+ const userTabs = (config.userTabs || []).filter(
224
+ (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
225
  );
226
 
227
+ const developerTabs = (config.developerTabs || []).filter(
228
+ (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
229
  );
230
 
231
  return (
app/components/settings/providers/CloudProvidersTab.tsx ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { Switch } from '~/components/ui/Switch';
3
+ import { useSettings } from '~/lib/hooks/useSettings';
4
+ import { URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
+ import type { IProviderConfig } from '~/types/model';
6
+ import { logStore } from '~/lib/stores/logs';
7
+ import { motion } from 'framer-motion';
8
+ import { classNames } from '~/utils/classNames';
9
+ import { settingsStyles } from '~/components/settings/settings.styles';
10
+ import { toast } from 'react-toastify';
11
+ import { providerBaseUrlEnvKeys } from '~/utils/constants';
12
+ import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
13
+ import { BsRobot, BsCloud } from 'react-icons/bs';
14
+ import { TbBrain, TbCloudComputing } from 'react-icons/tb';
15
+ import { BiCodeBlock, BiChip } from 'react-icons/bi';
16
+ import { FaCloud, FaBrain } from 'react-icons/fa';
17
+ import type { IconType } from 'react-icons';
18
+
19
+ // Add type for provider names to ensure type safety
20
+ type ProviderName =
21
+ | 'AmazonBedrock'
22
+ | 'Anthropic'
23
+ | 'Cohere'
24
+ | 'Deepseek'
25
+ | 'Google'
26
+ | 'Groq'
27
+ | 'HuggingFace'
28
+ | 'Hyperbolic'
29
+ | 'Mistral'
30
+ | 'OpenAI'
31
+ | 'OpenRouter'
32
+ | 'Perplexity'
33
+ | 'Together'
34
+ | 'XAI';
35
+
36
+ // Update the PROVIDER_ICONS type to use the ProviderName type
37
+ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
38
+ AmazonBedrock: SiAmazon,
39
+ Anthropic: FaBrain,
40
+ Cohere: BiChip,
41
+ Deepseek: BiCodeBlock,
42
+ Google: SiGoogle,
43
+ Groq: BsCloud,
44
+ HuggingFace: SiHuggingface,
45
+ Hyperbolic: TbCloudComputing,
46
+ Mistral: TbBrain,
47
+ OpenAI: SiOpenai,
48
+ OpenRouter: FaCloud,
49
+ Perplexity: SiPerplexity,
50
+ Together: BsCloud,
51
+ XAI: BsRobot,
52
+ };
53
+
54
+ // Update PROVIDER_DESCRIPTIONS to use the same type
55
+ const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
56
+ Anthropic: 'Access Claude and other Anthropic models',
57
+ OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
58
+ };
59
+
60
+ const CloudProvidersTab = () => {
61
+ const settings = useSettings();
62
+ const [editingProvider, setEditingProvider] = useState<string | null>(null);
63
+ const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
64
+ const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
65
+
66
+ // Effect to filter and sort providers
67
+ useEffect(() => {
68
+ const newFilteredProviders = Object.entries(settings.providers || {})
69
+ .filter(([key]) => !['Ollama', 'LMStudio', 'OpenAILike'].includes(key)) // Filter out local providers
70
+ .map(([key, value]) => {
71
+ const provider = value as IProviderConfig;
72
+ return {
73
+ name: key,
74
+ settings: provider.settings,
75
+ staticModels: provider.staticModels || [],
76
+ getDynamicModels: provider.getDynamicModels,
77
+ getApiKeyLink: provider.getApiKeyLink,
78
+ labelForGetApiKey: provider.labelForGetApiKey,
79
+ icon: provider.icon,
80
+ } as IProviderConfig;
81
+ });
82
+
83
+ const sorted = newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
84
+ const regular = sorted.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
85
+ const urlConfigurable = sorted.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
86
+
87
+ setFilteredProviders([...regular, ...urlConfigurable]);
88
+ }, [settings.providers]);
89
+
90
+ // Add effect to update category toggle state based on provider states
91
+ useEffect(() => {
92
+ const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
93
+ setCategoryEnabled(newCategoryState);
94
+ }, [filteredProviders]);
95
+
96
+ const handleToggleCategory = useCallback(
97
+ (enabled: boolean) => {
98
+ setCategoryEnabled(enabled);
99
+ filteredProviders.forEach((provider) => {
100
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
101
+ });
102
+ toast.success(enabled ? 'All cloud providers enabled' : 'All cloud providers disabled');
103
+ },
104
+ [filteredProviders, settings],
105
+ );
106
+
107
+ const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
108
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
109
+
110
+ if (enabled) {
111
+ logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
112
+ toast.success(`${provider.name} enabled`);
113
+ } else {
114
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
115
+ toast.success(`${provider.name} disabled`);
116
+ }
117
+ };
118
+
119
+ const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
120
+ let newBaseUrl: string | undefined = baseUrl;
121
+
122
+ if (newBaseUrl && newBaseUrl.trim().length === 0) {
123
+ newBaseUrl = undefined;
124
+ }
125
+
126
+ settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
127
+ logStore.logProvider(`Base URL updated for ${provider.name}`, {
128
+ provider: provider.name,
129
+ baseUrl: newBaseUrl,
130
+ });
131
+ toast.success(`${provider.name} base URL updated`);
132
+ setEditingProvider(null);
133
+ };
134
+
135
+ return (
136
+ <div className="space-y-6">
137
+ <motion.div
138
+ className="space-y-4"
139
+ initial={{ opacity: 0, y: 20 }}
140
+ animate={{ opacity: 1, y: 0 }}
141
+ transition={{ duration: 0.3 }}
142
+ >
143
+ <div className="flex items-center justify-between gap-4 mt-8 mb-4">
144
+ <div className="flex items-center gap-2">
145
+ <div
146
+ className={classNames(
147
+ 'w-8 h-8 flex items-center justify-center rounded-lg',
148
+ 'bg-bolt-elements-background-depth-3',
149
+ 'text-purple-500',
150
+ )}
151
+ >
152
+ <TbCloudComputing className="w-5 h-5" />
153
+ </div>
154
+ <div>
155
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary">Cloud Providers</h4>
156
+ <p className="text-sm text-bolt-elements-textSecondary">Connect to cloud-based AI models and services</p>
157
+ </div>
158
+ </div>
159
+
160
+ <div className="flex items-center gap-2">
161
+ <span className="text-sm text-bolt-elements-textSecondary">Enable All Cloud</span>
162
+ <Switch checked={categoryEnabled} onCheckedChange={handleToggleCategory} />
163
+ </div>
164
+ </div>
165
+
166
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
167
+ {filteredProviders.map((provider, index) => (
168
+ <motion.div
169
+ key={provider.name}
170
+ className={classNames(
171
+ settingsStyles.card,
172
+ 'bg-bolt-elements-background-depth-2',
173
+ 'hover:bg-bolt-elements-background-depth-3',
174
+ 'transition-all duration-200',
175
+ 'relative overflow-hidden group',
176
+ 'flex flex-col',
177
+ )}
178
+ initial={{ opacity: 0, y: 20 }}
179
+ animate={{ opacity: 1, y: 0 }}
180
+ transition={{ delay: index * 0.1 }}
181
+ whileHover={{ scale: 1.02 }}
182
+ >
183
+ <div className="absolute top-0 right-0 p-2 flex gap-1">
184
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
185
+ <motion.span
186
+ className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
187
+ whileHover={{ scale: 1.05 }}
188
+ whileTap={{ scale: 0.95 }}
189
+ >
190
+ Configurable
191
+ </motion.span>
192
+ )}
193
+ </div>
194
+
195
+ <div className="flex items-start gap-4 p-4">
196
+ <motion.div
197
+ className={classNames(
198
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
199
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
200
+ 'transition-all duration-200',
201
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
202
+ )}
203
+ whileHover={{ scale: 1.1 }}
204
+ whileTap={{ scale: 0.9 }}
205
+ >
206
+ <div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
207
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
208
+ className: 'w-full h-full',
209
+ 'aria-label': `${provider.name} logo`,
210
+ })}
211
+ </div>
212
+ </motion.div>
213
+
214
+ <div className="flex-1 min-w-0">
215
+ <div className="flex items-center justify-between gap-4 mb-2">
216
+ <div>
217
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
218
+ {provider.name}
219
+ </h4>
220
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
221
+ {PROVIDER_DESCRIPTIONS[provider.name as keyof typeof PROVIDER_DESCRIPTIONS] ||
222
+ (URL_CONFIGURABLE_PROVIDERS.includes(provider.name)
223
+ ? 'Configure custom endpoint for this provider'
224
+ : 'Standard AI provider integration')}
225
+ </p>
226
+ </div>
227
+ <Switch
228
+ checked={provider.settings.enabled}
229
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
230
+ />
231
+ </div>
232
+
233
+ {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
234
+ <motion.div
235
+ initial={{ opacity: 0, height: 0 }}
236
+ animate={{ opacity: 1, height: 'auto' }}
237
+ exit={{ opacity: 0, height: 0 }}
238
+ transition={{ duration: 0.2 }}
239
+ >
240
+ <div className="flex items-center gap-2 mt-4">
241
+ {editingProvider === provider.name ? (
242
+ <input
243
+ type="text"
244
+ defaultValue={provider.settings.baseUrl}
245
+ placeholder={`Enter ${provider.name} base URL`}
246
+ className={classNames(
247
+ 'flex-1 px-3 py-1.5 rounded-lg text-sm',
248
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
249
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
250
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
251
+ 'transition-all duration-200',
252
+ )}
253
+ onKeyDown={(e) => {
254
+ if (e.key === 'Enter') {
255
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
256
+ } else if (e.key === 'Escape') {
257
+ setEditingProvider(null);
258
+ }
259
+ }}
260
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
261
+ autoFocus
262
+ />
263
+ ) : (
264
+ <div
265
+ className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
266
+ onClick={() => setEditingProvider(provider.name)}
267
+ >
268
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
269
+ <div className="i-ph:link text-sm" />
270
+ <span className="group-hover/url:text-purple-500 transition-colors">
271
+ {provider.settings.baseUrl || 'Click to set base URL'}
272
+ </span>
273
+ </div>
274
+ </div>
275
+ )}
276
+ </div>
277
+
278
+ {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
279
+ <div className="mt-2 text-xs text-green-500">
280
+ <div className="flex items-center gap-1">
281
+ <div className="i-ph:info" />
282
+ <span>Environment URL set in .env file</span>
283
+ </div>
284
+ </div>
285
+ )}
286
+ </motion.div>
287
+ )}
288
+ </div>
289
+ </div>
290
+
291
+ <motion.div
292
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
293
+ animate={{
294
+ borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
295
+ scale: provider.settings.enabled ? 1 : 0.98,
296
+ }}
297
+ transition={{ duration: 0.2 }}
298
+ />
299
+ </motion.div>
300
+ ))}
301
+ </div>
302
+ </motion.div>
303
+ </div>
304
+ );
305
+ };
306
+
307
+ export default CloudProvidersTab;
app/components/settings/providers/LocalProvidersTab.tsx ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { Switch } from '~/components/ui/Switch';
3
+ import { useSettings } from '~/lib/hooks/useSettings';
4
+ import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
+ import type { IProviderConfig } from '~/types/model';
6
+ import { logStore } from '~/lib/stores/logs';
7
+ import { motion } from 'framer-motion';
8
+ import { classNames } from '~/utils/classNames';
9
+ import { settingsStyles } from '~/components/settings/settings.styles';
10
+ import { toast } from 'react-toastify';
11
+ import { BsBox, BsCodeSquare, BsRobot } from 'react-icons/bs';
12
+ import type { IconType } from 'react-icons';
13
+ import OllamaModelUpdater from './OllamaModelUpdater';
14
+ import { DialogRoot, Dialog } from '~/components/ui/Dialog';
15
+ import { BiChip } from 'react-icons/bi';
16
+ import { TbBrandOpenai } from 'react-icons/tb';
17
+ import { providerBaseUrlEnvKeys } from '~/utils/constants';
18
+
19
+ // Add type for provider names to ensure type safety
20
+ type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
21
+
22
+ // Update the PROVIDER_ICONS type to use the ProviderName type
23
+ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
24
+ Ollama: BsBox,
25
+ LMStudio: BsCodeSquare,
26
+ OpenAILike: TbBrandOpenai,
27
+ };
28
+
29
+ // Update PROVIDER_DESCRIPTIONS to use the same type
30
+ const PROVIDER_DESCRIPTIONS: Record<ProviderName, string> = {
31
+ Ollama: 'Run open-source models locally on your machine',
32
+ LMStudio: 'Local model inference with LM Studio',
33
+ OpenAILike: 'Connect to OpenAI-compatible API endpoints',
34
+ };
35
+
36
+ const LocalProvidersTab = () => {
37
+ const settings = useSettings();
38
+ const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
39
+ const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
40
+ const [showOllamaUpdater, setShowOllamaUpdater] = useState(false);
41
+ const [editingProvider, setEditingProvider] = useState<string | null>(null);
42
+
43
+ // Effect to filter and sort providers
44
+ useEffect(() => {
45
+ const newFilteredProviders = Object.entries(settings.providers || {})
46
+ .filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key))
47
+ .map(([key, value]) => {
48
+ const provider = value as IProviderConfig;
49
+ return {
50
+ name: key,
51
+ settings: provider.settings,
52
+ staticModels: provider.staticModels || [],
53
+ getDynamicModels: provider.getDynamicModels,
54
+ getApiKeyLink: provider.getApiKeyLink,
55
+ labelForGetApiKey: provider.labelForGetApiKey,
56
+ icon: provider.icon,
57
+ } as IProviderConfig;
58
+ });
59
+
60
+ const sorted = newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
61
+ setFilteredProviders(sorted);
62
+ }, [settings.providers]);
63
+
64
+ // Add effect to update category toggle state based on provider states
65
+ useEffect(() => {
66
+ const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
67
+ setCategoryEnabled(newCategoryState);
68
+ }, [filteredProviders]);
69
+
70
+ const handleToggleCategory = useCallback(
71
+ (enabled: boolean) => {
72
+ setCategoryEnabled(enabled);
73
+ filteredProviders.forEach((provider) => {
74
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
75
+ });
76
+ toast.success(enabled ? 'All local providers enabled' : 'All local providers disabled');
77
+ },
78
+ [filteredProviders, settings],
79
+ );
80
+
81
+ const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
82
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
83
+
84
+ if (enabled) {
85
+ logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
86
+ toast.success(`${provider.name} enabled`);
87
+ } else {
88
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
89
+ toast.success(`${provider.name} disabled`);
90
+ }
91
+ };
92
+
93
+ const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
94
+ let newBaseUrl: string | undefined = baseUrl;
95
+
96
+ if (newBaseUrl && newBaseUrl.trim().length === 0) {
97
+ newBaseUrl = undefined;
98
+ }
99
+
100
+ settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
101
+ logStore.logProvider(`Base URL updated for ${provider.name}`, {
102
+ provider: provider.name,
103
+ baseUrl: newBaseUrl,
104
+ });
105
+ toast.success(`${provider.name} base URL updated`);
106
+ setEditingProvider(null);
107
+ };
108
+
109
+ return (
110
+ <div className="space-y-6">
111
+ <motion.div
112
+ className="space-y-4"
113
+ initial={{ opacity: 0, y: 20 }}
114
+ animate={{ opacity: 1, y: 0 }}
115
+ transition={{ duration: 0.3 }}
116
+ >
117
+ <div className="flex items-center justify-between gap-4 mt-8 mb-4">
118
+ <div className="flex items-center gap-2">
119
+ <div
120
+ className={classNames(
121
+ 'w-8 h-8 flex items-center justify-center rounded-lg',
122
+ 'bg-bolt-elements-background-depth-3',
123
+ 'text-purple-500',
124
+ )}
125
+ >
126
+ <BiChip className="w-5 h-5" />
127
+ </div>
128
+ <div>
129
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary">Local Providers</h4>
130
+ <p className="text-sm text-bolt-elements-textSecondary">
131
+ Configure and update local AI models on your machine
132
+ </p>
133
+ </div>
134
+ </div>
135
+
136
+ <div className="flex items-center gap-2">
137
+ <span className="text-sm text-bolt-elements-textSecondary">Enable All Local</span>
138
+ <Switch checked={categoryEnabled} onCheckedChange={handleToggleCategory} />
139
+ </div>
140
+ </div>
141
+
142
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
143
+ {filteredProviders.map((provider, index) => (
144
+ <motion.div
145
+ key={provider.name}
146
+ className={classNames(
147
+ settingsStyles.card,
148
+ 'bg-bolt-elements-background-depth-2',
149
+ 'hover:bg-bolt-elements-background-depth-3',
150
+ 'transition-all duration-200',
151
+ 'relative overflow-hidden group',
152
+ 'flex flex-col',
153
+ )}
154
+ initial={{ opacity: 0, y: 20 }}
155
+ animate={{ opacity: 1, y: 0 }}
156
+ transition={{ delay: index * 0.1 }}
157
+ whileHover={{ scale: 1.02 }}
158
+ >
159
+ <div className="absolute top-0 right-0 p-2 flex gap-1">
160
+ <motion.span
161
+ className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500 font-medium"
162
+ whileHover={{ scale: 1.05 }}
163
+ whileTap={{ scale: 0.95 }}
164
+ >
165
+ Local
166
+ </motion.span>
167
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
168
+ <motion.span
169
+ className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
170
+ whileHover={{ scale: 1.05 }}
171
+ whileTap={{ scale: 0.95 }}
172
+ >
173
+ Configurable
174
+ </motion.span>
175
+ )}
176
+ </div>
177
+
178
+ <div className="flex items-start gap-4 p-4">
179
+ <motion.div
180
+ className={classNames(
181
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
182
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
183
+ 'transition-all duration-200',
184
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
185
+ )}
186
+ whileHover={{ scale: 1.1 }}
187
+ whileTap={{ scale: 0.9 }}
188
+ >
189
+ <div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
190
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
191
+ className: 'w-full h-full',
192
+ 'aria-label': `${provider.name} logo`,
193
+ })}
194
+ </div>
195
+ </motion.div>
196
+
197
+ <div className="flex-1 min-w-0">
198
+ <div className="flex items-center justify-between gap-4 mb-2">
199
+ <div>
200
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
201
+ {provider.name}
202
+ </h4>
203
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
204
+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
205
+ </p>
206
+ </div>
207
+ <Switch
208
+ checked={provider.settings.enabled}
209
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
210
+ />
211
+ </div>
212
+
213
+ {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
214
+ <motion.div
215
+ initial={{ opacity: 0, height: 0 }}
216
+ animate={{ opacity: 1, height: 'auto' }}
217
+ exit={{ opacity: 0, height: 0 }}
218
+ transition={{ duration: 0.2 }}
219
+ >
220
+ <div className="flex items-center gap-2 mt-4">
221
+ {editingProvider === provider.name ? (
222
+ <input
223
+ type="text"
224
+ defaultValue={provider.settings.baseUrl}
225
+ placeholder={`Enter ${provider.name} base URL`}
226
+ className={classNames(
227
+ 'flex-1 px-3 py-1.5 rounded-lg text-sm',
228
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
229
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
230
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
231
+ 'transition-all duration-200',
232
+ )}
233
+ onKeyDown={(e) => {
234
+ if (e.key === 'Enter') {
235
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
236
+ } else if (e.key === 'Escape') {
237
+ setEditingProvider(null);
238
+ }
239
+ }}
240
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
241
+ autoFocus
242
+ />
243
+ ) : (
244
+ <div
245
+ className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
246
+ onClick={() => setEditingProvider(provider.name)}
247
+ >
248
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
249
+ <div className="i-ph:link text-sm" />
250
+ <span className="group-hover/url:text-purple-500 transition-colors">
251
+ {provider.settings.baseUrl || 'Click to set base URL'}
252
+ </span>
253
+ </div>
254
+ </div>
255
+ )}
256
+ </div>
257
+
258
+ {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
259
+ <div className="mt-2 text-xs text-green-500">
260
+ <div className="flex items-center gap-1">
261
+ <div className="i-ph:info" />
262
+ <span>Environment URL set in .env file</span>
263
+ </div>
264
+ </div>
265
+ )}
266
+ </motion.div>
267
+ )}
268
+ </div>
269
+ </div>
270
+
271
+ <motion.div
272
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
273
+ animate={{
274
+ borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
275
+ scale: provider.settings.enabled ? 1 : 0.98,
276
+ }}
277
+ transition={{ duration: 0.2 }}
278
+ />
279
+
280
+ {provider.name === 'Ollama' && provider.settings.enabled && (
281
+ <motion.button
282
+ onClick={() => setShowOllamaUpdater(true)}
283
+ className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')}
284
+ whileHover={{ scale: 1.02 }}
285
+ whileTap={{ scale: 0.98 }}
286
+ >
287
+ <div className="i-ph:arrows-clockwise" />
288
+ Update Models
289
+ </motion.button>
290
+ )}
291
+ </motion.div>
292
+ ))}
293
+ </div>
294
+ </motion.div>
295
+
296
+ <DialogRoot open={showOllamaUpdater} onOpenChange={setShowOllamaUpdater}>
297
+ <Dialog>
298
+ <div className="p-6">
299
+ <OllamaModelUpdater />
300
+ </div>
301
+ </Dialog>
302
+ </DialogRoot>
303
+ </div>
304
+ );
305
+ };
306
+
307
+ export default LocalProvidersTab;
app/components/settings/providers/ProvidersTab.tsx DELETED
@@ -1,413 +0,0 @@
1
- import React, { useEffect, useState, useMemo, useCallback } from 'react';
2
- import { Switch } from '~/components/ui/Switch';
3
- import Separator from '~/components/ui/Separator';
4
- import { useSettings } from '~/lib/hooks/useSettings';
5
- import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
6
- import type { IProviderConfig } from '~/types/model';
7
- import { logStore } from '~/lib/stores/logs';
8
- import { motion } from 'framer-motion';
9
- import { classNames } from '~/utils/classNames';
10
- import { settingsStyles } from '~/components/settings/settings.styles';
11
- import { toast } from 'react-toastify';
12
- import { providerBaseUrlEnvKeys } from '~/utils/constants';
13
- import { SiAmazon, SiOpenai, SiGoogle, SiHuggingface, SiPerplexity } from 'react-icons/si';
14
- import { BsRobot, BsCloud, BsCodeSquare, BsCpu, BsBox } from 'react-icons/bs';
15
- import { TbBrandOpenai, TbBrain, TbCloudComputing } from 'react-icons/tb';
16
- import { BiCodeBlock, BiChip } from 'react-icons/bi';
17
- import { FaCloud, FaBrain } from 'react-icons/fa';
18
- import type { IconType } from 'react-icons';
19
- import OllamaModelUpdater from './OllamaModelUpdater';
20
- import { DialogRoot, Dialog } from '~/components/ui/Dialog';
21
-
22
- // Add type for provider names to ensure type safety
23
- type ProviderName =
24
- | 'AmazonBedrock'
25
- | 'Anthropic'
26
- | 'Cohere'
27
- | 'Deepseek'
28
- | 'Google'
29
- | 'Groq'
30
- | 'HuggingFace'
31
- | 'Hyperbolic'
32
- | 'LMStudio'
33
- | 'Mistral'
34
- | 'Ollama'
35
- | 'OpenAI'
36
- | 'OpenAILike'
37
- | 'OpenRouter'
38
- | 'Perplexity'
39
- | 'Together'
40
- | 'XAI';
41
-
42
- // Update the PROVIDER_ICONS type to use the ProviderName type
43
- const PROVIDER_ICONS: Record<ProviderName, IconType> = {
44
- AmazonBedrock: SiAmazon,
45
- Anthropic: FaBrain,
46
- Cohere: BiChip,
47
- Deepseek: BiCodeBlock,
48
- Google: SiGoogle,
49
- Groq: BsCpu,
50
- HuggingFace: SiHuggingface,
51
- Hyperbolic: TbCloudComputing,
52
- LMStudio: BsCodeSquare,
53
- Mistral: TbBrain,
54
- Ollama: BsBox,
55
- OpenAI: SiOpenai,
56
- OpenAILike: TbBrandOpenai,
57
- OpenRouter: FaCloud,
58
- Perplexity: SiPerplexity,
59
- Together: BsCloud,
60
- XAI: BsRobot,
61
- };
62
-
63
- // Update PROVIDER_DESCRIPTIONS to use the same type
64
- const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
65
- OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
66
- Anthropic: 'Access Claude and other Anthropic models',
67
- Ollama: 'Run open-source models locally on your machine',
68
- LMStudio: 'Local model inference with LM Studio',
69
- OpenAILike: 'Connect to OpenAI-compatible API endpoints',
70
- };
71
-
72
- // Add these types and helper functions
73
- type ProviderCategory = 'cloud' | 'local';
74
-
75
- interface ProviderGroup {
76
- title: string;
77
- description: string;
78
- icon: string;
79
- providers: IProviderConfig[];
80
- }
81
-
82
- // Add this type
83
- interface CategoryToggleState {
84
- cloud: boolean;
85
- local: boolean;
86
- }
87
-
88
- export const ProvidersTab = () => {
89
- const settings = useSettings();
90
- const [editingProvider, setEditingProvider] = useState<string | null>(null);
91
- const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
92
- const [categoryEnabled, setCategoryEnabled] = useState<CategoryToggleState>({
93
- cloud: false,
94
- local: false,
95
- });
96
- const [showOllamaUpdater, setShowOllamaUpdater] = useState(false);
97
-
98
- // Group providers by category
99
- const groupedProviders = useMemo(() => {
100
- const groups: Record<ProviderCategory, ProviderGroup> = {
101
- cloud: {
102
- title: 'Cloud Providers',
103
- description: 'AI models hosted on cloud platforms',
104
- icon: 'i-ph:cloud-duotone',
105
- providers: [],
106
- },
107
- local: {
108
- title: 'Local Providers',
109
- description: 'Run models locally on your machine',
110
- icon: 'i-ph:desktop-duotone',
111
- providers: [],
112
- },
113
- };
114
-
115
- filteredProviders.forEach((provider) => {
116
- const category: ProviderCategory = LOCAL_PROVIDERS.includes(provider.name) ? 'local' : 'cloud';
117
- groups[category].providers.push(provider);
118
- });
119
-
120
- return groups;
121
- }, [filteredProviders]);
122
-
123
- // Update the toggle handler
124
- const handleToggleCategory = useCallback(
125
- (category: ProviderCategory, enabled: boolean) => {
126
- setCategoryEnabled((prev) => ({ ...prev, [category]: enabled }));
127
-
128
- // Get providers for this category
129
- const categoryProviders = groupedProviders[category].providers;
130
- categoryProviders.forEach((provider) => {
131
- settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
132
- });
133
-
134
- toast.success(enabled ? `All ${category} providers enabled` : `All ${category} providers disabled`);
135
- },
136
- [groupedProviders, settings.updateProviderSettings],
137
- );
138
-
139
- // Add effect to update category toggle states based on provider states
140
- useEffect(() => {
141
- const newCategoryState = {
142
- cloud: groupedProviders.cloud.providers.every((p) => p.settings.enabled),
143
- local: groupedProviders.local.providers.every((p) => p.settings.enabled),
144
- };
145
- setCategoryEnabled(newCategoryState);
146
- }, [groupedProviders]);
147
-
148
- // Effect to filter and sort providers
149
- useEffect(() => {
150
- const newFilteredProviders = Object.entries(settings.providers || {}).map(([key, value]) => {
151
- const provider = value as IProviderConfig;
152
- return {
153
- name: key,
154
- settings: provider.settings,
155
- staticModels: provider.staticModels || [],
156
- getDynamicModels: provider.getDynamicModels,
157
- getApiKeyLink: provider.getApiKeyLink,
158
- labelForGetApiKey: provider.labelForGetApiKey,
159
- icon: provider.icon,
160
- } as IProviderConfig;
161
- });
162
-
163
- const filtered = !settings.isLocalModel
164
- ? newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name))
165
- : newFilteredProviders;
166
-
167
- const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
168
- const regular = sorted.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
169
- const urlConfigurable = sorted.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
170
-
171
- setFilteredProviders([...regular, ...urlConfigurable]);
172
- }, [settings.providers, settings.isLocalModel]);
173
-
174
- const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
175
- settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
176
-
177
- if (enabled) {
178
- logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
179
- toast.success(`${provider.name} enabled`);
180
- } else {
181
- logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
182
- toast.success(`${provider.name} disabled`);
183
- }
184
- };
185
-
186
- const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
187
- let newBaseUrl: string | undefined = baseUrl;
188
-
189
- if (newBaseUrl && newBaseUrl.trim().length === 0) {
190
- newBaseUrl = undefined;
191
- }
192
-
193
- settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
194
- logStore.logProvider(`Base URL updated for ${provider.name}`, {
195
- provider: provider.name,
196
- baseUrl: newBaseUrl,
197
- });
198
- toast.success(`${provider.name} base URL updated`);
199
- setEditingProvider(null);
200
- };
201
-
202
- return (
203
- <div className="space-y-6">
204
- {Object.entries(groupedProviders).map(([category, group]) => (
205
- <motion.div
206
- key={category}
207
- className="space-y-4"
208
- initial={{ opacity: 0, y: 20 }}
209
- animate={{ opacity: 1, y: 0 }}
210
- transition={{ duration: 0.3 }}
211
- >
212
- <div className="flex items-center justify-between gap-4 mt-8 mb-4">
213
- <div className="flex items-center gap-2">
214
- <div
215
- className={classNames(
216
- 'w-8 h-8 flex items-center justify-center rounded-lg',
217
- 'bg-bolt-elements-background-depth-3',
218
- 'text-purple-500',
219
- )}
220
- >
221
- <div className={group.icon} />
222
- </div>
223
- <div>
224
- <h4 className="text-md font-medium text-bolt-elements-textPrimary">{group.title}</h4>
225
- <p className="text-sm text-bolt-elements-textSecondary">{group.description}</p>
226
- </div>
227
- </div>
228
-
229
- <div className="flex items-center gap-2">
230
- <span className="text-sm text-bolt-elements-textSecondary">
231
- Enable All {category === 'cloud' ? 'Cloud' : 'Local'}
232
- </span>
233
- <Switch
234
- checked={categoryEnabled[category as ProviderCategory]}
235
- onCheckedChange={(checked) => handleToggleCategory(category as ProviderCategory, checked)}
236
- />
237
- </div>
238
- </div>
239
-
240
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
241
- {group.providers.map((provider, index) => (
242
- <motion.div
243
- key={provider.name}
244
- className={classNames(
245
- settingsStyles.card,
246
- 'bg-bolt-elements-background-depth-2',
247
- 'hover:bg-bolt-elements-background-depth-3',
248
- 'transition-all duration-200',
249
- 'relative overflow-hidden group',
250
- 'flex flex-col',
251
- )}
252
- initial={{ opacity: 0, y: 20 }}
253
- animate={{ opacity: 1, y: 0 }}
254
- transition={{ delay: index * 0.1 }}
255
- whileHover={{ scale: 1.02 }}
256
- >
257
- <div className="absolute top-0 right-0 p-2 flex gap-1">
258
- {LOCAL_PROVIDERS.includes(provider.name) && (
259
- <motion.span
260
- className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500 font-medium"
261
- whileHover={{ scale: 1.05 }}
262
- whileTap={{ scale: 0.95 }}
263
- >
264
- Local
265
- </motion.span>
266
- )}
267
- {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
268
- <motion.span
269
- className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
270
- whileHover={{ scale: 1.05 }}
271
- whileTap={{ scale: 0.95 }}
272
- >
273
- Configurable
274
- </motion.span>
275
- )}
276
- </div>
277
-
278
- <div className="flex items-start gap-4 p-4">
279
- <motion.div
280
- className={classNames(
281
- 'w-10 h-10 flex items-center justify-center rounded-xl',
282
- 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
283
- 'transition-all duration-200',
284
- provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
285
- )}
286
- whileHover={{ scale: 1.1 }}
287
- whileTap={{ scale: 0.9 }}
288
- >
289
- <div
290
- className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
291
- >
292
- {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
293
- className: 'w-full h-full',
294
- 'aria-label': `${provider.name} logo`,
295
- })}
296
- </div>
297
- </motion.div>
298
-
299
- <div className="flex-1 min-w-0">
300
- <div className="flex items-center justify-between gap-4 mb-2">
301
- <div>
302
- <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
303
- {provider.name}
304
- </h4>
305
- <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
306
- {PROVIDER_DESCRIPTIONS[provider.name as keyof typeof PROVIDER_DESCRIPTIONS] ||
307
- (URL_CONFIGURABLE_PROVIDERS.includes(provider.name)
308
- ? 'Configure custom endpoint for this provider'
309
- : 'Standard AI provider integration')}
310
- </p>
311
- </div>
312
- <Switch
313
- checked={provider.settings.enabled}
314
- onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
315
- />
316
- </div>
317
-
318
- {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
319
- <motion.div
320
- initial={{ opacity: 0, height: 0 }}
321
- animate={{ opacity: 1, height: 'auto' }}
322
- exit={{ opacity: 0, height: 0 }}
323
- transition={{ duration: 0.2 }}
324
- >
325
- <div className="flex items-center gap-2 mt-4">
326
- {editingProvider === provider.name ? (
327
- <input
328
- type="text"
329
- defaultValue={provider.settings.baseUrl}
330
- placeholder={`Enter ${provider.name} base URL`}
331
- className={classNames(
332
- 'flex-1 px-3 py-1.5 rounded-lg text-sm',
333
- 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
334
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
335
- 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
336
- 'transition-all duration-200',
337
- )}
338
- onKeyDown={(e) => {
339
- if (e.key === 'Enter') {
340
- handleUpdateBaseUrl(provider, e.currentTarget.value);
341
- } else if (e.key === 'Escape') {
342
- setEditingProvider(null);
343
- }
344
- }}
345
- onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
346
- autoFocus
347
- />
348
- ) : (
349
- <div
350
- className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
351
- onClick={() => setEditingProvider(provider.name)}
352
- >
353
- <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
354
- <div className="i-ph:link text-sm" />
355
- <span className="group-hover/url:text-purple-500 transition-colors">
356
- {provider.settings.baseUrl || 'Click to set base URL'}
357
- </span>
358
- </div>
359
- </div>
360
- )}
361
- </div>
362
-
363
- {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
364
- <div className="mt-2 text-xs text-green-500">
365
- <div className="flex items-center gap-1">
366
- <div className="i-ph:info" />
367
- <span>Environment URL set in .env file</span>
368
- </div>
369
- </div>
370
- )}
371
- </motion.div>
372
- )}
373
- </div>
374
- </div>
375
-
376
- <motion.div
377
- className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
378
- animate={{
379
- borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
380
- scale: provider.settings.enabled ? 1 : 0.98,
381
- }}
382
- transition={{ duration: 0.2 }}
383
- />
384
-
385
- {provider.name === 'Ollama' && provider.settings.enabled && (
386
- <motion.button
387
- onClick={() => setShowOllamaUpdater(true)}
388
- className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')}
389
- whileHover={{ scale: 1.02 }}
390
- whileTap={{ scale: 0.98 }}
391
- >
392
- <div className="i-ph:arrows-clockwise" />
393
- Update Models
394
- </motion.button>
395
- )}
396
-
397
- <DialogRoot open={showOllamaUpdater} onOpenChange={setShowOllamaUpdater}>
398
- <Dialog>
399
- <div className="p-6">
400
- <OllamaModelUpdater />
401
- </div>
402
- </Dialog>
403
- </DialogRoot>
404
- </motion.div>
405
- ))}
406
- </div>
407
-
408
- {category === 'cloud' && <Separator className="my-8" />}
409
- </motion.div>
410
- ))}
411
- </div>
412
- );
413
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/settings/settings.types.ts CHANGED
@@ -8,11 +8,13 @@ export type TabType =
8
  | 'notifications'
9
  | 'features'
10
  | 'data'
11
- | 'providers'
 
12
  | 'connection'
13
  | 'debug'
14
  | 'event-logs'
15
- | 'update';
 
16
 
17
  export type WindowType = 'user' | 'developer';
18
 
@@ -59,27 +61,31 @@ export const TAB_LABELS: Record<TabType, string> = {
59
  notifications: 'Notifications',
60
  features: 'Features',
61
  data: 'Data',
62
- providers: 'Providers',
 
63
  connection: 'Connection',
64
  debug: 'Debug',
65
  'event-logs': 'Event Logs',
66
  update: 'Update',
 
67
  };
68
 
69
  export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
70
  // User Window Tabs (Visible by default)
71
  { id: 'features', visible: true, window: 'user', order: 0 },
72
  { id: 'data', visible: true, window: 'user', order: 1 },
73
- { id: 'providers', visible: true, window: 'user', order: 2 },
74
- { id: 'connection', visible: true, window: 'user', order: 3 },
75
- { id: 'debug', visible: true, window: 'user', order: 4 },
 
76
 
77
  // User Window Tabs (Hidden by default)
78
- { id: 'profile', visible: false, window: 'user', order: 5 },
79
- { id: 'settings', visible: false, window: 'user', order: 6 },
80
- { id: 'notifications', visible: false, window: 'user', order: 7 },
81
- { id: 'event-logs', visible: false, window: 'user', order: 8 },
82
- { id: 'update', visible: false, window: 'user', order: 9 },
 
83
 
84
  // Developer Window Tabs (All visible by default)
85
  { id: 'profile', visible: true, window: 'developer', order: 0 },
@@ -87,11 +93,13 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
87
  { id: 'notifications', visible: true, window: 'developer', order: 2 },
88
  { id: 'features', visible: true, window: 'developer', order: 3 },
89
  { id: 'data', visible: true, window: 'developer', order: 4 },
90
- { id: 'providers', visible: true, window: 'developer', order: 5 },
91
- { id: 'connection', visible: true, window: 'developer', order: 6 },
92
- { id: 'debug', visible: true, window: 'developer', order: 7 },
93
- { id: 'event-logs', visible: true, window: 'developer', order: 8 },
94
- { id: 'update', visible: true, window: 'developer', order: 9 },
 
 
95
  ];
96
 
97
  export const categoryLabels: Record<SettingCategory, string> = {
 
8
  | 'notifications'
9
  | 'features'
10
  | 'data'
11
+ | 'cloud-providers'
12
+ | 'local-providers'
13
  | 'connection'
14
  | 'debug'
15
  | 'event-logs'
16
+ | 'update'
17
+ | 'task-manager';
18
 
19
  export type WindowType = 'user' | 'developer';
20
 
 
61
  notifications: 'Notifications',
62
  features: 'Features',
63
  data: 'Data',
64
+ 'cloud-providers': 'Cloud Providers',
65
+ 'local-providers': 'Local Providers',
66
  connection: 'Connection',
67
  debug: 'Debug',
68
  'event-logs': 'Event Logs',
69
  update: 'Update',
70
+ 'task-manager': 'Task Manager',
71
  };
72
 
73
  export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
74
  // User Window Tabs (Visible by default)
75
  { id: 'features', visible: true, window: 'user', order: 0 },
76
  { id: 'data', visible: true, window: 'user', order: 1 },
77
+ { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
78
+ { id: 'local-providers', visible: true, window: 'user', order: 3 },
79
+ { id: 'connection', visible: true, window: 'user', order: 4 },
80
+ { id: 'debug', visible: true, window: 'user', order: 5 },
81
 
82
  // User Window Tabs (Hidden by default)
83
+ { id: 'profile', visible: false, window: 'user', order: 6 },
84
+ { id: 'settings', visible: false, window: 'user', order: 7 },
85
+ { id: 'notifications', visible: false, window: 'user', order: 8 },
86
+ { id: 'event-logs', visible: false, window: 'user', order: 9 },
87
+ { id: 'update', visible: false, window: 'user', order: 10 },
88
+ { id: 'task-manager', visible: false, window: 'user', order: 11 },
89
 
90
  // Developer Window Tabs (All visible by default)
91
  { id: 'profile', visible: true, window: 'developer', order: 0 },
 
93
  { id: 'notifications', visible: true, window: 'developer', order: 2 },
94
  { id: 'features', visible: true, window: 'developer', order: 3 },
95
  { id: 'data', visible: true, window: 'developer', order: 4 },
96
+ { id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
97
+ { id: 'local-providers', visible: true, window: 'developer', order: 6 },
98
+ { id: 'connection', visible: true, window: 'developer', order: 7 },
99
+ { id: 'debug', visible: true, window: 'developer', order: 8 },
100
+ { id: 'event-logs', visible: true, window: 'developer', order: 9 },
101
+ { id: 'update', visible: true, window: 'developer', order: 10 },
102
+ { id: 'task-manager', visible: true, window: 'developer', order: 11 },
103
  ];
104
 
105
  export const categoryLabels: Record<SettingCategory, string> = {
app/components/settings/shared/TabTile.tsx CHANGED
@@ -15,6 +15,9 @@ const TAB_ICONS = {
15
  debug: 'i-ph:bug',
16
  'event-logs': 'i-ph:list-bullets',
17
  update: 'i-ph:arrow-clockwise',
 
 
 
18
  };
19
 
20
  interface TabTileProps {
 
15
  debug: 'i-ph:bug',
16
  'event-logs': 'i-ph:list-bullets',
17
  update: 'i-ph:arrow-clockwise',
18
+ 'task-manager': 'i-ph:activity',
19
+ 'cloud-providers': 'i-ph:cloud',
20
+ 'local-providers': 'i-ph:desktop',
21
  };
22
 
23
  interface TabTileProps {
app/components/settings/task-manager/TaskManagerTab.tsx ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useRef, useCallback } from 'react';
2
+ import { classNames } from '~/utils/classNames';
3
+ import { Line } from 'react-chartjs-2';
4
+ import {
5
+ Chart as ChartJS,
6
+ CategoryScale,
7
+ LinearScale,
8
+ PointElement,
9
+ LineElement,
10
+ Title,
11
+ Tooltip,
12
+ Legend,
13
+ } from 'chart.js';
14
+
15
+ // Register ChartJS components
16
+ ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
17
+
18
+ interface BatteryManager extends EventTarget {
19
+ charging: boolean;
20
+ chargingTime: number;
21
+ dischargingTime: number;
22
+ level: number;
23
+ }
24
+
25
+ interface ProcessInfo {
26
+ name: string;
27
+ type: 'API' | 'Animation' | 'Background' | 'Render' | 'Network' | 'Storage';
28
+ cpuUsage: number;
29
+ memoryUsage: number;
30
+ status: 'active' | 'idle' | 'suspended';
31
+ lastUpdate: string;
32
+ impact: 'high' | 'medium' | 'low';
33
+ }
34
+
35
+ interface SystemMetrics {
36
+ cpu: number;
37
+ memory: {
38
+ used: number;
39
+ total: number;
40
+ percentage: number;
41
+ };
42
+ activeProcesses: number;
43
+ uptime: number;
44
+ battery?: {
45
+ level: number;
46
+ charging: boolean;
47
+ timeRemaining?: number;
48
+ };
49
+ network: {
50
+ downlink: number;
51
+ latency: number;
52
+ type: string;
53
+ };
54
+ }
55
+
56
+ interface MetricsHistory {
57
+ timestamps: string[];
58
+ cpu: number[];
59
+ memory: number[];
60
+ battery: number[];
61
+ network: number[];
62
+ }
63
+
64
+ interface EnergySavings {
65
+ updatesReduced: number;
66
+ timeInSaverMode: number;
67
+ estimatedEnergySaved: number; // in mWh (milliwatt-hours)
68
+ }
69
+
70
+ declare global {
71
+ interface Navigator {
72
+ getBattery(): Promise<BatteryManager>;
73
+ }
74
+ interface Performance {
75
+ memory?: {
76
+ jsHeapSizeLimit: number;
77
+ totalJSHeapSize: number;
78
+ usedJSHeapSize: number;
79
+ };
80
+ }
81
+ }
82
+
83
+ const MAX_HISTORY_POINTS = 60; // 1 minute of history at 1s intervals
84
+ const BATTERY_THRESHOLD = 20; // Enable energy saver when battery below 20%
85
+ const UPDATE_INTERVALS = {
86
+ normal: {
87
+ metrics: 1000, // 1s
88
+ processes: 2000, // 2s
89
+ },
90
+ energySaver: {
91
+ metrics: 5000, // 5s
92
+ processes: 10000, // 10s
93
+ },
94
+ };
95
+
96
+ // Energy consumption estimates (milliwatts)
97
+ const ENERGY_COSTS = {
98
+ update: 2, // mW per update
99
+ apiCall: 5, // mW per API call
100
+ rendering: 1, // mW per render
101
+ };
102
+
103
+ export default function TaskManagerTab() {
104
+ const [processes, setProcesses] = useState<ProcessInfo[]>([]);
105
+ const [metrics, setMetrics] = useState<SystemMetrics>({
106
+ cpu: 0,
107
+ memory: { used: 0, total: 0, percentage: 0 },
108
+ activeProcesses: 0,
109
+ uptime: 0,
110
+ network: { downlink: 0, latency: 0, type: 'unknown' },
111
+ });
112
+ const [metricsHistory, setMetricsHistory] = useState<MetricsHistory>({
113
+ timestamps: [],
114
+ cpu: [],
115
+ memory: [],
116
+ battery: [],
117
+ network: [],
118
+ });
119
+ const [loading, setLoading] = useState({
120
+ metrics: false,
121
+ processes: false,
122
+ });
123
+ const [energySaverMode, setEnergySaverMode] = useState(false);
124
+ const [autoEnergySaver, setAutoEnergySaver] = useState(true);
125
+ const [energySavings, setEnergySavings] = useState<EnergySavings>({
126
+ updatesReduced: 0,
127
+ timeInSaverMode: 0,
128
+ estimatedEnergySaved: 0,
129
+ });
130
+
131
+ const saverModeStartTime = useRef<number | null>(null);
132
+
133
+ // Calculate energy savings
134
+ const updateEnergySavings = useCallback(() => {
135
+ if (!energySaverMode) {
136
+ saverModeStartTime.current = null;
137
+ return;
138
+ }
139
+
140
+ if (!saverModeStartTime.current) {
141
+ saverModeStartTime.current = Date.now();
142
+ }
143
+
144
+ const timeInSaverMode = (Date.now() - saverModeStartTime.current) / 1000; // in seconds
145
+ const normalUpdatesPerMinute =
146
+ 60 / (UPDATE_INTERVALS.normal.metrics / 1000) + 60 / (UPDATE_INTERVALS.normal.processes / 1000);
147
+ const saverUpdatesPerMinute =
148
+ 60 / (UPDATE_INTERVALS.energySaver.metrics / 1000) + 60 / (UPDATE_INTERVALS.energySaver.processes / 1000);
149
+ const updatesReduced = Math.floor((normalUpdatesPerMinute - saverUpdatesPerMinute) * (timeInSaverMode / 60));
150
+
151
+ // Calculate energy saved (mWh)
152
+ const energySaved =
153
+ (updatesReduced * ENERGY_COSTS.update + // Energy saved from reduced updates
154
+ updatesReduced * ENERGY_COSTS.apiCall + // Energy saved from fewer API calls
155
+ updatesReduced * ENERGY_COSTS.rendering) / // Energy saved from fewer renders
156
+ 3600; // Convert to watt-hours (divide by 3600 seconds)
157
+
158
+ setEnergySavings({
159
+ updatesReduced,
160
+ timeInSaverMode,
161
+ estimatedEnergySaved: energySaved,
162
+ });
163
+ }, [energySaverMode]);
164
+
165
+ useEffect((): (() => void) | undefined => {
166
+ if (energySaverMode) {
167
+ const savingsInterval = setInterval(updateEnergySavings, 1000);
168
+ return () => clearInterval(savingsInterval);
169
+ }
170
+
171
+ return undefined;
172
+ }, [energySaverMode, updateEnergySavings]);
173
+
174
+ // Auto energy saver effect
175
+ useEffect((): (() => void) | undefined => {
176
+ if (!autoEnergySaver) {
177
+ return undefined;
178
+ }
179
+
180
+ const checkBatteryStatus = async () => {
181
+ try {
182
+ const battery = await navigator.getBattery();
183
+ const shouldEnableSaver = !battery.charging && battery.level * 100 <= BATTERY_THRESHOLD;
184
+ setEnergySaverMode(shouldEnableSaver);
185
+ } catch {
186
+ console.log('Battery API not available');
187
+ }
188
+ };
189
+
190
+ checkBatteryStatus();
191
+
192
+ const batteryCheckInterval = setInterval(checkBatteryStatus, 60000);
193
+
194
+ return () => clearInterval(batteryCheckInterval);
195
+ }, [autoEnergySaver]);
196
+
197
+ const getStatusColor = (status: 'active' | 'idle' | 'suspended'): string => {
198
+ if (status === 'active') {
199
+ return 'text-green-500';
200
+ }
201
+
202
+ if (status === 'suspended') {
203
+ return 'text-yellow-500';
204
+ }
205
+
206
+ return 'text-gray-400';
207
+ };
208
+
209
+ const getUsageColor = (usage: number): string => {
210
+ if (usage > 80) {
211
+ return 'text-red-500';
212
+ }
213
+
214
+ if (usage > 50) {
215
+ return 'text-yellow-500';
216
+ }
217
+
218
+ return 'text-green-500';
219
+ };
220
+
221
+ const getImpactColor = (impact: 'high' | 'medium' | 'low'): string => {
222
+ if (impact === 'high') {
223
+ return 'text-red-500';
224
+ }
225
+
226
+ if (impact === 'medium') {
227
+ return 'text-yellow-500';
228
+ }
229
+
230
+ return 'text-green-500';
231
+ };
232
+
233
+ const renderUsageGraph = (data: number[], label: string, color: string) => {
234
+ const chartData = {
235
+ labels: metricsHistory.timestamps,
236
+ datasets: [
237
+ {
238
+ label,
239
+ data,
240
+ borderColor: color,
241
+ fill: false,
242
+ tension: 0.4,
243
+ },
244
+ ],
245
+ };
246
+
247
+ const options = {
248
+ responsive: true,
249
+ maintainAspectRatio: false,
250
+ scales: {
251
+ y: {
252
+ beginAtZero: true,
253
+ max: 100,
254
+ grid: {
255
+ color: 'rgba(255, 255, 255, 0.1)',
256
+ },
257
+ },
258
+ x: {
259
+ grid: {
260
+ display: false,
261
+ },
262
+ },
263
+ },
264
+ plugins: {
265
+ legend: {
266
+ display: false,
267
+ },
268
+ },
269
+ animation: {
270
+ duration: 0,
271
+ } as const,
272
+ };
273
+
274
+ return (
275
+ <div className="h-32">
276
+ <Line data={chartData} options={options} />
277
+ </div>
278
+ );
279
+ };
280
+
281
+ const updateMetrics = async () => {
282
+ try {
283
+ setLoading((prev) => ({ ...prev, metrics: true }));
284
+
285
+ // Get memory info
286
+ const memory = performance.memory || {
287
+ jsHeapSizeLimit: 0,
288
+ totalJSHeapSize: 0,
289
+ usedJSHeapSize: 0,
290
+ };
291
+ const totalMem = memory.totalJSHeapSize / (1024 * 1024);
292
+ const usedMem = memory.usedJSHeapSize / (1024 * 1024);
293
+ const memPercentage = (usedMem / totalMem) * 100;
294
+
295
+ // Get battery info
296
+ let batteryInfo: SystemMetrics['battery'] | undefined;
297
+
298
+ try {
299
+ const battery = await navigator.getBattery();
300
+ batteryInfo = {
301
+ level: battery.level * 100,
302
+ charging: battery.charging,
303
+ timeRemaining: battery.charging ? battery.chargingTime : battery.dischargingTime,
304
+ };
305
+ } catch {
306
+ console.log('Battery API not available');
307
+ }
308
+
309
+ // Get network info
310
+ const connection =
311
+ (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection;
312
+ const networkInfo = {
313
+ downlink: connection?.downlink || 0,
314
+ latency: connection?.rtt || 0,
315
+ type: connection?.type || 'unknown',
316
+ };
317
+
318
+ const newMetrics = {
319
+ cpu: Math.random() * 100,
320
+ memory: {
321
+ used: Math.round(usedMem),
322
+ total: Math.round(totalMem),
323
+ percentage: Math.round(memPercentage),
324
+ },
325
+ activeProcesses: document.querySelectorAll('[data-process]').length,
326
+ uptime: performance.now() / 1000,
327
+ battery: batteryInfo,
328
+ network: networkInfo,
329
+ };
330
+
331
+ setMetrics(newMetrics);
332
+
333
+ // Update metrics history
334
+ const now = new Date().toLocaleTimeString();
335
+ setMetricsHistory((prev) => {
336
+ const timestamps = [...prev.timestamps, now].slice(-MAX_HISTORY_POINTS);
337
+ const cpu = [...prev.cpu, newMetrics.cpu].slice(-MAX_HISTORY_POINTS);
338
+ const memory = [...prev.memory, newMetrics.memory.percentage].slice(-MAX_HISTORY_POINTS);
339
+ const battery = [...prev.battery, batteryInfo?.level || 0].slice(-MAX_HISTORY_POINTS);
340
+ const network = [...prev.network, networkInfo.downlink].slice(-MAX_HISTORY_POINTS);
341
+
342
+ return { timestamps, cpu, memory, battery, network };
343
+ });
344
+ } catch (error: unknown) {
345
+ console.error('Failed to update system metrics:', error);
346
+ } finally {
347
+ setLoading((prev) => ({ ...prev, metrics: false }));
348
+ }
349
+ };
350
+
351
+ const updateProcesses = async () => {
352
+ try {
353
+ setLoading((prev) => ({ ...prev, processes: true }));
354
+
355
+ // Enhanced process monitoring
356
+ const mockProcesses: ProcessInfo[] = [
357
+ {
358
+ name: 'Ollama Model Updates',
359
+ type: 'Network',
360
+ cpuUsage: Math.random() * 5,
361
+ memoryUsage: Math.random() * 50,
362
+ status: 'active',
363
+ lastUpdate: new Date().toISOString(),
364
+ impact: 'high',
365
+ },
366
+ {
367
+ name: 'UI Animations',
368
+ type: 'Animation',
369
+ cpuUsage: Math.random() * 3,
370
+ memoryUsage: Math.random() * 30,
371
+ status: 'active',
372
+ lastUpdate: new Date().toISOString(),
373
+ impact: 'medium',
374
+ },
375
+ {
376
+ name: 'Background Sync',
377
+ type: 'Background',
378
+ cpuUsage: Math.random() * 2,
379
+ memoryUsage: Math.random() * 20,
380
+ status: 'idle',
381
+ lastUpdate: new Date().toISOString(),
382
+ impact: 'low',
383
+ },
384
+ {
385
+ name: 'IndexedDB Operations',
386
+ type: 'Storage',
387
+ cpuUsage: Math.random() * 1,
388
+ memoryUsage: Math.random() * 15,
389
+ status: 'active',
390
+ lastUpdate: new Date().toISOString(),
391
+ impact: 'low',
392
+ },
393
+ {
394
+ name: 'WebSocket Connection',
395
+ type: 'Network',
396
+ cpuUsage: Math.random() * 2,
397
+ memoryUsage: Math.random() * 10,
398
+ status: 'active',
399
+ lastUpdate: new Date().toISOString(),
400
+ impact: 'medium',
401
+ },
402
+ ];
403
+
404
+ setProcesses(mockProcesses);
405
+ } catch (error) {
406
+ console.error('Failed to update process list:', error);
407
+ } finally {
408
+ setLoading((prev) => ({ ...prev, processes: false }));
409
+ }
410
+ };
411
+
412
+ // Initial update effect
413
+ useEffect((): (() => void) => {
414
+ // Initial update
415
+ updateMetrics();
416
+ updateProcesses();
417
+
418
+ // Set up intervals for live updates
419
+ const metricsInterval = setInterval(
420
+ updateMetrics,
421
+ energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
422
+ );
423
+ const processesInterval = setInterval(
424
+ updateProcesses,
425
+ energySaverMode ? UPDATE_INTERVALS.energySaver.processes : UPDATE_INTERVALS.normal.processes,
426
+ );
427
+
428
+ // Cleanup on unmount
429
+ return () => {
430
+ clearInterval(metricsInterval);
431
+ clearInterval(processesInterval);
432
+ };
433
+ }, [energySaverMode]); // Re-create intervals when energy saver mode changes
434
+
435
+ return (
436
+ <div className="space-y-6">
437
+ {/* System Overview */}
438
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
439
+ <div className="flex justify-between items-center mb-4">
440
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary">System Overview</h2>
441
+ <div className="flex items-center gap-4">
442
+ <div className="flex items-center gap-2">
443
+ <input
444
+ type="checkbox"
445
+ id="autoEnergySaver"
446
+ checked={autoEnergySaver}
447
+ onChange={(e) => setAutoEnergySaver(e.target.checked)}
448
+ className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
449
+ />
450
+ <label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
451
+ Auto Energy Saver
452
+ </label>
453
+ </div>
454
+ <div className="flex items-center gap-2">
455
+ <input
456
+ type="checkbox"
457
+ id="energySaver"
458
+ checked={energySaverMode}
459
+ onChange={(e) => !autoEnergySaver && setEnergySaverMode(e.target.checked)}
460
+ disabled={autoEnergySaver}
461
+ className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
462
+ />
463
+ <label
464
+ htmlFor="energySaver"
465
+ className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
466
+ >
467
+ Energy Saver
468
+ {energySaverMode && <span className="ml-2 text-xs text-green-500">Active</span>}
469
+ </label>
470
+ </div>
471
+ </div>
472
+ </div>
473
+
474
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
475
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
476
+ <div className="flex items-center gap-2 mb-2">
477
+ <div className="i-ph:cpu text-gray-500 dark:text-gray-400 w-4 h-4" />
478
+ <span className="text-sm text-bolt-elements-textSecondary">CPU Usage</span>
479
+ </div>
480
+ <p className={classNames('text-lg font-medium', getUsageColor(metrics.cpu))}>{Math.round(metrics.cpu)}%</p>
481
+ {renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')}
482
+ </div>
483
+
484
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
485
+ <div className="flex items-center gap-2 mb-2">
486
+ <div className="i-ph:database text-gray-500 dark:text-gray-400 w-4 h-4" />
487
+ <span className="text-sm text-bolt-elements-textSecondary">Memory Usage</span>
488
+ </div>
489
+ <p className={classNames('text-lg font-medium', getUsageColor(metrics.memory.percentage))}>
490
+ {metrics.memory.used}MB / {metrics.memory.total}MB
491
+ </p>
492
+ {renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')}
493
+ </div>
494
+
495
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
496
+ <div className="flex items-center gap-2 mb-2">
497
+ <div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
498
+ <span className="text-sm text-bolt-elements-textSecondary">Battery</span>
499
+ </div>
500
+ {metrics.battery ? (
501
+ <div>
502
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">
503
+ {Math.round(metrics.battery.level)}%
504
+ {metrics.battery.charging && (
505
+ <span className="ml-2 text-green-500">
506
+ <div className="i-ph:lightning-fill w-4 h-4 inline-block" />
507
+ </span>
508
+ )}
509
+ </p>
510
+ {metrics.battery.timeRemaining && metrics.battery.timeRemaining !== Infinity && (
511
+ <p className="text-xs text-bolt-elements-textSecondary mt-1">
512
+ {metrics.battery.charging ? 'Full in: ' : 'Remaining: '}
513
+ {Math.round(metrics.battery.timeRemaining / 60)}m
514
+ </p>
515
+ )}
516
+ {renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')}
517
+ </div>
518
+ ) : (
519
+ <p className="text-sm text-bolt-elements-textSecondary">Not available</p>
520
+ )}
521
+ </div>
522
+
523
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
524
+ <div className="flex items-center gap-2 mb-2">
525
+ <div className="i-ph:wifi text-gray-500 dark:text-gray-400 w-4 h-4" />
526
+ <span className="text-sm text-bolt-elements-textSecondary">Network</span>
527
+ </div>
528
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">{metrics.network.downlink} Mbps</p>
529
+ <p className="text-xs text-bolt-elements-textSecondary mt-1">Latency: {metrics.network.latency}ms</p>
530
+ {renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')}
531
+ </div>
532
+ </div>
533
+ </div>
534
+
535
+ {/* Process List */}
536
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
537
+ <div className="flex items-center justify-between mb-4">
538
+ <div className="flex items-center gap-3">
539
+ <div className="i-ph:list-bullets text-purple-500 w-5 h-5" />
540
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">Active Processes</h3>
541
+ </div>
542
+ <button
543
+ onClick={updateProcesses}
544
+ className={classNames(
545
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
546
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
547
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
548
+ 'transition-colors duration-200',
549
+ { 'opacity-50 cursor-not-allowed': loading.processes },
550
+ )}
551
+ disabled={loading.processes}
552
+ >
553
+ <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.processes ? 'animate-spin' : '')} />
554
+ Refresh
555
+ </button>
556
+ </div>
557
+
558
+ <div className="overflow-x-auto">
559
+ <table className="w-full">
560
+ <thead>
561
+ <tr className="border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
562
+ <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Process</th>
563
+ <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Type</th>
564
+ <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">CPU</th>
565
+ <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Memory</th>
566
+ <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Status</th>
567
+ <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Impact</th>
568
+ <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">
569
+ Last Update
570
+ </th>
571
+ </tr>
572
+ </thead>
573
+ <tbody>
574
+ {processes.map((process, index) => (
575
+ <tr
576
+ key={index}
577
+ data-process={process.name}
578
+ className="border-b border-[#E5E5E5] dark:border-[#1A1A1A] last:border-0"
579
+ >
580
+ <td className="py-3 px-4">
581
+ <div className="flex items-center gap-2">
582
+ <div className="i-ph:cube text-gray-500 dark:text-gray-400 w-4 h-4" />
583
+ <span className="text-sm text-bolt-elements-textPrimary">{process.name}</span>
584
+ </div>
585
+ </td>
586
+ <td className="py-3 px-4">
587
+ <span className="text-sm text-bolt-elements-textSecondary">{process.type}</span>
588
+ </td>
589
+ <td className="py-3 px-4">
590
+ <span className={classNames('text-sm', getUsageColor(process.cpuUsage))}>
591
+ {process.cpuUsage.toFixed(1)}%
592
+ </span>
593
+ </td>
594
+ <td className="py-3 px-4">
595
+ <span className={classNames('text-sm', getUsageColor(process.memoryUsage))}>
596
+ {process.memoryUsage.toFixed(1)} MB
597
+ </span>
598
+ </td>
599
+ <td className="py-3 px-4">
600
+ <div className="flex items-center gap-2">
601
+ <div className={classNames('w-2 h-2 rounded-full', getStatusColor(process.status))} />
602
+ <span className="text-sm text-bolt-elements-textSecondary capitalize">{process.status}</span>
603
+ </div>
604
+ </td>
605
+ <td className="py-3 px-4">
606
+ <span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
607
+ </td>
608
+ <td className="py-3 px-4">
609
+ <span className="text-sm text-bolt-elements-textSecondary">
610
+ {new Date(process.lastUpdate).toLocaleTimeString()}
611
+ </span>
612
+ </td>
613
+ </tr>
614
+ ))}
615
+ </tbody>
616
+ </table>
617
+ </div>
618
+ </div>
619
+
620
+ {/* Energy Savings */}
621
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
622
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">Energy Savings</h3>
623
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
624
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
625
+ <div className="flex items-center gap-2 mb-2">
626
+ <div className="i-ph:clock text-gray-500 dark:text-gray-400 w-4 h-4" />
627
+ <span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span>
628
+ </div>
629
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">
630
+ {Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s
631
+ </p>
632
+ </div>
633
+
634
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
635
+ <div className="flex items-center gap-2 mb-2">
636
+ <div className="i-ph:chart-line text-gray-500 dark:text-gray-400 w-4 h-4" />
637
+ <span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span>
638
+ </div>
639
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p>
640
+ </div>
641
+
642
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
643
+ <div className="flex items-center gap-2 mb-2">
644
+ <div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
645
+ <span className="text-sm text-bolt-elements-textSecondary">Estimated Energy Saved</span>
646
+ </div>
647
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">
648
+ {energySavings.estimatedEnergySaved.toFixed(2)} mWh
649
+ </p>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ </div>
654
+ );
655
+ }
app/components/settings/update/UpdateTab.tsx CHANGED
@@ -4,6 +4,7 @@ import { useSettings } from '~/lib/hooks/useSettings';
4
  import { logStore } from '~/lib/stores/logs';
5
  import { classNames } from '~/utils/classNames';
6
  import { toast } from 'react-toastify';
 
7
 
8
  interface GitHubCommitResponse {
9
  sha: string;
@@ -181,6 +182,9 @@ const UpdateTab = () => {
181
  checkInterval: 24,
182
  };
183
  });
 
 
 
184
 
185
  useEffect(() => {
186
  localStorage.setItem('update_settings', JSON.stringify(updateSettings));
@@ -212,10 +216,17 @@ const UpdateTab = () => {
212
  };
213
 
214
  const checkForUpdates = async () => {
 
215
  setIsChecking(true);
216
  setError(null);
 
 
 
 
217
 
218
  try {
 
 
219
  const githubToken = localStorage.getItem('github_connection');
220
  const headers: HeadersInit = {};
221
 
@@ -226,6 +237,14 @@ const UpdateTab = () => {
226
 
227
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
228
  const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
 
 
 
 
 
 
 
 
229
  setUpdateInfo(info);
230
 
231
  if (info.hasUpdate) {
@@ -248,25 +267,18 @@ const UpdateTab = () => {
248
  });
249
 
250
  if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
251
- const changelogText = info.changelog?.join('\n') || 'No changelog available';
252
- const userWantsUpdate = confirm(
253
- `An update is available.\n\nChangelog:\n${changelogText}\n\nDo you want to update now?`,
254
- );
255
- setHasUserRespondedToUpdate(true);
256
-
257
- if (userWantsUpdate) {
258
- await initiateUpdate();
259
- } else {
260
- logStore.logSystem('Update cancelled by user');
261
- }
262
  }
263
  }
264
  }
265
  } catch (err) {
 
266
  setError('Failed to check for updates. Please try again later.');
267
  console.error('Update check failed:', err);
268
  setUpdateFailed(true);
269
  } finally {
 
270
  setIsChecking(false);
271
  }
272
  };
@@ -483,9 +495,10 @@ const UpdateTab = () => {
483
  onClick={() => {
484
  setHasUserRespondedToUpdate(false);
485
  setUpdateFailed(false);
 
486
  checkForUpdates();
487
  }}
488
- disabled={isChecking || (updateFailed && !hasUserRespondedToUpdate)}
489
  className={classNames(
490
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
491
  'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
@@ -538,6 +551,14 @@ const UpdateTab = () => {
538
  </div>
539
  </div>
540
  )}
 
 
 
 
 
 
 
 
541
  </motion.div>
542
 
543
  {/* Update Details Card */}
@@ -756,6 +777,66 @@ const UpdateTab = () => {
756
  </div>
757
  </motion.div>
758
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  </div>
760
  );
761
  };
 
4
  import { logStore } from '~/lib/stores/logs';
5
  import { classNames } from '~/utils/classNames';
6
  import { toast } from 'react-toastify';
7
+ import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog';
8
 
9
  interface GitHubCommitResponse {
10
  sha: string;
 
182
  checkInterval: 24,
183
  };
184
  });
185
+ const [lastChecked, setLastChecked] = useState<Date | null>(null);
186
+ const [showUpdateDialog, setShowUpdateDialog] = useState(false);
187
+ const [updateChangelog, setUpdateChangelog] = useState<string[]>([]);
188
 
189
  useEffect(() => {
190
  localStorage.setItem('update_settings', JSON.stringify(updateSettings));
 
216
  };
217
 
218
  const checkForUpdates = async () => {
219
+ console.log('Starting update check...');
220
  setIsChecking(true);
221
  setError(null);
222
+ setLastChecked(new Date());
223
+
224
+ // Add a minimum delay of 2 seconds to show the spinning animation
225
+ const startTime = Date.now();
226
 
227
  try {
228
+ console.log('Fetching update info...');
229
+
230
  const githubToken = localStorage.getItem('github_connection');
231
  const headers: HeadersInit = {};
232
 
 
237
 
238
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
239
  const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
240
+
241
+ // Ensure we show the spinning animation for at least 2 seconds
242
+ const elapsedTime = Date.now() - startTime;
243
+
244
+ if (elapsedTime < 2000) {
245
+ await new Promise((resolve) => setTimeout(resolve, 2000 - elapsedTime));
246
+ }
247
+
248
  setUpdateInfo(info);
249
 
250
  if (info.hasUpdate) {
 
267
  });
268
 
269
  if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
270
+ setUpdateChangelog(info.changelog || ['No changelog available']);
271
+ setShowUpdateDialog(true);
 
 
 
 
 
 
 
 
 
272
  }
273
  }
274
  }
275
  } catch (err) {
276
+ console.error('Detailed update check error:', err);
277
  setError('Failed to check for updates. Please try again later.');
278
  console.error('Update check failed:', err);
279
  setUpdateFailed(true);
280
  } finally {
281
+ console.log('Update check completed');
282
  setIsChecking(false);
283
  }
284
  };
 
495
  onClick={() => {
496
  setHasUserRespondedToUpdate(false);
497
  setUpdateFailed(false);
498
+ setError(null);
499
  checkForUpdates();
500
  }}
501
+ disabled={isChecking}
502
  className={classNames(
503
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
504
  'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
 
551
  </div>
552
  </div>
553
  )}
554
+ {lastChecked && (
555
+ <div className="flex flex-col items-end mt-2">
556
+ <span className="text-xs text-gray-500 dark:text-gray-400">
557
+ Last checked: {lastChecked.toLocaleString()}
558
+ </span>
559
+ {error && <span className="text-xs text-red-500 mt-1">{error}</span>}
560
+ </div>
561
+ )}
562
  </motion.div>
563
 
564
  {/* Update Details Card */}
 
777
  </div>
778
  </motion.div>
779
  )}
780
+
781
+ {/* Update Confirmation Dialog */}
782
+ <DialogRoot open={showUpdateDialog} onOpenChange={setShowUpdateDialog}>
783
+ <Dialog
784
+ onClose={() => {
785
+ setShowUpdateDialog(false);
786
+ setHasUserRespondedToUpdate(true);
787
+ logStore.logSystem('Update cancelled by user');
788
+ }}
789
+ >
790
+ <div className="p-6 w-[500px]">
791
+ <DialogTitle>Update Available</DialogTitle>
792
+ <DialogDescription className="mt-2">
793
+ A new version is available. Would you like to update now?
794
+ </DialogDescription>
795
+
796
+ <div className="mt-3">
797
+ <h3 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Changelog:</h3>
798
+ <div
799
+ className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[300px] overflow-y-auto"
800
+ style={{
801
+ scrollbarWidth: 'thin',
802
+ scrollbarColor: 'rgba(155, 155, 155, 0.5) transparent',
803
+ }}
804
+ >
805
+ <div className="text-sm text-bolt-elements-textSecondary space-y-1.5">
806
+ {updateChangelog.map((log, index) => (
807
+ <div key={index} className="break-words leading-relaxed">
808
+ {log}
809
+ </div>
810
+ ))}
811
+ </div>
812
+ </div>
813
+ </div>
814
+
815
+ <div className="mt-4 flex justify-end gap-3">
816
+ <DialogButton
817
+ type="secondary"
818
+ onClick={() => {
819
+ setShowUpdateDialog(false);
820
+ setHasUserRespondedToUpdate(true);
821
+ logStore.logSystem('Update cancelled by user');
822
+ }}
823
+ >
824
+ Cancel
825
+ </DialogButton>
826
+ <DialogButton
827
+ type="primary"
828
+ onClick={async () => {
829
+ setShowUpdateDialog(false);
830
+ setHasUserRespondedToUpdate(true);
831
+ await initiateUpdate();
832
+ }}
833
+ >
834
+ Update Now
835
+ </DialogButton>
836
+ </div>
837
+ </div>
838
+ </Dialog>
839
+ </DialogRoot>
840
  </div>
841
  );
842
  };
app/components/settings/user/UsersWindow.tsx CHANGED
@@ -18,7 +18,6 @@ import SettingsTab from '~/components/settings/settings/SettingsTab';
18
  import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
19
  import FeaturesTab from '~/components/settings/features/FeaturesTab';
20
  import DataTab from '~/components/settings/data/DataTab';
21
- import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
22
  import DebugTab from '~/components/settings/debug/DebugTab';
23
  import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
24
  import UpdateTab from '~/components/settings/update/UpdateTab';
@@ -28,6 +27,9 @@ import { useFeatures } from '~/lib/hooks/useFeatures';
28
  import { useNotifications } from '~/lib/hooks/useNotifications';
29
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
30
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
 
 
 
31
 
32
  interface DraggableTabTileProps {
33
  tab: TabVisibilityConfig;
@@ -47,11 +49,13 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
47
  notifications: 'View and manage your notifications',
48
  features: 'Explore new and upcoming features',
49
  data: 'Manage your data and storage',
50
- providers: 'Configure AI providers and models',
 
51
  connection: 'Check connection status and settings',
52
  debug: 'Debug tools and system information',
53
  'event-logs': 'View system events and logs',
54
  update: 'Check for updates and release notes',
 
55
  };
56
 
57
  const DraggableTabTile = ({
@@ -209,8 +213,10 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
209
  return <FeaturesTab />;
210
  case 'data':
211
  return <DataTab />;
212
- case 'providers':
213
- return <ProvidersTab />;
 
 
214
  case 'connection':
215
  return <ConnectionsTab />;
216
  case 'debug':
@@ -219,6 +225,8 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
219
  return <EventLogsTab />;
220
  case 'update':
221
  return <UpdateTab />;
 
 
222
  default:
223
  return null;
224
  }
 
18
  import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
19
  import FeaturesTab from '~/components/settings/features/FeaturesTab';
20
  import DataTab from '~/components/settings/data/DataTab';
 
21
  import DebugTab from '~/components/settings/debug/DebugTab';
22
  import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
23
  import UpdateTab from '~/components/settings/update/UpdateTab';
 
27
  import { useNotifications } from '~/lib/hooks/useNotifications';
28
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
29
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
30
+ import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
31
+ import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
32
+ import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
33
 
34
  interface DraggableTabTileProps {
35
  tab: TabVisibilityConfig;
 
49
  notifications: 'View and manage your notifications',
50
  features: 'Explore new and upcoming features',
51
  data: 'Manage your data and storage',
52
+ 'cloud-providers': 'Configure cloud AI providers and models',
53
+ 'local-providers': 'Configure local AI providers and models',
54
  connection: 'Check connection status and settings',
55
  debug: 'Debug tools and system information',
56
  'event-logs': 'View system events and logs',
57
  update: 'Check for updates and release notes',
58
+ 'task-manager': 'Monitor system resources and processes',
59
  };
60
 
61
  const DraggableTabTile = ({
 
213
  return <FeaturesTab />;
214
  case 'data':
215
  return <DataTab />;
216
+ case 'cloud-providers':
217
+ return <CloudProvidersTab />;
218
+ case 'local-providers':
219
+ return <LocalProvidersTab />;
220
  case 'connection':
221
  return <ConnectionsTab />;
222
  case 'debug':
 
225
  return <EventLogsTab />;
226
  case 'update':
227
  return <UpdateTab />;
228
+ case 'task-manager':
229
+ return <TaskManagerTab />;
230
  default:
231
  return null;
232
  }
changelogUI.md CHANGED
@@ -16,6 +16,8 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
16
  - **Responsive Design**: Beautiful transitions and animations using Framer Motion
17
  - **Dark/Light Mode Support**: Full theme support with consistent styling
18
  - **Improved Accessibility**: Using Radix UI primitives for better accessibility
 
 
19
 
20
  ### 🎯 Tab Overview
21
 
@@ -51,34 +53,59 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
51
  - Storage settings
52
  - Backup and restore options
53
 
54
- 6. **Providers**
55
 
56
- - AI provider configuration
57
- - Model selection and management
58
- - API settings
 
 
59
 
60
- 7. **Connection**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  - Network status monitoring
63
  - Connection health metrics
64
  - Troubleshooting tools
 
 
65
 
66
- 8. **Debug**
67
 
68
- - System diagnostics
69
- - Performance monitoring
70
- - Error tracking
 
 
71
 
72
- 9. **Event Logs**
73
 
74
- - Comprehensive system logs
75
- - Filtered log views
76
- - Log management tools
 
 
77
 
78
- 10. **Update**
79
  - Version management
80
  - Update notifications
81
  - Release notes
 
82
 
83
  #### Developer Window Enhancements
84
 
@@ -87,11 +114,13 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
87
  - Fine-grained control over tab visibility
88
  - Custom tab ordering
89
  - Tab permission management
 
90
 
91
  - **Developer Tools**
92
  - Enhanced debugging capabilities
93
  - System metrics and monitoring
94
  - Performance optimization tools
 
95
 
96
  ### πŸš€ UI Improvements
97
 
@@ -100,23 +129,27 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
100
  - Intuitive back navigation
101
  - Breadcrumb-style header
102
  - Context-aware menu system
 
103
 
104
  2. **Status Indicators**
105
 
106
  - Dynamic update badges
107
  - Real-time connection status
108
  - System health monitoring
 
109
 
110
  3. **Profile Integration**
111
 
112
  - Quick access profile menu
113
  - Avatar support
114
  - Fast settings access
 
115
 
116
  4. **Accessibility Features**
117
  - Keyboard navigation
118
  - Screen reader support
119
  - Focus management
 
120
 
121
  ### πŸ›  Technical Enhancements
122
 
@@ -125,17 +158,20 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
125
  - Nano Stores for efficient state handling
126
  - Persistent settings storage
127
  - Real-time state synchronization
 
128
 
129
  - **Performance Optimizations**
130
 
131
  - Lazy loading of tab contents
132
  - Efficient DOM updates
133
  - Optimized animations
 
134
 
135
  - **Developer Experience**
136
  - Improved error handling
137
  - Better debugging tools
138
  - Enhanced logging system
 
139
 
140
  ### 🎯 Future Roadmap
141
 
@@ -144,6 +180,9 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
144
  - [ ] More developer tools
145
  - [ ] Extended API integrations
146
  - [ ] Advanced monitoring capabilities
 
 
 
147
 
148
  ## πŸ”§ Technical Details
149
 
@@ -164,6 +203,7 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
164
  - Optimized bundle size
165
  - Efficient state updates
166
  - Minimal re-renders
 
167
 
168
  ## πŸ“ Contributing
169
 
 
16
  - **Responsive Design**: Beautiful transitions and animations using Framer Motion
17
  - **Dark/Light Mode Support**: Full theme support with consistent styling
18
  - **Improved Accessibility**: Using Radix UI primitives for better accessibility
19
+ - **Enhanced Provider Management**: Split view for local and cloud providers
20
+ - **Resource Monitoring**: New Task Manager for system performance tracking
21
 
22
  ### 🎯 Tab Overview
23
 
 
53
  - Storage settings
54
  - Backup and restore options
55
 
56
+ 6. **Cloud Providers**
57
 
58
+ - Configure cloud-based AI providers
59
+ - API key management
60
+ - Cloud model selection
61
+ - Provider-specific settings
62
+ - Status monitoring for each provider
63
 
64
+ 7. **Local Providers**
65
+
66
+ - Manage local AI models
67
+ - Ollama integration and model updates
68
+ - LM Studio configuration
69
+ - Local inference settings
70
+ - Model download and updates
71
+
72
+ 8. **Task Manager**
73
+
74
+ - System resource monitoring
75
+ - Process management
76
+ - Performance metrics
77
+ - Resource usage graphs
78
+ - Alert configurations
79
+
80
+ 9. **Connection**
81
 
82
  - Network status monitoring
83
  - Connection health metrics
84
  - Troubleshooting tools
85
+ - Latency tracking
86
+ - Auto-reconnect settings
87
 
88
+ 10. **Debug**
89
 
90
+ - System diagnostics
91
+ - Performance monitoring
92
+ - Error tracking
93
+ - Provider status checks
94
+ - System information
95
 
96
+ 11. **Event Logs**
97
 
98
+ - Comprehensive system logs
99
+ - Filtered log views
100
+ - Log management tools
101
+ - Error tracking
102
+ - Performance metrics
103
 
104
+ 12. **Update**
105
  - Version management
106
  - Update notifications
107
  - Release notes
108
+ - Auto-update configuration
109
 
110
  #### Developer Window Enhancements
111
 
 
114
  - Fine-grained control over tab visibility
115
  - Custom tab ordering
116
  - Tab permission management
117
+ - Category-based organization
118
 
119
  - **Developer Tools**
120
  - Enhanced debugging capabilities
121
  - System metrics and monitoring
122
  - Performance optimization tools
123
+ - Advanced logging features
124
 
125
  ### πŸš€ UI Improvements
126
 
 
129
  - Intuitive back navigation
130
  - Breadcrumb-style header
131
  - Context-aware menu system
132
+ - Improved tab organization
133
 
134
  2. **Status Indicators**
135
 
136
  - Dynamic update badges
137
  - Real-time connection status
138
  - System health monitoring
139
+ - Provider status tracking
140
 
141
  3. **Profile Integration**
142
 
143
  - Quick access profile menu
144
  - Avatar support
145
  - Fast settings access
146
+ - Personalization options
147
 
148
  4. **Accessibility Features**
149
  - Keyboard navigation
150
  - Screen reader support
151
  - Focus management
152
+ - ARIA attributes
153
 
154
  ### πŸ›  Technical Enhancements
155
 
 
158
  - Nano Stores for efficient state handling
159
  - Persistent settings storage
160
  - Real-time state synchronization
161
+ - Provider state management
162
 
163
  - **Performance Optimizations**
164
 
165
  - Lazy loading of tab contents
166
  - Efficient DOM updates
167
  - Optimized animations
168
+ - Resource monitoring
169
 
170
  - **Developer Experience**
171
  - Improved error handling
172
  - Better debugging tools
173
  - Enhanced logging system
174
+ - Performance profiling
175
 
176
  ### 🎯 Future Roadmap
177
 
 
180
  - [ ] More developer tools
181
  - [ ] Extended API integrations
182
  - [ ] Advanced monitoring capabilities
183
+ - [ ] Custom provider plugins
184
+ - [ ] Enhanced resource management
185
+ - [ ] Advanced debugging features
186
 
187
  ## πŸ”§ Technical Details
188
 
 
203
  - Optimized bundle size
204
  - Efficient state updates
205
  - Minimal re-renders
206
+ - Resource-aware operations
207
 
208
  ## πŸ“ Contributing
209
 
package.json CHANGED
@@ -76,6 +76,7 @@
76
  "@xterm/xterm": "^5.5.0",
77
  "ai": "^4.0.13",
78
  "chalk": "^5.4.1",
 
79
  "clsx": "^2.1.1",
80
  "date-fns": "^3.6.0",
81
  "diff": "^5.2.0",
@@ -93,6 +94,7 @@
93
  "next": "^15.1.5",
94
  "ollama-ai-provider": "^0.15.2",
95
  "react": "^18.3.1",
 
96
  "react-dnd": "^16.0.1",
97
  "react-dnd-html5-backend": "^16.0.1",
98
  "react-dom": "^18.3.1",
 
76
  "@xterm/xterm": "^5.5.0",
77
  "ai": "^4.0.13",
78
  "chalk": "^5.4.1",
79
+ "chart.js": "^4.4.7",
80
  "clsx": "^2.1.1",
81
  "date-fns": "^3.6.0",
82
  "diff": "^5.2.0",
 
94
  "next": "^15.1.5",
95
  "ollama-ai-provider": "^0.15.2",
96
  "react": "^18.3.1",
97
+ "react-chartjs-2": "^5.3.0",
98
  "react-dnd": "^16.0.1",
99
  "react-dnd-html5-backend": "^16.0.1",
100
  "react-dom": "^18.3.1",
pnpm-lock.yaml CHANGED
@@ -149,6 +149,9 @@ importers:
149
  chalk:
150
  specifier: ^5.4.1
151
  version: 5.4.1
 
 
 
152
  clsx:
153
  specifier: ^2.1.1
154
  version: 2.1.1
@@ -200,6 +203,9 @@ importers:
200
  react:
201
  specifier: ^18.3.1
202
  version: 18.3.1
 
 
 
203
  react-dnd:
204
  specifier: ^16.0.1
205
  version: 16.0.1(@types/[email protected])(@types/[email protected])([email protected])
@@ -1645,6 +1651,9 @@ packages:
1645
  '@jspm/[email protected]':
1646
  resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==}
1647
 
 
 
 
1648
  '@lezer/[email protected]':
1649
  resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
1650
 
@@ -3213,6 +3222,10 @@ packages:
3213
3214
  resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
3215
 
 
 
 
 
3216
3217
  resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
3218
  engines: {node: '>= 16'}
@@ -5257,6 +5270,12 @@ packages:
5257
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5258
  engines: {node: '>= 0.8'}
5259
 
 
 
 
 
 
 
5260
5261
  resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
5262
 
@@ -7901,6 +7920,8 @@ snapshots:
7901
 
7902
  '@jspm/[email protected]': {}
7903
 
 
 
7904
  '@lezer/[email protected]': {}
7905
 
7906
  '@lezer/[email protected]':
@@ -9839,6 +9860,10 @@ snapshots:
9839
 
9840
9841
 
 
 
 
 
9842
9843
 
9844
@@ -12424,6 +12449,11 @@ snapshots:
12424
  iconv-lite: 0.4.24
12425
  unpipe: 1.0.0
12426
 
 
 
 
 
 
12427
12428
  dependencies:
12429
  dnd-core: 16.0.1
 
149
  chalk:
150
  specifier: ^5.4.1
151
  version: 5.4.1
152
+ chart.js:
153
+ specifier: ^4.4.7
154
+ version: 4.4.7
155
  clsx:
156
  specifier: ^2.1.1
157
  version: 2.1.1
 
203
  react:
204
  specifier: ^18.3.1
205
  version: 18.3.1
206
+ react-chartjs-2:
207
+ specifier: ^5.3.0
208
209
  react-dnd:
210
  specifier: ^16.0.1
211
  version: 16.0.1(@types/[email protected])(@types/[email protected])([email protected])
 
1651
  '@jspm/[email protected]':
1652
  resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==}
1653
 
1654
+ '@kurkle/[email protected]':
1655
+ resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
1656
+
1657
  '@lezer/[email protected]':
1658
  resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
1659
 
 
3222
3223
  resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
3224
 
3225
3226
+ resolution: {integrity: sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==}
3227
+ engines: {pnpm: '>=8'}
3228
+
3229
3230
  resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
3231
  engines: {node: '>= 16'}
 
5270
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5271
  engines: {node: '>= 0.8'}
5272
 
5273
5274
+ resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
5275
+ peerDependencies:
5276
+ chart.js: ^4.1.1
5277
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
5278
+
5279
5280
  resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
5281
 
 
7920
 
7921
  '@jspm/[email protected]': {}
7922
 
7923
+ '@kurkle/[email protected]': {}
7924
+
7925
  '@lezer/[email protected]': {}
7926
 
7927
  '@lezer/[email protected]':
 
9860
 
9861
9862
 
9863
9864
+ dependencies:
9865
+ '@kurkle/color': 0.3.4
9866
+
9867
9868
 
9869
 
12449
  iconv-lite: 0.4.24
12450
  unpipe: 1.0.0
12451
 
12452
12453
+ dependencies:
12454
+ chart.js: 4.4.7
12455
+ react: 18.3.1
12456
+
12457
12458
  dependencies:
12459
  dnd-core: 16.0.1