diff --git a/web-app/src/constants/localStorage.ts b/web-app/src/constants/localStorage.ts index a60d4caa2..06ba50fbd 100644 --- a/web-app/src/constants/localStorage.ts +++ b/web-app/src/constants/localStorage.ts @@ -1,4 +1,4 @@ -export const localStoregeKey = { +export const localStorageKey = { LeftPanel: 'left-panel', threads: 'threads', messages: 'messages', @@ -10,4 +10,5 @@ export const localStoregeKey = { settingCodeBlock: 'setting-code-block', settingMCPSevers: 'setting-mcp-servers', settingLocalApiServer: 'setting-local-api-server', + settingHardware: 'setting-hardware', } diff --git a/web-app/src/constants/routes.ts b/web-app/src/constants/routes.ts index 95566bd12..59dbb69b8 100644 --- a/web-app/src/constants/routes.ts +++ b/web-app/src/constants/routes.ts @@ -13,6 +13,7 @@ export const route = { local_api_server: '/settings/local-api-server', mcp_servers: '/settings/mcp-servers', https_proxy: '/settings/https-proxy', + hardware: '/settings/hardware', }, hub: '/hub', localApiServerlogs: '/local-api-server/logs', diff --git a/web-app/src/containers/SettingsMenu.tsx b/web-app/src/containers/SettingsMenu.tsx index ab4025464..4494aa44c 100644 --- a/web-app/src/containers/SettingsMenu.tsx +++ b/web-app/src/containers/SettingsMenu.tsx @@ -20,6 +20,10 @@ const menuSettings = [ title: 'common.keyboardShortcuts', route: route.settings.shortcuts, }, + { + title: 'Hardware', + route: route.settings.hardware, + }, { title: 'MCP Servers', route: route.settings.mcp_servers, diff --git a/web-app/src/hooks/useAppearance.ts b/web-app/src/hooks/useAppearance.ts index 0935a637a..80d4863eb 100644 --- a/web-app/src/hooks/useAppearance.ts +++ b/web-app/src/hooks/useAppearance.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' import { RgbaColor } from 'react-colorful' import { rgb, oklch, formatCss } from 'culori' import { useTheme } from './useTheme' @@ -533,7 +533,7 @@ export const useAppearance = create()( } }, { - name: localStoregeKey.settingAppearance, + name: localStorageKey.settingAppearance, storage: createJSONStorage(() => localStorage), // Apply settings when hydrating from storage onRehydrateStorage: () => (state) => { diff --git a/web-app/src/hooks/useAssistant.ts b/web-app/src/hooks/useAssistant.ts index e22083029..59c1fa5f2 100644 --- a/web-app/src/hooks/useAssistant.ts +++ b/web-app/src/hooks/useAssistant.ts @@ -1,8 +1,7 @@ -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' import { create } from 'zustand' import { persist } from 'zustand/middleware' - interface AssistantState { assistants: Assistant[] currentAssistant: Assistant @@ -42,7 +41,7 @@ export const useAssistant = create()( }, }), { - name: localStoregeKey.assistant, + name: localStorageKey.assistant, } ) ) diff --git a/web-app/src/hooks/useCodeblock.ts b/web-app/src/hooks/useCodeblock.ts index 7fac15297..cfc35fa7e 100644 --- a/web-app/src/hooks/useCodeblock.ts +++ b/web-app/src/hooks/useCodeblock.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' export type CodeBlockStyle = string @@ -39,7 +39,7 @@ export const useCodeblock = create()( } }, { - name: localStoregeKey.settingCodeBlock, + name: localStorageKey.settingCodeBlock, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useGeneralSetting.ts b/web-app/src/hooks/useGeneralSetting.ts index 2e22e38d6..9ed0abaa5 100644 --- a/web-app/src/hooks/useGeneralSetting.ts +++ b/web-app/src/hooks/useGeneralSetting.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' type LeftPanelStoreState = { // @ts-ignore @@ -20,7 +20,7 @@ export const useGeneralSetting = create()( setCurrentLanguage: (value) => set({ currentLanguage: value }), }), { - name: localStoregeKey.settingGeneral, + name: localStorageKey.settingGeneral, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useHardware.ts b/web-app/src/hooks/useHardware.ts new file mode 100644 index 000000000..67b6caa9a --- /dev/null +++ b/web-app/src/hooks/useHardware.ts @@ -0,0 +1,217 @@ +import { create } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware' +import { localStorageKey } from '@/constants/localStorage' + +// Hardware data types +export interface CPU { + arch: string + cores: number + instructions: string[] + model: string + usage: number +} + +export interface GPUAdditionalInfo { + compute_cap: string + driver_version: string +} + +export interface GPU { + activated: boolean + additional_information: GPUAdditionalInfo + free_vram: number + id: string + name: string + total_vram: number + uuid: string + version: string +} + +export interface OS { + name: string + version: string +} + +export interface RAM { + available: number + total: number +} + +export interface HardwareData { + cpu: CPU + gpus: GPU[] + os: OS + ram: RAM +} + +// Default values +const defaultHardwareData: HardwareData = { + cpu: { + arch: '', + cores: 0, + instructions: [], + model: '', + usage: 0, + }, + gpus: [], + os: { + name: '', + version: '', + }, + ram: { + available: 0, + total: 0, + }, +} + +interface HardwareStore { + // Hardware data + hardwareData: HardwareData + + // Update functions + setCPU: (cpu: CPU) => void + setGPUs: (gpus: GPU[]) => void + setOS: (os: OS) => void + setRAM: (ram: RAM) => void + + // Update entire hardware data at once + setHardwareData: (data: HardwareData) => void + + // Update individual GPU + updateGPU: (index: number, gpu: GPU) => void + + // Update CPU usage + updateCPUUsage: (usage: number) => void + + // Update RAM available + updateRAMAvailable: (available: number) => void + + // Toggle GPU activation + toggleGPUActivation: (index: number) => void + + // Reorder GPUs + reorderGPUs: (oldIndex: number, newIndex: number) => void +} + +export const useHardware = create()( + persist( + (set) => ({ + hardwareData: defaultHardwareData, + + setCPU: (cpu) => + set((state) => ({ + hardwareData: { + ...state.hardwareData, + cpu, + }, + })), + + setGPUs: (gpus) => + set((state) => ({ + hardwareData: { + ...state.hardwareData, + gpus, + }, + })), + + setOS: (os) => + set((state) => ({ + hardwareData: { + ...state.hardwareData, + os, + }, + })), + + setRAM: (ram) => + set((state) => ({ + hardwareData: { + ...state.hardwareData, + ram, + }, + })), + + setHardwareData: (data) => + set({ + hardwareData: data, + }), + + updateGPU: (index, gpu) => + set((state) => { + const newGPUs = [...state.hardwareData.gpus] + if (index >= 0 && index < newGPUs.length) { + newGPUs[index] = gpu + } + return { + hardwareData: { + ...state.hardwareData, + gpus: newGPUs, + }, + } + }), + + updateCPUUsage: (usage) => + set((state) => ({ + hardwareData: { + ...state.hardwareData, + cpu: { + ...state.hardwareData.cpu, + usage, + }, + }, + })), + + updateRAMAvailable: (available) => + set((state) => ({ + hardwareData: { + ...state.hardwareData, + ram: { + ...state.hardwareData.ram, + available, + }, + }, + })), + + toggleGPUActivation: (index) => + set((state) => { + const newGPUs = [...state.hardwareData.gpus] + if (index >= 0 && index < newGPUs.length) { + newGPUs[index] = { + ...newGPUs[index], + activated: !newGPUs[index].activated, + } + } + return { + hardwareData: { + ...state.hardwareData, + gpus: newGPUs, + }, + } + }), + + reorderGPUs: (oldIndex, newIndex) => + set((state) => { + const newGPUs = [...state.hardwareData.gpus] + // Move the GPU from oldIndex to newIndex + if ( + oldIndex >= 0 && + oldIndex < newGPUs.length && + newIndex >= 0 && + newIndex < newGPUs.length + ) { + const [removed] = newGPUs.splice(oldIndex, 1) + newGPUs.splice(newIndex, 0, removed) + } + return { + hardwareData: { + ...state.hardwareData, + gpus: newGPUs, + }, + } + }), + }), + { + name: localStorageKey.settingHardware, + storage: createJSONStorage(() => localStorage), + } + ) +) diff --git a/web-app/src/hooks/useLeftPanel.ts b/web-app/src/hooks/useLeftPanel.ts index b27bfafca..46440e0e9 100644 --- a/web-app/src/hooks/useLeftPanel.ts +++ b/web-app/src/hooks/useLeftPanel.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' type LeftPanelStoreState = { open: boolean @@ -14,7 +14,7 @@ export const useLeftPanel = create()( setLeftPanel: (value) => set({ open: value }), }), { - name: localStoregeKey.LeftPanel, + name: localStorageKey.LeftPanel, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useLocalApiServer.ts b/web-app/src/hooks/useLocalApiServer.ts index 65d1c1efa..d6e8615d1 100644 --- a/web-app/src/hooks/useLocalApiServer.ts +++ b/web-app/src/hooks/useLocalApiServer.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' type LocalApiServerState = { // Run local API server once app opens @@ -40,7 +40,7 @@ export const useLocalApiServer = create()( setVerboseLogs: (value) => set({ verboseLogs: value }), }), { - name: localStoregeKey.settingLocalApiServer, + name: localStorageKey.settingLocalApiServer, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useMCPServers.ts b/web-app/src/hooks/useMCPServers.ts index 4871495c1..b63c7c0de 100644 --- a/web-app/src/hooks/useMCPServers.ts +++ b/web-app/src/hooks/useMCPServers.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' import { updateMCPConfig } from '@/services/mcp' // Define the structure of an MCP server configuration @@ -77,7 +77,7 @@ export const useMCPServers = create()( }), }), { - name: localStoregeKey.settingMCPSevers, // Using existing key for now + name: localStorageKey.settingMCPSevers, // Using existing key for now storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useMessages.ts b/web-app/src/hooks/useMessages.ts index 10da6e099..21d75f08b 100644 --- a/web-app/src/hooks/useMessages.ts +++ b/web-app/src/hooks/useMessages.ts @@ -1,7 +1,7 @@ import { create } from 'zustand' import { ThreadMessage } from '@janhq/core' import { createJSONStorage, persist } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' import { createMessage, deleteMessage as deleteMessageExt, @@ -57,7 +57,7 @@ export const useMessages = create()( }, }), { - name: localStoregeKey.messages, + name: localStorageKey.messages, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useModelProvider.ts b/web-app/src/hooks/useModelProvider.ts index 24a2084cc..eadbb2f08 100644 --- a/web-app/src/hooks/useModelProvider.ts +++ b/web-app/src/hooks/useModelProvider.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' type ModelProviderState = { providers: ModelProvider[] @@ -116,7 +116,7 @@ export const useModelProvider = create()( }, }), { - name: localStoregeKey.modelProvider, + name: localStorageKey.modelProvider, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useProxyConfig.ts b/web-app/src/hooks/useProxyConfig.ts index ed7201265..8863bc9ef 100644 --- a/web-app/src/hooks/useProxyConfig.ts +++ b/web-app/src/hooks/useProxyConfig.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' type ProxyConfigState = { proxyEnabled: boolean @@ -52,7 +52,7 @@ export const useProxyConfig = create()( setNoProxy: (noProxy) => set({ noProxy }), }), { - name: localStoregeKey.settingLocalApiServer, + name: localStorageKey.settingLocalApiServer, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useTheme.ts b/web-app/src/hooks/useTheme.ts index 00cab624f..aaf855d3b 100644 --- a/web-app/src/hooks/useTheme.ts +++ b/web-app/src/hooks/useTheme.ts @@ -1,7 +1,7 @@ import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' import { getCurrentWindow, Theme } from '@tauri-apps/api/window' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' // Function to check if OS prefers dark mode export const checkOSDarkMode = (): boolean => { @@ -48,7 +48,7 @@ export const useTheme = create()( return initialState }, { - name: localStoregeKey.theme, + name: localStorageKey.theme, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/hooks/useThreads.ts b/web-app/src/hooks/useThreads.ts index f83a9d69d..a88f3ce4d 100644 --- a/web-app/src/hooks/useThreads.ts +++ b/web-app/src/hooks/useThreads.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' import { ulid } from 'ulidx' import { createThread, deleteThread, updateThread } from '@/services/threads' import Fuse from 'fuse.js' @@ -247,7 +247,7 @@ export const useThreads = create()( }, }), { - name: localStoregeKey.threads, + name: localStorageKey.threads, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/i18n.ts b/web-app/src/i18n.ts index a5d4367be..854ce84e0 100644 --- a/web-app/src/i18n.ts +++ b/web-app/src/i18n.ts @@ -11,9 +11,9 @@ import enSettings from '@/locales/en/settings.json' import idSettings from '@/locales/id/settings.json' import vnSettings from '@/locales/vn/settings.json' -import { localStoregeKey } from '@/constants/localStorage' +import { localStorageKey } from '@/constants/localStorage' -const stored = localStorage.getItem(localStoregeKey.settingGeneral) +const stored = localStorage.getItem(localStorageKey.settingGeneral) const parsed = stored ? JSON.parse(stored) : {} const defaultLang = parsed?.state?.currentLanguage diff --git a/web-app/src/routeTree.gen.ts b/web-app/src/routeTree.gen.ts index 7bec48167..3e18921a1 100644 --- a/web-app/src/routeTree.gen.ts +++ b/web-app/src/routeTree.gen.ts @@ -20,6 +20,7 @@ import { Route as SettingsPrivacyImport } from './routes/settings/privacy' import { Route as SettingsMcpServersImport } from './routes/settings/mcp-servers' import { Route as SettingsLocalApiServerImport } from './routes/settings/local-api-server' import { Route as SettingsHttpsProxyImport } from './routes/settings/https-proxy' +import { Route as SettingsHardwareImport } from './routes/settings/hardware' import { Route as SettingsGeneralImport } from './routes/settings/general' import { Route as SettingsExtensionsImport } from './routes/settings/extensions' import { Route as SettingsAppearanceImport } from './routes/settings/appearance' @@ -82,6 +83,12 @@ const SettingsHttpsProxyRoute = SettingsHttpsProxyImport.update({ getParentRoute: () => rootRoute, } as any) +const SettingsHardwareRoute = SettingsHardwareImport.update({ + id: '/settings/hardware', + path: '/settings/hardware', + getParentRoute: () => rootRoute, +} as any) + const SettingsGeneralRoute = SettingsGeneralImport.update({ id: '/settings/general', path: '/settings/general', @@ -166,6 +173,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsGeneralImport parentRoute: typeof rootRoute } + '/settings/hardware': { + id: '/settings/hardware' + path: '/settings/hardware' + fullPath: '/settings/hardware' + preLoaderRoute: typeof SettingsHardwareImport + parentRoute: typeof rootRoute + } '/settings/https-proxy': { id: '/settings/https-proxy' path: '/settings/https-proxy' @@ -228,6 +242,7 @@ export interface FileRoutesByFullPath { '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute '/settings/general': typeof SettingsGeneralRoute + '/settings/hardware': typeof SettingsHardwareRoute '/settings/https-proxy': typeof SettingsHttpsProxyRoute '/settings/local-api-server': typeof SettingsLocalApiServerRoute '/settings/mcp-servers': typeof SettingsMcpServersRoute @@ -245,6 +260,7 @@ export interface FileRoutesByTo { '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute '/settings/general': typeof SettingsGeneralRoute + '/settings/hardware': typeof SettingsHardwareRoute '/settings/https-proxy': typeof SettingsHttpsProxyRoute '/settings/local-api-server': typeof SettingsLocalApiServerRoute '/settings/mcp-servers': typeof SettingsMcpServersRoute @@ -263,6 +279,7 @@ export interface FileRoutesById { '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute '/settings/general': typeof SettingsGeneralRoute + '/settings/hardware': typeof SettingsHardwareRoute '/settings/https-proxy': typeof SettingsHttpsProxyRoute '/settings/local-api-server': typeof SettingsLocalApiServerRoute '/settings/mcp-servers': typeof SettingsMcpServersRoute @@ -282,6 +299,7 @@ export interface FileRouteTypes { | '/settings/appearance' | '/settings/extensions' | '/settings/general' + | '/settings/hardware' | '/settings/https-proxy' | '/settings/local-api-server' | '/settings/mcp-servers' @@ -298,6 +316,7 @@ export interface FileRouteTypes { | '/settings/appearance' | '/settings/extensions' | '/settings/general' + | '/settings/hardware' | '/settings/https-proxy' | '/settings/local-api-server' | '/settings/mcp-servers' @@ -314,6 +333,7 @@ export interface FileRouteTypes { | '/settings/appearance' | '/settings/extensions' | '/settings/general' + | '/settings/hardware' | '/settings/https-proxy' | '/settings/local-api-server' | '/settings/mcp-servers' @@ -332,6 +352,7 @@ export interface RootRouteChildren { SettingsAppearanceRoute: typeof SettingsAppearanceRoute SettingsExtensionsRoute: typeof SettingsExtensionsRoute SettingsGeneralRoute: typeof SettingsGeneralRoute + SettingsHardwareRoute: typeof SettingsHardwareRoute SettingsHttpsProxyRoute: typeof SettingsHttpsProxyRoute SettingsLocalApiServerRoute: typeof SettingsLocalApiServerRoute SettingsMcpServersRoute: typeof SettingsMcpServersRoute @@ -349,6 +370,7 @@ const rootRouteChildren: RootRouteChildren = { SettingsAppearanceRoute: SettingsAppearanceRoute, SettingsExtensionsRoute: SettingsExtensionsRoute, SettingsGeneralRoute: SettingsGeneralRoute, + SettingsHardwareRoute: SettingsHardwareRoute, SettingsHttpsProxyRoute: SettingsHttpsProxyRoute, SettingsLocalApiServerRoute: SettingsLocalApiServerRoute, SettingsMcpServersRoute: SettingsMcpServersRoute, @@ -375,6 +397,7 @@ export const routeTree = rootRoute "/settings/appearance", "/settings/extensions", "/settings/general", + "/settings/hardware", "/settings/https-proxy", "/settings/local-api-server", "/settings/mcp-servers", @@ -405,6 +428,9 @@ export const routeTree = rootRoute "/settings/general": { "filePath": "settings/general.tsx" }, + "/settings/hardware": { + "filePath": "settings/hardware.tsx" + }, "/settings/https-proxy": { "filePath": "settings/https-proxy.tsx" }, diff --git a/web-app/src/routes/settings/hardware.tsx b/web-app/src/routes/settings/hardware.tsx new file mode 100644 index 000000000..04acf131b --- /dev/null +++ b/web-app/src/routes/settings/hardware.tsx @@ -0,0 +1,380 @@ +import { createFileRoute } from '@tanstack/react-router' +import { route } from '@/constants/routes' +import SettingsMenu from '@/containers/SettingsMenu' +import HeaderPage from '@/containers/HeaderPage' +import { Card, CardItem } from '@/containers/Card' +import { Switch } from '@/components/ui/switch' +import { Progress } from '@/components/ui/progress' +import { useTranslation } from 'react-i18next' +import { useHardware } from '@/hooks/useHardware' +import type { GPU } from '@/hooks/useHardware' +import { useEffect } from 'react' +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core' +import { + SortableContext, + verticalListSortingStrategy, + useSortable, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { IconGripVertical } from '@tabler/icons-react' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const Route = createFileRoute(route.settings.hardware as any)({ + component: Hardware, +}) + +const fetchHardwareData = () => { + return { + cpu: { + arch: 'x86_64', + cores: 8, + instructions: ['SSE4.1', 'SSE4.2', 'AVX2'], + model: 'Apple M4 chip (10-core CPU, 10-core GPU)', + usage: Math.random() * 100, // Simulate changing CPU usage + }, + gpus: [ + { + activated: true, + additional_information: { + compute_cap: '7.5', + driver_version: '535.129.03', + }, + free_vram: Math.floor(Math.random() * 4 * 1024 * 1024 * 1024), // Random free VRAM + id: '0', + name: 'NVIDIA GeForce RTX 3080', + total_vram: 10 * 1024 * 1024 * 1024, // 10GB in bytes + uuid: 'GPU-123456789-0', + version: '7.5', + }, + { + activated: true, + additional_information: { + compute_cap: '8.6', + driver_version: '535.129.03', + }, + free_vram: Math.floor(Math.random() * 8 * 1024 * 1024 * 1024), // Random free VRAM + id: '1', + name: 'NVIDIA GeForce RTX 4070', + total_vram: 12 * 1024 * 1024 * 1024, // 12GB in bytes + uuid: 'GPU-123456789-1', + version: '8.6', + }, + { + activated: false, + additional_information: { + compute_cap: '6.1', + driver_version: '535.129.03', + }, + free_vram: Math.floor(Math.random() * 6 * 1024 * 1024 * 1024), // Random free VRAM + id: '2', + name: 'NVIDIA GeForce GTX 1660 Ti', + total_vram: 6 * 1024 * 1024 * 1024, // 6GB in bytes + uuid: 'GPU-123456789-2', + version: '6.1', + }, + ], + os: { + name: 'macOS', + version: '14.0', + }, + ram: { + available: Math.floor(Math.random() * 16 * 1024 * 1024 * 1024), // Random available RAM + total: 32 * 1024 * 1024 * 1024, // 32GB in bytes + }, + } +} + +// Format bytes to a human-readable format +const formatBytes = (bytes: number, decimals = 2) => { + if (bytes === 0) return '0 Bytes' + + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] + + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] +} + +function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: gpu.id || index }) + + const { toggleGPUActivation } = useHardware() + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + position: 'relative' as const, + zIndex: isDragging ? 1 : 0, + } + + return ( +
+ +
+ +
+ {gpu.name} +
+ } + actions={ +
+ toggleGPUActivation(index)} + /> +
+ } + /> +
+ + {formatBytes(gpu.free_vram)} free of {formatBytes(gpu.total_vram)} + + } + /> + + {gpu.additional_information.driver_version} + + } + /> + + {gpu.additional_information.compute_cap} + + } + /> +
+ + ) +} + +function Hardware() { + const { t } = useTranslation() + const { + hardwareData, + setHardwareData, + updateCPUUsage, + updateRAMAvailable, + reorderGPUs, + } = useHardware() + + // Set up DnD sensors + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor) + ) + + // Handle drag end event + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event + + if (over && active.id !== over.id) { + // Find the indices of the dragged item and the drop target + const oldIndex = hardwareData.gpus.findIndex( + (gpu) => gpu.id === active.id + ) + const newIndex = hardwareData.gpus.findIndex((gpu) => gpu.id === over.id) + + if (oldIndex !== -1 && newIndex !== -1) { + reorderGPUs(oldIndex, newIndex) + } + } + } + + useEffect(() => { + const data = fetchHardwareData() + setHardwareData(data) + + const intervalId = setInterval(() => { + const newData = fetchHardwareData() + updateCPUUsage(newData.cpu.usage) + updateRAMAvailable(newData.ram.available) + }, 5000) + + return () => clearInterval(intervalId) + }, [setHardwareData, updateCPUUsage, updateRAMAvailable]) + + return ( +
+ +

{t('common.settings')}

+
+
+ +
+
+ {/* OS Information */} + + + {hardwareData.os.name} + + } + /> + + {hardwareData.os.version} + + } + /> + + + {/* CPU Information */} + + + {hardwareData.cpu.model} + + } + /> + + {hardwareData.cpu.arch} + + } + /> + + {hardwareData.cpu.cores} + + } + /> + + {hardwareData.cpu.instructions.join(', ')} + + } + /> + + + + {hardwareData.cpu.usage.toFixed(2)}% + +
+ } + /> + + + {/* RAM Information */} + + + {formatBytes(hardwareData.ram.total)} + + } + /> + + {formatBytes(hardwareData.ram.available)} + + } + /> + + + + {( + ((hardwareData.ram.total - hardwareData.ram.available) / + hardwareData.ram.total) * + 100 + ).toFixed(2)} + % + +
+ } + /> + + + {/* GPU Information */} + + {hardwareData.gpus.length > 0 ? ( + + gpu.id)} + strategy={verticalListSortingStrategy} + > + {hardwareData.gpus.map((gpu, index) => ( + + ))} + + + ) : ( + } /> + )} + +
+
+ + + ) +}