diff --git a/app/components/@settings/tabs/event-logs/EventLogsTab.tsx b/app/components/@settings/tabs/event-logs/EventLogsTab.tsx
new file mode 100644
index 0000000000..c414d6cf2a
--- /dev/null
+++ b/app/components/@settings/tabs/event-logs/EventLogsTab.tsx
@@ -0,0 +1,613 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { motion } from 'framer-motion';
+import { Switch } from '~/components/ui/Switch';
+import { logStore, type LogEntry } from '~/lib/stores/logs';
+import { useStore } from '@nanostores/react';
+import { classNames } from '~/utils/classNames';
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
+
+interface SelectOption {
+ value: string;
+ label: string;
+ icon?: string;
+ color?: string;
+}
+
+const logLevelOptions: SelectOption[] = [
+ {
+ value: 'all',
+ label: 'All Types',
+ icon: 'i-ph:funnel',
+ color: '#9333ea',
+ },
+ {
+ value: 'provider',
+ label: 'LLM',
+ icon: 'i-ph:robot',
+ color: '#10b981',
+ },
+ {
+ value: 'api',
+ label: 'API',
+ icon: 'i-ph:cloud',
+ color: '#3b82f6',
+ },
+ {
+ value: 'error',
+ label: 'Errors',
+ icon: 'i-ph:warning-circle',
+ color: '#ef4444',
+ },
+ {
+ value: 'warning',
+ label: 'Warnings',
+ icon: 'i-ph:warning',
+ color: '#f59e0b',
+ },
+ {
+ value: 'info',
+ label: 'Info',
+ icon: 'i-ph:info',
+ color: '#3b82f6',
+ },
+ {
+ value: 'debug',
+ label: 'Debug',
+ icon: 'i-ph:bug',
+ color: '#6b7280',
+ },
+];
+
+interface LogEntryItemProps {
+ log: LogEntry;
+ isExpanded: boolean;
+ use24Hour: boolean;
+ showTimestamp: boolean;
+}
+
+const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp }: LogEntryItemProps) => {
+ const [localExpanded, setLocalExpanded] = useState(forceExpanded);
+
+ useEffect(() => {
+ setLocalExpanded(forceExpanded);
+ }, [forceExpanded]);
+
+ const timestamp = useMemo(() => {
+ const date = new Date(log.timestamp);
+ return date.toLocaleTimeString('en-US', { hour12: !use24Hour });
+ }, [log.timestamp, use24Hour]);
+
+ const style = useMemo(() => {
+ if (log.category === 'provider') {
+ return {
+ icon: 'i-ph:robot',
+ color: 'text-emerald-500 dark:text-emerald-400',
+ bg: 'hover:bg-emerald-500/10 dark:hover:bg-emerald-500/20',
+ badge: 'text-emerald-500 bg-emerald-50 dark:bg-emerald-500/10',
+ };
+ }
+
+ if (log.category === 'api') {
+ return {
+ icon: 'i-ph:cloud',
+ color: 'text-blue-500 dark:text-blue-400',
+ bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
+ badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
+ };
+ }
+
+ switch (log.level) {
+ case 'error':
+ return {
+ icon: 'i-ph:warning-circle',
+ color: 'text-red-500 dark:text-red-400',
+ bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20',
+ badge: 'text-red-500 bg-red-50 dark:bg-red-500/10',
+ };
+ case 'warning':
+ return {
+ icon: 'i-ph:warning',
+ color: 'text-yellow-500 dark:text-yellow-400',
+ bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
+ badge: 'text-yellow-500 bg-yellow-50 dark:bg-yellow-500/10',
+ };
+ case 'debug':
+ return {
+ icon: 'i-ph:bug',
+ color: 'text-gray-500 dark:text-gray-400',
+ bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20',
+ badge: 'text-gray-500 bg-gray-50 dark:bg-gray-500/10',
+ };
+ default:
+ return {
+ icon: 'i-ph:info',
+ color: 'text-blue-500 dark:text-blue-400',
+ bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
+ badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
+ };
+ }
+ }, [log.level, log.category]);
+
+ const renderDetails = (details: any) => {
+ if (log.category === 'provider') {
+ return (
+
+
+ Model: {details.model}
+ •
+ Tokens: {details.totalTokens}
+ •
+ Duration: {details.duration}ms
+
+ {details.prompt && (
+
+
Prompt:
+
+ {details.prompt}
+
+
+ )}
+ {details.response && (
+
+
Response:
+
+ {details.response}
+
+
+ )}
+
+ );
+ }
+
+ if (log.category === 'api') {
+ return (
+
+
+ {details.method}
+ •
+ Status: {details.statusCode}
+ •
+ Duration: {details.duration}ms
+
+
{details.url}
+ {details.request && (
+
+
Request:
+
+ {JSON.stringify(details.request, null, 2)}
+
+
+ )}
+ {details.response && (
+
+
Response:
+
+ {JSON.stringify(details.response, null, 2)}
+
+
+ )}
+ {details.error && (
+
+
Error:
+
+ {JSON.stringify(details.error, null, 2)}
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {JSON.stringify(details, null, 2)}
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
{log.message}
+ {log.details && (
+ <>
+
setLocalExpanded(!localExpanded)}
+ className="text-xs text-gray-500 dark:text-gray-400 hover:text-purple-500 dark:hover:text-purple-400 transition-colors"
+ >
+ {localExpanded ? 'Hide' : 'Show'} Details
+
+ {localExpanded && renderDetails(log.details)}
+ >
+ )}
+
+
+ {log.level}
+
+ {log.category && (
+
+ {log.category}
+
+ )}
+
+
+
+ {showTimestamp &&
{timestamp} }
+
+
+ );
+};
+
+export function EventLogsTab() {
+ const logs = useStore(logStore.logs);
+ const [selectedLevel, setSelectedLevel] = useState<'all' | string>('all');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [use24Hour, setUse24Hour] = useState(false);
+ const [autoExpand, setAutoExpand] = useState(false);
+ const [showTimestamps, setShowTimestamps] = useState(true);
+ const [showLevelFilter, setShowLevelFilter] = useState(false);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const levelFilterRef = useRef
(null);
+
+ const filteredLogs = useMemo(() => {
+ const allLogs = Object.values(logs);
+
+ if (selectedLevel === 'all') {
+ return allLogs.filter((log) =>
+ searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true,
+ );
+ }
+
+ return allLogs.filter((log) => {
+ const matchesType = log.category === selectedLevel || log.level === selectedLevel;
+ const matchesSearch = searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true;
+
+ return matchesType && matchesSearch;
+ });
+ }, [logs, selectedLevel, searchQuery]);
+
+ // Add performance tracking on mount
+ useEffect(() => {
+ const startTime = performance.now();
+
+ logStore.logInfo('Event Logs tab mounted', {
+ type: 'component_mount',
+ message: 'Event Logs tab component mounted',
+ component: 'EventLogsTab',
+ });
+
+ return () => {
+ const duration = performance.now() - startTime;
+ logStore.logPerformanceMetric('EventLogsTab', 'mount-duration', duration);
+ };
+ }, []);
+
+ // Log filter changes
+ const handleLevelFilterChange = useCallback(
+ (newLevel: string) => {
+ logStore.logInfo('Log level filter changed', {
+ type: 'filter_change',
+ message: `Log level filter changed from ${selectedLevel} to ${newLevel}`,
+ component: 'EventLogsTab',
+ previousLevel: selectedLevel,
+ newLevel,
+ });
+ setSelectedLevel(newLevel as string);
+ setShowLevelFilter(false);
+ },
+ [selectedLevel],
+ );
+
+ // Log search changes with debounce
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ if (searchQuery) {
+ logStore.logInfo('Log search performed', {
+ type: 'search',
+ message: `Search performed with query "${searchQuery}" (${filteredLogs.length} results)`,
+ component: 'EventLogsTab',
+ query: searchQuery,
+ resultsCount: filteredLogs.length,
+ });
+ }
+ }, 1000);
+
+ return () => clearTimeout(timeoutId);
+ }, [searchQuery, filteredLogs.length]);
+
+ // Enhanced export logs handler
+ const handleExportLogs = useCallback(() => {
+ const startTime = performance.now();
+
+ try {
+ const exportData = {
+ timestamp: new Date().toISOString(),
+ logs: filteredLogs,
+ filters: {
+ level: selectedLevel,
+ searchQuery,
+ },
+ };
+
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `bolt-logs-${new Date().toISOString()}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ const duration = performance.now() - startTime;
+ logStore.logSuccess('Logs exported successfully', {
+ type: 'export',
+ message: `Successfully exported ${filteredLogs.length} logs`,
+ component: 'EventLogsTab',
+ exportedCount: filteredLogs.length,
+ filters: {
+ level: selectedLevel,
+ searchQuery,
+ },
+ duration,
+ });
+ } catch (error) {
+ logStore.logError('Failed to export logs', error, {
+ type: 'export_error',
+ message: 'Failed to export logs',
+ component: 'EventLogsTab',
+ });
+ }
+ }, [filteredLogs, selectedLevel, searchQuery]);
+
+ // Enhanced refresh handler
+ const handleRefresh = useCallback(async () => {
+ const startTime = performance.now();
+ setIsRefreshing(true);
+
+ try {
+ await logStore.refreshLogs();
+
+ const duration = performance.now() - startTime;
+
+ logStore.logSuccess('Logs refreshed successfully', {
+ type: 'refresh',
+ message: `Successfully refreshed ${Object.keys(logs).length} logs`,
+ component: 'EventLogsTab',
+ duration,
+ logsCount: Object.keys(logs).length,
+ });
+ } catch (error) {
+ logStore.logError('Failed to refresh logs', error, {
+ type: 'refresh_error',
+ message: 'Failed to refresh logs',
+ component: 'EventLogsTab',
+ });
+ } finally {
+ setTimeout(() => setIsRefreshing(false), 500);
+ }
+ }, [logs]);
+
+ // Log preference changes
+ const handlePreferenceChange = useCallback((type: string, value: boolean) => {
+ logStore.logInfo('Log preference changed', {
+ type: 'preference_change',
+ message: `Log preference "${type}" changed to ${value}`,
+ component: 'EventLogsTab',
+ preference: type,
+ value,
+ });
+
+ switch (type) {
+ case 'timestamps':
+ setShowTimestamps(value);
+ break;
+ case '24hour':
+ setUse24Hour(value);
+ break;
+ case 'autoExpand':
+ setAutoExpand(value);
+ break;
+ }
+ }, []);
+
+ // Close filters when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (levelFilterRef.current && !levelFilterRef.current.contains(event.target as Node)) {
+ setShowLevelFilter(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
+
+ return (
+
+
+
+
+
+
+ {selectedLevelOption?.label || 'All Types'}
+
+
+
+
+
+
+ {logLevelOptions.map((option) => (
+ handleLevelFilterChange(option.value)}
+ >
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+
+ handlePreferenceChange('timestamps', value)}
+ className="data-[state=checked]:bg-purple-500"
+ />
+ Show Timestamps
+
+
+
+ handlePreferenceChange('24hour', value)}
+ className="data-[state=checked]:bg-purple-500"
+ />
+ 24h Time
+
+
+
+ handlePreferenceChange('autoExpand', value)}
+ className="data-[state=checked]:bg-purple-500"
+ />
+ Auto Expand
+
+
+
+
+
+
+ Refresh
+
+
+
+
+ Export
+
+
+
+
+
+
+
setSearchQuery(e.target.value)}
+ className={classNames(
+ 'w-full px-4 py-2 pl-10 rounded-lg',
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
+ 'text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400',
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500',
+ 'transition-all duration-200',
+ )}
+ />
+
+
+
+ {filteredLogs.length === 0 ? (
+
+
+
+
No Logs Found
+
Try adjusting your search or filters
+
+
+ ) : (
+ filteredLogs.map((log) => (
+
+ ))
+ )}
+
+
+ );
+}
diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/@settings/tabs/features/FeaturesTab.tsx
similarity index 84%
rename from app/components/settings/features/FeaturesTab.tsx
rename to app/components/@settings/tabs/features/FeaturesTab.tsx
index 2cf735d64b..9aa838c4ce 100644
--- a/app/components/settings/features/FeaturesTab.tsx
+++ b/app/components/@settings/tabs/features/FeaturesTab.tsx
@@ -111,44 +111,66 @@ export default function FeaturesTab() {
isLatestBranch,
contextOptimizationEnabled,
eventLogs,
- isLocalModel,
setAutoSelectTemplate,
enableLatestBranch,
enableContextOptimization,
setEventLogs,
- enableLocalModels,
setPromptId,
promptId,
} = useSettings();
+ // Enable features by default on first load
+ React.useEffect(() => {
+ // Only enable if they haven't been explicitly set before
+ if (isLatestBranch === undefined) {
+ enableLatestBranch(true);
+ }
+
+ if (contextOptimizationEnabled === undefined) {
+ enableContextOptimization(true);
+ }
+
+ if (autoSelectTemplate === undefined) {
+ setAutoSelectTemplate(true);
+ }
+
+ if (eventLogs === undefined) {
+ setEventLogs(true);
+ }
+ }, []); // Only run once on component mount
+
const handleToggleFeature = useCallback(
(id: string, enabled: boolean) => {
switch (id) {
- case 'latestBranch':
+ case 'latestBranch': {
enableLatestBranch(enabled);
toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
break;
- case 'autoSelectTemplate':
+ }
+
+ case 'autoSelectTemplate': {
setAutoSelectTemplate(enabled);
toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
break;
- case 'contextOptimization':
+ }
+
+ case 'contextOptimization': {
enableContextOptimization(enabled);
toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
break;
- case 'eventLogs':
+ }
+
+ case 'eventLogs': {
setEventLogs(enabled);
toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
break;
- case 'localModels':
- enableLocalModels(enabled);
- toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
- break;
+ }
+
default:
break;
}
},
- [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs, enableLocalModels],
+ [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs],
);
const features = {
@@ -159,7 +181,7 @@ export default function FeaturesTab() {
description: 'Get the latest updates from the main branch',
icon: 'i-ph:git-branch',
enabled: isLatestBranch,
- tooltip: 'Enable to receive updates from the main development branch',
+ tooltip: 'Enabled by default to receive updates from the main development branch',
},
{
id: 'autoSelectTemplate',
@@ -167,7 +189,7 @@ export default function FeaturesTab() {
description: 'Automatically select starter template',
icon: 'i-ph:selection',
enabled: autoSelectTemplate,
- tooltip: 'Automatically select the most appropriate starter template',
+ tooltip: 'Enabled by default to automatically select the most appropriate starter template',
},
{
id: 'contextOptimization',
@@ -175,7 +197,7 @@ export default function FeaturesTab() {
description: 'Optimize context for better responses',
icon: 'i-ph:brain',
enabled: contextOptimizationEnabled,
- tooltip: 'Enable context optimization for improved AI responses',
+ tooltip: 'Enabled by default for improved AI responses',
},
{
id: 'eventLogs',
@@ -183,30 +205,19 @@ export default function FeaturesTab() {
description: 'Enable detailed event logging and history',
icon: 'i-ph:list-bullets',
enabled: eventLogs,
- tooltip: 'Record detailed logs of system events and user actions',
+ tooltip: 'Enabled by default to record detailed logs of system events and user actions',
},
],
beta: [],
- experimental: [
- {
- id: 'localModels',
- title: 'Experimental Providers',
- description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
- icon: 'i-ph:robot',
- enabled: isLocalModel,
- experimental: true,
- tooltip: 'Try out new AI providers and models in development',
- },
- ],
};
return (
@@ -220,16 +231,6 @@ export default function FeaturesTab() {
/>
)}
- {features.experimental.length > 0 && (
-
- )}
-
) => {
+ const file = e.target.files?.[0];
+
+ if (!file) {
+ return;
+ }
+
+ try {
+ setIsUploading(true);
+
+ // Convert the file to base64
+ const reader = new FileReader();
+
+ reader.onloadend = () => {
+ const base64String = reader.result as string;
+ updateProfile({ avatar: base64String });
+ setIsUploading(false);
+ toast.success('Profile picture updated');
+ };
+
+ reader.onerror = () => {
+ console.error('Error reading file:', reader.error);
+ setIsUploading(false);
+ toast.error('Failed to update profile picture');
+ };
+ reader.readAsDataURL(file);
+ } catch (error) {
+ console.error('Error uploading avatar:', error);
+ setIsUploading(false);
+ toast.error('Failed to update profile picture');
+ }
+ };
+
+ const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
+ updateProfile({ [field]: value });
+
+ // Only show toast for completed typing (after 1 second of no typing)
+ const debounceToast = setTimeout(() => {
+ toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
+ }, 1000);
+
+ return () => clearTimeout(debounceToast);
+ };
+
+ return (
+
+
+ {/* Personal Information Section */}
+
+ {/* Avatar Upload */}
+
+
+ {profile.avatar ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isUploading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Profile Picture
+
+
Upload a profile picture or avatar
+
+
+
+ {/* Username Input */}
+
+
Username
+
+
+
handleProfileUpdate('username', e.target.value)}
+ className={classNames(
+ 'w-full pl-11 pr-4 py-2.5 rounded-xl',
+ 'bg-white dark:bg-gray-800/50',
+ 'border border-gray-200 dark:border-gray-700/50',
+ 'text-gray-900 dark:text-white',
+ 'placeholder-gray-400 dark:placeholder-gray-500',
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50',
+ 'transition-all duration-300 ease-out',
+ )}
+ placeholder="Enter your username"
+ />
+
+
+
+ {/* Bio Input */}
+
+
+
+
+ );
+}
diff --git a/app/components/settings/providers/CloudProvidersTab.tsx b/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx
similarity index 100%
rename from app/components/settings/providers/CloudProvidersTab.tsx
rename to app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx
diff --git a/app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx b/app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx
new file mode 100644
index 0000000000..fc7630a308
--- /dev/null
+++ b/app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx
@@ -0,0 +1,718 @@
+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, AnimatePresence } from 'framer-motion';
+import { classNames } from '~/utils/classNames';
+import { BsRobot } from 'react-icons/bs';
+import type { IconType } from 'react-icons';
+import { BiChip } from 'react-icons/bi';
+import { TbBrandOpenai } from 'react-icons/tb';
+import { providerBaseUrlEnvKeys } from '~/utils/constants';
+import { useToast } from '~/components/ui/use-toast';
+import { Progress } from '~/components/ui/Progress';
+import OllamaModelInstaller from './OllamaModelInstaller';
+
+// 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: BsRobot,
+ LMStudio: BsRobot,
+ 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',
+};
+
+// Add a constant for the Ollama API base URL
+const OLLAMA_API_URL = 'http://127.0.0.1:11434';
+
+interface OllamaModel {
+ name: string;
+ digest: string;
+ size: number;
+ modified_at: string;
+ details?: {
+ family: string;
+ parameter_size: string;
+ quantization_level: string;
+ };
+ status?: 'idle' | 'updating' | 'updated' | 'error' | 'checking';
+ error?: string;
+ newDigest?: string;
+ progress?: {
+ current: number;
+ total: number;
+ status: string;
+ };
+}
+
+interface OllamaPullResponse {
+ status: string;
+ completed?: number;
+ total?: number;
+ digest?: string;
+}
+
+const isOllamaPullResponse = (data: unknown): data is OllamaPullResponse => {
+ return (
+ typeof data === 'object' &&
+ data !== null &&
+ 'status' in data &&
+ typeof (data as OllamaPullResponse).status === 'string'
+ );
+};
+
+export default function LocalProvidersTab() {
+ const { providers, updateProviderSettings } = useSettings();
+ const [filteredProviders, setFilteredProviders] = useState([]);
+ const [categoryEnabled, setCategoryEnabled] = useState(false);
+ const [ollamaModels, setOllamaModels] = useState([]);
+ const [isLoadingModels, setIsLoadingModels] = useState(false);
+ const [editingProvider, setEditingProvider] = useState(null);
+ const { toast } = useToast();
+
+ // Effect to filter and sort providers
+ useEffect(() => {
+ const newFilteredProviders = Object.entries(providers || {})
+ .filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key))
+ .map(([key, value]) => {
+ const provider = value as IProviderConfig;
+ const envKey = providerBaseUrlEnvKeys[key]?.baseUrlKey;
+
+ // Get environment URL safely
+ const envUrl = envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
+
+ console.log(`Checking env URL for ${key}:`, {
+ envKey,
+ envUrl,
+ currentBaseUrl: provider.settings.baseUrl,
+ });
+
+ // If there's an environment URL and no base URL set, update it
+ if (envUrl && !provider.settings.baseUrl) {
+ console.log(`Setting base URL for ${key} from env:`, envUrl);
+ updateProviderSettings(key, {
+ ...provider.settings,
+ baseUrl: envUrl,
+ });
+ }
+
+ return {
+ name: key,
+ settings: {
+ ...provider.settings,
+ baseUrl: provider.settings.baseUrl || envUrl,
+ },
+ staticModels: provider.staticModels || [],
+ getDynamicModels: provider.getDynamicModels,
+ getApiKeyLink: provider.getApiKeyLink,
+ labelForGetApiKey: provider.labelForGetApiKey,
+ icon: provider.icon,
+ } as IProviderConfig;
+ });
+
+ // Custom sort function to ensure LMStudio appears before OpenAILike
+ const sorted = newFilteredProviders.sort((a, b) => {
+ if (a.name === 'LMStudio') {
+ return -1;
+ }
+
+ if (b.name === 'LMStudio') {
+ return 1;
+ }
+
+ if (a.name === 'OpenAILike') {
+ return 1;
+ }
+
+ if (b.name === 'OpenAILike') {
+ return -1;
+ }
+
+ return a.name.localeCompare(b.name);
+ });
+ setFilteredProviders(sorted);
+ }, [providers, updateProviderSettings]);
+
+ // Add effect to update category toggle state based on provider states
+ useEffect(() => {
+ const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
+ setCategoryEnabled(newCategoryState);
+ }, [filteredProviders]);
+
+ // Fetch Ollama models when enabled
+ useEffect(() => {
+ const ollamaProvider = filteredProviders.find((p) => p.name === 'Ollama');
+
+ if (ollamaProvider?.settings.enabled) {
+ fetchOllamaModels();
+ }
+ }, [filteredProviders]);
+
+ const fetchOllamaModels = async () => {
+ try {
+ setIsLoadingModels(true);
+
+ const response = await fetch('http://127.0.0.1:11434/api/tags');
+ const data = (await response.json()) as { models: OllamaModel[] };
+
+ setOllamaModels(
+ data.models.map((model) => ({
+ ...model,
+ status: 'idle' as const,
+ })),
+ );
+ } catch (error) {
+ console.error('Error fetching Ollama models:', error);
+ } finally {
+ setIsLoadingModels(false);
+ }
+ };
+
+ const updateOllamaModel = async (modelName: string): Promise => {
+ try {
+ const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: modelName }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update ${modelName}`);
+ }
+
+ const reader = response.body?.getReader();
+
+ if (!reader) {
+ throw new Error('No response reader available');
+ }
+
+ while (true) {
+ const { done, value } = await reader.read();
+
+ if (done) {
+ break;
+ }
+
+ const text = new TextDecoder().decode(value);
+ const lines = text.split('\n').filter(Boolean);
+
+ for (const line of lines) {
+ const rawData = JSON.parse(line);
+
+ if (!isOllamaPullResponse(rawData)) {
+ console.error('Invalid response format:', rawData);
+ continue;
+ }
+
+ setOllamaModels((current) =>
+ current.map((m) =>
+ m.name === modelName
+ ? {
+ ...m,
+ progress: {
+ current: rawData.completed || 0,
+ total: rawData.total || 0,
+ status: rawData.status,
+ },
+ newDigest: rawData.digest,
+ }
+ : m,
+ ),
+ );
+ }
+ }
+
+ const updatedResponse = await fetch('http://127.0.0.1:11434/api/tags');
+ const updatedData = (await updatedResponse.json()) as { models: OllamaModel[] };
+ const updatedModel = updatedData.models.find((m) => m.name === modelName);
+
+ return updatedModel !== undefined;
+ } catch (error) {
+ console.error(`Error updating ${modelName}:`, error);
+ return false;
+ }
+ };
+
+ const handleToggleCategory = useCallback(
+ async (enabled: boolean) => {
+ filteredProviders.forEach((provider) => {
+ updateProviderSettings(provider.name, { ...provider.settings, enabled });
+ });
+ toast(enabled ? 'All local providers enabled' : 'All local providers disabled');
+ },
+ [filteredProviders, updateProviderSettings],
+ );
+
+ const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
+ updateProviderSettings(provider.name, {
+ ...provider.settings,
+ enabled,
+ });
+
+ if (enabled) {
+ logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
+ toast(`${provider.name} enabled`);
+ } else {
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
+ toast(`${provider.name} disabled`);
+ }
+ };
+
+ const handleUpdateBaseUrl = (provider: IProviderConfig, newBaseUrl: string) => {
+ updateProviderSettings(provider.name, {
+ ...provider.settings,
+ baseUrl: newBaseUrl,
+ });
+ toast(`${provider.name} base URL updated`);
+ setEditingProvider(null);
+ };
+
+ const handleUpdateOllamaModel = async (modelName: string) => {
+ const updateSuccess = await updateOllamaModel(modelName);
+
+ if (updateSuccess) {
+ toast(`Updated ${modelName}`);
+ } else {
+ toast(`Failed to update ${modelName}`);
+ }
+ };
+
+ const handleDeleteOllamaModel = async (modelName: string) => {
+ try {
+ const response = await fetch(`${OLLAMA_API_URL}/api/delete`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ name: modelName }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to delete ${modelName}`);
+ }
+
+ setOllamaModels((current) => current.filter((m) => m.name !== modelName));
+ toast(`Deleted ${modelName}`);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
+ console.error(`Error deleting ${modelName}:`, errorMessage);
+ toast(`Failed to delete ${modelName}`);
+ }
+ };
+
+ // Update model details display
+ const ModelDetails = ({ model }: { model: OllamaModel }) => (
+
+
+
+
{model.digest.substring(0, 7)}
+
+ {model.details && (
+ <>
+
+
+
{model.details.parameter_size}
+
+
+
+
{model.details.quantization_level}
+
+ >
+ )}
+
+ );
+
+ // Update model actions to not use Tooltip
+ const ModelActions = ({
+ model,
+ onUpdate,
+ onDelete,
+ }: {
+ model: OllamaModel;
+ onUpdate: () => void;
+ onDelete: () => void;
+ }) => (
+
+
+ {model.status === 'updating' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+
+ return (
+
+
+ {/* Header section */}
+
+
+
+
+
+
+
Local AI Models
+
Configure and manage your local AI providers
+
+
+
+
+ Enable All
+
+
+
+
+ {/* Ollama Section */}
+ {filteredProviders
+ .filter((provider) => provider.name === 'Ollama')
+ .map((provider) => (
+
+ {/* Provider Header */}
+
+
+
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
+ className: 'w-7 h-7',
+ 'aria-label': `${provider.name} icon`,
+ })}
+
+
+
+
{provider.name}
+ Local
+
+
+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
+
+
+
+
handleToggleProvider(provider, checked)}
+ aria-label={`Toggle ${provider.name} provider`}
+ />
+
+
+ {/* Ollama Models Section */}
+ {provider.settings.enabled && (
+
+
+
+ {isLoadingModels ? (
+
+ ) : (
+
+ {ollamaModels.length} models available
+
+ )}
+
+
+
+ {isLoadingModels ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : ollamaModels.length === 0 ? (
+
+
+
No models installed yet
+
Install your first model below
+
+ ) : (
+ ollamaModels.map((model) => (
+
+
+
+
handleUpdateOllamaModel(model.name)}
+ onDelete={() => {
+ if (window.confirm(`Are you sure you want to delete ${model.name}?`)) {
+ handleDeleteOllamaModel(model.name);
+ }
+ }}
+ />
+
+ {model.progress && (
+
+
+
+ {model.progress.status}
+ {Math.round((model.progress.current / model.progress.total) * 100)}%
+
+
+ )}
+
+ ))
+ )}
+
+
+ {/* Model Installation Section */}
+
+
+ )}
+
+ ))}
+
+ {/* Other Providers Section */}
+
+
Other Local Providers
+
+ {filteredProviders
+ .filter((provider) => provider.name !== 'Ollama')
+ .map((provider, index) => (
+
+ {/* Provider Header */}
+
+
+
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
+ className: 'w-7 h-7',
+ 'aria-label': `${provider.name} icon`,
+ })}
+
+
+
+
{provider.name}
+
+
+ Local
+
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
+
+ Configurable
+
+ )}
+
+
+
+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
+
+
+
+
handleToggleProvider(provider, checked)}
+ aria-label={`Toggle ${provider.name} provider`}
+ />
+
+
+ {/* URL Configuration Section */}
+
+ {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
+
+
+
API Endpoint
+ {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)}
+ className={classNames(
+ 'w-full px-3 py-2 rounded-lg text-sm cursor-pointer',
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
+ 'hover:border-purple-500/30 hover:bg-bolt-elements-background-depth-4',
+ 'transition-all duration-200',
+ )}
+ >
+
+
+
{provider.settings.baseUrl || 'Click to set base URL'}
+
+
+ )}
+
+
+ )}
+
+
+ ))}
+
+
+
+
+ );
+}
+
+// Helper component for model status badge
+function ModelStatusBadge({ status }: { status?: string }) {
+ if (!status || status === 'idle') {
+ return null;
+ }
+
+ const statusConfig = {
+ updating: { bg: 'bg-yellow-500/10', text: 'text-yellow-500', label: 'Updating' },
+ updated: { bg: 'bg-green-500/10', text: 'text-green-500', label: 'Updated' },
+ error: { bg: 'bg-red-500/10', text: 'text-red-500', label: 'Error' },
+ };
+
+ const config = statusConfig[status as keyof typeof statusConfig];
+
+ if (!config) {
+ return null;
+ }
+
+ return (
+
+ {config.label}
+
+ );
+}
diff --git a/app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx b/app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx
new file mode 100644
index 0000000000..b31bb74475
--- /dev/null
+++ b/app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx
@@ -0,0 +1,597 @@
+import React, { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+import { classNames } from '~/utils/classNames';
+import { Progress } from '~/components/ui/Progress';
+import { useToast } from '~/components/ui/use-toast';
+
+interface OllamaModelInstallerProps {
+ onModelInstalled: () => void;
+}
+
+interface InstallProgress {
+ status: string;
+ progress: number;
+ downloadedSize?: string;
+ totalSize?: string;
+ speed?: string;
+}
+
+interface ModelInfo {
+ name: string;
+ desc: string;
+ size: string;
+ tags: string[];
+ installedVersion?: string;
+ latestVersion?: string;
+ needsUpdate?: boolean;
+ status?: 'idle' | 'installing' | 'updating' | 'updated' | 'error';
+ details?: {
+ family: string;
+ parameter_size: string;
+ quantization_level: string;
+ };
+}
+
+const POPULAR_MODELS: ModelInfo[] = [
+ {
+ name: 'deepseek-coder:6.7b',
+ desc: "DeepSeek's code generation model",
+ size: '4.1GB',
+ tags: ['coding', 'popular'],
+ },
+ {
+ name: 'llama2:7b',
+ desc: "Meta's Llama 2 (7B parameters)",
+ size: '3.8GB',
+ tags: ['general', 'popular'],
+ },
+ {
+ name: 'mistral:7b',
+ desc: "Mistral's 7B model",
+ size: '4.1GB',
+ tags: ['general', 'popular'],
+ },
+ {
+ name: 'gemma:7b',
+ desc: "Google's Gemma model",
+ size: '4.0GB',
+ tags: ['general', 'new'],
+ },
+ {
+ name: 'codellama:7b',
+ desc: "Meta's Code Llama model",
+ size: '4.1GB',
+ tags: ['coding', 'popular'],
+ },
+ {
+ name: 'neural-chat:7b',
+ desc: "Intel's Neural Chat model",
+ size: '4.1GB',
+ tags: ['chat', 'popular'],
+ },
+ {
+ name: 'phi:latest',
+ desc: "Microsoft's Phi-2 model",
+ size: '2.7GB',
+ tags: ['small', 'fast'],
+ },
+ {
+ name: 'qwen:7b',
+ desc: "Alibaba's Qwen model",
+ size: '4.1GB',
+ tags: ['general'],
+ },
+ {
+ name: 'solar:10.7b',
+ desc: "Upstage's Solar model",
+ size: '6.1GB',
+ tags: ['large', 'powerful'],
+ },
+ {
+ name: 'openchat:7b',
+ desc: 'Open-source chat model',
+ size: '4.1GB',
+ tags: ['chat', 'popular'],
+ },
+ {
+ name: 'dolphin-phi:2.7b',
+ desc: 'Lightweight chat model',
+ size: '1.6GB',
+ tags: ['small', 'fast'],
+ },
+ {
+ name: 'stable-code:3b',
+ desc: 'Lightweight coding model',
+ size: '1.8GB',
+ tags: ['coding', 'small'],
+ },
+];
+
+function formatBytes(bytes: number): string {
+ if (bytes === 0) {
+ return '0 B';
+ }
+
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
+}
+
+function formatSpeed(bytesPerSecond: number): string {
+ return `${formatBytes(bytesPerSecond)}/s`;
+}
+
+// Add Ollama Icon SVG component
+function OllamaIcon({ className }: { className?: string }) {
+ return (
+
+
+
+ );
+}
+
+export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelInstallerProps) {
+ const [modelString, setModelString] = useState('');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isInstalling, setIsInstalling] = useState(false);
+ const [isChecking, setIsChecking] = useState(false);
+ const [installProgress, setInstallProgress] = useState(null);
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [models, setModels] = useState(POPULAR_MODELS);
+ const { toast } = useToast();
+
+ // Function to check installed models and their versions
+ const checkInstalledModels = async () => {
+ try {
+ const response = await fetch('http://127.0.0.1:11434/api/tags', {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch installed models');
+ }
+
+ const data = (await response.json()) as { models: Array<{ name: string; digest: string; latest: string }> };
+ const installedModels = data.models || [];
+
+ // Update models with installed versions
+ setModels((prevModels) =>
+ prevModels.map((model) => {
+ const installed = installedModels.find((m) => m.name.toLowerCase() === model.name.toLowerCase());
+
+ if (installed) {
+ return {
+ ...model,
+ installedVersion: installed.digest.substring(0, 8),
+ needsUpdate: installed.digest !== installed.latest,
+ latestVersion: installed.latest?.substring(0, 8),
+ };
+ }
+
+ return model;
+ }),
+ );
+ } catch (error) {
+ console.error('Error checking installed models:', error);
+ }
+ };
+
+ // Check installed models on mount and after installation
+ useEffect(() => {
+ checkInstalledModels();
+ }, []);
+
+ const handleCheckUpdates = async () => {
+ setIsChecking(true);
+
+ try {
+ await checkInstalledModels();
+ toast('Model versions checked');
+ } catch (err) {
+ console.error('Failed to check model versions:', err);
+ toast('Failed to check model versions');
+ } finally {
+ setIsChecking(false);
+ }
+ };
+
+ const filteredModels = models.filter((model) => {
+ const matchesSearch =
+ searchQuery === '' ||
+ model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ model.desc.toLowerCase().includes(searchQuery.toLowerCase());
+ const matchesTags = selectedTags.length === 0 || selectedTags.some((tag) => model.tags.includes(tag));
+
+ return matchesSearch && matchesTags;
+ });
+
+ const handleInstallModel = async (modelToInstall: string) => {
+ if (!modelToInstall) {
+ return;
+ }
+
+ try {
+ setIsInstalling(true);
+ setInstallProgress({
+ status: 'Starting download...',
+ progress: 0,
+ downloadedSize: '0 B',
+ totalSize: 'Calculating...',
+ speed: '0 B/s',
+ });
+ setModelString('');
+ setSearchQuery('');
+
+ const response = await fetch('http://127.0.0.1:11434/api/pull', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ name: modelToInstall }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const reader = response.body?.getReader();
+
+ if (!reader) {
+ throw new Error('Failed to get response reader');
+ }
+
+ let lastTime = Date.now();
+ let lastBytes = 0;
+
+ while (true) {
+ const { done, value } = await reader.read();
+
+ if (done) {
+ break;
+ }
+
+ const text = new TextDecoder().decode(value);
+ const lines = text.split('\n').filter(Boolean);
+
+ for (const line of lines) {
+ try {
+ const data = JSON.parse(line);
+
+ if ('status' in data) {
+ const currentTime = Date.now();
+ const timeDiff = (currentTime - lastTime) / 1000; // Convert to seconds
+ const bytesDiff = (data.completed || 0) - lastBytes;
+ const speed = bytesDiff / timeDiff;
+
+ setInstallProgress({
+ status: data.status,
+ progress: data.completed && data.total ? (data.completed / data.total) * 100 : 0,
+ downloadedSize: formatBytes(data.completed || 0),
+ totalSize: data.total ? formatBytes(data.total) : 'Calculating...',
+ speed: formatSpeed(speed),
+ });
+
+ lastTime = currentTime;
+ lastBytes = data.completed || 0;
+ }
+ } catch (err) {
+ console.error('Error parsing progress:', err);
+ }
+ }
+ }
+
+ toast('Successfully installed ' + modelToInstall + '. The model list will refresh automatically.');
+
+ // Ensure we call onModelInstalled after successful installation
+ setTimeout(() => {
+ onModelInstalled();
+ }, 1000);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
+ console.error(`Error installing ${modelToInstall}:`, errorMessage);
+ toast(`Failed to install ${modelToInstall}. ${errorMessage}`);
+ } finally {
+ setIsInstalling(false);
+ setInstallProgress(null);
+ }
+ };
+
+ const handleUpdateModel = async (modelToUpdate: string) => {
+ try {
+ setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'updating' } : m)));
+
+ const response = await fetch('http://127.0.0.1:11434/api/pull', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ name: modelToUpdate }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const reader = response.body?.getReader();
+
+ if (!reader) {
+ throw new Error('Failed to get response reader');
+ }
+
+ let lastTime = Date.now();
+ let lastBytes = 0;
+
+ while (true) {
+ const { done, value } = await reader.read();
+
+ if (done) {
+ break;
+ }
+
+ const text = new TextDecoder().decode(value);
+ const lines = text.split('\n').filter(Boolean);
+
+ for (const line of lines) {
+ try {
+ const data = JSON.parse(line);
+
+ if ('status' in data) {
+ const currentTime = Date.now();
+ const timeDiff = (currentTime - lastTime) / 1000;
+ const bytesDiff = (data.completed || 0) - lastBytes;
+ const speed = bytesDiff / timeDiff;
+
+ setInstallProgress({
+ status: data.status,
+ progress: data.completed && data.total ? (data.completed / data.total) * 100 : 0,
+ downloadedSize: formatBytes(data.completed || 0),
+ totalSize: data.total ? formatBytes(data.total) : 'Calculating...',
+ speed: formatSpeed(speed),
+ });
+
+ lastTime = currentTime;
+ lastBytes = data.completed || 0;
+ }
+ } catch (err) {
+ console.error('Error parsing progress:', err);
+ }
+ }
+ }
+
+ toast('Successfully updated ' + modelToUpdate);
+
+ // Refresh model list after update
+ await checkInstalledModels();
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
+ console.error(`Error updating ${modelToUpdate}:`, errorMessage);
+ toast(`Failed to update ${modelToUpdate}. ${errorMessage}`);
+ setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'error' } : m)));
+ } finally {
+ setInstallProgress(null);
+ }
+ };
+
+ const allTags = Array.from(new Set(POPULAR_MODELS.flatMap((model) => model.tags)));
+
+ return (
+
+
+
+
+
+
Ollama Models
+
Install and manage your Ollama models
+
+
+
+ {isChecking ? (
+
+ ) : (
+
+ )}
+ Check Updates
+
+
+
+
+
+
+
{
+ const value = e.target.value;
+ setSearchQuery(value);
+ setModelString(value);
+ }}
+ disabled={isInstalling}
+ />
+
+ Browse models at{' '}
+
+ ollama.com/library
+
+ {' '}
+ and copy model names to install
+
+
+
+
handleInstallModel(modelString)}
+ disabled={!modelString || isInstalling}
+ className={classNames(
+ 'rounded-xl px-6 py-3',
+ 'bg-purple-500 text-white',
+ 'hover:bg-purple-600',
+ 'transition-all duration-200',
+ { 'opacity-50 cursor-not-allowed': !modelString || isInstalling },
+ )}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ {isInstalling ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {allTags.map((tag) => (
+ {
+ setSelectedTags((prev) => (prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]));
+ }}
+ className={classNames(
+ 'px-3 py-1 rounded-full text-xs font-medium transition-all duration-200',
+ selectedTags.includes(tag)
+ ? 'bg-purple-500 text-white'
+ : 'bg-bolt-elements-background-depth-3 text-bolt-elements-textSecondary hover:bg-bolt-elements-background-depth-4',
+ )}
+ >
+ {tag}
+
+ ))}
+
+
+
+ {filteredModels.map((model) => (
+
+
+
+
+
+
{model.name}
+
{model.desc}
+
+
+
{model.size}
+ {model.installedVersion && (
+
+ v{model.installedVersion}
+ {model.needsUpdate && model.latestVersion && (
+ v{model.latestVersion} available
+ )}
+
+ )}
+
+
+
+
+ {model.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {model.installedVersion ? (
+ model.needsUpdate ? (
+
handleUpdateModel(model.name)}
+ className={classNames(
+ 'px-2 py-0.5 rounded-lg text-xs',
+ 'bg-purple-500 text-white',
+ 'hover:bg-purple-600',
+ 'transition-all duration-200',
+ 'flex items-center gap-1',
+ )}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+ Update
+
+ ) : (
+
Up to date
+ )
+ ) : (
+
handleInstallModel(model.name)}
+ className={classNames(
+ 'px-2 py-0.5 rounded-lg text-xs',
+ 'bg-purple-500 text-white',
+ 'hover:bg-purple-600',
+ 'transition-all duration-200',
+ 'flex items-center gap-1',
+ )}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+ Install
+
+ )}
+
+
+
+
+ ))}
+
+
+ {installProgress && (
+
+
+
{installProgress.status}
+
+
+ {installProgress.downloadedSize} / {installProgress.totalSize}
+
+ {installProgress.speed}
+ {Math.round(installProgress.progress)}%
+
+
+
+
+ )}
+
+ );
+}
diff --git a/app/components/settings/providers/service-status/ServiceStatusTab.tsx b/app/components/@settings/tabs/providers/service-status/ServiceStatusTab.tsx
similarity index 100%
rename from app/components/settings/providers/service-status/ServiceStatusTab.tsx
rename to app/components/@settings/tabs/providers/service-status/ServiceStatusTab.tsx
diff --git a/app/components/settings/providers/service-status/base-provider.ts b/app/components/@settings/tabs/providers/service-status/base-provider.ts
similarity index 100%
rename from app/components/settings/providers/service-status/base-provider.ts
rename to app/components/@settings/tabs/providers/service-status/base-provider.ts
diff --git a/app/components/settings/providers/service-status/provider-factory.ts b/app/components/@settings/tabs/providers/service-status/provider-factory.ts
similarity index 100%
rename from app/components/settings/providers/service-status/provider-factory.ts
rename to app/components/@settings/tabs/providers/service-status/provider-factory.ts
diff --git a/app/components/settings/providers/service-status/providers/amazon-bedrock.ts b/app/components/@settings/tabs/providers/service-status/providers/amazon-bedrock.ts
similarity index 93%
rename from app/components/settings/providers/service-status/providers/amazon-bedrock.ts
rename to app/components/@settings/tabs/providers/service-status/providers/amazon-bedrock.ts
index ce5170906e..dff9d9a1fb 100644
--- a/app/components/settings/providers/service-status/providers/amazon-bedrock.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/amazon-bedrock.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class AmazonBedrockStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/anthropic.ts b/app/components/@settings/tabs/providers/service-status/providers/anthropic.ts
similarity index 93%
rename from app/components/settings/providers/service-status/providers/anthropic.ts
rename to app/components/@settings/tabs/providers/service-status/providers/anthropic.ts
index f302c73121..dccbf66b39 100644
--- a/app/components/settings/providers/service-status/providers/anthropic.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/anthropic.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class AnthropicStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/cohere.ts b/app/components/@settings/tabs/providers/service-status/providers/cohere.ts
similarity index 94%
rename from app/components/settings/providers/service-status/providers/cohere.ts
rename to app/components/@settings/tabs/providers/service-status/providers/cohere.ts
index 370be5b342..7707f7377d 100644
--- a/app/components/settings/providers/service-status/providers/cohere.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/cohere.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class CohereStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/deepseek.ts b/app/components/@settings/tabs/providers/service-status/providers/deepseek.ts
similarity index 86%
rename from app/components/settings/providers/service-status/providers/deepseek.ts
rename to app/components/@settings/tabs/providers/service-status/providers/deepseek.ts
index 9aea1308dc..7aa88bac42 100644
--- a/app/components/settings/providers/service-status/providers/deepseek.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/deepseek.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class DeepseekStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/google.ts b/app/components/@settings/tabs/providers/service-status/providers/google.ts
similarity index 93%
rename from app/components/settings/providers/service-status/providers/google.ts
rename to app/components/@settings/tabs/providers/service-status/providers/google.ts
index 2e892480f8..80b5ecf81c 100644
--- a/app/components/settings/providers/service-status/providers/google.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/google.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class GoogleStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/groq.ts b/app/components/@settings/tabs/providers/service-status/providers/groq.ts
similarity index 92%
rename from app/components/settings/providers/service-status/providers/groq.ts
rename to app/components/@settings/tabs/providers/service-status/providers/groq.ts
index f9bfac0f2e..c465cedd81 100644
--- a/app/components/settings/providers/service-status/providers/groq.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/groq.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class GroqStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/huggingface.ts b/app/components/@settings/tabs/providers/service-status/providers/huggingface.ts
similarity index 95%
rename from app/components/settings/providers/service-status/providers/huggingface.ts
rename to app/components/@settings/tabs/providers/service-status/providers/huggingface.ts
index 6f8b062ccb..80dcfe848d 100644
--- a/app/components/settings/providers/service-status/providers/huggingface.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/huggingface.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class HuggingFaceStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/hyperbolic.ts b/app/components/@settings/tabs/providers/service-status/providers/hyperbolic.ts
similarity index 86%
rename from app/components/settings/providers/service-status/providers/hyperbolic.ts
rename to app/components/@settings/tabs/providers/service-status/providers/hyperbolic.ts
index b1896e778d..6dca268fb7 100644
--- a/app/components/settings/providers/service-status/providers/hyperbolic.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/hyperbolic.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class HyperbolicStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/mistral.ts b/app/components/@settings/tabs/providers/service-status/providers/mistral.ts
similarity index 92%
rename from app/components/settings/providers/service-status/providers/mistral.ts
rename to app/components/@settings/tabs/providers/service-status/providers/mistral.ts
index 53e9fcb512..5966682cff 100644
--- a/app/components/settings/providers/service-status/providers/mistral.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/mistral.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class MistralStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/openai.ts b/app/components/@settings/tabs/providers/service-status/providers/openai.ts
similarity index 94%
rename from app/components/settings/providers/service-status/providers/openai.ts
rename to app/components/@settings/tabs/providers/service-status/providers/openai.ts
index 3ef3a7e362..252c16ea1b 100644
--- a/app/components/settings/providers/service-status/providers/openai.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/openai.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class OpenAIStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/openrouter.ts b/app/components/@settings/tabs/providers/service-status/providers/openrouter.ts
similarity index 94%
rename from app/components/settings/providers/service-status/providers/openrouter.ts
rename to app/components/@settings/tabs/providers/service-status/providers/openrouter.ts
index f7144c3667..f05edb98a6 100644
--- a/app/components/settings/providers/service-status/providers/openrouter.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/openrouter.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class OpenRouterStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/perplexity.ts b/app/components/@settings/tabs/providers/service-status/providers/perplexity.ts
similarity index 94%
rename from app/components/settings/providers/service-status/providers/perplexity.ts
rename to app/components/@settings/tabs/providers/service-status/providers/perplexity.ts
index caab8544be..31a8088e3c 100644
--- a/app/components/settings/providers/service-status/providers/perplexity.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/perplexity.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class PerplexityStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/together.ts b/app/components/@settings/tabs/providers/service-status/providers/together.ts
similarity index 94%
rename from app/components/settings/providers/service-status/providers/together.ts
rename to app/components/@settings/tabs/providers/service-status/providers/together.ts
index c42a0f4ae3..77abce9810 100644
--- a/app/components/settings/providers/service-status/providers/together.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/together.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class TogetherStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/providers/xai.ts b/app/components/@settings/tabs/providers/service-status/providers/xai.ts
similarity index 85%
rename from app/components/settings/providers/service-status/providers/xai.ts
rename to app/components/@settings/tabs/providers/service-status/providers/xai.ts
index d0f880381e..7b98c6a382 100644
--- a/app/components/settings/providers/service-status/providers/xai.ts
+++ b/app/components/@settings/tabs/providers/service-status/providers/xai.ts
@@ -1,5 +1,5 @@
-import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
-import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
+import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
+import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
export class XAIStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise {
diff --git a/app/components/settings/providers/service-status/types.ts b/app/components/@settings/tabs/providers/service-status/types.ts
similarity index 100%
rename from app/components/settings/providers/service-status/types.ts
rename to app/components/@settings/tabs/providers/service-status/types.ts
diff --git a/app/components/settings/providers/ServiceStatusTab.tsx b/app/components/@settings/tabs/providers/status/ServiceStatusTab.tsx
similarity index 100%
rename from app/components/settings/providers/ServiceStatusTab.tsx
rename to app/components/@settings/tabs/providers/status/ServiceStatusTab.tsx
diff --git a/app/components/settings/settings/SettingsTab.tsx b/app/components/@settings/tabs/settings/SettingsTab.tsx
similarity index 99%
rename from app/components/settings/settings/SettingsTab.tsx
rename to app/components/@settings/tabs/settings/SettingsTab.tsx
index c042b8fc5e..d2cffde9ab 100644
--- a/app/components/settings/settings/SettingsTab.tsx
+++ b/app/components/@settings/tabs/settings/SettingsTab.tsx
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import { Switch } from '~/components/ui/Switch';
import { themeStore, kTheme } from '~/lib/stores/theme';
-import type { UserProfile } from '~/components/settings/settings.types';
+import type { UserProfile } from '~/components/@settings/core/types';
import { useStore } from '@nanostores/react';
import { shortcutsStore } from '~/lib/stores/settings';
diff --git a/app/components/settings/task-manager/TaskManagerTab.tsx b/app/components/@settings/tabs/task-manager/TaskManagerTab.tsx
similarity index 78%
rename from app/components/settings/task-manager/TaskManagerTab.tsx
rename to app/components/@settings/tabs/task-manager/TaskManagerTab.tsx
index 936ee904e6..48c26b5b3f 100644
--- a/app/components/settings/task-manager/TaskManagerTab.tsx
+++ b/app/components/@settings/tabs/task-manager/TaskManagerTab.tsx
@@ -1,4 +1,5 @@
-import React, { useEffect, useState, useRef, useCallback } from 'react';
+import * as React from 'react';
+import { useEffect, useState, useRef, useCallback } from 'react';
import { classNames } from '~/utils/classNames';
import { Line } from 'react-chartjs-2';
import {
@@ -12,6 +13,9 @@ import {
Legend,
} from 'chart.js';
import { toast } from 'react-toastify'; // Import toast
+import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
+import { tabConfigurationStore, type TabConfig } from '~/lib/stores/tabConfigurationStore';
+import { useStore } from 'zustand';
// Register ChartJS components
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
@@ -74,12 +78,6 @@ interface SystemMetrics {
lcp: number;
};
};
- storage: {
- total: number;
- used: number;
- free: number;
- type: string;
- };
health: {
score: number;
issues: string[];
@@ -134,37 +132,46 @@ declare global {
}
}
-const MAX_HISTORY_POINTS = 60; // 1 minute of history at 1s intervals
-const BATTERY_THRESHOLD = 20; // Enable energy saver when battery below 20%
+// Constants for update intervals
const UPDATE_INTERVALS = {
normal: {
- metrics: 1000, // 1s
+ metrics: 1000, // 1 second
+ animation: 16, // ~60fps
},
energySaver: {
- metrics: 5000, // 5s
+ metrics: 5000, // 5 seconds
+ animation: 32, // ~30fps
},
};
-// Energy consumption estimates (milliwatts)
-const ENERGY_COSTS = {
- update: 2, // mW per update
- apiCall: 5, // mW per API call
- rendering: 1, // mW per render
+// Constants for performance thresholds
+const PERFORMANCE_THRESHOLDS = {
+ cpu: {
+ warning: 70,
+ critical: 90,
+ },
+ memory: {
+ warning: 80,
+ critical: 95,
+ },
+ fps: {
+ warning: 30,
+ critical: 15,
+ },
};
-const PERFORMANCE_THRESHOLDS = {
- cpu: { warning: 70, critical: 90 },
- memory: { warning: 80, critical: 95 },
- fps: { warning: 30, critical: 15 },
- loadTime: { warning: 3000, critical: 5000 },
+// Constants for energy calculations
+const ENERGY_COSTS = {
+ update: 0.1, // mWh per update
};
+// Default power profiles
const POWER_PROFILES: PowerProfile[] = [
{
name: 'Performance',
- description: 'Maximum performance, higher power consumption',
+ description: 'Maximum performance with frequent updates',
settings: {
- updateInterval: 1000,
+ updateInterval: UPDATE_INTERVALS.normal.metrics,
enableAnimations: true,
backgroundProcessing: true,
networkThrottling: false,
@@ -172,7 +179,7 @@ const POWER_PROFILES: PowerProfile[] = [
},
{
name: 'Balanced',
- description: 'Balance between performance and power saving',
+ description: 'Optimal balance between performance and energy efficiency',
settings: {
updateInterval: 2000,
enableAnimations: true,
@@ -181,10 +188,10 @@ const POWER_PROFILES: PowerProfile[] = [
},
},
{
- name: 'Power Saver',
- description: 'Maximum power saving, reduced performance',
+ name: 'Energy Saver',
+ description: 'Maximum energy efficiency with reduced updates',
settings: {
- updateInterval: 5000,
+ updateInterval: UPDATE_INTERVALS.energySaver.metrics,
enableAnimations: false,
backgroundProcessing: false,
networkThrottling: true,
@@ -192,50 +199,271 @@ const POWER_PROFILES: PowerProfile[] = [
},
];
-export default function TaskManagerTab() {
- const [metrics, setMetrics] = useState({
- cpu: { usage: 0, cores: [] },
- memory: { used: 0, total: 0, percentage: 0, heap: { used: 0, total: 0, limit: 0 } },
- uptime: 0,
- network: { downlink: 0, latency: 0, type: 'unknown', bytesReceived: 0, bytesSent: 0 },
- performance: {
- fps: 0,
- pageLoad: 0,
- domReady: 0,
- resources: { total: 0, size: 0, loadTime: 0 },
- timing: { ttfb: 0, fcp: 0, lcp: 0 },
+// Default metrics state
+const DEFAULT_METRICS_STATE: SystemMetrics = {
+ cpu: {
+ usage: 0,
+ cores: [],
+ },
+ memory: {
+ used: 0,
+ total: 0,
+ percentage: 0,
+ heap: {
+ used: 0,
+ total: 0,
+ limit: 0,
},
- storage: { total: 0, used: 0, free: 0, type: 'unknown' },
- health: { score: 0, issues: [], suggestions: [] },
- });
- const [metricsHistory, setMetricsHistory] = useState({
- timestamps: [],
- cpu: [],
- memory: [],
- battery: [],
- network: [],
- });
- const [energySaverMode, setEnergySaverMode] = useState(() => {
- // Initialize from localStorage, default to false
- const saved = localStorage.getItem('energySaverMode');
- return saved ? JSON.parse(saved) : false;
- });
-
- const [autoEnergySaver, setAutoEnergySaver] = useState(() => {
- // Initialize from localStorage, default to false
- const saved = localStorage.getItem('autoEnergySaver');
- return saved ? JSON.parse(saved) : false;
- });
-
- const [energySavings, setEnergySavings] = useState({
+ },
+ uptime: 0,
+ network: {
+ downlink: 0,
+ latency: 0,
+ type: 'unknown',
+ bytesReceived: 0,
+ bytesSent: 0,
+ },
+ performance: {
+ fps: 0,
+ pageLoad: 0,
+ domReady: 0,
+ resources: {
+ total: 0,
+ size: 0,
+ loadTime: 0,
+ },
+ timing: {
+ ttfb: 0,
+ fcp: 0,
+ lcp: 0,
+ },
+ },
+ health: {
+ score: 0,
+ issues: [],
+ suggestions: [],
+ },
+};
+
+// Default metrics history
+const DEFAULT_METRICS_HISTORY: MetricsHistory = {
+ timestamps: Array(10).fill(new Date().toLocaleTimeString()),
+ cpu: Array(10).fill(0),
+ memory: Array(10).fill(0),
+ battery: Array(10).fill(0),
+ network: Array(10).fill(0),
+};
+
+// Battery threshold for auto energy saver mode
+const BATTERY_THRESHOLD = 20; // percentage
+
+// Maximum number of history points to keep
+const MAX_HISTORY_POINTS = 10;
+
+const TaskManagerTab: React.FC = () => {
+ // Initialize metrics state with defaults
+ const [metrics, setMetrics] = useState(() => DEFAULT_METRICS_STATE);
+ const [metricsHistory, setMetricsHistory] = useState(() => DEFAULT_METRICS_HISTORY);
+ const [energySaverMode, setEnergySaverMode] = useState(false);
+ const [autoEnergySaver, setAutoEnergySaver] = useState(false);
+ const [energySavings, setEnergySavings] = useState(() => ({
updatesReduced: 0,
timeInSaverMode: 0,
estimatedEnergySaved: 0,
- });
-
- const saverModeStartTime = useRef(null);
- const [selectedProfile, setSelectedProfile] = useState(POWER_PROFILES[1]); // Default to Balanced
+ }));
+ const [selectedProfile, setSelectedProfile] = useState(() => POWER_PROFILES[1]);
const [alerts, setAlerts] = useState([]);
+ const saverModeStartTime = useRef(null);
+
+ // Get update status and tab configuration
+ const { hasUpdate } = useUpdateCheck();
+ const tabConfig = useStore(tabConfigurationStore);
+
+ const resetTabConfiguration = useCallback(() => {
+ tabConfig.reset();
+ return tabConfig.get();
+ }, [tabConfig]);
+
+ // Effect to handle tab visibility
+ useEffect(() => {
+ const handleTabVisibility = () => {
+ const currentConfig = tabConfig.get();
+ const controlledTabs = ['debug', 'update'];
+
+ // Update visibility based on conditions
+ const updatedTabs = currentConfig.userTabs.map((tab: TabConfig) => {
+ if (controlledTabs.includes(tab.id)) {
+ return {
+ ...tab,
+ visible: tab.id === 'debug' ? metrics.cpu.usage > 80 : hasUpdate,
+ };
+ }
+
+ return tab;
+ });
+
+ tabConfig.set({
+ ...currentConfig,
+ userTabs: updatedTabs,
+ });
+ };
+
+ const checkInterval = setInterval(handleTabVisibility, 5000);
+
+ return () => {
+ clearInterval(checkInterval);
+ };
+ }, [metrics.cpu.usage, hasUpdate, tabConfig]);
+
+ // Effect to handle reset and initialization
+ useEffect(() => {
+ const resetToDefaults = () => {
+ console.log('TaskManagerTab: Resetting to defaults');
+
+ // Reset metrics and local state
+ setMetrics(DEFAULT_METRICS_STATE);
+ setMetricsHistory(DEFAULT_METRICS_HISTORY);
+ setEnergySaverMode(false);
+ setAutoEnergySaver(false);
+ setEnergySavings({
+ updatesReduced: 0,
+ timeInSaverMode: 0,
+ estimatedEnergySaved: 0,
+ });
+ setSelectedProfile(POWER_PROFILES[1]);
+ setAlerts([]);
+ saverModeStartTime.current = null;
+
+ // Reset tab configuration to ensure proper visibility
+ const defaultConfig = resetTabConfiguration();
+ console.log('TaskManagerTab: Reset tab configuration:', defaultConfig);
+ };
+
+ // Listen for both storage changes and custom reset event
+ const handleReset = (event: Event | StorageEvent) => {
+ if (event instanceof StorageEvent) {
+ if (event.key === 'tabConfiguration' && event.newValue === null) {
+ resetToDefaults();
+ }
+ } else if (event instanceof CustomEvent && event.type === 'tabConfigReset') {
+ resetToDefaults();
+ }
+ };
+
+ // Initial setup
+ const initializeTab = async () => {
+ try {
+ // Load saved preferences
+ const savedEnergySaver = localStorage.getItem('energySaverMode');
+ const savedAutoSaver = localStorage.getItem('autoEnergySaver');
+ const savedProfile = localStorage.getItem('selectedProfile');
+
+ if (savedEnergySaver) {
+ setEnergySaverMode(JSON.parse(savedEnergySaver));
+ }
+
+ if (savedAutoSaver) {
+ setAutoEnergySaver(JSON.parse(savedAutoSaver));
+ }
+
+ if (savedProfile) {
+ const profile = POWER_PROFILES.find((p) => p.name === savedProfile);
+
+ if (profile) {
+ setSelectedProfile(profile);
+ }
+ }
+
+ await updateMetrics();
+ } catch (error) {
+ console.error('Failed to initialize TaskManagerTab:', error);
+ resetToDefaults();
+ }
+ };
+
+ window.addEventListener('storage', handleReset);
+ window.addEventListener('tabConfigReset', handleReset);
+ initializeTab();
+
+ return () => {
+ window.removeEventListener('storage', handleReset);
+ window.removeEventListener('tabConfigReset', handleReset);
+ };
+ }, []);
+
+ // Get detailed performance metrics
+ const getPerformanceMetrics = async (): Promise> => {
+ try {
+ // Get FPS
+ const fps = await measureFrameRate();
+
+ // Get page load metrics
+ const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
+ const pageLoad = navigation.loadEventEnd - navigation.startTime;
+ const domReady = navigation.domContentLoadedEventEnd - navigation.startTime;
+
+ // Get resource metrics
+ const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
+ const resourceMetrics = {
+ total: resources.length,
+ size: resources.reduce((total, r) => total + (r.transferSize || 0), 0),
+ loadTime: Math.max(0, ...resources.map((r) => r.duration)),
+ };
+
+ // Get Web Vitals
+ const ttfb = navigation.responseStart - navigation.requestStart;
+ const paintEntries = performance.getEntriesByType('paint');
+ const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0;
+ const lcpEntry = await getLargestContentfulPaint();
+
+ return {
+ fps,
+ pageLoad,
+ domReady,
+ resources: resourceMetrics,
+ timing: {
+ ttfb,
+ fcp,
+ lcp: lcpEntry?.startTime || 0,
+ },
+ };
+ } catch (error) {
+ console.error('Failed to get performance metrics:', error);
+ return {};
+ }
+ };
+
+ // Single useEffect for metrics updates
+ useEffect(() => {
+ let isComponentMounted = true;
+
+ const updateMetricsWrapper = async () => {
+ if (!isComponentMounted) {
+ return;
+ }
+
+ try {
+ await updateMetrics();
+ } catch (error) {
+ console.error('Failed to update metrics:', error);
+ }
+ };
+
+ // Initial update
+ updateMetricsWrapper();
+
+ // Set up interval with immediate assignment
+ const metricsInterval = setInterval(
+ updateMetricsWrapper,
+ energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
+ );
+
+ // Cleanup function
+ return () => {
+ isComponentMounted = false;
+ clearInterval(metricsInterval);
+ };
+ }, [energySaverMode]); // Only depend on energySaverMode
// Handle energy saver mode changes
const handleEnergySaverChange = (checked: boolean) => {
@@ -296,48 +524,6 @@ export default function TaskManagerTab() {
return () => clearInterval(interval);
}, [updateEnergySavings]);
- // Get detailed performance metrics
- const getPerformanceMetrics = async (): Promise> => {
- try {
- // Get FPS
- const fps = await measureFrameRate();
-
- // Get page load metrics
- const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
- const pageLoad = navigation.loadEventEnd - navigation.startTime;
- const domReady = navigation.domContentLoadedEventEnd - navigation.startTime;
-
- // Get resource metrics
- const resources = performance.getEntriesByType('resource');
- const resourceMetrics = {
- total: resources.length,
- size: resources.reduce((total, r) => total + (r as any).transferSize || 0, 0),
- loadTime: Math.max(...resources.map((r) => r.duration)),
- };
-
- // Get Web Vitals
- const ttfb = navigation.responseStart - navigation.requestStart;
- const paintEntries = performance.getEntriesByType('paint');
- const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0;
- const lcpEntry = await getLargestContentfulPaint();
-
- return {
- fps,
- pageLoad,
- domReady,
- resources: resourceMetrics,
- timing: {
- ttfb,
- fcp,
- lcp: lcpEntry?.startTime || 0,
- },
- };
- } catch (error) {
- console.error('Failed to get performance metrics:', error);
- return {};
- }
- };
-
// Measure frame rate
const measureFrameRate = async (): Promise => {
return new Promise((resolve) => {
@@ -486,12 +672,6 @@ export default function TaskManagerTab() {
battery: batteryInfo,
network: networkInfo,
performance: performanceMetrics as SystemMetrics['performance'],
- storage: {
- total: 0,
- used: 0,
- free: 0,
- type: 'unknown',
- },
health: { score: 0, issues: [], suggestions: [] },
};
@@ -597,23 +777,6 @@ export default function TaskManagerTab() {
};
}, [energySaverMode]);
- // Initial update effect
- useEffect((): (() => void) => {
- // Initial update
- updateMetrics();
-
- // Set up intervals for live updates
- const metricsInterval = setInterval(
- updateMetrics,
- energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
- );
-
- // Cleanup on unmount
- return () => {
- clearInterval(metricsInterval);
- };
- }, [energySaverMode]); // Re-create intervals when energy saver mode changes
-
const getUsageColor = (usage: number): string => {
if (usage > 80) {
return 'text-red-500';
@@ -761,6 +924,7 @@ export default function TaskManagerTab() {
onChange={(e) => handleAutoEnergySaverChange(e.target.checked)}
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
/>
+
Auto Energy Saver
@@ -774,6 +938,7 @@ export default function TaskManagerTab() {
disabled={autoEnergySaver}
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
/>
+
Active}
- {
- const profile = POWER_PROFILES.find((p) => p.name === e.target.value);
-
- if (profile) {
- setSelectedProfile(profile);
- toast.success(`Switched to ${profile.name} power profile`);
- }
- }}
- className="px-3 py-1 rounded-md bg-[#F8F8F8] dark:bg-[#141414] border border-[#E5E5E5] dark:border-[#1A1A1A] text-sm"
- >
- {POWER_PROFILES.map((profile) => (
-
- {profile.name}
-
- ))}
-
+
+
{
+ const profile = POWER_PROFILES.find((p) => p.name === e.target.value);
+
+ if (profile) {
+ setSelectedProfile(profile);
+ toast.success(`Switched to ${profile.name} power profile`);
+ }
+ }}
+ className="pl-8 pr-8 py-1.5 rounded-md bg-bolt-background-secondary dark:bg-[#1E1E1E] border border-bolt-border dark:border-bolt-borderDark text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:border-bolt-action-primary dark:hover:border-bolt-action-primary focus:outline-none focus:ring-1 focus:ring-bolt-action-primary appearance-none min-w-[160px] cursor-pointer transition-colors duration-150"
+ style={{ WebkitAppearance: 'none', MozAppearance: 'none' }}
+ >
+ {POWER_PROFILES.map((profile) => (
+
+ {profile.name}
+
+ ))}
+
+
+
+