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>
This commit is contained in:
Louis 2025-05-20 20:05:47 +07:00 committed by GitHub
parent 3e887deb3e
commit 81c4dc516b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 108 additions and 99 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "@janhq/web-app", "name": "@janhq/web-app",
"private": true, "private": true,
"version": "0.0.0", "version": "0.5.18",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -1,6 +1,7 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware' import { persist, createJSONStorage } from 'zustand/middleware'
import { localStorageKey } from '@/constants/localStorage' import { localStorageKey } from '@/constants/localStorage'
import { setActiveGpus } from '@/services/hardware'
// Hardware data types // Hardware data types
export interface CPU { export interface CPU {
@ -171,7 +172,7 @@ export const useHardware = create<HardwareStore>()(
}, },
})), })),
toggleGPUActivation: (index) => toggleGPUActivation: (index) => {
set((state) => { set((state) => {
const newGPUs = [...state.hardwareData.gpus] const newGPUs = [...state.hardwareData.gpus]
if (index >= 0 && index < newGPUs.length) { if (index >= 0 && index < newGPUs.length) {
@ -180,13 +181,17 @@ export const useHardware = create<HardwareStore>()(
activated: !newGPUs[index].activated, activated: !newGPUs[index].activated,
} }
} }
setActiveGpus({
gpus: newGPUs.filter((e) => e.activated).map((e) => e.id as unknown as number),
})
return { return {
hardwareData: { hardwareData: {
...state.hardwareData, ...state.hardwareData,
gpus: newGPUs, gpus: newGPUs,
}, },
} }
}), })
},
reorderGPUs: (oldIndex, newIndex) => reorderGPUs: (oldIndex, newIndex) =>
set((state) => { set((state) => {

View File

@ -63,7 +63,7 @@ function General() {
title="App Version" title="App Version"
actions={ actions={
<> <>
<span className="text-main-view-fg/80">v16.0.0</span> <span className="text-main-view-fg/80">v{VERSION}</span>
</> </>
} }
/> />

View File

@ -7,7 +7,7 @@ import { Switch } from '@/components/ui/switch'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useHardware } from '@/hooks/useHardware' import { useHardware } from '@/hooks/useHardware'
import type { GPU } from '@/hooks/useHardware' import type { GPU, HardwareData } from '@/hooks/useHardware'
import { useEffect } from 'react' import { useEffect } from 'react'
import { import {
DndContext, DndContext,
@ -25,84 +25,21 @@ import {
} from '@dnd-kit/sortable' } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { IconGripVertical } from '@tabler/icons-react' import { IconGripVertical } from '@tabler/icons-react'
import { getHardwareInfo } from '@/services/hardware'
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.settings.hardware as any)({ export const Route = createFileRoute(route.settings.hardware as any)({
component: Hardware, 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 // Format bytes to a human-readable format
const formatBytes = (bytes: number, decimals = 2) => { function formatMegaBytes(mb: number) {
if (bytes === 0) return '0 Bytes' const tb = mb / (1024 * 1024)
if (tb >= 1) {
const k = 1024 return `${tb.toFixed(2)} TB`
const dm = decimals < 0 ? 0 : decimals } else {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] const gb = mb / 1024
return `${gb.toFixed(2)} GB`
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 }) { function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
@ -154,7 +91,8 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
title="VRAM" title="VRAM"
actions={ actions={
<span className="text-main-view-fg/80"> <span className="text-main-view-fg/80">
{formatBytes(gpu.free_vram)} free of {formatBytes(gpu.total_vram)} {formatMegaBytes(gpu.free_vram)} free of{' '}
{formatMegaBytes(gpu.total_vram)}
</span> </span>
} }
/> />
@ -189,6 +127,12 @@ function Hardware() {
reorderGPUs, reorderGPUs,
} = useHardware() } = useHardware()
useEffect(() => {
getHardwareInfo().then((data) =>
setHardwareData(data as unknown as HardwareData)
)
}, [setHardwareData])
// Set up DnD sensors // Set up DnD sensors
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
@ -213,13 +157,12 @@ function Hardware() {
} }
useEffect(() => { useEffect(() => {
const data = fetchHardwareData()
setHardwareData(data)
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const newData = fetchHardwareData() getHardwareInfo().then((data) => {
updateCPUUsage(newData.cpu.usage) setHardwareData(data as unknown as HardwareData)
updateRAMAvailable(newData.ram.available) updateCPUUsage(data.cpu.usage)
updateRAMAvailable(data.ram.available)
})
}, 5000) }, 5000)
return () => clearInterval(intervalId) return () => clearInterval(intervalId)
@ -280,14 +223,16 @@ function Hardware() {
</span> </span>
} }
/> />
<CardItem {hardwareData.cpu.instructions.join(', ').length > 0 && (
title="Instructions" <CardItem
actions={ title="Instructions"
<span className="text-main-view-fg/80"> actions={
{hardwareData.cpu.instructions.join(', ')} <span className="text-main-view-fg/80">
</span> {hardwareData.cpu.instructions.join(', ')}
} </span>
/> }
/>
)}
<CardItem <CardItem
title="Usage" title="Usage"
actions={ actions={
@ -310,7 +255,7 @@ function Hardware() {
title="Total RAM" title="Total RAM"
actions={ actions={
<span className="text-main-view-fg/80"> <span className="text-main-view-fg/80">
{formatBytes(hardwareData.ram.total)} {formatMegaBytes(hardwareData.ram.total)}
</span> </span>
} }
/> />
@ -318,7 +263,7 @@ function Hardware() {
title="Available RAM" title="Available RAM"
actions={ actions={
<span className="text-main-view-fg/80"> <span className="text-main-view-fg/80">
{formatBytes(hardwareData.ram.available)} {formatMegaBytes(hardwareData.ram.available)}
</span> </span>
} }
/> />

View File

@ -0,0 +1,45 @@
import { ExtensionManager } from '@/lib/extension'
import { ExtensionTypeEnum, HardwareManagementExtension } from '@janhq/core'
/**
* Get hardware information from the HardwareManagementExtension.
* @returns {Promise<HardwareInfo>} A promise that resolves to the hardware information.
*/
export const getHardwareInfo = async () => {
const extension =
ExtensionManager.getInstance().get<HardwareManagementExtension>(
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<HardwareManagementExtension>(
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
}
}

View File

@ -19,4 +19,5 @@ declare global {
let IS_IOS: boolean let IS_IOS: boolean
let IS_ANDROID: boolean let IS_ANDROID: boolean
let PLATFORM: string let PLATFORM: string
let VERSION: string
} }

View File

@ -4,6 +4,7 @@ import tailwindcss from '@tailwindcss/vite'
import path from 'path' import path from 'path'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite' import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills' import { nodePolyfills } from 'vite-plugin-node-polyfills'
import packageJson from './package.json'
const host = process.env.TAURI_DEV_HOST const host = process.env.TAURI_DEV_HOST
// https://vite.dev/config/ // https://vite.dev/config/
@ -23,12 +24,24 @@ export default defineConfig({
}, },
define: { define: {
IS_TAURI: JSON.stringify(process.env.IS_TAURI), IS_TAURI: JSON.stringify(process.env.IS_TAURI),
IS_MACOS: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('darwin') ?? 'false'), IS_MACOS: JSON.stringify(
IS_WINDOWS: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('windows') ?? 'false'), process.env.TAURI_ENV_PLATFORM?.includes('darwin') ?? '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_WINDOWS: JSON.stringify(
IS_ANDROID: JSON.stringify(process.env.TAURI_ENV_PLATFORM?.includes('android') ?? 'false'), 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), 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` // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`