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