From 6d98affc3d6882598d80fab423d1da4d26bd849a Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:18:17 +0100 Subject: [PATCH] Add new features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bolt DIY UI ## New User Interface Features ### 🎨 Redesigned Control Panel The Bolt DIY interface has been completely redesigned with a modern, intuitive layout featuring two main components: 1. **Users Window** - Main control panel for regular users 2. **Developer Window** - Advanced settings and debugging tools ### 💡 Core Features - **Drag & Drop Tab Management**: Customize tab order in both User and Developer windows - **Dynamic Status Updates**: Real-time status indicators for updates, notifications, and system health - **Responsive Design**: Beautiful transitions and animations using Framer Motion - **Dark/Light Mode Support**: Full theme support with consistent styling - **Improved Accessibility**: Using Radix UI primitives for better accessibility - **Enhanced Provider Management**: Split view for local and cloud providers - **Resource Monitoring**: New Task Manager for system performance tracking ### 🎯 Tab Overview #### User Window Tabs 1. **Profile** - Manage user profile and account settings - Avatar customization - Account preferences 2. **Settings** - Configure application preferences - Customize UI behavior - Manage general settings 3. **Notifications** - Real-time notification center - Unread notification tracking - Notification preferences 4. **Features** - Explore new and upcoming features - Feature preview toggles - Early access options 5. **Data** - Data management tools - Storage settings - Backup and restore options 6. **Cloud Providers** - Configure cloud-based AI providers - API key management - Cloud model selection - Provider-specific settings - Status monitoring for each provider 7. **Local Providers** - Manage local AI models - Ollama integration and model updates - LM Studio configuration - Local inference settings - Model download and updates 8. **Task Manager** - System resource monitoring - Process management - Performance metrics - Resource usage graphs - Alert configurations 9. **Connection** - Network status monitoring - Connection health metrics - Troubleshooting tools - Latency tracking - Auto-reconnect settings 10. **Debug** - System diagnostics - Performance monitoring - Error tracking - Provider status checks - System information 11. **Event Logs** - Comprehensive system logs - Filtered log views - Log management tools - Error tracking - Performance metrics 12. **Update** - Version management - Update notifications - Release notes - Auto-update configuration #### Developer Window Enhancements - **Advanced Tab Management** - Fine-grained control over tab visibility - Custom tab ordering - Tab permission management - Category-based organization - **Developer Tools** - Enhanced debugging capabilities - System metrics and monitoring - Performance optimization tools - Advanced logging features ### 🚀 UI Improvements 1. **Enhanced Navigation** - Intuitive back navigation - Breadcrumb-style header - Context-aware menu system - Improved tab organization 2. **Status Indicators** - Dynamic update badges - Real-time connection status - System health monitoring - Provider status tracking 3. **Profile Integration** - Quick access profile menu - Avatar support - Fast settings access - Personalization options 4. **Accessibility Features** - Keyboard navigation - Screen reader support - Focus management - ARIA attributes ### 🛠 Technical Enhancements - **State Management** - Nano Stores for efficient state handling - Persistent settings storage - Real-time state synchronization - Provider state management - **Performance Optimizations** - Lazy loading of tab contents - Efficient DOM updates - Optimized animations - Resource monitoring - **Developer Experience** - Improved error handling - Better debugging tools - Enhanced logging system - Performance profiling ### 🎯 Future Roadmap - [ ] Additional customization options - [ ] Enhanced theme support - [ ] More developer tools - [ ] Extended API integrations - [ ] Advanced monitoring capabilities - [ ] Custom provider plugins - [ ] Enhanced resource management - [ ] Advanced debugging features ## 🔧 Technical Details ### Dependencies - Radix UI for accessible components - Framer Motion for animations - React DnD for drag and drop - Nano Stores for state management ### Browser Support - Modern browsers (Chrome, Firefox, Safari, Edge) - Progressive enhancement for older browsers ### Performance - Optimized bundle size - Efficient state updates - Minimal re-renders - Resource-aware operations ## 📝 Contributing We welcome contributions! Please see our contributing guidelines for more information. ## 📄 License MIT License - see LICENSE for details --- .gitignore | 3 +- .../settings/developer/DeveloperWindow.tsx | 25 +- .../settings/developer/TabManagement.tsx | 17 +- .../settings/providers/CloudProvidersTab.tsx | 307 ++++++++ .../settings/providers/LocalProvidersTab.tsx | 307 ++++++++ .../settings/providers/ProvidersTab.tsx | 413 ----------- app/components/settings/settings.types.ts | 40 +- app/components/settings/shared/TabTile.tsx | 3 + .../settings/task-manager/TaskManagerTab.tsx | 655 ++++++++++++++++++ app/components/settings/update/UpdateTab.tsx | 105 ++- app/components/settings/user/UsersWindow.tsx | 16 +- changelogUI.md | 68 +- package.json | 2 + pnpm-lock.yaml | 30 + 14 files changed, 1520 insertions(+), 471 deletions(-) create mode 100644 app/components/settings/providers/CloudProvidersTab.tsx create mode 100644 app/components/settings/providers/LocalProvidersTab.tsx delete mode 100644 app/components/settings/providers/ProvidersTab.tsx create mode 100644 app/components/settings/task-manager/TaskManagerTab.tsx diff --git a/.gitignore b/.gitignore index 53eb036..b0fad68 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ modelfiles site # commit file ignore -app/commit.json \ No newline at end of file +app/commit.json +changelogUI.md diff --git a/app/components/settings/developer/DeveloperWindow.tsx b/app/components/settings/developer/DeveloperWindow.tsx index 37374ba..6463567 100644 --- a/app/components/settings/developer/DeveloperWindow.tsx +++ b/app/components/settings/developer/DeveloperWindow.tsx @@ -13,7 +13,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import DebugTab from '~/components/settings/debug/DebugTab'; import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab'; import UpdateTab from '~/components/settings/update/UpdateTab'; -import { ProvidersTab } from '~/components/settings/providers/ProvidersTab'; import DataTab from '~/components/settings/data/DataTab'; import FeaturesTab from '~/components/settings/features/FeaturesTab'; 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 { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks'; 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 { tab: TabVisibilityConfig; @@ -41,11 +43,13 @@ const TAB_DESCRIPTIONS: Record = { notifications: 'View and manage your notifications', features: 'Explore new and upcoming features', 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', debug: 'Debug tools and system information', 'event-logs': 'View system events and logs', update: 'Check for updates and release notes', + 'task-manager': 'Monitor system resources and processes', }; const DraggableTabTile = ({ @@ -209,8 +213,10 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => { return ; case 'data': return ; - case 'providers': - return ; + case 'cloud-providers': + return ; + case 'local-providers': + return ; case 'connection': return ; case 'debug': @@ -219,6 +225,8 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => { return ; case 'update': return ; + case 'task-manager': + return ; default: return null; } @@ -412,6 +420,15 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => { )} + handleTabClick('task-manager')} + > +
+
+
+ Task Manager + = { notifications: 'i-ph:bell-fill', features: 'i-ph:sparkle-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', debug: 'i-ph:bug-fill', 'event-logs': 'i-ph:list-bullets-fill', update: 'i-ph:arrow-clockwise-fill', + 'task-manager': 'i-ph:gauge-fill', }; interface TabGroupProps { @@ -174,14 +176,15 @@ export const TabManagement = () => { const [searchQuery, setSearchQuery] = useState(''); // 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[] = [ 'profile', 'settings', 'notifications', 'features', 'data', - 'providers', + 'local-providers', + 'cloud-providers', 'connection', 'debug', 'event-logs', @@ -217,12 +220,12 @@ export const TabManagement = () => { }; // Filter tabs based on search and window - const userTabs = config.userTabs.filter((tab) => - TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()), + const userTabs = (config.userTabs || []).filter( + (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()), ); - const developerTabs = config.developerTabs.filter((tab) => - TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()), + const developerTabs = (config.developerTabs || []).filter( + (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()), ); return ( diff --git a/app/components/settings/providers/CloudProvidersTab.tsx b/app/components/settings/providers/CloudProvidersTab.tsx new file mode 100644 index 0000000..866a9f6 --- /dev/null +++ b/app/components/settings/providers/CloudProvidersTab.tsx @@ -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 = { + 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> = { + 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(null); + const [filteredProviders, setFilteredProviders] = useState([]); + const [categoryEnabled, setCategoryEnabled] = useState(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 ( +
+ +
+
+
+ +
+
+

Cloud Providers

+

Connect to cloud-based AI models and services

+
+
+ +
+ Enable All Cloud + +
+
+ +
+ {filteredProviders.map((provider, index) => ( + +
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( + + Configurable + + )} +
+ +
+ +
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, { + className: 'w-full h-full', + 'aria-label': `${provider.name} logo`, + })} +
+
+ +
+
+
+

+ {provider.name} +

+

+ {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')} +

+
+ handleToggleProvider(provider, checked)} + /> +
+ + {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( + +
+ {editingProvider === provider.name ? ( + { + if (e.key === 'Enter') { + handleUpdateBaseUrl(provider, e.currentTarget.value); + } else if (e.key === 'Escape') { + setEditingProvider(null); + } + }} + onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)} + autoFocus + /> + ) : ( +
setEditingProvider(provider.name)} + > +
+
+ + {provider.settings.baseUrl || 'Click to set base URL'} + +
+
+ )} +
+ + {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && ( +
+
+
+ Environment URL set in .env file +
+
+ )} + + )} +
+
+ + + + ))} +
+ +
+ ); +}; + +export default CloudProvidersTab; diff --git a/app/components/settings/providers/LocalProvidersTab.tsx b/app/components/settings/providers/LocalProvidersTab.tsx new file mode 100644 index 0000000..e1b8f2c --- /dev/null +++ b/app/components/settings/providers/LocalProvidersTab.tsx @@ -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 = { + Ollama: BsBox, + LMStudio: BsCodeSquare, + OpenAILike: TbBrandOpenai, +}; + +// Update PROVIDER_DESCRIPTIONS to use the same type +const PROVIDER_DESCRIPTIONS: Record = { + 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([]); + const [categoryEnabled, setCategoryEnabled] = useState(false); + const [showOllamaUpdater, setShowOllamaUpdater] = useState(false); + const [editingProvider, setEditingProvider] = useState(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 ( +
+ +
+
+
+ +
+
+

Local Providers

+

+ Configure and update local AI models on your machine +

+
+
+ +
+ Enable All Local + +
+
+ +
+ {filteredProviders.map((provider, index) => ( + +
+ + Local + + {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( + + Configurable + + )} +
+ +
+ +
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, { + className: 'w-full h-full', + 'aria-label': `${provider.name} logo`, + })} +
+
+ +
+
+
+

+ {provider.name} +

+

+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]} +

+
+ handleToggleProvider(provider, checked)} + /> +
+ + {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( + +
+ {editingProvider === provider.name ? ( + { + if (e.key === 'Enter') { + handleUpdateBaseUrl(provider, e.currentTarget.value); + } else if (e.key === 'Escape') { + setEditingProvider(null); + } + }} + onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)} + autoFocus + /> + ) : ( +
setEditingProvider(provider.name)} + > +
+
+ + {provider.settings.baseUrl || 'Click to set base URL'} + +
+
+ )} +
+ + {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && ( +
+
+
+ Environment URL set in .env file +
+
+ )} + + )} +
+
+ + + + {provider.name === 'Ollama' && provider.settings.enabled && ( + setShowOllamaUpdater(true)} + className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ Update Models + + )} + + ))} +
+
+ + + +
+ +
+
+
+
+ ); +}; + +export default LocalProvidersTab; diff --git a/app/components/settings/providers/ProvidersTab.tsx b/app/components/settings/providers/ProvidersTab.tsx deleted file mode 100644 index 1e2c572..0000000 --- a/app/components/settings/providers/ProvidersTab.tsx +++ /dev/null @@ -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 = { - 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> = { - 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(null); - const [filteredProviders, setFilteredProviders] = useState([]); - const [categoryEnabled, setCategoryEnabled] = useState({ - cloud: false, - local: false, - }); - const [showOllamaUpdater, setShowOllamaUpdater] = useState(false); - - // Group providers by category - const groupedProviders = useMemo(() => { - const groups: Record = { - 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 ( -
- {Object.entries(groupedProviders).map(([category, group]) => ( - -
-
-
-
-
-
-

{group.title}

-

{group.description}

-
-
- -
- - Enable All {category === 'cloud' ? 'Cloud' : 'Local'} - - handleToggleCategory(category as ProviderCategory, checked)} - /> -
-
- -
- {group.providers.map((provider, index) => ( - -
- {LOCAL_PROVIDERS.includes(provider.name) && ( - - Local - - )} - {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( - - Configurable - - )} -
- -
- -
- {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, { - className: 'w-full h-full', - 'aria-label': `${provider.name} logo`, - })} -
-
- -
-
-
-

- {provider.name} -

-

- {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')} -

-
- handleToggleProvider(provider, checked)} - /> -
- - {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( - -
- {editingProvider === provider.name ? ( - { - if (e.key === 'Enter') { - handleUpdateBaseUrl(provider, e.currentTarget.value); - } else if (e.key === 'Escape') { - setEditingProvider(null); - } - }} - onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)} - autoFocus - /> - ) : ( -
setEditingProvider(provider.name)} - > -
-
- - {provider.settings.baseUrl || 'Click to set base URL'} - -
-
- )} -
- - {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && ( -
-
-
- Environment URL set in .env file -
-
- )} - - )} -
-
- - - - {provider.name === 'Ollama' && provider.settings.enabled && ( - setShowOllamaUpdater(true)} - className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > -
- Update Models - - )} - - - -
- -
-
-
- - ))} -
- - {category === 'cloud' && } -
- ))} -
- ); -}; diff --git a/app/components/settings/settings.types.ts b/app/components/settings/settings.types.ts index 6f684cb..b72c163 100644 --- a/app/components/settings/settings.types.ts +++ b/app/components/settings/settings.types.ts @@ -8,11 +8,13 @@ export type TabType = | 'notifications' | 'features' | 'data' - | 'providers' + | 'cloud-providers' + | 'local-providers' | 'connection' | 'debug' | 'event-logs' - | 'update'; + | 'update' + | 'task-manager'; export type WindowType = 'user' | 'developer'; @@ -59,27 +61,31 @@ export const TAB_LABELS: Record = { notifications: 'Notifications', features: 'Features', data: 'Data', - providers: 'Providers', + 'cloud-providers': 'Cloud Providers', + 'local-providers': 'Local Providers', connection: 'Connection', debug: 'Debug', 'event-logs': 'Event Logs', update: 'Update', + 'task-manager': 'Task Manager', }; export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [ // User Window Tabs (Visible by default) { id: 'features', visible: true, window: 'user', order: 0 }, { id: 'data', visible: true, window: 'user', order: 1 }, - { id: 'providers', visible: true, window: 'user', order: 2 }, - { id: 'connection', visible: true, window: 'user', order: 3 }, - { id: 'debug', visible: true, window: 'user', order: 4 }, + { id: 'cloud-providers', visible: true, window: 'user', order: 2 }, + { id: 'local-providers', visible: true, window: 'user', order: 3 }, + { id: 'connection', visible: true, window: 'user', order: 4 }, + { id: 'debug', visible: true, window: 'user', order: 5 }, // User Window Tabs (Hidden by default) - { id: 'profile', visible: false, window: 'user', order: 5 }, - { id: 'settings', visible: false, window: 'user', order: 6 }, - { id: 'notifications', visible: false, window: 'user', order: 7 }, - { id: 'event-logs', visible: false, window: 'user', order: 8 }, - { id: 'update', visible: false, window: 'user', order: 9 }, + { id: 'profile', visible: false, window: 'user', order: 6 }, + { id: 'settings', visible: false, window: 'user', order: 7 }, + { id: 'notifications', visible: false, window: 'user', order: 8 }, + { id: 'event-logs', 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) { 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: 'features', visible: true, window: 'developer', order: 3 }, { id: 'data', visible: true, window: 'developer', order: 4 }, - { id: 'providers', visible: true, window: 'developer', order: 5 }, - { id: 'connection', visible: true, window: 'developer', order: 6 }, - { id: 'debug', visible: true, window: 'developer', order: 7 }, - { id: 'event-logs', visible: true, window: 'developer', order: 8 }, - { id: 'update', visible: true, window: 'developer', order: 9 }, + { id: 'cloud-providers', visible: true, window: 'developer', order: 5 }, + { id: 'local-providers', visible: true, window: 'developer', order: 6 }, + { id: 'connection', visible: true, window: 'developer', order: 7 }, + { id: 'debug', visible: true, window: 'developer', order: 8 }, + { 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 = { diff --git a/app/components/settings/shared/TabTile.tsx b/app/components/settings/shared/TabTile.tsx index 8512faa..f4358cb 100644 --- a/app/components/settings/shared/TabTile.tsx +++ b/app/components/settings/shared/TabTile.tsx @@ -15,6 +15,9 @@ const TAB_ICONS = { debug: 'i-ph:bug', 'event-logs': 'i-ph:list-bullets', update: 'i-ph:arrow-clockwise', + 'task-manager': 'i-ph:activity', + 'cloud-providers': 'i-ph:cloud', + 'local-providers': 'i-ph:desktop', }; interface TabTileProps { diff --git a/app/components/settings/task-manager/TaskManagerTab.tsx b/app/components/settings/task-manager/TaskManagerTab.tsx new file mode 100644 index 0000000..61d85cf --- /dev/null +++ b/app/components/settings/task-manager/TaskManagerTab.tsx @@ -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; + } + 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([]); + const [metrics, setMetrics] = useState({ + cpu: 0, + memory: { used: 0, total: 0, percentage: 0 }, + activeProcesses: 0, + uptime: 0, + network: { downlink: 0, latency: 0, type: 'unknown' }, + }); + const [metricsHistory, setMetricsHistory] = useState({ + 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({ + updatesReduced: 0, + timeInSaverMode: 0, + estimatedEnergySaved: 0, + }); + + const saverModeStartTime = useRef(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 ( +
+ +
+ ); + }; + + 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 ( +
+ {/* System Overview */} +
+
+

System Overview

+
+
+ setAutoEnergySaver(e.target.checked)} + className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700" + /> + +
+
+ !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" + /> + +
+
+
+ +
+
+
+
+ CPU Usage +
+

{Math.round(metrics.cpu)}%

+ {renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')} +
+ +
+
+
+ Memory Usage +
+

+ {metrics.memory.used}MB / {metrics.memory.total}MB +

+ {renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')} +
+ +
+
+
+ Battery +
+ {metrics.battery ? ( +
+

+ {Math.round(metrics.battery.level)}% + {metrics.battery.charging && ( + +

+ + )} +

+ {metrics.battery.timeRemaining && metrics.battery.timeRemaining !== Infinity && ( +

+ {metrics.battery.charging ? 'Full in: ' : 'Remaining: '} + {Math.round(metrics.battery.timeRemaining / 60)}m +

+ )} + {renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')} +
+ ) : ( +

Not available

+ )} +
+ +
+
+
+ Network +
+

{metrics.network.downlink} Mbps

+

Latency: {metrics.network.latency}ms

+ {renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')} +
+
+
+ + {/* Process List */} +
+
+
+
+

Active Processes

+
+ +
+ +
+ + + + + + + + + + + + + + {processes.map((process, index) => ( + + + + + + + + + + ))} + +
ProcessTypeCPUMemoryStatusImpact + Last Update +
+
+
+ {process.name} +
+
+ {process.type} + + + {process.cpuUsage.toFixed(1)}% + + + + {process.memoryUsage.toFixed(1)} MB + + +
+
+ {process.status} +
+
+ {process.impact} + + + {new Date(process.lastUpdate).toLocaleTimeString()} + +
+
+
+ + {/* Energy Savings */} +
+

Energy Savings

+
+
+
+
+ Time in Saver Mode +
+

+ {Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s +

+
+ +
+
+
+ Updates Reduced +
+

{energySavings.updatesReduced}

+
+ +
+
+
+ Estimated Energy Saved +
+

+ {energySavings.estimatedEnergySaved.toFixed(2)} mWh +

+
+
+
+
+ ); +} diff --git a/app/components/settings/update/UpdateTab.tsx b/app/components/settings/update/UpdateTab.tsx index db811fb..afa9532 100644 --- a/app/components/settings/update/UpdateTab.tsx +++ b/app/components/settings/update/UpdateTab.tsx @@ -4,6 +4,7 @@ import { useSettings } from '~/lib/hooks/useSettings'; import { logStore } from '~/lib/stores/logs'; import { classNames } from '~/utils/classNames'; import { toast } from 'react-toastify'; +import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog'; interface GitHubCommitResponse { sha: string; @@ -181,6 +182,9 @@ const UpdateTab = () => { checkInterval: 24, }; }); + const [lastChecked, setLastChecked] = useState(null); + const [showUpdateDialog, setShowUpdateDialog] = useState(false); + const [updateChangelog, setUpdateChangelog] = useState([]); useEffect(() => { localStorage.setItem('update_settings', JSON.stringify(updateSettings)); @@ -212,10 +216,17 @@ const UpdateTab = () => { }; const checkForUpdates = async () => { + console.log('Starting update check...'); setIsChecking(true); setError(null); + setLastChecked(new Date()); + + // Add a minimum delay of 2 seconds to show the spinning animation + const startTime = Date.now(); try { + console.log('Fetching update info...'); + const githubToken = localStorage.getItem('github_connection'); const headers: HeadersInit = {}; @@ -226,6 +237,14 @@ const UpdateTab = () => { const branchToCheck = isLatestBranch ? 'main' : 'stable'; 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); if (info.hasUpdate) { @@ -248,25 +267,18 @@ const UpdateTab = () => { }); if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) { - const changelogText = info.changelog?.join('\n') || 'No changelog available'; - const userWantsUpdate = confirm( - `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'); - } + setUpdateChangelog(info.changelog || ['No changelog available']); + setShowUpdateDialog(true); } } } } catch (err) { + console.error('Detailed update check error:', err); setError('Failed to check for updates. Please try again later.'); console.error('Update check failed:', err); setUpdateFailed(true); } finally { + console.log('Update check completed'); setIsChecking(false); } }; @@ -483,9 +495,10 @@ const UpdateTab = () => { onClick={() => { setHasUserRespondedToUpdate(false); setUpdateFailed(false); + setError(null); checkForUpdates(); }} - disabled={isChecking || (updateFailed && !hasUserRespondedToUpdate)} + disabled={isChecking} className={classNames( 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm', 'bg-[#F5F5F5] dark:bg-[#1A1A1A]', @@ -538,6 +551,14 @@ const UpdateTab = () => {
)} + {lastChecked && ( +
+ + Last checked: {lastChecked.toLocaleString()} + + {error && {error}} +
+ )} {/* Update Details Card */} @@ -756,6 +777,66 @@ const UpdateTab = () => {
)} + + {/* Update Confirmation Dialog */} + + { + setShowUpdateDialog(false); + setHasUserRespondedToUpdate(true); + logStore.logSystem('Update cancelled by user'); + }} + > +
+ Update Available + + A new version is available. Would you like to update now? + + +
+

Changelog:

+
+
+ {updateChangelog.map((log, index) => ( +
+ {log} +
+ ))} +
+
+
+ +
+ { + setShowUpdateDialog(false); + setHasUserRespondedToUpdate(true); + logStore.logSystem('Update cancelled by user'); + }} + > + Cancel + + { + setShowUpdateDialog(false); + setHasUserRespondedToUpdate(true); + await initiateUpdate(); + }} + > + Update Now + +
+
+
+
); }; diff --git a/app/components/settings/user/UsersWindow.tsx b/app/components/settings/user/UsersWindow.tsx index f2e3331..6f31b3e 100644 --- a/app/components/settings/user/UsersWindow.tsx +++ b/app/components/settings/user/UsersWindow.tsx @@ -18,7 +18,6 @@ import SettingsTab from '~/components/settings/settings/SettingsTab'; import NotificationsTab from '~/components/settings/notifications/NotificationsTab'; import FeaturesTab from '~/components/settings/features/FeaturesTab'; import DataTab from '~/components/settings/data/DataTab'; -import { ProvidersTab } from '~/components/settings/providers/ProvidersTab'; import DebugTab from '~/components/settings/debug/DebugTab'; import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab'; import UpdateTab from '~/components/settings/update/UpdateTab'; @@ -28,6 +27,9 @@ import { useFeatures } from '~/lib/hooks/useFeatures'; import { useNotifications } from '~/lib/hooks/useNotifications'; import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus'; import { useDebugStatus } from '~/lib/hooks/useDebugStatus'; +import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab'; +import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab'; +import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab'; interface DraggableTabTileProps { tab: TabVisibilityConfig; @@ -47,11 +49,13 @@ const TAB_DESCRIPTIONS: Record = { notifications: 'View and manage your notifications', features: 'Explore new and upcoming features', 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', debug: 'Debug tools and system information', 'event-logs': 'View system events and logs', update: 'Check for updates and release notes', + 'task-manager': 'Monitor system resources and processes', }; const DraggableTabTile = ({ @@ -209,8 +213,10 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { return ; case 'data': return ; - case 'providers': - return ; + case 'cloud-providers': + return ; + case 'local-providers': + return ; case 'connection': return ; case 'debug': @@ -219,6 +225,8 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { return ; case 'update': return ; + case 'task-manager': + return ; default: return null; } diff --git a/changelogUI.md b/changelogUI.md index 5d2a017..012bcf4 100644 --- a/changelogUI.md +++ b/changelogUI.md @@ -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 - **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 @@ -51,34 +53,59 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l - Storage settings - Backup and restore options -6. **Providers** +6. **Cloud Providers** - - AI provider configuration - - Model selection and management - - API settings + - Configure cloud-based AI providers + - API key management + - 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 - Connection health metrics - Troubleshooting tools + - Latency tracking + - Auto-reconnect settings -8. **Debug** +10. **Debug** - - System diagnostics - - Performance monitoring - - Error tracking + - System diagnostics + - Performance monitoring + - Error tracking + - Provider status checks + - System information -9. **Event Logs** +11. **Event Logs** - - Comprehensive system logs - - Filtered log views - - Log management tools + - Comprehensive system logs + - Filtered log views + - Log management tools + - Error tracking + - Performance metrics -10. **Update** +12. **Update** - Version management - Update notifications - Release notes + - Auto-update configuration #### 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 - 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 @@ -100,23 +129,27 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l - 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 @@ -125,17 +158,20 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l - 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 @@ -144,6 +180,9 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l - [ ] More developer tools - [ ] Extended API integrations - [ ] Advanced monitoring capabilities +- [ ] Custom provider plugins +- [ ] Enhanced resource management +- [ ] Advanced debugging features ## 🔧 Technical Details @@ -164,6 +203,7 @@ The Bolt DIY interface has been completely redesigned with a modern, intuitive l - Optimized bundle size - Efficient state updates - Minimal re-renders +- Resource-aware operations ## 📝 Contributing diff --git a/package.json b/package.json index e8b4003..8a2d24e 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@xterm/xterm": "^5.5.0", "ai": "^4.0.13", "chalk": "^5.4.1", + "chart.js": "^4.4.7", "clsx": "^2.1.1", "date-fns": "^3.6.0", "diff": "^5.2.0", @@ -93,6 +94,7 @@ "next": "^15.1.5", "ollama-ai-provider": "^0.15.2", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9edd758..c3b6c4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: chalk: specifier: ^5.4.1 version: 5.4.1 + chart.js: + specifier: ^4.4.7 + version: 4.4.7 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -200,6 +203,9 @@ importers: react: specifier: ^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: specifier: ^16.0.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': resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@lezer/common@1.2.3': resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} @@ -3213,6 +3222,10 @@ packages: character-reference-invalid@2.0.1: 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: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -5257,6 +5270,12 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} 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: resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==} @@ -7901,6 +7920,8 @@ snapshots: '@jspm/core@2.0.1': {} + '@kurkle/color@0.3.4': {} + '@lezer/common@1.2.3': {} '@lezer/cpp@1.1.2': @@ -9839,6 +9860,10 @@ snapshots: character-reference-invalid@2.0.1: {} + chart.js@4.4.7: + dependencies: + '@kurkle/color': 0.3.4 + check-error@2.1.1: {} chokidar@3.6.0: @@ -12424,6 +12449,11 @@ snapshots: iconv-lite: 0.4.24 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: dependencies: dnd-core: 16.0.1