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
This commit is contained in:
localai-org-maint-bot committed 2026-10-09 03:12:39 +00:00
1 parent 8db1d165dd
commit 65623166f0
7 files changed
+27 -18

No files matched your search

+4 -2
View File
@@ -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)
+6 -4
View File
@@ -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 }])
+5 -3
View File
@@ -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 }) => {
@@ -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`)
})
+1 -2
View File
@@ -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)
})
})
@@ -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); }
}
+3
View File
@@ -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