mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-09 14:42:41 -04:00
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:
1 parent
8db1d165dd
commit
65623166f0
7 files changed
+27
-18
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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 }])
|
||||
|
||||
@@ -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`)
|
||||
})
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user