Spaces:
Runtime error
Runtime error
import * as React from 'react'; | |
import { 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'; | |
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); | |
interface BatteryManager extends EventTarget { | |
charging: boolean; | |
chargingTime: number; | |
dischargingTime: number; | |
level: number; | |
} | |
interface SystemMetrics { | |
cpu: { | |
usage: number; | |
cores: number[]; | |
temperature?: number; | |
frequency?: number; | |
}; | |
memory: { | |
used: number; | |
total: number; | |
percentage: number; | |
heap: { | |
used: number; | |
total: number; | |
limit: number; | |
}; | |
cache?: number; | |
}; | |
uptime: number; | |
battery?: { | |
level: number; | |
charging: boolean; | |
timeRemaining?: number; | |
temperature?: number; | |
cycles?: number; | |
health?: number; | |
}; | |
network: { | |
downlink: number; | |
uplink?: number; | |
latency: number; | |
type: string; | |
activeConnections?: number; | |
bytesReceived: number; | |
bytesSent: number; | |
}; | |
performance: { | |
fps: number; | |
pageLoad: number; | |
domReady: number; | |
resources: { | |
total: number; | |
size: number; | |
loadTime: number; | |
}; | |
timing: { | |
ttfb: number; | |
fcp: number; | |
lcp: number; | |
}; | |
}; | |
health: { | |
score: number; | |
issues: string[]; | |
suggestions: 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) | |
} | |
interface PowerProfile { | |
name: string; | |
description: string; | |
settings: { | |
updateInterval: number; | |
enableAnimations: boolean; | |
backgroundProcessing: boolean; | |
networkThrottling: boolean; | |
}; | |
} | |
interface PerformanceAlert { | |
type: 'warning' | 'error' | 'info'; | |
message: string; | |
timestamp: number; | |
metric: string; | |
threshold: number; | |
value: number; | |
} | |
declare global { | |
interface Navigator { | |
getBattery(): Promise<BatteryManager>; | |
} | |
interface Performance { | |
memory?: { | |
jsHeapSizeLimit: number; | |
totalJSHeapSize: number; | |
usedJSHeapSize: number; | |
}; | |
} | |
} | |
// Constants for update intervals | |
const UPDATE_INTERVALS = { | |
normal: { | |
metrics: 1000, // 1 second | |
animation: 16, // ~60fps | |
}, | |
energySaver: { | |
metrics: 5000, // 5 seconds | |
animation: 32, // ~30fps | |
}, | |
}; | |
// Constants for performance thresholds | |
const PERFORMANCE_THRESHOLDS = { | |
cpu: { | |
warning: 70, | |
critical: 90, | |
}, | |
memory: { | |
warning: 80, | |
critical: 95, | |
}, | |
fps: { | |
warning: 30, | |
critical: 15, | |
}, | |
}; | |
// 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 with frequent updates', | |
settings: { | |
updateInterval: UPDATE_INTERVALS.normal.metrics, | |
enableAnimations: true, | |
backgroundProcessing: true, | |
networkThrottling: false, | |
}, | |
}, | |
{ | |
name: 'Balanced', | |
description: 'Optimal balance between performance and energy efficiency', | |
settings: { | |
updateInterval: 2000, | |
enableAnimations: true, | |
backgroundProcessing: true, | |
networkThrottling: false, | |
}, | |
}, | |
{ | |
name: 'Energy Saver', | |
description: 'Maximum energy efficiency with reduced updates', | |
settings: { | |
updateInterval: UPDATE_INTERVALS.energySaver.metrics, | |
enableAnimations: false, | |
backgroundProcessing: false, | |
networkThrottling: true, | |
}, | |
}, | |
]; | |
// 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, | |
}, | |
}, | |
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<SystemMetrics>(() => DEFAULT_METRICS_STATE); | |
const [metricsHistory, setMetricsHistory] = useState<MetricsHistory>(() => DEFAULT_METRICS_HISTORY); | |
const [energySaverMode, setEnergySaverMode] = useState<boolean>(false); | |
const [autoEnergySaver, setAutoEnergySaver] = useState<boolean>(false); | |
const [energySavings, setEnergySavings] = useState<EnergySavings>(() => ({ | |
updatesReduced: 0, | |
timeInSaverMode: 0, | |
estimatedEnergySaved: 0, | |
})); | |
const [selectedProfile, setSelectedProfile] = useState<PowerProfile>(() => POWER_PROFILES[1]); | |
const [alerts, setAlerts] = useState<PerformanceAlert[]>([]); | |
const saverModeStartTime = useRef<number | null>(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<Partial<SystemMetrics['performance']>> => { | |
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) => { | |
setEnergySaverMode(checked); | |
localStorage.setItem('energySaverMode', JSON.stringify(checked)); | |
toast.success(checked ? 'Energy Saver mode enabled' : 'Energy Saver mode disabled'); | |
}; | |
// Handle auto energy saver changes | |
const handleAutoEnergySaverChange = (checked: boolean) => { | |
setAutoEnergySaver(checked); | |
localStorage.setItem('autoEnergySaver', JSON.stringify(checked)); | |
toast.success(checked ? 'Auto Energy Saver enabled' : 'Auto Energy Saver disabled'); | |
if (!checked) { | |
// When disabling auto mode, also disable energy saver mode | |
setEnergySaverMode(false); | |
localStorage.setItem('energySaverMode', 'false'); | |
} | |
}; | |
// Update energy savings calculation | |
const updateEnergySavings = useCallback(() => { | |
if (!energySaverMode) { | |
saverModeStartTime.current = null; | |
setEnergySavings({ | |
updatesReduced: 0, | |
timeInSaverMode: 0, | |
estimatedEnergySaved: 0, | |
}); | |
return; | |
} | |
if (!saverModeStartTime.current) { | |
saverModeStartTime.current = Date.now(); | |
} | |
const timeInSaverMode = Math.max(0, (Date.now() - (saverModeStartTime.current || Date.now())) / 1000); | |
const normalUpdatesPerMinute = 60 / (UPDATE_INTERVALS.normal.metrics / 1000); | |
const saverUpdatesPerMinute = 60 / (UPDATE_INTERVALS.energySaver.metrics / 1000); | |
const updatesReduced = Math.floor((normalUpdatesPerMinute - saverUpdatesPerMinute) * (timeInSaverMode / 60)); | |
const energyPerUpdate = ENERGY_COSTS.update; | |
const energySaved = (updatesReduced * energyPerUpdate) / 3600; | |
setEnergySavings({ | |
updatesReduced, | |
timeInSaverMode, | |
estimatedEnergySaved: energySaved, | |
}); | |
}, [energySaverMode]); | |
// Add interval for energy savings updates | |
useEffect(() => { | |
const interval = setInterval(updateEnergySavings, 1000); | |
return () => clearInterval(interval); | |
}, [updateEnergySavings]); | |
// Measure frame rate | |
const measureFrameRate = async (): Promise<number> => { | |
return new Promise((resolve) => { | |
const frameCount = { value: 0 }; | |
const startTime = performance.now(); | |
const countFrame = (time: number) => { | |
frameCount.value++; | |
if (time - startTime >= 1000) { | |
resolve(Math.round((frameCount.value * 1000) / (time - startTime))); | |
} else { | |
requestAnimationFrame(countFrame); | |
} | |
}; | |
requestAnimationFrame(countFrame); | |
}); | |
}; | |
// Get Largest Contentful Paint | |
const getLargestContentfulPaint = async (): Promise<PerformanceEntry | undefined> => { | |
return new Promise((resolve) => { | |
new PerformanceObserver((list) => { | |
const entries = list.getEntries(); | |
resolve(entries[entries.length - 1]); | |
}).observe({ entryTypes: ['largest-contentful-paint'] }); | |
// Resolve after 3 seconds if no LCP entry is found | |
setTimeout(() => resolve(undefined), 3000); | |
}); | |
}; | |
// Analyze system health | |
const analyzeSystemHealth = (currentMetrics: SystemMetrics): SystemMetrics['health'] => { | |
const issues: string[] = []; | |
const suggestions: string[] = []; | |
let score = 100; | |
// CPU analysis | |
if (currentMetrics.cpu.usage > PERFORMANCE_THRESHOLDS.cpu.critical) { | |
score -= 30; | |
issues.push('Critical CPU usage'); | |
suggestions.push('Consider closing resource-intensive applications'); | |
} else if (currentMetrics.cpu.usage > PERFORMANCE_THRESHOLDS.cpu.warning) { | |
score -= 15; | |
issues.push('High CPU usage'); | |
suggestions.push('Monitor system processes for unusual activity'); | |
} | |
// Memory analysis | |
if (currentMetrics.memory.percentage > PERFORMANCE_THRESHOLDS.memory.critical) { | |
score -= 30; | |
issues.push('Critical memory usage'); | |
suggestions.push('Close unused applications to free up memory'); | |
} else if (currentMetrics.memory.percentage > PERFORMANCE_THRESHOLDS.memory.warning) { | |
score -= 15; | |
issues.push('High memory usage'); | |
suggestions.push('Consider freeing up memory by closing background applications'); | |
} | |
// Performance analysis | |
if (currentMetrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.critical) { | |
score -= 20; | |
issues.push('Very low frame rate'); | |
suggestions.push('Disable animations or switch to power saver mode'); | |
} else if (currentMetrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.warning) { | |
score -= 10; | |
issues.push('Low frame rate'); | |
suggestions.push('Consider reducing visual effects'); | |
} | |
// Battery analysis | |
if (currentMetrics.battery && !currentMetrics.battery.charging && currentMetrics.battery.level < 20) { | |
score -= 10; | |
issues.push('Low battery'); | |
suggestions.push('Connect to power source or enable power saver mode'); | |
} | |
return { | |
score: Math.max(0, score), | |
issues, | |
suggestions, | |
}; | |
}; | |
// Update metrics with enhanced data | |
const updateMetrics = async () => { | |
try { | |
// Get memory info using Performance API | |
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 CPU usage using Performance API | |
const cpuUsage = await getCPUUsage(); | |
// 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 using Network Information API | |
const connection = | |
(navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection; | |
const networkInfo = { | |
downlink: connection?.downlink || 0, | |
uplink: connection?.uplink, | |
latency: connection?.rtt || 0, | |
type: connection?.type || 'unknown', | |
activeConnections: connection?.activeConnections, | |
bytesReceived: connection?.bytesReceived || 0, | |
bytesSent: connection?.bytesSent || 0, | |
}; | |
// Get enhanced performance metrics | |
const performanceMetrics = await getPerformanceMetrics(); | |
const metrics: SystemMetrics = { | |
cpu: { usage: cpuUsage, cores: [], temperature: undefined, frequency: undefined }, | |
memory: { | |
used: Math.round(usedMem), | |
total: Math.round(totalMem), | |
percentage: Math.round(memPercentage), | |
heap: { | |
used: Math.round(usedMem), | |
total: Math.round(totalMem), | |
limit: Math.round(totalMem), | |
}, | |
}, | |
uptime: performance.now() / 1000, | |
battery: batteryInfo, | |
network: networkInfo, | |
performance: performanceMetrics as SystemMetrics['performance'], | |
health: { score: 0, issues: [], suggestions: [] }, | |
}; | |
// Analyze system health | |
metrics.health = analyzeSystemHealth(metrics); | |
// Check for alerts | |
checkPerformanceAlerts(metrics); | |
setMetrics(metrics); | |
// Update metrics history | |
const now = new Date().toLocaleTimeString(); | |
setMetricsHistory((prev) => { | |
const timestamps = [...prev.timestamps, now].slice(-MAX_HISTORY_POINTS); | |
const cpu = [...prev.cpu, metrics.cpu.usage].slice(-MAX_HISTORY_POINTS); | |
const memory = [...prev.memory, metrics.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) { | |
console.error('Failed to update system metrics:', error); | |
} | |
}; | |
// Get real CPU usage using Performance API | |
const getCPUUsage = async (): Promise<number> => { | |
try { | |
const t0 = performance.now(); | |
// Create some actual work to measure and use the result | |
let result = 0; | |
for (let i = 0; i < 10000; i++) { | |
result += Math.random(); | |
} | |
// Use result to prevent optimization | |
if (result < 0) { | |
console.log('Unexpected negative result'); | |
} | |
const t1 = performance.now(); | |
const timeTaken = t1 - t0; | |
/* | |
* Normalize to percentage (0-100) | |
* Lower time = higher CPU availability | |
*/ | |
const maxExpectedTime = 50; // baseline in ms | |
const cpuAvailability = Math.max(0, Math.min(100, ((maxExpectedTime - timeTaken) / maxExpectedTime) * 100)); | |
return 100 - cpuAvailability; // Convert availability to usage | |
} catch (error) { | |
console.error('Failed to get CPU usage:', error); | |
return 0; | |
} | |
}; | |
// Add network change listener | |
useEffect(() => { | |
const connection = | |
(navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection; | |
if (!connection) { | |
return; | |
} | |
const updateNetworkInfo = () => { | |
setMetrics((prev) => ({ | |
...prev, | |
network: { | |
downlink: connection.downlink || 0, | |
latency: connection.rtt || 0, | |
type: connection.type || 'unknown', | |
bytesReceived: connection.bytesReceived || 0, | |
bytesSent: connection.bytesSent || 0, | |
}, | |
})); | |
}; | |
connection.addEventListener('change', updateNetworkInfo); | |
// eslint-disable-next-line consistent-return | |
return () => connection.removeEventListener('change', updateNetworkInfo); | |
}, []); | |
// Remove all animation and process monitoring | |
useEffect(() => { | |
const metricsInterval = setInterval( | |
() => { | |
if (!energySaverMode) { | |
updateMetrics(); | |
} | |
}, | |
energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics, | |
); | |
return () => { | |
clearInterval(metricsInterval); | |
}; | |
}, [energySaverMode]); | |
const getUsageColor = (usage: number): string => { | |
if (usage > 80) { | |
return 'text-red-500'; | |
} | |
if (usage > 50) { | |
return 'text-yellow-500'; | |
} | |
return 'text-gray-500'; | |
}; | |
const renderUsageGraph = (data: number[], label: string, color: string) => { | |
const chartData = { | |
labels: metricsHistory.timestamps, | |
datasets: [ | |
{ | |
label, | |
data, | |
borderColor: color, | |
fill: false, | |
tension: 0.4, | |
}, | |
], | |
}; | |
const options = { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
y: { | |
beginAtZero: true, | |
max: 100, | |
grid: { | |
color: 'rgba(255, 255, 255, 0.1)', | |
}, | |
}, | |
x: { | |
grid: { | |
display: false, | |
}, | |
}, | |
}, | |
plugins: { | |
legend: { | |
display: false, | |
}, | |
}, | |
animation: { | |
duration: 0, | |
} as const, | |
}; | |
return ( | |
<div className="h-32"> | |
<Line data={chartData} options={options} /> | |
</div> | |
); | |
}; | |
useEffect((): (() => void) | undefined => { | |
if (!autoEnergySaver) { | |
// If auto mode is disabled, clear any forced energy saver state | |
setEnergySaverMode(false); | |
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]); | |
// Check for performance alerts | |
const checkPerformanceAlerts = (currentMetrics: SystemMetrics) => { | |
const newAlerts: PerformanceAlert[] = []; | |
// CPU alert | |
if (currentMetrics.cpu.usage > PERFORMANCE_THRESHOLDS.cpu.critical) { | |
newAlerts.push({ | |
type: 'error', | |
message: 'Critical CPU usage detected', | |
timestamp: Date.now(), | |
metric: 'cpu', | |
threshold: PERFORMANCE_THRESHOLDS.cpu.critical, | |
value: currentMetrics.cpu.usage, | |
}); | |
} | |
// Memory alert | |
if (currentMetrics.memory.percentage > PERFORMANCE_THRESHOLDS.memory.critical) { | |
newAlerts.push({ | |
type: 'error', | |
message: 'Critical memory usage detected', | |
timestamp: Date.now(), | |
metric: 'memory', | |
threshold: PERFORMANCE_THRESHOLDS.memory.critical, | |
value: currentMetrics.memory.percentage, | |
}); | |
} | |
// Performance alert | |
if (currentMetrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.critical) { | |
newAlerts.push({ | |
type: 'warning', | |
message: 'Very low frame rate detected', | |
timestamp: Date.now(), | |
metric: 'fps', | |
threshold: PERFORMANCE_THRESHOLDS.fps.critical, | |
value: currentMetrics.performance.fps, | |
}); | |
} | |
if (newAlerts.length > 0) { | |
setAlerts((prev) => [...prev, ...newAlerts]); | |
newAlerts.forEach((alert) => { | |
toast.warning(alert.message); | |
}); | |
} | |
}; | |
return ( | |
<div className="flex flex-col gap-6"> | |
{/* Power Profile Selection */} | |
<div className="flex flex-col gap-4"> | |
<div className="flex items-center justify-between"> | |
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Power Management</h3> | |
<div className="flex items-center gap-4"> | |
<div className="flex items-center gap-2"> | |
<input | |
type="checkbox" | |
id="autoEnergySaver" | |
checked={autoEnergySaver} | |
onChange={(e) => handleAutoEnergySaverChange(e.target.checked)} | |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700" | |
/> | |
<div className="i-ph:gauge-duotone w-4 h-4 text-bolt-elements-textSecondary" /> | |
<label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary"> | |
Auto Energy Saver | |
</label> | |
</div> | |
<div className="flex items-center gap-2"> | |
<input | |
type="checkbox" | |
id="energySaver" | |
checked={energySaverMode} | |
onChange={(e) => !autoEnergySaver && handleEnergySaverChange(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" | |
/> | |
<div className="i-ph:leaf-duotone w-4 h-4 text-bolt-elements-textSecondary" /> | |
<label | |
htmlFor="energySaver" | |
className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })} | |
> | |
Energy Saver | |
{energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>} | |
</label> | |
</div> | |
<div className="relative"> | |
<select | |
value={selectedProfile.name} | |
onChange={(e) => { | |
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) => ( | |
<option | |
key={profile.name} | |
value={profile.name} | |
className="py-2 px-3 bg-bolt-background-secondary dark:bg-[#1E1E1E] text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:bg-bolt-background-tertiary dark:hover:bg-bolt-backgroundDark-tertiary cursor-pointer" | |
> | |
{profile.name} | |
</option> | |
))} | |
</select> | |
<div className="absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none"> | |
<div | |
className={classNames('w-4 h-4 text-bolt-elements-textSecondary', { | |
'i-ph:lightning-fill text-yellow-500': selectedProfile.name === 'Performance', | |
'i-ph:scales-fill text-blue-500': selectedProfile.name === 'Balanced', | |
'i-ph:leaf-fill text-green-500': selectedProfile.name === 'Energy Saver', | |
})} | |
/> | |
</div> | |
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none"> | |
<div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75" /> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="text-sm text-bolt-elements-textSecondary">{selectedProfile.description}</div> | |
</div> | |
{/* System Health Score */} | |
<div className="flex flex-col gap-4"> | |
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Health</h3> | |
<div className="grid grid-cols-1 gap-4"> | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm text-bolt-elements-textSecondary">Health Score</span> | |
<span | |
className={classNames('text-lg font-medium', { | |
'text-green-500': metrics.health.score >= 80, | |
'text-yellow-500': metrics.health.score >= 60 && metrics.health.score < 80, | |
'text-red-500': metrics.health.score < 60, | |
})} | |
> | |
{metrics.health.score}% | |
</span> | |
</div> | |
{metrics.health.issues.length > 0 && ( | |
<div className="mt-2"> | |
<div className="text-sm font-medium text-bolt-elements-textSecondary mb-1">Issues:</div> | |
<ul className="text-sm text-bolt-elements-textSecondary space-y-1"> | |
{metrics.health.issues.map((issue, index) => ( | |
<li key={index} className="flex items-center gap-2"> | |
<div className="i-ph:warning-circle-fill text-yellow-500 w-4 h-4" /> | |
{issue} | |
</li> | |
))} | |
</ul> | |
</div> | |
)} | |
{metrics.health.suggestions.length > 0 && ( | |
<div className="mt-2"> | |
<div className="text-sm font-medium text-bolt-elements-textSecondary mb-1">Suggestions:</div> | |
<ul className="text-sm text-bolt-elements-textSecondary space-y-1"> | |
{metrics.health.suggestions.map((suggestion, index) => ( | |
<li key={index} className="flex items-center gap-2"> | |
<div className="i-ph:lightbulb-fill text-purple-500 w-4 h-4" /> | |
{suggestion} | |
</li> | |
))} | |
</ul> | |
</div> | |
)} | |
</div> | |
</div> | |
</div> | |
{/* System Metrics */} | |
<div className="flex flex-col gap-4"> | |
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Metrics</h3> | |
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> | |
{/* CPU Usage */} | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm text-bolt-elements-textSecondary">CPU Usage</span> | |
<span className={classNames('text-sm font-medium', getUsageColor(metrics.cpu.usage))}> | |
{Math.round(metrics.cpu.usage)}% | |
</span> | |
</div> | |
{renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')} | |
{metrics.cpu.temperature && ( | |
<div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
Temperature: {metrics.cpu.temperature}°C | |
</div> | |
)} | |
{metrics.cpu.frequency && ( | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
Frequency: {(metrics.cpu.frequency / 1000).toFixed(1)} GHz | |
</div> | |
)} | |
</div> | |
{/* Memory Usage */} | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm text-bolt-elements-textSecondary">Memory Usage</span> | |
<span className={classNames('text-sm font-medium', getUsageColor(metrics.memory.percentage))}> | |
{Math.round(metrics.memory.percentage)}% | |
</span> | |
</div> | |
{renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')} | |
<div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
Used: {formatBytes(metrics.memory.used)} | |
</div> | |
<div className="text-xs text-bolt-elements-textSecondary">Total: {formatBytes(metrics.memory.total)}</div> | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
Heap: {formatBytes(metrics.memory.heap.used)} / {formatBytes(metrics.memory.heap.total)} | |
</div> | |
</div> | |
{/* Performance */} | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm text-bolt-elements-textSecondary">Performance</span> | |
<span | |
className={classNames('text-sm font-medium', { | |
'text-red-500': metrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.critical, | |
'text-yellow-500': metrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.warning, | |
'text-green-500': metrics.performance.fps >= PERFORMANCE_THRESHOLDS.fps.warning, | |
})} | |
> | |
{Math.round(metrics.performance.fps)} FPS | |
</span> | |
</div> | |
<div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
Page Load: {(metrics.performance.pageLoad / 1000).toFixed(2)}s | |
</div> | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
DOM Ready: {(metrics.performance.domReady / 1000).toFixed(2)}s | |
</div> | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
TTFB: {(metrics.performance.timing.ttfb / 1000).toFixed(2)}s | |
</div> | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
Resources: {metrics.performance.resources.total} ({formatBytes(metrics.performance.resources.size)}) | |
</div> | |
</div> | |
{/* Network */} | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm text-bolt-elements-textSecondary">Network</span> | |
<span className="text-sm font-medium text-bolt-elements-textPrimary"> | |
{metrics.network.downlink.toFixed(1)} Mbps | |
</span> | |
</div> | |
{renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')} | |
<div className="text-xs text-bolt-elements-textSecondary mt-2">Type: {metrics.network.type}</div> | |
<div className="text-xs text-bolt-elements-textSecondary">Latency: {metrics.network.latency}ms</div> | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
Received: {formatBytes(metrics.network.bytesReceived)} | |
</div> | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
Sent: {formatBytes(metrics.network.bytesSent)} | |
</div> | |
</div> | |
</div> | |
{/* Battery Section */} | |
{metrics.battery && ( | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm text-bolt-elements-textSecondary">Battery</span> | |
<div className="flex items-center gap-2"> | |
{metrics.battery.charging && <div className="i-ph:lightning-fill w-4 h-4 text-bolt-action-primary" />} | |
<span | |
className={classNames( | |
'text-sm font-medium', | |
metrics.battery.level > 20 ? 'text-bolt-elements-textPrimary' : 'text-red-500', | |
)} | |
> | |
{Math.round(metrics.battery.level)}% | |
</span> | |
</div> | |
</div> | |
{renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')} | |
{metrics.battery.timeRemaining && ( | |
<div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
{metrics.battery.charging ? 'Time to full: ' : 'Time remaining: '} | |
{formatTime(metrics.battery.timeRemaining)} | |
</div> | |
)} | |
{metrics.battery.temperature && ( | |
<div className="text-xs text-bolt-elements-textSecondary"> | |
Temperature: {metrics.battery.temperature}°C | |
</div> | |
)} | |
{metrics.battery.cycles && ( | |
<div className="text-xs text-bolt-elements-textSecondary">Charge cycles: {metrics.battery.cycles}</div> | |
)} | |
{metrics.battery.health && ( | |
<div className="text-xs text-bolt-elements-textSecondary">Battery health: {metrics.battery.health}%</div> | |
)} | |
</div> | |
)} | |
{/* Performance Alerts */} | |
{alerts.length > 0 && ( | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm font-medium text-bolt-elements-textPrimary">Recent Alerts</span> | |
<button | |
onClick={() => setAlerts([])} | |
className="text-xs text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary" | |
> | |
Clear All | |
</button> | |
</div> | |
<div className="space-y-2"> | |
{alerts.slice(-5).map((alert, index) => ( | |
<div | |
key={index} | |
className={classNames('flex items-center gap-2 text-sm', { | |
'text-red-500': alert.type === 'error', | |
'text-yellow-500': alert.type === 'warning', | |
'text-blue-500': alert.type === 'info', | |
})} | |
> | |
<div | |
className={classNames('w-4 h-4', { | |
'i-ph:warning-circle-fill': alert.type === 'warning', | |
'i-ph:x-circle-fill': alert.type === 'error', | |
'i-ph:info-fill': alert.type === 'info', | |
})} | |
/> | |
<span>{alert.message}</span> | |
<span className="text-xs text-bolt-elements-textSecondary ml-auto"> | |
{new Date(alert.timestamp).toLocaleTimeString()} | |
</span> | |
</div> | |
))} | |
</div> | |
</div> | |
)} | |
{/* Energy Savings */} | |
{energySaverMode && ( | |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">Energy Savings</h4> | |
<div className="grid grid-cols-3 gap-4"> | |
<div> | |
<span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span> | |
<p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p> | |
</div> | |
<div> | |
<span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span> | |
<p className="text-lg font-medium text-bolt-elements-textPrimary"> | |
{Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s | |
</p> | |
</div> | |
<div> | |
<span className="text-sm text-bolt-elements-textSecondary">Energy Saved</span> | |
<p className="text-lg font-medium text-bolt-elements-textPrimary"> | |
{energySavings.estimatedEnergySaved.toFixed(2)} mWh | |
</p> | |
</div> | |
</div> | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
}; | |
export default React.memo(TaskManagerTab); | |
// Helper function to format bytes | |
const formatBytes = (bytes: number): string => { | |
if (bytes === 0) { | |
return '0 B'; | |
} | |
const k = 1024; | |
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | |
const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; | |
}; | |
// Helper function to format time | |
const formatTime = (seconds: number): string => { | |
if (!isFinite(seconds) || seconds === 0) { | |
return 'Unknown'; | |
} | |
const hours = Math.floor(seconds / 3600); | |
const minutes = Math.floor((seconds % 3600) / 60); | |
if (hours > 0) { | |
return `${hours}h ${minutes}m`; | |
} | |
return `${minutes}m`; | |
}; | |