diff --git a/src-tauri/capabilities/system-monitor-window.json b/src-tauri/capabilities/system-monitor-window.json new file mode 100644 index 000000000..572cc0840 --- /dev/null +++ b/src-tauri/capabilities/system-monitor-window.json @@ -0,0 +1,14 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "system-monitor-window", + "description": "enables permissions for the system monitor window", + "windows": ["system-monitor-window"], + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "core:window:allow-set-theme", + "log:default", + "core:webview:allow-create-webview-window", + "core:window:allow-set-focus" + ] +} diff --git a/web-app/src/constants/routes.ts b/web-app/src/constants/routes.ts index 59dbb69b8..a0ffa6581 100644 --- a/web-app/src/constants/routes.ts +++ b/web-app/src/constants/routes.ts @@ -17,5 +17,6 @@ export const route = { }, hub: '/hub', localApiServerlogs: '/local-api-server/logs', + systemMonitor: '/system-monitor', threadsDetail: '/threads/$threadId', } diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index aa76edcb8..b00147bbe 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -37,7 +37,7 @@ export const useChat = () => { const provider = useMemo(() => { return getProviderByName(selectedProvider) }, [selectedProvider, getProviderByName]) - + const getCurrentThread = useCallback(async () => { let currentThread = retrieveThread() if (!currentThread) { diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index e43f98649..3da009e3d 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { clsx, type ClassValue } from 'clsx' +import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { @@ -131,4 +131,14 @@ export const toGigabytes = ( } else { return input + (options?.hideUnit ? '' : 'B') } -} \ No newline at end of file +} + +export function formatMegaBytes(mb: number) { + const tb = mb / (1024 * 1024) + if (tb >= 1) { + return `${tb.toFixed(2)} TB` + } else { + const gb = mb / 1024 + return `${gb.toFixed(2)} GB` + } +} diff --git a/web-app/src/routeTree.gen.ts b/web-app/src/routeTree.gen.ts index 3e18921a1..61e30f007 100644 --- a/web-app/src/routeTree.gen.ts +++ b/web-app/src/routeTree.gen.ts @@ -11,6 +11,7 @@ // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as SystemMonitorImport } from './routes/system-monitor' import { Route as HubImport } from './routes/hub' import { Route as AssistantImport } from './routes/assistant' import { Route as IndexImport } from './routes/index' @@ -29,6 +30,12 @@ import { Route as SettingsProvidersProviderNameImport } from './routes/settings/ // Create/Update Routes +const SystemMonitorRoute = SystemMonitorImport.update({ + id: '/system-monitor', + path: '/system-monitor', + getParentRoute: () => rootRoute, +} as any) + const HubRoute = HubImport.update({ id: '/hub', path: '/hub', @@ -145,6 +152,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HubImport parentRoute: typeof rootRoute } + '/system-monitor': { + id: '/system-monitor' + path: '/system-monitor' + fullPath: '/system-monitor' + preLoaderRoute: typeof SystemMonitorImport + parentRoute: typeof rootRoute + } '/local-api-server/logs': { id: '/local-api-server/logs' path: '/local-api-server/logs' @@ -238,6 +252,7 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/assistant': typeof AssistantRoute '/hub': typeof HubRoute + '/system-monitor': typeof SystemMonitorRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute @@ -256,6 +271,7 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/assistant': typeof AssistantRoute '/hub': typeof HubRoute + '/system-monitor': typeof SystemMonitorRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute @@ -275,6 +291,7 @@ export interface FileRoutesById { '/': typeof IndexRoute '/assistant': typeof AssistantRoute '/hub': typeof HubRoute + '/system-monitor': typeof SystemMonitorRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute @@ -295,6 +312,7 @@ export interface FileRouteTypes { | '/' | '/assistant' | '/hub' + | '/system-monitor' | '/local-api-server/logs' | '/settings/appearance' | '/settings/extensions' @@ -312,6 +330,7 @@ export interface FileRouteTypes { | '/' | '/assistant' | '/hub' + | '/system-monitor' | '/local-api-server/logs' | '/settings/appearance' | '/settings/extensions' @@ -329,6 +348,7 @@ export interface FileRouteTypes { | '/' | '/assistant' | '/hub' + | '/system-monitor' | '/local-api-server/logs' | '/settings/appearance' | '/settings/extensions' @@ -348,6 +368,7 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute AssistantRoute: typeof AssistantRoute HubRoute: typeof HubRoute + SystemMonitorRoute: typeof SystemMonitorRoute LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute SettingsAppearanceRoute: typeof SettingsAppearanceRoute SettingsExtensionsRoute: typeof SettingsExtensionsRoute @@ -366,6 +387,7 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AssistantRoute: AssistantRoute, HubRoute: HubRoute, + SystemMonitorRoute: SystemMonitorRoute, LocalApiServerLogsRoute: LocalApiServerLogsRoute, SettingsAppearanceRoute: SettingsAppearanceRoute, SettingsExtensionsRoute: SettingsExtensionsRoute, @@ -393,6 +415,7 @@ export const routeTree = rootRoute "/", "/assistant", "/hub", + "/system-monitor", "/local-api-server/logs", "/settings/appearance", "/settings/extensions", @@ -416,6 +439,9 @@ export const routeTree = rootRoute "/hub": { "filePath": "hub.tsx" }, + "/system-monitor": { + "filePath": "system-monitor.tsx" + }, "/local-api-server/logs": { "filePath": "local-api-server/logs.tsx" }, diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index 666df1167..413a4c577 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -18,9 +18,6 @@ export const Route = createRootRoute({ const AppLayout = () => { return ( - - -
{/* Fake absolute panel top to enable window drag */} @@ -62,13 +59,17 @@ const LogsLayout = () => { function RootLayout() { const router = useRouterState() const isLocalAPIServerLogsRoute = - router.location.pathname === route.localApiServerlogs + router.location.pathname === route.localApiServerlogs || + router.location.pathname === route.systemMonitor return ( + + + {isLocalAPIServerLogsRoute ? : } {/* */} diff --git a/web-app/src/routes/settings/hardware.tsx b/web-app/src/routes/settings/hardware.tsx index 257cbc512..9f7a446ef 100644 --- a/web-app/src/routes/settings/hardware.tsx +++ b/web-app/src/routes/settings/hardware.tsx @@ -24,23 +24,18 @@ import { useSortable, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { IconGripVertical } from '@tabler/icons-react' +import { + IconGripVertical, + IconDeviceDesktopAnalytics, +} from '@tabler/icons-react' import { getHardwareInfo } from '@/services/hardware' +import { WebviewWindow } from '@tauri-apps/api/webviewWindow' +import { formatMegaBytes } from '@/lib/utils' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.hardware as any)({ component: Hardware, }) -// Format bytes to a human-readable format -function formatMegaBytes(mb: number) { - const tb = mb / (1024 * 1024) - if (tb >= 1) { - return `${tb.toFixed(2)} TB` - } else { - const gb = mb / 1024 - return `${gb.toFixed(2)} GB` - } -} function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) { const { @@ -168,10 +163,56 @@ function Hardware() { return () => clearInterval(intervalId) }, [setHardwareData, updateCPUUsage, updateRAMAvailable]) + const handleClickSystemMonitor = async () => { + try { + // Check if system monitor window already exists + const existingWindow = await WebviewWindow.getByLabel( + 'system-monitor-window' + ) + + if (existingWindow) { + // If window exists, focus it + await existingWindow.setFocus() + console.log('Focused existing system monitor window') + } else { + // Create a new system monitor window + const monitorWindow = new WebviewWindow('system-monitor-window', { + url: route.systemMonitor, + title: 'System Monitor - Jan', + width: 900, + height: 600, + resizable: true, + center: true, + }) + + // Listen for window creation + monitorWindow.once('tauri://created', () => { + console.log('System monitor window created') + }) + + // Listen for window errors + monitorWindow.once('tauri://error', (e) => { + console.error('Error creating system monitor window:', e) + }) + } + } catch (error) { + console.error('Failed to open system monitor window:', error) + } + } + return (
-

{t('common.settings')}

+
+

{t('common.settings')}

+
+ +

System monitor

+
+
diff --git a/web-app/src/routes/settings/local-api-server.tsx b/web-app/src/routes/settings/local-api-server.tsx index b9e607721..28a6cd082 100644 --- a/web-app/src/routes/settings/local-api-server.tsx +++ b/web-app/src/routes/settings/local-api-server.tsx @@ -115,7 +115,13 @@ function LocalAPIServer() { Start an OpenAI-compatible local HTTP server.

-
diff --git a/web-app/src/routes/system-monitor.tsx b/web-app/src/routes/system-monitor.tsx new file mode 100644 index 000000000..a551f9aea --- /dev/null +++ b/web-app/src/routes/system-monitor.tsx @@ -0,0 +1,237 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useEffect } from 'react' +import { useHardware } from '@/hooks/useHardware' +import { getHardwareInfo } from '@/services/hardware' +import { Progress } from '@/components/ui/progress' +import type { HardwareData } from '@/hooks/useHardware' +import { route } from '@/constants/routes' +import { formatMegaBytes } from '@/lib/utils' +import { IconDeviceDesktopAnalytics } from '@tabler/icons-react' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const Route = createFileRoute(route.systemMonitor as any)({ + component: SystemMonitor, +}) + +function SystemMonitor() { + const { hardwareData, setHardwareData, updateCPUUsage, updateRAMAvailable } = + useHardware() + + useEffect(() => { + // Initial data fetch + getHardwareInfo().then((data) => { + setHardwareData(data as unknown as HardwareData) + }) + + // Set up interval for real-time updates + const intervalId = setInterval(() => { + getHardwareInfo().then((data) => { + setHardwareData(data as unknown as HardwareData) + updateCPUUsage(data.cpu.usage) + updateRAMAvailable(data.ram.available) + }) + }, 5000) + + return () => clearInterval(intervalId) + }, [setHardwareData, updateCPUUsage, updateRAMAvailable]) + + // Calculate RAM usage percentage + const ramUsagePercentage = + ((hardwareData.ram.total - hardwareData.ram.available) / + hardwareData.ram.total) * + 100 + + return ( +
+
+ +

System Monitor

+
+ +
+ {/* CPU Usage Card */} +
+

+ CPU Usage +

+
+
+ Model + + {hardwareData.cpu.model} + +
+
+ Cores + + {hardwareData.cpu.cores} + +
+
+ Architecture + {hardwareData.cpu.arch} +
+
+
+ Current Usage + + {hardwareData.cpu.usage.toFixed(2)}% + +
+ +
+
+
+ + {/* RAM Usage Card */} +
+

+ Memory Usage +

+
+
+ Total RAM + + {formatMegaBytes(hardwareData.ram.total)} + +
+
+ Available RAM + + {formatMegaBytes(hardwareData.ram.available)} + +
+
+ Used RAM + + {formatMegaBytes( + hardwareData.ram.total - hardwareData.ram.available + )} + +
+
+
+ Current Usage + + {ramUsagePercentage.toFixed(2)}% + +
+ +
+
+
+
+ + {/* Current Active Model Section */} +
+

+ Current Active Model +

+
+
+ GPT-4o +
+
+
+ Provider + OpenAI +
+
+ Context Length + 128K tokens +
+
+ Status + +
+ Running +
+
+
+
+
+
+ + {/* Active GPUs Section */} +
+

+ Active GPUs +

+ {hardwareData.gpus.length > 0 ? ( +
+ {hardwareData.gpus + .filter((gpu) => gpu.activated) + .map((gpu, index) => ( +
+
+ + {gpu.name} + +
Active
+
+
+
+ VRAM Usage + + {formatMegaBytes(gpu.total_vram - gpu.free_vram)} /{' '} + {formatMegaBytes(gpu.total_vram)} + +
+
+ + Driver Version: + + + {gpu.additional_information.driver_version} + +
+
+ + Compute Capability: + + + {gpu.additional_information.compute_cap} + +
+
+ + 80 + ? 'bg-red-500/30' + : ((gpu.total_vram - gpu.free_vram) / + gpu.total_vram) * + 100 > + 50 + ? 'bg-yellow-500/30' + : 'bg-green-500/30' + }`} + /> +
+
+
+ ))} +
+ ) : ( +
+ No GPUs detected +
+ )} + {hardwareData.gpus.length > 0 && + !hardwareData.gpus.some((gpu) => gpu.activated) && ( +
+ No active GPUs. All GPUs are currently disabled. +
+ )} +
+
+ ) +}