fix(react-ui): polish 'Fits in my GPU' filter to use design-system Toggle (#10030)

* fix(react-ui): polish 'Fits in my GPU' filter to use design-system Toggle

The recently added VRAM-fit filter in the Models page used a raw
<input type="checkbox"> next to the themed range slider, breaking the
visual language of the rest of the row. Swap it for the shared
<Toggle> component (already used by Backends, Settings, Traces,
AgentCreate), adopt the filter-bar-group__toggle class to drop the
duplicated inline styles, add a fa-microchip icon to mirror the
per-row fit indicator, and add a subtle left divider so the filter
reads as separate from the context-size slider on its left.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(react-ui): move 'Fits in GPU' filter to filter row and unify copy

Two follow-ups on the previous polish pass:

1. Move the toggle from the context-slider row into the filter-button
   row above. The toggle is a filter on the result set, not a config
   for VRAM estimation, so it belongs with the type chips and backend
   select. The context slider stays its own thing.

2. Unify the label copy. The same locale file had "Fits in my GPU"
   for the filter and "Fits in GPU" for the per-row indicator; pick
   the shorter, possessive-free variant everywhere (en/de/es/it/zh-CN).
   Update e2e selectors to match.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
LocalAI [bot]
2026-05-27 21:09:14 +02:00
committed by GitHub
parent 7a4ca8f60d
commit 02a0e70396
7 changed files with 19 additions and 21 deletions

View File

@@ -282,14 +282,14 @@ test.describe('Models Gallery - Fits In GPU Filter', () => {
await expect(page.locator('th', { hasText: 'Backend' })).toBeVisible({ timeout: 10_000 })
})
test('fits checkbox is visible when GPU resources are available', async ({ page }) => {
await expect(page.getByText('Fits in my GPU')).toBeVisible()
test('fits toggle is visible when GPU resources are available', async ({ page }) => {
await expect(page.getByText('Fits in GPU')).toBeVisible()
})
test('enabling fits filter hides models that exceed available VRAM', async ({ page }) => {
await expect(page.locator('tr', { hasText: 'stablediffusion-model' })).toBeVisible()
await page.getByLabel('Fits in my GPU').check()
await page.getByLabel('Fits in GPU').check()
await expect(page.locator('tr', { hasText: 'stablediffusion-model' })).toHaveCount(0)
await expect(page.locator('tr', { hasText: 'llama-model' })).toBeVisible()
@@ -298,8 +298,8 @@ test.describe('Models Gallery - Fits In GPU Filter', () => {
})
test('fits filter state persists after reload', async ({ page }) => {
await page.getByLabel('Fits in my GPU').check()
await page.getByLabel('Fits in GPU').check()
await page.reload()
await expect(page.getByLabel('Fits in my GPU')).toBeChecked()
await expect(page.getByLabel('Fits in GPU')).toBeChecked()
})
})

View File

@@ -23,7 +23,7 @@
"diarization": "Diarisierung",
"embedding": "Embedding",
"rerank": "Rerank",
"fitsGpu": "Passt in meine GPU",
"fitsGpu": "Passt in die GPU",
"allBackends": "Alle Backends",
"searchBackends": "Backends suchen..."
},

View File

@@ -29,7 +29,7 @@
"rerank": "Rerank",
"detection": "Detection",
"vad": "VAD",
"fitsGpu": "Fits in my GPU",
"fitsGpu": "Fits in GPU",
"allBackends": "All Backends",
"searchBackends": "Search backends..."
},

View File

@@ -23,7 +23,7 @@
"diarization": "Diarización",
"embedding": "Embedding",
"rerank": "Rerank",
"fitsGpu": "Cabe en mi GPU",
"fitsGpu": "Cabe en la GPU",
"allBackends": "Todos los backends",
"searchBackends": "Buscar backends..."
},

View File

@@ -23,7 +23,7 @@
"diarization": "Diarizzazione",
"embedding": "Embedding",
"rerank": "Rerank",
"fitsGpu": "Entra nella mia GPU",
"fitsGpu": "Entra nella GPU",
"allBackends": "Tutti i backend",
"searchBackends": "Cerca backend..."
},

View File

@@ -23,7 +23,7 @@
"diarization": "说话人分离",
"embedding": "嵌入",
"rerank": "重排",
"fitsGpu": "适合我的 GPU",
"fitsGpu": "适合 GPU",
"allBackends": "所有后端",
"searchBackends": "搜索后端..."
},

View File

@@ -9,6 +9,7 @@ import { useResources } from '../hooks/useResources'
import SearchableSelect from '../components/SearchableSelect'
import ConfirmDialog from '../components/ConfirmDialog'
import GalleryLoader from '../components/GalleryLoader'
import Toggle from '../components/Toggle'
import React from 'react'
@@ -325,6 +326,13 @@ export default function Models() {
</button>
)
})}
{totalGpuMemory > 0 && (
<label className="filter-bar-group__toggle" style={{ marginLeft: 'auto' }}>
<Toggle checked={fitsFilter} onChange={setFitsFilter} />
<i className="fas fa-microchip" />
<span>{t('filters.fitsGpu')}</span>
</label>
)}
{allBackends.length > 0 && (
<SearchableSelect
value={backendFilter}
@@ -333,7 +341,7 @@ export default function Models() {
placeholder={t('filters.allBackends')}
allOption={t('filters.allBackends')}
searchPlaceholder={t('filters.searchBackends')}
style={{ marginLeft: 'auto' }}
style={totalGpuMemory > 0 ? undefined : { marginLeft: 'auto' }}
/>
)}
</div>
@@ -355,16 +363,6 @@ export default function Models() {
<span style={{ fontWeight: 600, minWidth: '3em' }}>
{CONTEXT_LABELS[CONTEXT_SIZES.indexOf(contextSize)]}
</span>
{totalGpuMemory > 0 && (
<label style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 'var(--spacing-xs)', color: 'var(--color-text-secondary)', cursor: 'pointer' }}>
<input
type="checkbox"
checked={fitsFilter}
onChange={(e) => setFitsFilter(e.target.checked)}
/>
<span>{t('filters.fitsGpu')}</span>
</label>
)}
</div>
{/* Table */}