Add new features
Browse filesBolt 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 +2 -1
- app/components/settings/developer/DeveloperWindow.tsx +21 -4
- app/components/settings/developer/TabManagement.tsx +10 -7
- app/components/settings/providers/CloudProvidersTab.tsx +307 -0
- app/components/settings/providers/LocalProvidersTab.tsx +307 -0
- app/components/settings/providers/ProvidersTab.tsx +0 -413
- app/components/settings/settings.types.ts +24 -16
- app/components/settings/shared/TabTile.tsx +3 -0
- app/components/settings/task-manager/TaskManagerTab.tsx +655 -0
- app/components/settings/update/UpdateTab.tsx +93 -12
- app/components/settings/user/UsersWindow.tsx +12 -4
- changelogUI.md +54 -14
- package.json +2 -0
- pnpm-lock.yaml +30 -0
@@ -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
|
@@ -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 <
|
|
|
|
|
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}
|
@@ -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:
|
|
|
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(
|
221 |
-
TAB_LABELS[tab.id]
|
222 |
);
|
223 |
|
224 |
-
const developerTabs = config.developerTabs.filter(
|
225 |
-
TAB_LABELS[tab.id]
|
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 (
|
@@ -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;
|
@@ -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;
|
@@ -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 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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: '
|
75 |
-
{ id: '
|
|
|
76 |
|
77 |
// User Window Tabs (Hidden by default)
|
78 |
-
{ id: 'profile', visible: false, window: 'user', order:
|
79 |
-
{ id: 'settings', visible: false, window: 'user', order:
|
80 |
-
{ id: 'notifications', visible: false, window: 'user', order:
|
81 |
-
{ id: 'event-logs', visible: false, window: 'user', order:
|
82 |
-
{ id: 'update', visible: false, window: 'user', order:
|
|
|
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: '
|
92 |
-
{ id: '
|
93 |
-
{ id: '
|
94 |
-
{ id: '
|
|
|
|
|
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> = {
|
@@ -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 {
|
@@ -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 |
+
}
|
@@ -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 |
-
|
252 |
-
|
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
|
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 |
};
|
@@ -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 <
|
|
|
|
|
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 |
}
|
@@ -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
|
57 |
-
-
|
58 |
-
-
|
|
|
|
|
59 |
|
60 |
-
7. **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
- Network status monitoring
|
63 |
- Connection health metrics
|
64 |
- Troubleshooting tools
|
|
|
|
|
65 |
|
66 |
-
|
67 |
|
68 |
-
|
69 |
-
|
70 |
-
|
|
|
|
|
71 |
|
72 |
-
|
73 |
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
77 |
|
78 |
-
|
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 |
|
@@ -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",
|
@@ -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 |
+
version: 5.3.0([email protected])([email protected])
|
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
|