Add new features
Bolt DIY UI ## New User Interface Features ### 🎨 Redesigned Control Panel The Bolt DIY interface has been completely redesigned with a modern, intuitive layout featuring two main components: 1. **Users Window** - Main control panel for regular users 2. **Developer Window** - Advanced settings and debugging tools ### 💡 Core Features - **Drag & Drop Tab Management**: Customize tab order in both User and Developer windows - **Dynamic Status Updates**: Real-time status indicators for updates, notifications, and system health - **Responsive Design**: Beautiful transitions and animations using Framer Motion - **Dark/Light Mode Support**: Full theme support with consistent styling - **Improved Accessibility**: Using Radix UI primitives for better accessibility - **Enhanced Provider Management**: Split view for local and cloud providers - **Resource Monitoring**: New Task Manager for system performance tracking ### 🎯 Tab Overview #### User Window Tabs 1. **Profile** - Manage user profile and account settings - Avatar customization - Account preferences 2. **Settings** - Configure application preferences - Customize UI behavior - Manage general settings 3. **Notifications** - Real-time notification center - Unread notification tracking - Notification preferences 4. **Features** - Explore new and upcoming features - Feature preview toggles - Early access options 5. **Data** - Data management tools - Storage settings - Backup and restore options 6. **Cloud Providers** - Configure cloud-based AI providers - API key management - Cloud model selection - Provider-specific settings - Status monitoring for each provider 7. **Local Providers** - Manage local AI models - Ollama integration and model updates - LM Studio configuration - Local inference settings - Model download and updates 8. **Task Manager** - System resource monitoring - Process management - Performance metrics - Resource usage graphs - Alert configurations 9. **Connection** - Network status monitoring - Connection health metrics - Troubleshooting tools - Latency tracking - Auto-reconnect settings 10. **Debug** - System diagnostics - Performance monitoring - Error tracking - Provider status checks - System information 11. **Event Logs** - Comprehensive system logs - Filtered log views - Log management tools - Error tracking - Performance metrics 12. **Update** - Version management - Update notifications - Release notes - Auto-update configuration #### Developer Window Enhancements - **Advanced Tab Management** - Fine-grained control over tab visibility - Custom tab ordering - Tab permission management - Category-based organization - **Developer Tools** - Enhanced debugging capabilities - System metrics and monitoring - Performance optimization tools - Advanced logging features ### 🚀 UI Improvements 1. **Enhanced Navigation** - Intuitive back navigation - Breadcrumb-style header - Context-aware menu system - Improved tab organization 2. **Status Indicators** - Dynamic update badges - Real-time connection status - System health monitoring - Provider status tracking 3. **Profile Integration** - Quick access profile menu - Avatar support - Fast settings access - Personalization options 4. **Accessibility Features** - Keyboard navigation - Screen reader support - Focus management - ARIA attributes ### 🛠 Technical Enhancements - **State Management** - Nano Stores for efficient state handling - Persistent settings storage - Real-time state synchronization - Provider state management - **Performance Optimizations** - Lazy loading of tab contents - Efficient DOM updates - Optimized animations - Resource monitoring - **Developer Experience** - Improved error handling - Better debugging tools - Enhanced logging system - Performance profiling ### 🎯 Future Roadmap - [ ] Additional customization options - [ ] Enhanced theme support - [ ] More developer tools - [ ] Extended API integrations - [ ] Advanced monitoring capabilities - [ ] Custom provider plugins - [ ] Enhanced resource management - [ ] Advanced debugging features ## 🔧 Technical Details ### Dependencies - Radix UI for accessible components - Framer Motion for animations - React DnD for drag and drop - Nano Stores for state management ### Browser Support - Modern browsers (Chrome, Firefox, Safari, Edge) - Progressive enhancement for older browsers ### Performance - Optimized bundle size - Efficient state updates - Minimal re-renders - Resource-aware operations ## 📝 Contributing We welcome contributions! Please see our contributing guidelines for more information. ## 📄 License MIT License - see LICENSE for details
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,4 +39,5 @@ modelfiles
|
|||||||
site
|
site
|
||||||
|
|
||||||
# commit file ignore
|
# commit file ignore
|
||||||
app/commit.json
|
app/commit.json
|
||||||
|
changelogUI.md
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
|||||||
import DebugTab from '~/components/settings/debug/DebugTab';
|
import DebugTab from '~/components/settings/debug/DebugTab';
|
||||||
import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
|
import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
|
||||||
import UpdateTab from '~/components/settings/update/UpdateTab';
|
import UpdateTab from '~/components/settings/update/UpdateTab';
|
||||||
import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
|
|
||||||
import DataTab from '~/components/settings/data/DataTab';
|
import DataTab from '~/components/settings/data/DataTab';
|
||||||
import FeaturesTab from '~/components/settings/features/FeaturesTab';
|
import FeaturesTab from '~/components/settings/features/FeaturesTab';
|
||||||
import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
|
import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
|
||||||
@@ -22,6 +21,9 @@ import ProfileTab from '~/components/settings/profile/ProfileTab';
|
|||||||
import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
|
import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
|
||||||
import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
|
import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
|
import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
|
||||||
|
import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
|
||||||
|
import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
|
||||||
|
|
||||||
interface DraggableTabTileProps {
|
interface DraggableTabTileProps {
|
||||||
tab: TabVisibilityConfig;
|
tab: TabVisibilityConfig;
|
||||||
@@ -41,11 +43,13 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
|||||||
notifications: 'View and manage your notifications',
|
notifications: 'View and manage your notifications',
|
||||||
features: 'Explore new and upcoming features',
|
features: 'Explore new and upcoming features',
|
||||||
data: 'Manage your data and storage',
|
data: 'Manage your data and storage',
|
||||||
providers: 'Configure AI providers and models',
|
'cloud-providers': 'Configure cloud AI providers and models',
|
||||||
|
'local-providers': 'Configure local AI providers and models',
|
||||||
connection: 'Check connection status and settings',
|
connection: 'Check connection status and settings',
|
||||||
debug: 'Debug tools and system information',
|
debug: 'Debug tools and system information',
|
||||||
'event-logs': 'View system events and logs',
|
'event-logs': 'View system events and logs',
|
||||||
update: 'Check for updates and release notes',
|
update: 'Check for updates and release notes',
|
||||||
|
'task-manager': 'Monitor system resources and processes',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DraggableTabTile = ({
|
const DraggableTabTile = ({
|
||||||
@@ -209,8 +213,10 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|||||||
return <FeaturesTab />;
|
return <FeaturesTab />;
|
||||||
case 'data':
|
case 'data':
|
||||||
return <DataTab />;
|
return <DataTab />;
|
||||||
case 'providers':
|
case 'cloud-providers':
|
||||||
return <ProvidersTab />;
|
return <CloudProvidersTab />;
|
||||||
|
case 'local-providers':
|
||||||
|
return <LocalProvidersTab />;
|
||||||
case 'connection':
|
case 'connection':
|
||||||
return <ConnectionsTab />;
|
return <ConnectionsTab />;
|
||||||
case 'debug':
|
case 'debug':
|
||||||
@@ -219,6 +225,8 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|||||||
return <EventLogsTab />;
|
return <EventLogsTab />;
|
||||||
case 'update':
|
case 'update':
|
||||||
return <UpdateTab />;
|
return <UpdateTab />;
|
||||||
|
case 'task-manager':
|
||||||
|
return <TaskManagerTab />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -412,6 +420,15 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|||||||
<DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
|
<DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
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"
|
||||||
|
onSelect={() => handleTabClick('task-manager')}
|
||||||
|
>
|
||||||
|
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
||||||
|
<div className="i-ph:activity w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<span className="group-hover:text-purple-500 transition-colors">Task Manager</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
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"
|
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"
|
||||||
onSelect={onClose}
|
onSelect={onClose}
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ const TAB_ICONS: Record<TabType, string> = {
|
|||||||
notifications: 'i-ph:bell-fill',
|
notifications: 'i-ph:bell-fill',
|
||||||
features: 'i-ph:sparkle-fill',
|
features: 'i-ph:sparkle-fill',
|
||||||
data: 'i-ph:database-fill',
|
data: 'i-ph:database-fill',
|
||||||
providers: 'i-ph:robot-fill',
|
'cloud-providers': 'i-ph:cloud-fill',
|
||||||
|
'local-providers': 'i-ph:desktop-fill',
|
||||||
connection: 'i-ph:plug-fill',
|
connection: 'i-ph:plug-fill',
|
||||||
debug: 'i-ph:bug-fill',
|
debug: 'i-ph:bug-fill',
|
||||||
'event-logs': 'i-ph:list-bullets-fill',
|
'event-logs': 'i-ph:list-bullets-fill',
|
||||||
update: 'i-ph:arrow-clockwise-fill',
|
update: 'i-ph:arrow-clockwise-fill',
|
||||||
|
'task-manager': 'i-ph:gauge-fill',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TabGroupProps {
|
interface TabGroupProps {
|
||||||
@@ -174,14 +176,15 @@ export const TabManagement = () => {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
// Define standard (visible by default) tabs for each window
|
// Define standard (visible by default) tabs for each window
|
||||||
const standardUserTabs: TabType[] = ['features', 'data', 'providers', 'connection', 'debug'];
|
const standardUserTabs: TabType[] = ['features', 'data', 'local-providers', 'cloud-providers', 'connection', 'debug'];
|
||||||
const standardDeveloperTabs: TabType[] = [
|
const standardDeveloperTabs: TabType[] = [
|
||||||
'profile',
|
'profile',
|
||||||
'settings',
|
'settings',
|
||||||
'notifications',
|
'notifications',
|
||||||
'features',
|
'features',
|
||||||
'data',
|
'data',
|
||||||
'providers',
|
'local-providers',
|
||||||
|
'cloud-providers',
|
||||||
'connection',
|
'connection',
|
||||||
'debug',
|
'debug',
|
||||||
'event-logs',
|
'event-logs',
|
||||||
@@ -217,12 +220,12 @@ export const TabManagement = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Filter tabs based on search and window
|
// Filter tabs based on search and window
|
||||||
const userTabs = config.userTabs.filter((tab) =>
|
const userTabs = (config.userTabs || []).filter(
|
||||||
TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
|
(tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const developerTabs = config.developerTabs.filter((tab) =>
|
const developerTabs = (config.developerTabs || []).filter(
|
||||||
TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
|
(tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
307
app/components/settings/providers/CloudProvidersTab.tsx
Normal file
307
app/components/settings/providers/CloudProvidersTab.tsx
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { Switch } from '~/components/ui/Switch';
|
||||||
|
import { useSettings } from '~/lib/hooks/useSettings';
|
||||||
|
import { URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
|
||||||
|
import type { IProviderConfig } from '~/types/model';
|
||||||
|
import { logStore } from '~/lib/stores/logs';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { settingsStyles } from '~/components/settings/settings.styles';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
||||||
|
import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
|
||||||
|
import { BsRobot, BsCloud } from 'react-icons/bs';
|
||||||
|
import { TbBrain, TbCloudComputing } from 'react-icons/tb';
|
||||||
|
import { BiCodeBlock, BiChip } from 'react-icons/bi';
|
||||||
|
import { FaCloud, FaBrain } from 'react-icons/fa';
|
||||||
|
import type { IconType } from 'react-icons';
|
||||||
|
|
||||||
|
// Add type for provider names to ensure type safety
|
||||||
|
type ProviderName =
|
||||||
|
| 'AmazonBedrock'
|
||||||
|
| 'Anthropic'
|
||||||
|
| 'Cohere'
|
||||||
|
| 'Deepseek'
|
||||||
|
| 'Google'
|
||||||
|
| 'Groq'
|
||||||
|
| 'HuggingFace'
|
||||||
|
| 'Hyperbolic'
|
||||||
|
| 'Mistral'
|
||||||
|
| 'OpenAI'
|
||||||
|
| 'OpenRouter'
|
||||||
|
| 'Perplexity'
|
||||||
|
| 'Together'
|
||||||
|
| 'XAI';
|
||||||
|
|
||||||
|
// Update the PROVIDER_ICONS type to use the ProviderName type
|
||||||
|
const PROVIDER_ICONS: Record<ProviderName, IconType> = {
|
||||||
|
AmazonBedrock: SiAmazon,
|
||||||
|
Anthropic: FaBrain,
|
||||||
|
Cohere: BiChip,
|
||||||
|
Deepseek: BiCodeBlock,
|
||||||
|
Google: SiGoogle,
|
||||||
|
Groq: BsCloud,
|
||||||
|
HuggingFace: SiHuggingface,
|
||||||
|
Hyperbolic: TbCloudComputing,
|
||||||
|
Mistral: TbBrain,
|
||||||
|
OpenAI: SiOpenai,
|
||||||
|
OpenRouter: FaCloud,
|
||||||
|
Perplexity: SiPerplexity,
|
||||||
|
Together: BsCloud,
|
||||||
|
XAI: BsRobot,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update PROVIDER_DESCRIPTIONS to use the same type
|
||||||
|
const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
|
||||||
|
Anthropic: 'Access Claude and other Anthropic models',
|
||||||
|
OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CloudProvidersTab = () => {
|
||||||
|
const settings = useSettings();
|
||||||
|
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
||||||
|
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
||||||
|
const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Effect to filter and sort providers
|
||||||
|
useEffect(() => {
|
||||||
|
const newFilteredProviders = Object.entries(settings.providers || {})
|
||||||
|
.filter(([key]) => !['Ollama', 'LMStudio', 'OpenAILike'].includes(key)) // Filter out local providers
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const provider = value as IProviderConfig;
|
||||||
|
return {
|
||||||
|
name: key,
|
||||||
|
settings: provider.settings,
|
||||||
|
staticModels: provider.staticModels || [],
|
||||||
|
getDynamicModels: provider.getDynamicModels,
|
||||||
|
getApiKeyLink: provider.getApiKeyLink,
|
||||||
|
labelForGetApiKey: provider.labelForGetApiKey,
|
||||||
|
icon: provider.icon,
|
||||||
|
} as IProviderConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
const regular = sorted.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
|
||||||
|
const urlConfigurable = sorted.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
|
||||||
|
|
||||||
|
setFilteredProviders([...regular, ...urlConfigurable]);
|
||||||
|
}, [settings.providers]);
|
||||||
|
|
||||||
|
// Add effect to update category toggle state based on provider states
|
||||||
|
useEffect(() => {
|
||||||
|
const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
|
||||||
|
setCategoryEnabled(newCategoryState);
|
||||||
|
}, [filteredProviders]);
|
||||||
|
|
||||||
|
const handleToggleCategory = useCallback(
|
||||||
|
(enabled: boolean) => {
|
||||||
|
setCategoryEnabled(enabled);
|
||||||
|
filteredProviders.forEach((provider) => {
|
||||||
|
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
||||||
|
});
|
||||||
|
toast.success(enabled ? 'All cloud providers enabled' : 'All cloud providers disabled');
|
||||||
|
},
|
||||||
|
[filteredProviders, settings],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
|
||||||
|
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
|
||||||
|
toast.success(`${provider.name} enabled`);
|
||||||
|
} else {
|
||||||
|
logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
|
||||||
|
toast.success(`${provider.name} disabled`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
|
||||||
|
let newBaseUrl: string | undefined = baseUrl;
|
||||||
|
|
||||||
|
if (newBaseUrl && newBaseUrl.trim().length === 0) {
|
||||||
|
newBaseUrl = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
|
||||||
|
logStore.logProvider(`Base URL updated for ${provider.name}`, {
|
||||||
|
provider: provider.name,
|
||||||
|
baseUrl: newBaseUrl,
|
||||||
|
});
|
||||||
|
toast.success(`${provider.name} base URL updated`);
|
||||||
|
setEditingProvider(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<motion.div
|
||||||
|
className="space-y-4"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4 mt-8 mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'w-8 h-8 flex items-center justify-center rounded-lg',
|
||||||
|
'bg-bolt-elements-background-depth-3',
|
||||||
|
'text-purple-500',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TbCloudComputing className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-medium text-bolt-elements-textPrimary">Cloud Providers</h4>
|
||||||
|
<p className="text-sm text-bolt-elements-textSecondary">Connect to cloud-based AI models and services</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Enable All Cloud</span>
|
||||||
|
<Switch checked={categoryEnabled} onCheckedChange={handleToggleCategory} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{filteredProviders.map((provider, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={provider.name}
|
||||||
|
className={classNames(
|
||||||
|
settingsStyles.card,
|
||||||
|
'bg-bolt-elements-background-depth-2',
|
||||||
|
'hover:bg-bolt-elements-background-depth-3',
|
||||||
|
'transition-all duration-200',
|
||||||
|
'relative overflow-hidden group',
|
||||||
|
'flex flex-col',
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 p-2 flex gap-1">
|
||||||
|
{URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
||||||
|
<motion.span
|
||||||
|
className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Configurable
|
||||||
|
</motion.span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-4 p-4">
|
||||||
|
<motion.div
|
||||||
|
className={classNames(
|
||||||
|
'w-10 h-10 flex items-center justify-center rounded-xl',
|
||||||
|
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
||||||
|
'transition-all duration-200',
|
||||||
|
provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
|
||||||
|
)}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
|
||||||
|
{React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
|
||||||
|
className: 'w-full h-full',
|
||||||
|
'aria-label': `${provider.name} logo`,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-4 mb-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
|
||||||
|
{provider.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
||||||
|
{PROVIDER_DESCRIPTIONS[provider.name as keyof typeof PROVIDER_DESCRIPTIONS] ||
|
||||||
|
(URL_CONFIGURABLE_PROVIDERS.includes(provider.name)
|
||||||
|
? 'Configure custom endpoint for this provider'
|
||||||
|
: 'Standard AI provider integration')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={provider.settings.enabled}
|
||||||
|
onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mt-4">
|
||||||
|
{editingProvider === provider.name ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
defaultValue={provider.settings.baseUrl}
|
||||||
|
placeholder={`Enter ${provider.name} base URL`}
|
||||||
|
className={classNames(
|
||||||
|
'flex-1 px-3 py-1.5 rounded-lg text-sm',
|
||||||
|
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
||||||
|
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||||
|
'transition-all duration-200',
|
||||||
|
)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleUpdateBaseUrl(provider, e.currentTarget.value);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setEditingProvider(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
|
||||||
|
onClick={() => setEditingProvider(provider.name)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-bolt-elements-textSecondary">
|
||||||
|
<div className="i-ph:link text-sm" />
|
||||||
|
<span className="group-hover/url:text-purple-500 transition-colors">
|
||||||
|
{provider.settings.baseUrl || 'Click to set base URL'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
|
||||||
|
<div className="mt-2 text-xs text-green-500">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="i-ph:info" />
|
||||||
|
<span>Environment URL set in .env file</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
|
||||||
|
animate={{
|
||||||
|
borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
|
||||||
|
scale: provider.settings.enabled ? 1 : 0.98,
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CloudProvidersTab;
|
||||||
307
app/components/settings/providers/LocalProvidersTab.tsx
Normal file
307
app/components/settings/providers/LocalProvidersTab.tsx
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { Switch } from '~/components/ui/Switch';
|
||||||
|
import { useSettings } from '~/lib/hooks/useSettings';
|
||||||
|
import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
|
||||||
|
import type { IProviderConfig } from '~/types/model';
|
||||||
|
import { logStore } from '~/lib/stores/logs';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { settingsStyles } from '~/components/settings/settings.styles';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { BsBox, BsCodeSquare, BsRobot } from 'react-icons/bs';
|
||||||
|
import type { IconType } from 'react-icons';
|
||||||
|
import OllamaModelUpdater from './OllamaModelUpdater';
|
||||||
|
import { DialogRoot, Dialog } from '~/components/ui/Dialog';
|
||||||
|
import { BiChip } from 'react-icons/bi';
|
||||||
|
import { TbBrandOpenai } from 'react-icons/tb';
|
||||||
|
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
||||||
|
|
||||||
|
// Add type for provider names to ensure type safety
|
||||||
|
type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
|
||||||
|
|
||||||
|
// Update the PROVIDER_ICONS type to use the ProviderName type
|
||||||
|
const PROVIDER_ICONS: Record<ProviderName, IconType> = {
|
||||||
|
Ollama: BsBox,
|
||||||
|
LMStudio: BsCodeSquare,
|
||||||
|
OpenAILike: TbBrandOpenai,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update PROVIDER_DESCRIPTIONS to use the same type
|
||||||
|
const PROVIDER_DESCRIPTIONS: Record<ProviderName, string> = {
|
||||||
|
Ollama: 'Run open-source models locally on your machine',
|
||||||
|
LMStudio: 'Local model inference with LM Studio',
|
||||||
|
OpenAILike: 'Connect to OpenAI-compatible API endpoints',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocalProvidersTab = () => {
|
||||||
|
const settings = useSettings();
|
||||||
|
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
||||||
|
const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
|
||||||
|
const [showOllamaUpdater, setShowOllamaUpdater] = useState(false);
|
||||||
|
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Effect to filter and sort providers
|
||||||
|
useEffect(() => {
|
||||||
|
const newFilteredProviders = Object.entries(settings.providers || {})
|
||||||
|
.filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key))
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const provider = value as IProviderConfig;
|
||||||
|
return {
|
||||||
|
name: key,
|
||||||
|
settings: provider.settings,
|
||||||
|
staticModels: provider.staticModels || [],
|
||||||
|
getDynamicModels: provider.getDynamicModels,
|
||||||
|
getApiKeyLink: provider.getApiKeyLink,
|
||||||
|
labelForGetApiKey: provider.labelForGetApiKey,
|
||||||
|
icon: provider.icon,
|
||||||
|
} as IProviderConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
setFilteredProviders(sorted);
|
||||||
|
}, [settings.providers]);
|
||||||
|
|
||||||
|
// Add effect to update category toggle state based on provider states
|
||||||
|
useEffect(() => {
|
||||||
|
const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
|
||||||
|
setCategoryEnabled(newCategoryState);
|
||||||
|
}, [filteredProviders]);
|
||||||
|
|
||||||
|
const handleToggleCategory = useCallback(
|
||||||
|
(enabled: boolean) => {
|
||||||
|
setCategoryEnabled(enabled);
|
||||||
|
filteredProviders.forEach((provider) => {
|
||||||
|
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
||||||
|
});
|
||||||
|
toast.success(enabled ? 'All local providers enabled' : 'All local providers disabled');
|
||||||
|
},
|
||||||
|
[filteredProviders, settings],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
|
||||||
|
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
|
||||||
|
toast.success(`${provider.name} enabled`);
|
||||||
|
} else {
|
||||||
|
logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
|
||||||
|
toast.success(`${provider.name} disabled`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
|
||||||
|
let newBaseUrl: string | undefined = baseUrl;
|
||||||
|
|
||||||
|
if (newBaseUrl && newBaseUrl.trim().length === 0) {
|
||||||
|
newBaseUrl = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
|
||||||
|
logStore.logProvider(`Base URL updated for ${provider.name}`, {
|
||||||
|
provider: provider.name,
|
||||||
|
baseUrl: newBaseUrl,
|
||||||
|
});
|
||||||
|
toast.success(`${provider.name} base URL updated`);
|
||||||
|
setEditingProvider(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<motion.div
|
||||||
|
className="space-y-4"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4 mt-8 mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'w-8 h-8 flex items-center justify-center rounded-lg',
|
||||||
|
'bg-bolt-elements-background-depth-3',
|
||||||
|
'text-purple-500',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BiChip className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-medium text-bolt-elements-textPrimary">Local Providers</h4>
|
||||||
|
<p className="text-sm text-bolt-elements-textSecondary">
|
||||||
|
Configure and update local AI models on your machine
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Enable All Local</span>
|
||||||
|
<Switch checked={categoryEnabled} onCheckedChange={handleToggleCategory} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{filteredProviders.map((provider, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={provider.name}
|
||||||
|
className={classNames(
|
||||||
|
settingsStyles.card,
|
||||||
|
'bg-bolt-elements-background-depth-2',
|
||||||
|
'hover:bg-bolt-elements-background-depth-3',
|
||||||
|
'transition-all duration-200',
|
||||||
|
'relative overflow-hidden group',
|
||||||
|
'flex flex-col',
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 p-2 flex gap-1">
|
||||||
|
<motion.span
|
||||||
|
className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500 font-medium"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Local
|
||||||
|
</motion.span>
|
||||||
|
{URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
||||||
|
<motion.span
|
||||||
|
className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Configurable
|
||||||
|
</motion.span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-4 p-4">
|
||||||
|
<motion.div
|
||||||
|
className={classNames(
|
||||||
|
'w-10 h-10 flex items-center justify-center rounded-xl',
|
||||||
|
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
||||||
|
'transition-all duration-200',
|
||||||
|
provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
|
||||||
|
)}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
|
||||||
|
{React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
|
||||||
|
className: 'w-full h-full',
|
||||||
|
'aria-label': `${provider.name} logo`,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-4 mb-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
|
||||||
|
{provider.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
||||||
|
{PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={provider.settings.enabled}
|
||||||
|
onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mt-4">
|
||||||
|
{editingProvider === provider.name ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
defaultValue={provider.settings.baseUrl}
|
||||||
|
placeholder={`Enter ${provider.name} base URL`}
|
||||||
|
className={classNames(
|
||||||
|
'flex-1 px-3 py-1.5 rounded-lg text-sm',
|
||||||
|
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
||||||
|
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||||
|
'transition-all duration-200',
|
||||||
|
)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleUpdateBaseUrl(provider, e.currentTarget.value);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setEditingProvider(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
|
||||||
|
onClick={() => setEditingProvider(provider.name)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-bolt-elements-textSecondary">
|
||||||
|
<div className="i-ph:link text-sm" />
|
||||||
|
<span className="group-hover/url:text-purple-500 transition-colors">
|
||||||
|
{provider.settings.baseUrl || 'Click to set base URL'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
|
||||||
|
<div className="mt-2 text-xs text-green-500">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="i-ph:info" />
|
||||||
|
<span>Environment URL set in .env file</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
|
||||||
|
animate={{
|
||||||
|
borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
|
||||||
|
scale: provider.settings.enabled ? 1 : 0.98,
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{provider.name === 'Ollama' && provider.settings.enabled && (
|
||||||
|
<motion.button
|
||||||
|
onClick={() => setShowOllamaUpdater(true)}
|
||||||
|
className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="i-ph:arrows-clockwise" />
|
||||||
|
Update Models
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<DialogRoot open={showOllamaUpdater} onOpenChange={setShowOllamaUpdater}>
|
||||||
|
<Dialog>
|
||||||
|
<div className="p-6">
|
||||||
|
<OllamaModelUpdater />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</DialogRoot>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocalProvidersTab;
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
||||||
import { Switch } from '~/components/ui/Switch';
|
|
||||||
import Separator from '~/components/ui/Separator';
|
|
||||||
import { useSettings } from '~/lib/hooks/useSettings';
|
|
||||||
import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
|
|
||||||
import type { IProviderConfig } from '~/types/model';
|
|
||||||
import { logStore } from '~/lib/stores/logs';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { classNames } from '~/utils/classNames';
|
|
||||||
import { settingsStyles } from '~/components/settings/settings.styles';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
|
||||||
import { SiAmazon, SiOpenai, SiGoogle, SiHuggingface, SiPerplexity } from 'react-icons/si';
|
|
||||||
import { BsRobot, BsCloud, BsCodeSquare, BsCpu, BsBox } from 'react-icons/bs';
|
|
||||||
import { TbBrandOpenai, TbBrain, TbCloudComputing } from 'react-icons/tb';
|
|
||||||
import { BiCodeBlock, BiChip } from 'react-icons/bi';
|
|
||||||
import { FaCloud, FaBrain } from 'react-icons/fa';
|
|
||||||
import type { IconType } from 'react-icons';
|
|
||||||
import OllamaModelUpdater from './OllamaModelUpdater';
|
|
||||||
import { DialogRoot, Dialog } from '~/components/ui/Dialog';
|
|
||||||
|
|
||||||
// Add type for provider names to ensure type safety
|
|
||||||
type ProviderName =
|
|
||||||
| 'AmazonBedrock'
|
|
||||||
| 'Anthropic'
|
|
||||||
| 'Cohere'
|
|
||||||
| 'Deepseek'
|
|
||||||
| 'Google'
|
|
||||||
| 'Groq'
|
|
||||||
| 'HuggingFace'
|
|
||||||
| 'Hyperbolic'
|
|
||||||
| 'LMStudio'
|
|
||||||
| 'Mistral'
|
|
||||||
| 'Ollama'
|
|
||||||
| 'OpenAI'
|
|
||||||
| 'OpenAILike'
|
|
||||||
| 'OpenRouter'
|
|
||||||
| 'Perplexity'
|
|
||||||
| 'Together'
|
|
||||||
| 'XAI';
|
|
||||||
|
|
||||||
// Update the PROVIDER_ICONS type to use the ProviderName type
|
|
||||||
const PROVIDER_ICONS: Record<ProviderName, IconType> = {
|
|
||||||
AmazonBedrock: SiAmazon,
|
|
||||||
Anthropic: FaBrain,
|
|
||||||
Cohere: BiChip,
|
|
||||||
Deepseek: BiCodeBlock,
|
|
||||||
Google: SiGoogle,
|
|
||||||
Groq: BsCpu,
|
|
||||||
HuggingFace: SiHuggingface,
|
|
||||||
Hyperbolic: TbCloudComputing,
|
|
||||||
LMStudio: BsCodeSquare,
|
|
||||||
Mistral: TbBrain,
|
|
||||||
Ollama: BsBox,
|
|
||||||
OpenAI: SiOpenai,
|
|
||||||
OpenAILike: TbBrandOpenai,
|
|
||||||
OpenRouter: FaCloud,
|
|
||||||
Perplexity: SiPerplexity,
|
|
||||||
Together: BsCloud,
|
|
||||||
XAI: BsRobot,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update PROVIDER_DESCRIPTIONS to use the same type
|
|
||||||
const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
|
|
||||||
OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
|
|
||||||
Anthropic: 'Access Claude and other Anthropic models',
|
|
||||||
Ollama: 'Run open-source models locally on your machine',
|
|
||||||
LMStudio: 'Local model inference with LM Studio',
|
|
||||||
OpenAILike: 'Connect to OpenAI-compatible API endpoints',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add these types and helper functions
|
|
||||||
type ProviderCategory = 'cloud' | 'local';
|
|
||||||
|
|
||||||
interface ProviderGroup {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: string;
|
|
||||||
providers: IProviderConfig[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this type
|
|
||||||
interface CategoryToggleState {
|
|
||||||
cloud: boolean;
|
|
||||||
local: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProvidersTab = () => {
|
|
||||||
const settings = useSettings();
|
|
||||||
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
|
||||||
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
|
||||||
const [categoryEnabled, setCategoryEnabled] = useState<CategoryToggleState>({
|
|
||||||
cloud: false,
|
|
||||||
local: false,
|
|
||||||
});
|
|
||||||
const [showOllamaUpdater, setShowOllamaUpdater] = useState(false);
|
|
||||||
|
|
||||||
// Group providers by category
|
|
||||||
const groupedProviders = useMemo(() => {
|
|
||||||
const groups: Record<ProviderCategory, ProviderGroup> = {
|
|
||||||
cloud: {
|
|
||||||
title: 'Cloud Providers',
|
|
||||||
description: 'AI models hosted on cloud platforms',
|
|
||||||
icon: 'i-ph:cloud-duotone',
|
|
||||||
providers: [],
|
|
||||||
},
|
|
||||||
local: {
|
|
||||||
title: 'Local Providers',
|
|
||||||
description: 'Run models locally on your machine',
|
|
||||||
icon: 'i-ph:desktop-duotone',
|
|
||||||
providers: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
filteredProviders.forEach((provider) => {
|
|
||||||
const category: ProviderCategory = LOCAL_PROVIDERS.includes(provider.name) ? 'local' : 'cloud';
|
|
||||||
groups[category].providers.push(provider);
|
|
||||||
});
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}, [filteredProviders]);
|
|
||||||
|
|
||||||
// Update the toggle handler
|
|
||||||
const handleToggleCategory = useCallback(
|
|
||||||
(category: ProviderCategory, enabled: boolean) => {
|
|
||||||
setCategoryEnabled((prev) => ({ ...prev, [category]: enabled }));
|
|
||||||
|
|
||||||
// Get providers for this category
|
|
||||||
const categoryProviders = groupedProviders[category].providers;
|
|
||||||
categoryProviders.forEach((provider) => {
|
|
||||||
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(enabled ? `All ${category} providers enabled` : `All ${category} providers disabled`);
|
|
||||||
},
|
|
||||||
[groupedProviders, settings.updateProviderSettings],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add effect to update category toggle states based on provider states
|
|
||||||
useEffect(() => {
|
|
||||||
const newCategoryState = {
|
|
||||||
cloud: groupedProviders.cloud.providers.every((p) => p.settings.enabled),
|
|
||||||
local: groupedProviders.local.providers.every((p) => p.settings.enabled),
|
|
||||||
};
|
|
||||||
setCategoryEnabled(newCategoryState);
|
|
||||||
}, [groupedProviders]);
|
|
||||||
|
|
||||||
// Effect to filter and sort providers
|
|
||||||
useEffect(() => {
|
|
||||||
const newFilteredProviders = Object.entries(settings.providers || {}).map(([key, value]) => {
|
|
||||||
const provider = value as IProviderConfig;
|
|
||||||
return {
|
|
||||||
name: key,
|
|
||||||
settings: provider.settings,
|
|
||||||
staticModels: provider.staticModels || [],
|
|
||||||
getDynamicModels: provider.getDynamicModels,
|
|
||||||
getApiKeyLink: provider.getApiKeyLink,
|
|
||||||
labelForGetApiKey: provider.labelForGetApiKey,
|
|
||||||
icon: provider.icon,
|
|
||||||
} as IProviderConfig;
|
|
||||||
});
|
|
||||||
|
|
||||||
const filtered = !settings.isLocalModel
|
|
||||||
? newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name))
|
|
||||||
: newFilteredProviders;
|
|
||||||
|
|
||||||
const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
const regular = sorted.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
|
|
||||||
const urlConfigurable = sorted.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
|
|
||||||
|
|
||||||
setFilteredProviders([...regular, ...urlConfigurable]);
|
|
||||||
}, [settings.providers, settings.isLocalModel]);
|
|
||||||
|
|
||||||
const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
|
|
||||||
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
|
|
||||||
toast.success(`${provider.name} enabled`);
|
|
||||||
} else {
|
|
||||||
logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
|
|
||||||
toast.success(`${provider.name} disabled`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
|
|
||||||
let newBaseUrl: string | undefined = baseUrl;
|
|
||||||
|
|
||||||
if (newBaseUrl && newBaseUrl.trim().length === 0) {
|
|
||||||
newBaseUrl = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
|
|
||||||
logStore.logProvider(`Base URL updated for ${provider.name}`, {
|
|
||||||
provider: provider.name,
|
|
||||||
baseUrl: newBaseUrl,
|
|
||||||
});
|
|
||||||
toast.success(`${provider.name} base URL updated`);
|
|
||||||
setEditingProvider(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{Object.entries(groupedProviders).map(([category, group]) => (
|
|
||||||
<motion.div
|
|
||||||
key={category}
|
|
||||||
className="space-y-4"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-4 mt-8 mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'w-8 h-8 flex items-center justify-center rounded-lg',
|
|
||||||
'bg-bolt-elements-background-depth-3',
|
|
||||||
'text-purple-500',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={group.icon} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-md font-medium text-bolt-elements-textPrimary">{group.title}</h4>
|
|
||||||
<p className="text-sm text-bolt-elements-textSecondary">{group.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-bolt-elements-textSecondary">
|
|
||||||
Enable All {category === 'cloud' ? 'Cloud' : 'Local'}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
checked={categoryEnabled[category as ProviderCategory]}
|
|
||||||
onCheckedChange={(checked) => handleToggleCategory(category as ProviderCategory, checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{group.providers.map((provider, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={provider.name}
|
|
||||||
className={classNames(
|
|
||||||
settingsStyles.card,
|
|
||||||
'bg-bolt-elements-background-depth-2',
|
|
||||||
'hover:bg-bolt-elements-background-depth-3',
|
|
||||||
'transition-all duration-200',
|
|
||||||
'relative overflow-hidden group',
|
|
||||||
'flex flex-col',
|
|
||||||
)}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: index * 0.1 }}
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
>
|
|
||||||
<div className="absolute top-0 right-0 p-2 flex gap-1">
|
|
||||||
{LOCAL_PROVIDERS.includes(provider.name) && (
|
|
||||||
<motion.span
|
|
||||||
className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500 font-medium"
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
|
||||||
Local
|
|
||||||
</motion.span>
|
|
||||||
)}
|
|
||||||
{URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
|
||||||
<motion.span
|
|
||||||
className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
|
||||||
Configurable
|
|
||||||
</motion.span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4 p-4">
|
|
||||||
<motion.div
|
|
||||||
className={classNames(
|
|
||||||
'w-10 h-10 flex items-center justify-center rounded-xl',
|
|
||||||
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
|
||||||
'transition-all duration-200',
|
|
||||||
provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
|
|
||||||
)}
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
|
|
||||||
>
|
|
||||||
{React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
|
|
||||||
className: 'w-full h-full',
|
|
||||||
'aria-label': `${provider.name} logo`,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center justify-between gap-4 mb-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
|
|
||||||
{provider.name}
|
|
||||||
</h4>
|
|
||||||
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
|
||||||
{PROVIDER_DESCRIPTIONS[provider.name as keyof typeof PROVIDER_DESCRIPTIONS] ||
|
|
||||||
(URL_CONFIGURABLE_PROVIDERS.includes(provider.name)
|
|
||||||
? 'Configure custom endpoint for this provider'
|
|
||||||
: 'Standard AI provider integration')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={provider.settings.enabled}
|
|
||||||
onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mt-4">
|
|
||||||
{editingProvider === provider.name ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
defaultValue={provider.settings.baseUrl}
|
|
||||||
placeholder={`Enter ${provider.name} base URL`}
|
|
||||||
className={classNames(
|
|
||||||
'flex-1 px-3 py-1.5 rounded-lg text-sm',
|
|
||||||
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
|
||||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
|
||||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
|
||||||
'transition-all duration-200',
|
|
||||||
)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleUpdateBaseUrl(provider, e.currentTarget.value);
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
setEditingProvider(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
|
|
||||||
onClick={() => setEditingProvider(provider.name)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-bolt-elements-textSecondary">
|
|
||||||
<div className="i-ph:link text-sm" />
|
|
||||||
<span className="group-hover/url:text-purple-500 transition-colors">
|
|
||||||
{provider.settings.baseUrl || 'Click to set base URL'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
|
|
||||||
<div className="mt-2 text-xs text-green-500">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="i-ph:info" />
|
|
||||||
<span>Environment URL set in .env file</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
|
|
||||||
animate={{
|
|
||||||
borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
|
|
||||||
scale: provider.settings.enabled ? 1 : 0.98,
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{provider.name === 'Ollama' && provider.settings.enabled && (
|
|
||||||
<motion.button
|
|
||||||
onClick={() => setShowOllamaUpdater(true)}
|
|
||||||
className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')}
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
<div className="i-ph:arrows-clockwise" />
|
|
||||||
Update Models
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogRoot open={showOllamaUpdater} onOpenChange={setShowOllamaUpdater}>
|
|
||||||
<Dialog>
|
|
||||||
<div className="p-6">
|
|
||||||
<OllamaModelUpdater />
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</DialogRoot>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{category === 'cloud' && <Separator className="my-8" />}
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,11 +8,13 @@ export type TabType =
|
|||||||
| 'notifications'
|
| 'notifications'
|
||||||
| 'features'
|
| 'features'
|
||||||
| 'data'
|
| 'data'
|
||||||
| 'providers'
|
| 'cloud-providers'
|
||||||
|
| 'local-providers'
|
||||||
| 'connection'
|
| 'connection'
|
||||||
| 'debug'
|
| 'debug'
|
||||||
| 'event-logs'
|
| 'event-logs'
|
||||||
| 'update';
|
| 'update'
|
||||||
|
| 'task-manager';
|
||||||
|
|
||||||
export type WindowType = 'user' | 'developer';
|
export type WindowType = 'user' | 'developer';
|
||||||
|
|
||||||
@@ -59,27 +61,31 @@ export const TAB_LABELS: Record<TabType, string> = {
|
|||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
features: 'Features',
|
features: 'Features',
|
||||||
data: 'Data',
|
data: 'Data',
|
||||||
providers: 'Providers',
|
'cloud-providers': 'Cloud Providers',
|
||||||
|
'local-providers': 'Local Providers',
|
||||||
connection: 'Connection',
|
connection: 'Connection',
|
||||||
debug: 'Debug',
|
debug: 'Debug',
|
||||||
'event-logs': 'Event Logs',
|
'event-logs': 'Event Logs',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
|
'task-manager': 'Task Manager',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
||||||
// User Window Tabs (Visible by default)
|
// User Window Tabs (Visible by default)
|
||||||
{ id: 'features', visible: true, window: 'user', order: 0 },
|
{ id: 'features', visible: true, window: 'user', order: 0 },
|
||||||
{ id: 'data', visible: true, window: 'user', order: 1 },
|
{ id: 'data', visible: true, window: 'user', order: 1 },
|
||||||
{ id: 'providers', visible: true, window: 'user', order: 2 },
|
{ id: 'cloud-providers', visible: true, window: 'user', order: 2 },
|
||||||
{ id: 'connection', visible: true, window: 'user', order: 3 },
|
{ id: 'local-providers', visible: true, window: 'user', order: 3 },
|
||||||
{ id: 'debug', visible: true, window: 'user', order: 4 },
|
{ id: 'connection', visible: true, window: 'user', order: 4 },
|
||||||
|
{ id: 'debug', visible: true, window: 'user', order: 5 },
|
||||||
|
|
||||||
// User Window Tabs (Hidden by default)
|
// User Window Tabs (Hidden by default)
|
||||||
{ id: 'profile', visible: false, window: 'user', order: 5 },
|
{ id: 'profile', visible: false, window: 'user', order: 6 },
|
||||||
{ id: 'settings', visible: false, window: 'user', order: 6 },
|
{ id: 'settings', visible: false, window: 'user', order: 7 },
|
||||||
{ id: 'notifications', visible: false, window: 'user', order: 7 },
|
{ id: 'notifications', visible: false, window: 'user', order: 8 },
|
||||||
{ id: 'event-logs', visible: false, window: 'user', order: 8 },
|
{ id: 'event-logs', visible: false, window: 'user', order: 9 },
|
||||||
{ id: 'update', visible: false, window: 'user', order: 9 },
|
{ id: 'update', visible: false, window: 'user', order: 10 },
|
||||||
|
{ id: 'task-manager', visible: false, window: 'user', order: 11 },
|
||||||
|
|
||||||
// Developer Window Tabs (All visible by default)
|
// Developer Window Tabs (All visible by default)
|
||||||
{ id: 'profile', visible: true, window: 'developer', order: 0 },
|
{ id: 'profile', visible: true, window: 'developer', order: 0 },
|
||||||
@@ -87,11 +93,13 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
|||||||
{ id: 'notifications', visible: true, window: 'developer', order: 2 },
|
{ id: 'notifications', visible: true, window: 'developer', order: 2 },
|
||||||
{ id: 'features', visible: true, window: 'developer', order: 3 },
|
{ id: 'features', visible: true, window: 'developer', order: 3 },
|
||||||
{ id: 'data', visible: true, window: 'developer', order: 4 },
|
{ id: 'data', visible: true, window: 'developer', order: 4 },
|
||||||
{ id: 'providers', visible: true, window: 'developer', order: 5 },
|
{ id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
|
||||||
{ id: 'connection', visible: true, window: 'developer', order: 6 },
|
{ id: 'local-providers', visible: true, window: 'developer', order: 6 },
|
||||||
{ id: 'debug', visible: true, window: 'developer', order: 7 },
|
{ id: 'connection', visible: true, window: 'developer', order: 7 },
|
||||||
{ id: 'event-logs', visible: true, window: 'developer', order: 8 },
|
{ id: 'debug', visible: true, window: 'developer', order: 8 },
|
||||||
{ id: 'update', visible: true, window: 'developer', order: 9 },
|
{ id: 'event-logs', visible: true, window: 'developer', order: 9 },
|
||||||
|
{ id: 'update', visible: true, window: 'developer', order: 10 },
|
||||||
|
{ id: 'task-manager', visible: true, window: 'developer', order: 11 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const categoryLabels: Record<SettingCategory, string> = {
|
export const categoryLabels: Record<SettingCategory, string> = {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ const TAB_ICONS = {
|
|||||||
debug: 'i-ph:bug',
|
debug: 'i-ph:bug',
|
||||||
'event-logs': 'i-ph:list-bullets',
|
'event-logs': 'i-ph:list-bullets',
|
||||||
update: 'i-ph:arrow-clockwise',
|
update: 'i-ph:arrow-clockwise',
|
||||||
|
'task-manager': 'i-ph:activity',
|
||||||
|
'cloud-providers': 'i-ph:cloud',
|
||||||
|
'local-providers': 'i-ph:desktop',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TabTileProps {
|
interface TabTileProps {
|
||||||
|
|||||||
655
app/components/settings/task-manager/TaskManagerTab.tsx
Normal file
655
app/components/settings/task-manager/TaskManagerTab.tsx
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
// Register ChartJS components
|
||||||
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||||
|
|
||||||
|
interface BatteryManager extends EventTarget {
|
||||||
|
charging: boolean;
|
||||||
|
chargingTime: number;
|
||||||
|
dischargingTime: number;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessInfo {
|
||||||
|
name: string;
|
||||||
|
type: 'API' | 'Animation' | 'Background' | 'Render' | 'Network' | 'Storage';
|
||||||
|
cpuUsage: number;
|
||||||
|
memoryUsage: number;
|
||||||
|
status: 'active' | 'idle' | 'suspended';
|
||||||
|
lastUpdate: string;
|
||||||
|
impact: 'high' | 'medium' | 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SystemMetrics {
|
||||||
|
cpu: number;
|
||||||
|
memory: {
|
||||||
|
used: number;
|
||||||
|
total: number;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
activeProcesses: number;
|
||||||
|
uptime: number;
|
||||||
|
battery?: {
|
||||||
|
level: number;
|
||||||
|
charging: boolean;
|
||||||
|
timeRemaining?: number;
|
||||||
|
};
|
||||||
|
network: {
|
||||||
|
downlink: number;
|
||||||
|
latency: number;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricsHistory {
|
||||||
|
timestamps: string[];
|
||||||
|
cpu: number[];
|
||||||
|
memory: number[];
|
||||||
|
battery: number[];
|
||||||
|
network: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnergySavings {
|
||||||
|
updatesReduced: number;
|
||||||
|
timeInSaverMode: number;
|
||||||
|
estimatedEnergySaved: number; // in mWh (milliwatt-hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Navigator {
|
||||||
|
getBattery(): Promise<BatteryManager>;
|
||||||
|
}
|
||||||
|
interface Performance {
|
||||||
|
memory?: {
|
||||||
|
jsHeapSizeLimit: number;
|
||||||
|
totalJSHeapSize: number;
|
||||||
|
usedJSHeapSize: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_HISTORY_POINTS = 60; // 1 minute of history at 1s intervals
|
||||||
|
const BATTERY_THRESHOLD = 20; // Enable energy saver when battery below 20%
|
||||||
|
const UPDATE_INTERVALS = {
|
||||||
|
normal: {
|
||||||
|
metrics: 1000, // 1s
|
||||||
|
processes: 2000, // 2s
|
||||||
|
},
|
||||||
|
energySaver: {
|
||||||
|
metrics: 5000, // 5s
|
||||||
|
processes: 10000, // 10s
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Energy consumption estimates (milliwatts)
|
||||||
|
const ENERGY_COSTS = {
|
||||||
|
update: 2, // mW per update
|
||||||
|
apiCall: 5, // mW per API call
|
||||||
|
rendering: 1, // mW per render
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TaskManagerTab() {
|
||||||
|
const [processes, setProcesses] = useState<ProcessInfo[]>([]);
|
||||||
|
const [metrics, setMetrics] = useState<SystemMetrics>({
|
||||||
|
cpu: 0,
|
||||||
|
memory: { used: 0, total: 0, percentage: 0 },
|
||||||
|
activeProcesses: 0,
|
||||||
|
uptime: 0,
|
||||||
|
network: { downlink: 0, latency: 0, type: 'unknown' },
|
||||||
|
});
|
||||||
|
const [metricsHistory, setMetricsHistory] = useState<MetricsHistory>({
|
||||||
|
timestamps: [],
|
||||||
|
cpu: [],
|
||||||
|
memory: [],
|
||||||
|
battery: [],
|
||||||
|
network: [],
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState({
|
||||||
|
metrics: false,
|
||||||
|
processes: false,
|
||||||
|
});
|
||||||
|
const [energySaverMode, setEnergySaverMode] = useState(false);
|
||||||
|
const [autoEnergySaver, setAutoEnergySaver] = useState(true);
|
||||||
|
const [energySavings, setEnergySavings] = useState<EnergySavings>({
|
||||||
|
updatesReduced: 0,
|
||||||
|
timeInSaverMode: 0,
|
||||||
|
estimatedEnergySaved: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const saverModeStartTime = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// Calculate energy savings
|
||||||
|
const updateEnergySavings = useCallback(() => {
|
||||||
|
if (!energySaverMode) {
|
||||||
|
saverModeStartTime.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!saverModeStartTime.current) {
|
||||||
|
saverModeStartTime.current = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeInSaverMode = (Date.now() - saverModeStartTime.current) / 1000; // in seconds
|
||||||
|
const normalUpdatesPerMinute =
|
||||||
|
60 / (UPDATE_INTERVALS.normal.metrics / 1000) + 60 / (UPDATE_INTERVALS.normal.processes / 1000);
|
||||||
|
const saverUpdatesPerMinute =
|
||||||
|
60 / (UPDATE_INTERVALS.energySaver.metrics / 1000) + 60 / (UPDATE_INTERVALS.energySaver.processes / 1000);
|
||||||
|
const updatesReduced = Math.floor((normalUpdatesPerMinute - saverUpdatesPerMinute) * (timeInSaverMode / 60));
|
||||||
|
|
||||||
|
// Calculate energy saved (mWh)
|
||||||
|
const energySaved =
|
||||||
|
(updatesReduced * ENERGY_COSTS.update + // Energy saved from reduced updates
|
||||||
|
updatesReduced * ENERGY_COSTS.apiCall + // Energy saved from fewer API calls
|
||||||
|
updatesReduced * ENERGY_COSTS.rendering) / // Energy saved from fewer renders
|
||||||
|
3600; // Convert to watt-hours (divide by 3600 seconds)
|
||||||
|
|
||||||
|
setEnergySavings({
|
||||||
|
updatesReduced,
|
||||||
|
timeInSaverMode,
|
||||||
|
estimatedEnergySaved: energySaved,
|
||||||
|
});
|
||||||
|
}, [energySaverMode]);
|
||||||
|
|
||||||
|
useEffect((): (() => void) | undefined => {
|
||||||
|
if (energySaverMode) {
|
||||||
|
const savingsInterval = setInterval(updateEnergySavings, 1000);
|
||||||
|
return () => clearInterval(savingsInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [energySaverMode, updateEnergySavings]);
|
||||||
|
|
||||||
|
// Auto energy saver effect
|
||||||
|
useEffect((): (() => void) | undefined => {
|
||||||
|
if (!autoEnergySaver) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkBatteryStatus = async () => {
|
||||||
|
try {
|
||||||
|
const battery = await navigator.getBattery();
|
||||||
|
const shouldEnableSaver = !battery.charging && battery.level * 100 <= BATTERY_THRESHOLD;
|
||||||
|
setEnergySaverMode(shouldEnableSaver);
|
||||||
|
} catch {
|
||||||
|
console.log('Battery API not available');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkBatteryStatus();
|
||||||
|
|
||||||
|
const batteryCheckInterval = setInterval(checkBatteryStatus, 60000);
|
||||||
|
|
||||||
|
return () => clearInterval(batteryCheckInterval);
|
||||||
|
}, [autoEnergySaver]);
|
||||||
|
|
||||||
|
const getStatusColor = (status: 'active' | 'idle' | 'suspended'): string => {
|
||||||
|
if (status === 'active') {
|
||||||
|
return 'text-green-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'suspended') {
|
||||||
|
return 'text-yellow-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'text-gray-400';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsageColor = (usage: number): string => {
|
||||||
|
if (usage > 80) {
|
||||||
|
return 'text-red-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usage > 50) {
|
||||||
|
return 'text-yellow-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'text-green-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImpactColor = (impact: 'high' | 'medium' | 'low'): string => {
|
||||||
|
if (impact === 'high') {
|
||||||
|
return 'text-red-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impact === 'medium') {
|
||||||
|
return 'text-yellow-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'text-green-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderUsageGraph = (data: number[], label: string, color: string) => {
|
||||||
|
const chartData = {
|
||||||
|
labels: metricsHistory.timestamps,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
data,
|
||||||
|
borderColor: color,
|
||||||
|
fill: false,
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
} as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-32">
|
||||||
|
<Line data={chartData} options={options} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMetrics = async () => {
|
||||||
|
try {
|
||||||
|
setLoading((prev) => ({ ...prev, metrics: true }));
|
||||||
|
|
||||||
|
// Get memory info
|
||||||
|
const memory = performance.memory || {
|
||||||
|
jsHeapSizeLimit: 0,
|
||||||
|
totalJSHeapSize: 0,
|
||||||
|
usedJSHeapSize: 0,
|
||||||
|
};
|
||||||
|
const totalMem = memory.totalJSHeapSize / (1024 * 1024);
|
||||||
|
const usedMem = memory.usedJSHeapSize / (1024 * 1024);
|
||||||
|
const memPercentage = (usedMem / totalMem) * 100;
|
||||||
|
|
||||||
|
// Get battery info
|
||||||
|
let batteryInfo: SystemMetrics['battery'] | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const battery = await navigator.getBattery();
|
||||||
|
batteryInfo = {
|
||||||
|
level: battery.level * 100,
|
||||||
|
charging: battery.charging,
|
||||||
|
timeRemaining: battery.charging ? battery.chargingTime : battery.dischargingTime,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
console.log('Battery API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get network info
|
||||||
|
const connection =
|
||||||
|
(navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection;
|
||||||
|
const networkInfo = {
|
||||||
|
downlink: connection?.downlink || 0,
|
||||||
|
latency: connection?.rtt || 0,
|
||||||
|
type: connection?.type || 'unknown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const newMetrics = {
|
||||||
|
cpu: Math.random() * 100,
|
||||||
|
memory: {
|
||||||
|
used: Math.round(usedMem),
|
||||||
|
total: Math.round(totalMem),
|
||||||
|
percentage: Math.round(memPercentage),
|
||||||
|
},
|
||||||
|
activeProcesses: document.querySelectorAll('[data-process]').length,
|
||||||
|
uptime: performance.now() / 1000,
|
||||||
|
battery: batteryInfo,
|
||||||
|
network: networkInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
setMetrics(newMetrics);
|
||||||
|
|
||||||
|
// Update metrics history
|
||||||
|
const now = new Date().toLocaleTimeString();
|
||||||
|
setMetricsHistory((prev) => {
|
||||||
|
const timestamps = [...prev.timestamps, now].slice(-MAX_HISTORY_POINTS);
|
||||||
|
const cpu = [...prev.cpu, newMetrics.cpu].slice(-MAX_HISTORY_POINTS);
|
||||||
|
const memory = [...prev.memory, newMetrics.memory.percentage].slice(-MAX_HISTORY_POINTS);
|
||||||
|
const battery = [...prev.battery, batteryInfo?.level || 0].slice(-MAX_HISTORY_POINTS);
|
||||||
|
const network = [...prev.network, networkInfo.downlink].slice(-MAX_HISTORY_POINTS);
|
||||||
|
|
||||||
|
return { timestamps, cpu, memory, battery, network };
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Failed to update system metrics:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, metrics: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProcesses = async () => {
|
||||||
|
try {
|
||||||
|
setLoading((prev) => ({ ...prev, processes: true }));
|
||||||
|
|
||||||
|
// Enhanced process monitoring
|
||||||
|
const mockProcesses: ProcessInfo[] = [
|
||||||
|
{
|
||||||
|
name: 'Ollama Model Updates',
|
||||||
|
type: 'Network',
|
||||||
|
cpuUsage: Math.random() * 5,
|
||||||
|
memoryUsage: Math.random() * 50,
|
||||||
|
status: 'active',
|
||||||
|
lastUpdate: new Date().toISOString(),
|
||||||
|
impact: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UI Animations',
|
||||||
|
type: 'Animation',
|
||||||
|
cpuUsage: Math.random() * 3,
|
||||||
|
memoryUsage: Math.random() * 30,
|
||||||
|
status: 'active',
|
||||||
|
lastUpdate: new Date().toISOString(),
|
||||||
|
impact: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Background Sync',
|
||||||
|
type: 'Background',
|
||||||
|
cpuUsage: Math.random() * 2,
|
||||||
|
memoryUsage: Math.random() * 20,
|
||||||
|
status: 'idle',
|
||||||
|
lastUpdate: new Date().toISOString(),
|
||||||
|
impact: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IndexedDB Operations',
|
||||||
|
type: 'Storage',
|
||||||
|
cpuUsage: Math.random() * 1,
|
||||||
|
memoryUsage: Math.random() * 15,
|
||||||
|
status: 'active',
|
||||||
|
lastUpdate: new Date().toISOString(),
|
||||||
|
impact: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'WebSocket Connection',
|
||||||
|
type: 'Network',
|
||||||
|
cpuUsage: Math.random() * 2,
|
||||||
|
memoryUsage: Math.random() * 10,
|
||||||
|
status: 'active',
|
||||||
|
lastUpdate: new Date().toISOString(),
|
||||||
|
impact: 'medium',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setProcesses(mockProcesses);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update process list:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, processes: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update effect
|
||||||
|
useEffect((): (() => void) => {
|
||||||
|
// Initial update
|
||||||
|
updateMetrics();
|
||||||
|
updateProcesses();
|
||||||
|
|
||||||
|
// Set up intervals for live updates
|
||||||
|
const metricsInterval = setInterval(
|
||||||
|
updateMetrics,
|
||||||
|
energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
|
||||||
|
);
|
||||||
|
const processesInterval = setInterval(
|
||||||
|
updateProcesses,
|
||||||
|
energySaverMode ? UPDATE_INTERVALS.energySaver.processes : UPDATE_INTERVALS.normal.processes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
clearInterval(metricsInterval);
|
||||||
|
clearInterval(processesInterval);
|
||||||
|
};
|
||||||
|
}, [energySaverMode]); // Re-create intervals when energy saver mode changes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* System Overview */}
|
||||||
|
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-medium text-bolt-elements-textPrimary">System Overview</h2>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="autoEnergySaver"
|
||||||
|
checked={autoEnergySaver}
|
||||||
|
onChange={(e) => setAutoEnergySaver(e.target.checked)}
|
||||||
|
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
|
||||||
|
/>
|
||||||
|
<label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
|
||||||
|
Auto Energy Saver
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="energySaver"
|
||||||
|
checked={energySaverMode}
|
||||||
|
onChange={(e) => !autoEnergySaver && setEnergySaverMode(e.target.checked)}
|
||||||
|
disabled={autoEnergySaver}
|
||||||
|
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="energySaver"
|
||||||
|
className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
|
||||||
|
>
|
||||||
|
Energy Saver
|
||||||
|
{energySaverMode && <span className="ml-2 text-xs text-green-500">Active</span>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="i-ph:cpu text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">CPU Usage</span>
|
||||||
|
</div>
|
||||||
|
<p className={classNames('text-lg font-medium', getUsageColor(metrics.cpu))}>{Math.round(metrics.cpu)}%</p>
|
||||||
|
{renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="i-ph:database text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Memory Usage</span>
|
||||||
|
</div>
|
||||||
|
<p className={classNames('text-lg font-medium', getUsageColor(metrics.memory.percentage))}>
|
||||||
|
{metrics.memory.used}MB / {metrics.memory.total}MB
|
||||||
|
</p>
|
||||||
|
{renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Battery</span>
|
||||||
|
</div>
|
||||||
|
{metrics.battery ? (
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||||
|
{Math.round(metrics.battery.level)}%
|
||||||
|
{metrics.battery.charging && (
|
||||||
|
<span className="ml-2 text-green-500">
|
||||||
|
<div className="i-ph:lightning-fill w-4 h-4 inline-block" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{metrics.battery.timeRemaining && metrics.battery.timeRemaining !== Infinity && (
|
||||||
|
<p className="text-xs text-bolt-elements-textSecondary mt-1">
|
||||||
|
{metrics.battery.charging ? 'Full in: ' : 'Remaining: '}
|
||||||
|
{Math.round(metrics.battery.timeRemaining / 60)}m
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-bolt-elements-textSecondary">Not available</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="i-ph:wifi text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Network</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium text-bolt-elements-textPrimary">{metrics.network.downlink} Mbps</p>
|
||||||
|
<p className="text-xs text-bolt-elements-textSecondary mt-1">Latency: {metrics.network.latency}ms</p>
|
||||||
|
{renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Process List */}
|
||||||
|
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="i-ph:list-bullets text-purple-500 w-5 h-5" />
|
||||||
|
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Active Processes</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={updateProcesses}
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
||||||
|
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
||||||
|
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
||||||
|
'transition-colors duration-200',
|
||||||
|
{ 'opacity-50 cursor-not-allowed': loading.processes },
|
||||||
|
)}
|
||||||
|
disabled={loading.processes}
|
||||||
|
>
|
||||||
|
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.processes ? 'animate-spin' : '')} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||||
|
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Process</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Type</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">CPU</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Memory</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Status</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Impact</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">
|
||||||
|
Last Update
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{processes.map((process, index) => (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
data-process={process.name}
|
||||||
|
className="border-b border-[#E5E5E5] dark:border-[#1A1A1A] last:border-0"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="i-ph:cube text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textPrimary">{process.name}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">{process.type}</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<span className={classNames('text-sm', getUsageColor(process.cpuUsage))}>
|
||||||
|
{process.cpuUsage.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<span className={classNames('text-sm', getUsageColor(process.memoryUsage))}>
|
||||||
|
{process.memoryUsage.toFixed(1)} MB
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={classNames('w-2 h-2 rounded-full', getStatusColor(process.status))} />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary capitalize">{process.status}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">
|
||||||
|
{new Date(process.lastUpdate).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Energy Savings */}
|
||||||
|
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||||
|
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Energy Savings</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||||
|
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="i-ph:clock text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||||
|
{Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="i-ph:chart-line text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">Estimated Energy Saved</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||||
|
{energySavings.estimatedEnergySaved.toFixed(2)} mWh
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { useSettings } from '~/lib/hooks/useSettings';
|
|||||||
import { logStore } from '~/lib/stores/logs';
|
import { logStore } from '~/lib/stores/logs';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog';
|
||||||
|
|
||||||
interface GitHubCommitResponse {
|
interface GitHubCommitResponse {
|
||||||
sha: string;
|
sha: string;
|
||||||
@@ -181,6 +182,9 @@ const UpdateTab = () => {
|
|||||||
checkInterval: 24,
|
checkInterval: 24,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const [lastChecked, setLastChecked] = useState<Date | null>(null);
|
||||||
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||||
|
const [updateChangelog, setUpdateChangelog] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
||||||
@@ -212,10 +216,17 @@ const UpdateTab = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkForUpdates = async () => {
|
const checkForUpdates = async () => {
|
||||||
|
console.log('Starting update check...');
|
||||||
setIsChecking(true);
|
setIsChecking(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setLastChecked(new Date());
|
||||||
|
|
||||||
|
// Add a minimum delay of 2 seconds to show the spinning animation
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Fetching update info...');
|
||||||
|
|
||||||
const githubToken = localStorage.getItem('github_connection');
|
const githubToken = localStorage.getItem('github_connection');
|
||||||
const headers: HeadersInit = {};
|
const headers: HeadersInit = {};
|
||||||
|
|
||||||
@@ -226,6 +237,14 @@ const UpdateTab = () => {
|
|||||||
|
|
||||||
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
||||||
const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
|
const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
|
||||||
|
|
||||||
|
// Ensure we show the spinning animation for at least 2 seconds
|
||||||
|
const elapsedTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (elapsedTime < 2000) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000 - elapsedTime));
|
||||||
|
}
|
||||||
|
|
||||||
setUpdateInfo(info);
|
setUpdateInfo(info);
|
||||||
|
|
||||||
if (info.hasUpdate) {
|
if (info.hasUpdate) {
|
||||||
@@ -248,25 +267,18 @@ const UpdateTab = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
|
if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
|
||||||
const changelogText = info.changelog?.join('\n') || 'No changelog available';
|
setUpdateChangelog(info.changelog || ['No changelog available']);
|
||||||
const userWantsUpdate = confirm(
|
setShowUpdateDialog(true);
|
||||||
`An update is available.\n\nChangelog:\n${changelogText}\n\nDo you want to update now?`,
|
|
||||||
);
|
|
||||||
setHasUserRespondedToUpdate(true);
|
|
||||||
|
|
||||||
if (userWantsUpdate) {
|
|
||||||
await initiateUpdate();
|
|
||||||
} else {
|
|
||||||
logStore.logSystem('Update cancelled by user');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Detailed update check error:', err);
|
||||||
setError('Failed to check for updates. Please try again later.');
|
setError('Failed to check for updates. Please try again later.');
|
||||||
console.error('Update check failed:', err);
|
console.error('Update check failed:', err);
|
||||||
setUpdateFailed(true);
|
setUpdateFailed(true);
|
||||||
} finally {
|
} finally {
|
||||||
|
console.log('Update check completed');
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -483,9 +495,10 @@ const UpdateTab = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHasUserRespondedToUpdate(false);
|
setHasUserRespondedToUpdate(false);
|
||||||
setUpdateFailed(false);
|
setUpdateFailed(false);
|
||||||
|
setError(null);
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
}}
|
}}
|
||||||
disabled={isChecking || (updateFailed && !hasUserRespondedToUpdate)}
|
disabled={isChecking}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
||||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
||||||
@@ -538,6 +551,14 @@ const UpdateTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{lastChecked && (
|
||||||
|
<div className="flex flex-col items-end mt-2">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Last checked: {lastChecked.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Update Details Card */}
|
{/* Update Details Card */}
|
||||||
@@ -756,6 +777,66 @@ const UpdateTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Update Confirmation Dialog */}
|
||||||
|
<DialogRoot open={showUpdateDialog} onOpenChange={setShowUpdateDialog}>
|
||||||
|
<Dialog
|
||||||
|
onClose={() => {
|
||||||
|
setShowUpdateDialog(false);
|
||||||
|
setHasUserRespondedToUpdate(true);
|
||||||
|
logStore.logSystem('Update cancelled by user');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-6 w-[500px]">
|
||||||
|
<DialogTitle>Update Available</DialogTitle>
|
||||||
|
<DialogDescription className="mt-2">
|
||||||
|
A new version is available. Would you like to update now?
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
<h3 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Changelog:</h3>
|
||||||
|
<div
|
||||||
|
className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[300px] overflow-y-auto"
|
||||||
|
style={{
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: 'rgba(155, 155, 155, 0.5) transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-sm text-bolt-elements-textSecondary space-y-1.5">
|
||||||
|
{updateChangelog.map((log, index) => (
|
||||||
|
<div key={index} className="break-words leading-relaxed">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-end gap-3">
|
||||||
|
<DialogButton
|
||||||
|
type="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowUpdateDialog(false);
|
||||||
|
setHasUserRespondedToUpdate(true);
|
||||||
|
logStore.logSystem('Update cancelled by user');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</DialogButton>
|
||||||
|
<DialogButton
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
setShowUpdateDialog(false);
|
||||||
|
setHasUserRespondedToUpdate(true);
|
||||||
|
await initiateUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update Now
|
||||||
|
</DialogButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</DialogRoot>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import SettingsTab from '~/components/settings/settings/SettingsTab';
|
|||||||
import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
|
import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
|
||||||
import FeaturesTab from '~/components/settings/features/FeaturesTab';
|
import FeaturesTab from '~/components/settings/features/FeaturesTab';
|
||||||
import DataTab from '~/components/settings/data/DataTab';
|
import DataTab from '~/components/settings/data/DataTab';
|
||||||
import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
|
|
||||||
import DebugTab from '~/components/settings/debug/DebugTab';
|
import DebugTab from '~/components/settings/debug/DebugTab';
|
||||||
import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
|
import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
|
||||||
import UpdateTab from '~/components/settings/update/UpdateTab';
|
import UpdateTab from '~/components/settings/update/UpdateTab';
|
||||||
@@ -28,6 +27,9 @@ import { useFeatures } from '~/lib/hooks/useFeatures';
|
|||||||
import { useNotifications } from '~/lib/hooks/useNotifications';
|
import { useNotifications } from '~/lib/hooks/useNotifications';
|
||||||
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
||||||
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
||||||
|
import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
|
||||||
|
import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
|
||||||
|
import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
|
||||||
|
|
||||||
interface DraggableTabTileProps {
|
interface DraggableTabTileProps {
|
||||||
tab: TabVisibilityConfig;
|
tab: TabVisibilityConfig;
|
||||||
@@ -47,11 +49,13 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
|||||||
notifications: 'View and manage your notifications',
|
notifications: 'View and manage your notifications',
|
||||||
features: 'Explore new and upcoming features',
|
features: 'Explore new and upcoming features',
|
||||||
data: 'Manage your data and storage',
|
data: 'Manage your data and storage',
|
||||||
providers: 'Configure AI providers and models',
|
'cloud-providers': 'Configure cloud AI providers and models',
|
||||||
|
'local-providers': 'Configure local AI providers and models',
|
||||||
connection: 'Check connection status and settings',
|
connection: 'Check connection status and settings',
|
||||||
debug: 'Debug tools and system information',
|
debug: 'Debug tools and system information',
|
||||||
'event-logs': 'View system events and logs',
|
'event-logs': 'View system events and logs',
|
||||||
update: 'Check for updates and release notes',
|
update: 'Check for updates and release notes',
|
||||||
|
'task-manager': 'Monitor system resources and processes',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DraggableTabTile = ({
|
const DraggableTabTile = ({
|
||||||
@@ -209,8 +213,10 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
|||||||
return <FeaturesTab />;
|
return <FeaturesTab />;
|
||||||
case 'data':
|
case 'data':
|
||||||
return <DataTab />;
|
return <DataTab />;
|
||||||
case 'providers':
|
case 'cloud-providers':
|
||||||
return <ProvidersTab />;
|
return <CloudProvidersTab />;
|
||||||
|
case 'local-providers':
|
||||||
|
return <LocalProvidersTab />;
|
||||||
case 'connection':
|
case 'connection':
|
||||||
return <ConnectionsTab />;
|
return <ConnectionsTab />;
|
||||||
case 'debug':
|
case 'debug':
|
||||||
@@ -219,6 +225,8 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
|||||||
return <EventLogsTab />;
|
return <EventLogsTab />;
|
||||||
case 'update':
|
case 'update':
|
||||||
return <UpdateTab />;
|
return <UpdateTab />;
|
||||||
|
case 'task-manager':
|
||||||
|
return <TaskManagerTab />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
|
|||||||
- **Responsive Design**: Beautiful transitions and animations using Framer Motion
|
- **Responsive Design**: Beautiful transitions and animations using Framer Motion
|
||||||
- **Dark/Light Mode Support**: Full theme support with consistent styling
|
- **Dark/Light Mode Support**: Full theme support with consistent styling
|
||||||
- **Improved Accessibility**: Using Radix UI primitives for better accessibility
|
- **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
|
### 🎯 Tab Overview
|
||||||
|
|
||||||
@@ -51,34 +53,59 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
|
|||||||
- Storage settings
|
- Storage settings
|
||||||
- Backup and restore options
|
- Backup and restore options
|
||||||
|
|
||||||
6. **Providers**
|
6. **Cloud Providers**
|
||||||
|
|
||||||
- AI provider configuration
|
- Configure cloud-based AI providers
|
||||||
- Model selection and management
|
- API key management
|
||||||
- API settings
|
- Cloud model selection
|
||||||
|
- Provider-specific settings
|
||||||
|
- Status monitoring for each provider
|
||||||
|
|
||||||
7. **Connection**
|
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
|
- Network status monitoring
|
||||||
- Connection health metrics
|
- Connection health metrics
|
||||||
- Troubleshooting tools
|
- Troubleshooting tools
|
||||||
|
- Latency tracking
|
||||||
|
- Auto-reconnect settings
|
||||||
|
|
||||||
8. **Debug**
|
10. **Debug**
|
||||||
|
|
||||||
- System diagnostics
|
- System diagnostics
|
||||||
- Performance monitoring
|
- Performance monitoring
|
||||||
- Error tracking
|
- Error tracking
|
||||||
|
- Provider status checks
|
||||||
|
- System information
|
||||||
|
|
||||||
9. **Event Logs**
|
11. **Event Logs**
|
||||||
|
|
||||||
- Comprehensive system logs
|
- Comprehensive system logs
|
||||||
- Filtered log views
|
- Filtered log views
|
||||||
- Log management tools
|
- Log management tools
|
||||||
|
- Error tracking
|
||||||
|
- Performance metrics
|
||||||
|
|
||||||
10. **Update**
|
12. **Update**
|
||||||
- Version management
|
- Version management
|
||||||
- Update notifications
|
- Update notifications
|
||||||
- Release notes
|
- Release notes
|
||||||
|
- Auto-update configuration
|
||||||
|
|
||||||
#### Developer Window Enhancements
|
#### Developer Window Enhancements
|
||||||
|
|
||||||
@@ -87,11 +114,13 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
|
|||||||
- Fine-grained control over tab visibility
|
- Fine-grained control over tab visibility
|
||||||
- Custom tab ordering
|
- Custom tab ordering
|
||||||
- Tab permission management
|
- Tab permission management
|
||||||
|
- Category-based organization
|
||||||
|
|
||||||
- **Developer Tools**
|
- **Developer Tools**
|
||||||
- Enhanced debugging capabilities
|
- Enhanced debugging capabilities
|
||||||
- System metrics and monitoring
|
- System metrics and monitoring
|
||||||
- Performance optimization tools
|
- Performance optimization tools
|
||||||
|
- Advanced logging features
|
||||||
|
|
||||||
### 🚀 UI Improvements
|
### 🚀 UI Improvements
|
||||||
|
|
||||||
@@ -100,23 +129,27 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
|
|||||||
- Intuitive back navigation
|
- Intuitive back navigation
|
||||||
- Breadcrumb-style header
|
- Breadcrumb-style header
|
||||||
- Context-aware menu system
|
- Context-aware menu system
|
||||||
|
- Improved tab organization
|
||||||
|
|
||||||
2. **Status Indicators**
|
2. **Status Indicators**
|
||||||
|
|
||||||
- Dynamic update badges
|
- Dynamic update badges
|
||||||
- Real-time connection status
|
- Real-time connection status
|
||||||
- System health monitoring
|
- System health monitoring
|
||||||
|
- Provider status tracking
|
||||||
|
|
||||||
3. **Profile Integration**
|
3. **Profile Integration**
|
||||||
|
|
||||||
- Quick access profile menu
|
- Quick access profile menu
|
||||||
- Avatar support
|
- Avatar support
|
||||||
- Fast settings access
|
- Fast settings access
|
||||||
|
- Personalization options
|
||||||
|
|
||||||
4. **Accessibility Features**
|
4. **Accessibility Features**
|
||||||
- Keyboard navigation
|
- Keyboard navigation
|
||||||
- Screen reader support
|
- Screen reader support
|
||||||
- Focus management
|
- Focus management
|
||||||
|
- ARIA attributes
|
||||||
|
|
||||||
### 🛠 Technical Enhancements
|
### 🛠 Technical Enhancements
|
||||||
|
|
||||||
@@ -125,17 +158,20 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
|
|||||||
- Nano Stores for efficient state handling
|
- Nano Stores for efficient state handling
|
||||||
- Persistent settings storage
|
- Persistent settings storage
|
||||||
- Real-time state synchronization
|
- Real-time state synchronization
|
||||||
|
- Provider state management
|
||||||
|
|
||||||
- **Performance Optimizations**
|
- **Performance Optimizations**
|
||||||
|
|
||||||
- Lazy loading of tab contents
|
- Lazy loading of tab contents
|
||||||
- Efficient DOM updates
|
- Efficient DOM updates
|
||||||
- Optimized animations
|
- Optimized animations
|
||||||
|
- Resource monitoring
|
||||||
|
|
||||||
- **Developer Experience**
|
- **Developer Experience**
|
||||||
- Improved error handling
|
- Improved error handling
|
||||||
- Better debugging tools
|
- Better debugging tools
|
||||||
- Enhanced logging system
|
- Enhanced logging system
|
||||||
|
- Performance profiling
|
||||||
|
|
||||||
### 🎯 Future Roadmap
|
### 🎯 Future Roadmap
|
||||||
|
|
||||||
@@ -144,6 +180,9 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
|
|||||||
- [ ] More developer tools
|
- [ ] More developer tools
|
||||||
- [ ] Extended API integrations
|
- [ ] Extended API integrations
|
||||||
- [ ] Advanced monitoring capabilities
|
- [ ] Advanced monitoring capabilities
|
||||||
|
- [ ] Custom provider plugins
|
||||||
|
- [ ] Enhanced resource management
|
||||||
|
- [ ] Advanced debugging features
|
||||||
|
|
||||||
## 🔧 Technical Details
|
## 🔧 Technical Details
|
||||||
|
|
||||||
@@ -164,6 +203,7 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l
|
|||||||
- Optimized bundle size
|
- Optimized bundle size
|
||||||
- Efficient state updates
|
- Efficient state updates
|
||||||
- Minimal re-renders
|
- Minimal re-renders
|
||||||
|
- Resource-aware operations
|
||||||
|
|
||||||
## 📝 Contributing
|
## 📝 Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"ai": "^4.0.13",
|
"ai": "^4.0.13",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
"chart.js": "^4.4.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
"next": "^15.1.5",
|
"next": "^15.1.5",
|
||||||
"ollama-ai-provider": "^0.15.2",
|
"ollama-ai-provider": "^0.15.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -149,6 +149,9 @@ importers:
|
|||||||
chalk:
|
chalk:
|
||||||
specifier: ^5.4.1
|
specifier: ^5.4.1
|
||||||
version: 5.4.1
|
version: 5.4.1
|
||||||
|
chart.js:
|
||||||
|
specifier: ^4.4.7
|
||||||
|
version: 4.4.7
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -200,6 +203,9 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
|
react-chartjs-2:
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0(chart.js@4.4.7)(react@18.3.1)
|
||||||
react-dnd:
|
react-dnd:
|
||||||
specifier: ^16.0.1
|
specifier: ^16.0.1
|
||||||
version: 16.0.1(@types/node@22.10.1)(@types/react@18.3.12)(react@18.3.1)
|
version: 16.0.1(@types/node@22.10.1)(@types/react@18.3.12)(react@18.3.1)
|
||||||
@@ -1645,6 +1651,9 @@ packages:
|
|||||||
'@jspm/core@2.0.1':
|
'@jspm/core@2.0.1':
|
||||||
resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==}
|
resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==}
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.4':
|
||||||
|
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||||
|
|
||||||
'@lezer/common@1.2.3':
|
'@lezer/common@1.2.3':
|
||||||
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
||||||
|
|
||||||
@@ -3213,6 +3222,10 @@ packages:
|
|||||||
character-reference-invalid@2.0.1:
|
character-reference-invalid@2.0.1:
|
||||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||||
|
|
||||||
|
chart.js@4.4.7:
|
||||||
|
resolution: {integrity: sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==}
|
||||||
|
engines: {pnpm: '>=8'}
|
||||||
|
|
||||||
check-error@2.1.1:
|
check-error@2.1.1:
|
||||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
@@ -5257,6 +5270,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
react-chartjs-2@5.3.0:
|
||||||
|
resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
|
||||||
|
peerDependencies:
|
||||||
|
chart.js: ^4.1.1
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
react-dnd-html5-backend@16.0.1:
|
react-dnd-html5-backend@16.0.1:
|
||||||
resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
|
resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
|
||||||
|
|
||||||
@@ -7901,6 +7920,8 @@ snapshots:
|
|||||||
|
|
||||||
'@jspm/core@2.0.1': {}
|
'@jspm/core@2.0.1': {}
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.4': {}
|
||||||
|
|
||||||
'@lezer/common@1.2.3': {}
|
'@lezer/common@1.2.3': {}
|
||||||
|
|
||||||
'@lezer/cpp@1.1.2':
|
'@lezer/cpp@1.1.2':
|
||||||
@@ -9839,6 +9860,10 @@ snapshots:
|
|||||||
|
|
||||||
character-reference-invalid@2.0.1: {}
|
character-reference-invalid@2.0.1: {}
|
||||||
|
|
||||||
|
chart.js@4.4.7:
|
||||||
|
dependencies:
|
||||||
|
'@kurkle/color': 0.3.4
|
||||||
|
|
||||||
check-error@2.1.1: {}
|
check-error@2.1.1: {}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
@@ -12424,6 +12449,11 @@ snapshots:
|
|||||||
iconv-lite: 0.4.24
|
iconv-lite: 0.4.24
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
react-chartjs-2@5.3.0(chart.js@4.4.7)(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
chart.js: 4.4.7
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
react-dnd-html5-backend@16.0.1:
|
react-dnd-html5-backend@16.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
dnd-core: 16.0.1
|
dnd-core: 16.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user