From 81c4dc516ba9da7789bc5a973a8d6ac7e072fa04 Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 20 May 2025 20:05:47 +0700 Subject: [PATCH] chore: handle hardware settings (#5041) * chore: handle hardware settings * chore: activate GPUs * Update web-app/src/services/hardware.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web-app/package.json | 2 +- web-app/src/hooks/useHardware.ts | 9 +- web-app/src/routes/settings/general.tsx | 2 +- web-app/src/routes/settings/hardware.tsx | 125 +++++++---------------- web-app/src/services/hardware.ts | 45 ++++++++ web-app/src/types/global.d.ts | 1 + web-app/vite.config.ts | 23 ++++- 7 files changed, 108 insertions(+), 99 deletions(-) create mode 100644 web-app/src/services/hardware.ts diff --git a/web-app/package.json b/web-app/package.json index 089cdaa58..3df983e64 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/web-app", "private": true, - "version": "0.0.0", + "version": "0.5.18", "type": "module", "scripts": { "dev": "vite", diff --git a/web-app/src/hooks/useHardware.ts b/web-app/src/hooks/useHardware.ts index 67b6caa9a..bac87f80b 100644 --- a/web-app/src/hooks/useHardware.ts +++ b/web-app/src/hooks/useHardware.ts @@ -1,6 +1,7 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' import { localStorageKey } from '@/constants/localStorage' +import { setActiveGpus } from '@/services/hardware' // Hardware data types export interface CPU { @@ -171,7 +172,7 @@ export const useHardware = create()( }, })), - toggleGPUActivation: (index) => + toggleGPUActivation: (index) => { set((state) => { const newGPUs = [...state.hardwareData.gpus] if (index >= 0 && index < newGPUs.length) { @@ -180,13 +181,17 @@ export const useHardware = create()( activated: !newGPUs[index].activated, } } + setActiveGpus({ + gpus: newGPUs.filter((e) => e.activated).map((e) => e.id as unknown as number), + }) return { hardwareData: { ...state.hardwareData, gpus: newGPUs, }, } - }), + }) + }, reorderGPUs: (oldIndex, newIndex) => set((state) => { diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx index 3ae6c2659..46dc124d8 100644 --- a/web-app/src/routes/settings/general.tsx +++ b/web-app/src/routes/settings/general.tsx @@ -63,7 +63,7 @@ function General() { title="App Version" actions={ <> - v16.0.0 + v{VERSION} } /> diff --git a/web-app/src/routes/settings/hardware.tsx b/web-app/src/routes/settings/hardware.tsx index 04acf131b..257cbc512 100644 --- a/web-app/src/routes/settings/hardware.tsx +++ b/web-app/src/routes/settings/hardware.tsx @@ -7,7 +7,7 @@ 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 type { GPU, HardwareData } from '@/hooks/useHardware' import { useEffect } from 'react' import { DndContext, @@ -25,84 +25,21 @@ import { } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { IconGripVertical } from '@tabler/icons-react' +import { getHardwareInfo } from '@/services/hardware' // 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 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 }) { @@ -154,7 +91,8 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) { title="VRAM" actions={ - {formatBytes(gpu.free_vram)} free of {formatBytes(gpu.total_vram)} + {formatMegaBytes(gpu.free_vram)} free of{' '} + {formatMegaBytes(gpu.total_vram)} } /> @@ -189,6 +127,12 @@ function Hardware() { reorderGPUs, } = useHardware() + useEffect(() => { + getHardwareInfo().then((data) => + setHardwareData(data as unknown as HardwareData) + ) + }, [setHardwareData]) + // Set up DnD sensors const sensors = useSensors( useSensor(PointerSensor), @@ -213,13 +157,12 @@ function Hardware() { } useEffect(() => { - const data = fetchHardwareData() - setHardwareData(data) - const intervalId = setInterval(() => { - const newData = fetchHardwareData() - updateCPUUsage(newData.cpu.usage) - updateRAMAvailable(newData.ram.available) + getHardwareInfo().then((data) => { + setHardwareData(data as unknown as HardwareData) + updateCPUUsage(data.cpu.usage) + updateRAMAvailable(data.ram.available) + }) }, 5000) return () => clearInterval(intervalId) @@ -280,14 +223,16 @@ function Hardware() { } /> - - {hardwareData.cpu.instructions.join(', ')} - - } - /> + {hardwareData.cpu.instructions.join(', ').length > 0 && ( + + {hardwareData.cpu.instructions.join(', ')} + + } + /> + )} - {formatBytes(hardwareData.ram.total)} + {formatMegaBytes(hardwareData.ram.total)} } /> @@ -318,7 +263,7 @@ function Hardware() { title="Available RAM" actions={ - {formatBytes(hardwareData.ram.available)} + {formatMegaBytes(hardwareData.ram.available)} } /> diff --git a/web-app/src/services/hardware.ts b/web-app/src/services/hardware.ts new file mode 100644 index 000000000..217bbf0c6 --- /dev/null +++ b/web-app/src/services/hardware.ts @@ -0,0 +1,45 @@ +import { ExtensionManager } from '@/lib/extension' +import { ExtensionTypeEnum, HardwareManagementExtension } from '@janhq/core' + +/** + * Get hardware information from the HardwareManagementExtension. + * @returns {Promise} A promise that resolves to the hardware information. + */ +export const getHardwareInfo = async () => { + const extension = + ExtensionManager.getInstance().get( + ExtensionTypeEnum.Hardware + ) + + if (!extension) throw new Error('Hardware extension not found') + + try { + return await extension?.getHardware() + } catch (error) { + console.error('Failed to download model:', error) + throw error + } +} + +/** + * Set gpus activate + * @returns A Promise that resolves set gpus activate. + */ +export const setActiveGpus = async (data: { gpus: number[] }) => { + const extension = + ExtensionManager.getInstance().get( + ExtensionTypeEnum.Hardware + ) + + if (!extension) { + throw new Error('Extension is not available') + } + + try { + const response = await extension.setAvtiveGpu(data) + return response + } catch (error) { + console.error('Failed to install engine variant:', error) + throw error + } +} diff --git a/web-app/src/types/global.d.ts b/web-app/src/types/global.d.ts index 3b1b2b422..040cf9679 100644 --- a/web-app/src/types/global.d.ts +++ b/web-app/src/types/global.d.ts @@ -19,4 +19,5 @@ declare global { let IS_IOS: boolean let IS_ANDROID: boolean let PLATFORM: string + let VERSION: string } \ No newline at end of file diff --git a/web-app/vite.config.ts b/web-app/vite.config.ts index 384369873..c8e69ec21 100644 --- a/web-app/vite.config.ts +++ b/web-app/vite.config.ts @@ -4,6 +4,7 @@ import tailwindcss from '@tailwindcss/vite' import path from 'path' import { TanStackRouterVite } from '@tanstack/router-plugin/vite' import { nodePolyfills } from 'vite-plugin-node-polyfills' +import packageJson from './package.json' const host = process.env.TAURI_DEV_HOST // https://vite.dev/config/ @@ -23,12 +24,24 @@ export default defineConfig({ }, define: { IS_TAURI: JSON.stringify(process.env.IS_TAURI), - IS_MACOS: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('darwin') ?? 'false'), - IS_WINDOWS: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('windows') ?? 'false'), - IS_LINUX: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('unix') ?? 'false'), - IS_IOS: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('ios') ?? 'false'), - IS_ANDROID: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('android') ?? 'false'), + IS_MACOS: JSON.stringify( + process.env.TAURI_ENV_PLATFORM?.includes('darwin') ?? 'false' + ), + IS_WINDOWS: JSON.stringify( + process.env.TAURI_ENV_PLATFORM?.includes('windows') ?? 'false' + ), + IS_LINUX: JSON.stringify( + process.env.TAURI_ENV_PLATFORM?.includes('unix') ?? 'false' + ), + IS_IOS: JSON.stringify( + process.env.TAURI_ENV_PLATFORM?.includes('ios') ?? 'false' + ), + IS_ANDROID: JSON.stringify( + process.env.TAURI_ENV_PLATFORM?.includes('android') ?? 'false' + ), PLATFORM: JSON.stringify(process.env.TAURI_ENV_PLATFORM), + + VERSION: JSON.stringify(packageJson.version), }, // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`