From 65623166f0d546c8dbe82f319a44aefc39adde9a Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Fri, 9 Oct 2026 03:12:28 +0000 Subject: [PATCH] fix(ui): keep model actions visible on phones Let model descriptions wrap without losing the install action offscreen. Give page table rules precedence over the shared table styles. Separate background facet probes from navigation assertions and wait for asynchronous size sorting and masonry layout. Fix the chat fixture clock so runs near midnight keep the expected conversation groups and order. Assisted-by: Codex:gpt-6 --- core/http/react-ui/e2e/chat-history.spec.js | 6 ++++-- core/http/react-ui/e2e/home-console.spec.js | 10 ++++++---- core/http/react-ui/e2e/model-page.spec.js | 8 +++++--- core/http/react-ui/e2e/models-cleanup.spec.js | 9 ++++----- core/http/react-ui/e2e/studio-work.spec.js | 3 +-- .../react-ui/src/components/models/page/model-page.css | 6 ++++-- docs/content/getting-started/models.md | 3 +++ 7 files changed, 27 insertions(+), 18 deletions(-) diff --git a/core/http/react-ui/e2e/chat-history.spec.js b/core/http/react-ui/e2e/chat-history.spec.js index 05f024ed5..568cd177c 100644 --- a/core/http/react-ui/e2e/chat-history.spec.js +++ b/core/http/react-ui/e2e/chat-history.spec.js @@ -18,13 +18,15 @@ async function openMenu(page) { } test.describe('Chat conversations menu', () => { + const now = new Date('2026-10-09T12:00:00Z').getTime() test.beforeEach(async ({ page }) => { + await page.clock.setFixedTime(now) await mockChat(page, { models: [ { id: 'qwen3-8b-instruct', capabilities: ['FLAG_CHAT'] }, { id: 'gemma-4-e4b-it-qat-q4_0', capabilities: ['FLAG_CHAT'] }, { id: 'qwen2.5-vl-7b-instruct', capabilities: ['FLAG_CHAT'] }, ], loaded: ['qwen3-8b-instruct'] }) - await openChat(page, sampleChats()) + await openChat(page, sampleChats(now)) await expect(page.getByTestId('chat-message').first()).toBeVisible() }) @@ -153,7 +155,7 @@ test.describe('Chat conversations menu', () => { }) test('the undo time runs out by itself after six seconds', async ({ page }) => { - await page.clock.install() + await page.clock.install({ time: now }) await page.reload() await page.getByTestId('chat-composer').waitFor() await openMenu(page) diff --git a/core/http/react-ui/e2e/home-console.spec.js b/core/http/react-ui/e2e/home-console.spec.js index f50cb00bd..42a5c677d 100644 --- a/core/http/react-ui/e2e/home-console.spec.js +++ b/core/http/react-ui/e2e/home-console.spec.js @@ -317,9 +317,11 @@ test.describe('Home in a cluster', () => { }) test.describe('Home resume list', () => { + const now = new Date('2026-10-09T12:00:00Z').getTime() test.beforeEach(async ({ page }) => { + await page.clock.setFixedTime(now) await mockHome(page) - await seedChats(page) + await seedChats(page, sampleChats(now)) }) test('groups conversations by day, one card per day', async ({ page }) => { @@ -408,7 +410,7 @@ test.describe('Home resume list', () => { }) test('without undo the delete becomes final when the time ends', async ({ page }) => { - await page.clock.install() + await page.clock.install({ time: now }) await ready(page) const row = page.getByTestId('home-conversation').filter({ hasText: 'invoice' }) await row.hover() @@ -430,12 +432,12 @@ test.describe('Home resume list', () => { }) test('old conversations wait behind one button', async ({ page }) => { - const chats = sampleChats() + const chats = sampleChats(now) const old = Array.from({ length: 8 }, (_, i) => ({ ...chats[0], id: `old-${i}`, name: `Old chat ${i}`, - updatedAt: Date.now() - (10 + i) * 24 * 60 * 60 * 1000, + updatedAt: now - (10 + i) * 24 * 60 * 60 * 1000, })) await page.addInitScript(([key, data]) => localStorage.setItem(key, JSON.stringify(data)), [CHATS_KEY, { chats: [...chats, ...old], activeChatId: chats[0].id }]) diff --git a/core/http/react-ui/e2e/model-page.spec.js b/core/http/react-ui/e2e/model-page.spec.js index 9a07d8993..5ee36ecb5 100644 --- a/core/http/react-ui/e2e/model-page.spec.js +++ b/core/http/react-ui/e2e/model-page.spec.js @@ -199,15 +199,17 @@ test.describe('Model page - walking and going back', () => { const { state } = await setup(page) await expect(rows(page).first()).toBeVisible({ timeout: 15_000 }) await page.waitForTimeout(500) - const before = state.listingRequests.length + // Facet counts load in the background; they do not reload the list. + const listings = () => state.listingRequests.filter(search => new URLSearchParams(search).get('items') !== '1') + const before = listings().length await page.locator('[data-entity="qwen3-8b-instruct"] [data-row-open]').click() await expect(name(page)).toHaveText('qwen3-8b-instruct') await page.keyboard.press('Escape') await expect(rows(page).first()).toBeVisible() await page.waitForTimeout(500) // The page itself looks its own entry up once; the list asks for nothing. - expect(state.listingRequests.length - before).toBeLessThanOrEqual(1) - expect(state.listingRequests.slice(before).every(search => search.includes('term=qwen3-8b-instruct'))).toBe(true) + expect(listings().length - before).toBeLessThanOrEqual(1) + expect(listings().slice(before).every(search => search.includes('term=qwen3-8b-instruct'))).toBe(true) }) test('changing tabs keeps the walker, and Back still leaves for the list in one step', async ({ page }) => { diff --git a/core/http/react-ui/e2e/models-cleanup.spec.js b/core/http/react-ui/e2e/models-cleanup.spec.js index 08b1870fe..11c4ee8cc 100644 --- a/core/http/react-ui/e2e/models-cleanup.spec.js +++ b/core/http/react-ui/e2e/models-cleanup.spec.js @@ -92,11 +92,10 @@ test.describe('Cleanup sheet - what it knows and what it does not', () => { test('each tier is ranked by size and a model with no known size goes last', async ({ page }) => { await openSheet(page) - const names = await tier(page, 'call').locator('.ledger-cr__name').evaluateAll( - (els) => els.map((e) => e.firstChild.textContent.trim()), - ) - expect(names[0]).toBe('llama-3.3-70b-instruct-iq2') - expect(names[names.length - 1]).toBe('my-finetune-q4') + // Estimates arrive after the sheet becomes interactive and can reorder it. + const names = tier(page, 'call').locator('.ledger-cr__name') + await expect(names.first()).toContainText('llama-3.3-70b-instruct-iq2') + await expect(names.last()).toContainText('my-finetune-q4') await expect(tier(page, 'call').locator('li').last()).toContainText('size unknown') await expect(tier(page, 'call').locator('li').first()).toContainText(`${gb1(sizeOf('llama-3.3-70b-instruct-iq2'))} GB`) }) diff --git a/core/http/react-ui/e2e/studio-work.spec.js b/core/http/react-ui/e2e/studio-work.spec.js index a7623d0cf..4f1f31cc1 100644 --- a/core/http/react-ui/e2e/studio-work.spec.js +++ b/core/http/react-ui/e2e/studio-work.spec.js @@ -135,9 +135,8 @@ test.describe('Your work', () => { test('the masonry uses fewer columns on a narrow screen', async ({ page }) => { await open(page) - const wide = await page.locator('.studio-masonry').getAttribute('data-columns') + await expect.poll(async () => Number(await page.locator('.studio-masonry').getAttribute('data-columns'))).toBeGreaterThan(1) await page.setViewportSize({ width: 390, height: 844 }) await expect.poll(() => page.locator('.studio-masonry').getAttribute('data-columns')).toBe('1') - expect(Number(wide)).toBeGreaterThan(1) }) }) diff --git a/core/http/react-ui/src/components/models/page/model-page.css b/core/http/react-ui/src/components/models/page/model-page.css index d650afe6a..c59a10466 100644 --- a/core/http/react-ui/src/components/models/page/model-page.css +++ b/core/http/react-ui/src/components/models/page/model-page.css @@ -262,8 +262,8 @@ a.modelpage-chip { cursor: pointer; text-decoration: none; color: inherit; } .modelpage-keys { display: none; } } -.modelpage-table .dk-table-sub { max-width: none; white-space: normal; overflow-wrap: anywhere; } -.modelpage-table .dk-table-sub .modelpage-inline-badge:first-child { margin-left: 0; } +.modelpage-table .dk-table .dk-table-sub { max-width: none; white-space: normal; overflow-wrap: anywhere; } +.modelpage-table .dk-table .dk-table-sub .modelpage-inline-badge:first-child { margin-left: 0; } /* A grid child is as wide as its content unless told otherwise; a table inside one would push the whole page sideways instead of scrolling in its own box. */ @@ -271,6 +271,8 @@ a.modelpage-chip { cursor: pointer; text-decoration: none; color: inherit; } .modelpage-variants__size { display: none; } @media (max-width: 640px) { .modelpage-variants__size { display: inline; } + /* Leave room for Fit and Install while long build names wrap. */ + .modelpage-table .dk-table .dk-table-sub { max-width: 14ch; } .modelpage-table .dk-table-actions .dk-btn { padding-inline: var(--dk-space-2); } } diff --git a/docs/content/getting-started/models.md b/docs/content/getting-started/models.md index 188b34222..0e29d7b52 100644 --- a/docs/content/getting-started/models.md +++ b/docs/content/getting-started/models.md @@ -74,6 +74,9 @@ Note: The galleries available in LocalAI can be customized to point to a differe Visit [models.localai.io](https://models.localai.io) to browse all available models in your browser. +On a phone, the model page wraps build descriptions in the **Variants** table +to keep the fit status and install buttons visible. + ## Method 1.5: Import Models via WebUI The WebUI import page (**Build → Import**) takes either a source to resolve or a