chore: intial new window system monitor

This commit is contained in:
Faisal Amir 2025-05-21 12:27:54 +07:00
parent 81c4dc516b
commit 2812a8978a
9 changed files with 357 additions and 20 deletions

View File

@ -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"
]
}

View File

@ -17,5 +17,6 @@ export const route = {
},
hub: '/hub',
localApiServerlogs: '/local-api-server/logs',
systemMonitor: '/system-monitor',
threadsDetail: '/threads/$threadId',
}

View File

@ -37,7 +37,7 @@ export const useChat = () => {
const provider = useMemo(() => {
return getProviderByName(selectedProvider)
}, [selectedProvider, getProviderByName])
const getCurrentThread = useCallback(async () => {
let currentThread = retrieveThread()
if (!currentThread) {

View File

@ -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')
}
}
}
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`
}
}

View File

@ -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"
},

View File

@ -18,9 +18,6 @@ export const Route = createRootRoute({
const AppLayout = () => {
return (
<Fragment>
<ExtensionProvider>
<DataProvider />
</ExtensionProvider>
<KeyboardShortcutsProvider />
<main className="relative h-svh text-sm antialiased select-none bg-app">
{/* 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 (
<Fragment>
<ThemeProvider />
<AppearanceProvider />
<ToasterProvider />
<ExtensionProvider>
<DataProvider />
</ExtensionProvider>
{isLocalAPIServerLogsRoute ? <LogsLayout /> : <AppLayout />}
{/* <TanStackRouterDevtools position="bottom-right" /> */}
</Fragment>

View File

@ -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 {
@ -171,7 +166,54 @@ function Hardware() {
return (
<div className="flex flex-col h-full">
<HeaderPage>
<h1 className="font-medium">{t('common.settings')}</h1>
<div className="flex items-center gap-2 justify-between w-full pr-3">
<h1 className="font-medium">{t('common.settings')}</h1>
<div
className="flex items-center gap-1 hover:bg-main-view-fg/8 px-1.5 py-0.5 rounded relative z-10 cursor-pointer"
onClick={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)
}
}}
>
<IconDeviceDesktopAnalytics className="text-main-view-fg/50 size-5" />
<p>System monitor</p>
</div>
</div>
</HeaderPage>
<div className="flex h-full w-full">
<SettingsMenu />

View File

@ -115,7 +115,13 @@ function LocalAPIServer() {
Start an OpenAI-compatible local HTTP server.
</p>
</div>
<Button onClick={toggleAPIServer}>
<Button
onClick={toggleAPIServer}
variant={
serverStatus === 'running' ? 'destructive' : 'default'
}
size="sm"
>
{`${serverStatus === 'running' ? 'Stop' : 'Start'}`} Server
</Button>
</div>

View File

@ -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 (
<div className="flex flex-col h-full bg-main-view overflow-y-auto p-6">
<div className="flex items-center mb-4 gap-2">
<IconDeviceDesktopAnalytics className="text-main-view-fg/80 size-6" />
<h1 className="text-xl font-bold text-main-view-fg">System Monitor</h1>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* CPU Usage Card */}
<div className="bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
<h2 className="text-base font-semibold text-main-view-fg mb-4">
CPU Usage
</h2>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Model</span>
<span className="text-main-view-fg">
{hardwareData.cpu.model}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Cores</span>
<span className="text-main-view-fg">
{hardwareData.cpu.cores}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Architecture</span>
<span className="text-main-view-fg">{hardwareData.cpu.arch}</span>
</div>
<div className="mt-4">
<div className="flex justify-between items-center mb-2">
<span className="text-main-view-fg/70">Current Usage</span>
<span className="text-main-view-fg font-bold">
{hardwareData.cpu.usage.toFixed(2)}%
</span>
</div>
<Progress value={hardwareData.cpu.usage} className="h-3 w-full" />
</div>
</div>
</div>
{/* RAM Usage Card */}
<div className="bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
<h2 className="text-base font-semibold text-main-view-fg mb-4">
Memory Usage
</h2>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Total RAM</span>
<span className="text-main-view-fg">
{formatMegaBytes(hardwareData.ram.total)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Available RAM</span>
<span className="text-main-view-fg">
{formatMegaBytes(hardwareData.ram.available)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Used RAM</span>
<span className="text-main-view-fg">
{formatMegaBytes(
hardwareData.ram.total - hardwareData.ram.available
)}
</span>
</div>
<div className="mt-4">
<div className="flex justify-between items-center mb-2">
<span className="text-main-view-fg/70">Current Usage</span>
<span className="text-main-view-fg font-bold">
{ramUsagePercentage.toFixed(2)}%
</span>
</div>
<Progress value={ramUsagePercentage} className="h-3 w-full" />
</div>
</div>
</div>
</div>
{/* Current Active Model Section */}
<div className="mt-6 bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
<h2 className="text-base font-semibold text-main-view-fg mb-4">
Current Active Model
</h2>
<div className="bg-main-view-fg/3 rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<span className="font-semibold text-main-view-fg">GPT-4o</span>
</div>
<div className="flex flex-col gap-2 mt-3">
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Provider</span>
<span className="text-main-view-fg">OpenAI</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Context Length</span>
<span className="text-main-view-fg">128K tokens</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">Status</span>
<span className="text-main-view-fg">
<div className="bg-green-500/20 px-1 font-bold py-0.5 rounded text-green-700 text-xs">
Running
</div>
</span>
</div>
</div>
</div>
</div>
{/* Active GPUs Section */}
<div className="mt-6 bg-main-view-fg/2 rounded-lg p-6 shadow-sm">
<h2 className="text-base font-semibold text-main-view-fg mb-4">
Active GPUs
</h2>
{hardwareData.gpus.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{hardwareData.gpus
.filter((gpu) => gpu.activated)
.map((gpu, index) => (
<div
key={gpu.id || index}
className="bg-main-view-fg/3 rounded-lg p-4"
>
<div className="flex justify-between items-center mb-2">
<span className="font-semibold text-main-view-fg">
{gpu.name}
</span>
<div className="bg-green-500/20">Active</div>
</div>
<div className="flex flex-col gap-2 mt-3">
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">VRAM Usage</span>
<span className="text-main-view-fg">
{formatMegaBytes(gpu.total_vram - gpu.free_vram)} /{' '}
{formatMegaBytes(gpu.total_vram)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">
Driver Version:
</span>
<span className="text-main-view-fg">
{gpu.additional_information.driver_version}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">
Compute Capability:
</span>
<span className="text-main-view-fg">
{gpu.additional_information.compute_cap}
</span>
</div>
<div className="mt-2">
<Progress
value={
((gpu.total_vram - gpu.free_vram) / gpu.total_vram) *
100
}
className={`h-2 w-full ${
((gpu.total_vram - gpu.free_vram) / gpu.total_vram) *
100 >
80
? 'bg-red-500/30'
: ((gpu.total_vram - gpu.free_vram) /
gpu.total_vram) *
100 >
50
? 'bg-yellow-500/30'
: 'bg-green-500/30'
}`}
/>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center text-main-view-fg/50 py-4">
No GPUs detected
</div>
)}
{hardwareData.gpus.length > 0 &&
!hardwareData.gpus.some((gpu) => gpu.activated) && (
<div className="text-center text-main-view-fg/50 py-4">
No active GPUs. All GPUs are currently disabled.
</div>
)}
</div>
</div>
)
}