mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
The model word under the composer opens @hanzo/build's own menu since 0.2.73 (Search models, Enso's row, model rows carrying data-model and data-paused), not a panel with @hanzo/ui ModelPicker inside it, and effort is a word of its own. chat.ts reads that menu; picker, limits, upgrade and named measure it. ModelPicker's research-preview row has no counterpart in build's menu, which lists what /v1/models serves, so picker.spec stops asserting it and LLM.md says where Zen 7 is asked for instead. The usage bars are UsageMeter rows keyed by class (usage-meter-row), as the settings gates already read them. An empty chat shows @hanzo/build Suggestions since @hanzo/rooms 0.1.83, and a starter does its act on the press; starters and local-cloud hold that, with the kept rows answered so no model is asked. Agents are seated from + › Agents, which lists the org's own agents; together.spec gives the org the core team's rows and holds the same room, briefs and names. The thread on /models is found by its accessible name: rooms' Ask no longer draws a placeholder, so TryChat stops passing one. Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
100 lines
4.4 KiB
TypeScript
100 lines
4.4 KiB
TypeScript
import { expect, test, type Locator, type Page } from '@playwright/test'
|
|
import { FREE, PAID, arrive, chip, effort, enso, panel, row } from './chat'
|
|
import { serveExport } from './export'
|
|
|
|
/**
|
|
* THE WORD UNDER THE COMPOSER NAMES WHAT ANSWERS, AND ITS MENU IS SOLID.
|
|
*
|
|
* The model's word says Enso while Enso routes the turn and names the model
|
|
* once the reader picks one; the effort is a word of its own beside it while
|
|
* the model takes one. The word opens the model menu (@hanzo/build Tune, since
|
|
* 0.2.73 its own menu: Search models, Enso's row, the catalog by family), on
|
|
* screen with an opaque ground, and the search narrows its rows. Held at a
|
|
* laptop and a phone; the `webkit` project runs this file in WebKit too.
|
|
*
|
|
* The research preview's row ("Request access") was @hanzo/ui ModelPicker's,
|
|
* which appends `RESEARCH` to the catalog. The composer stopped drawing that
|
|
* picker when @hanzo/build drew its own menu over `GET /v1/models`, so the menu
|
|
* lists what the gateway serves and no preview; /research-access is where Zen 7
|
|
* is asked for.
|
|
*/
|
|
|
|
const SIZES = [
|
|
{ width: 1440, height: 900 },
|
|
{ width: 390, height: 844 },
|
|
] as const
|
|
|
|
/** The alpha of an element's computed background colour; 1 is a solid surface. */
|
|
function ground(el: Locator): Promise<number> {
|
|
return el.evaluate((node) => {
|
|
const paint = getComputedStyle(node).backgroundColor
|
|
if (paint === 'transparent') return 0
|
|
const slash = paint.match(/\/\s*([\d.]+)(%?)\s*\)$/)
|
|
if (slash) return slash[2] ? Number(slash[1]) / 100 : Number(slash[1])
|
|
const parts = paint.match(/\(([^)]+)\)/)?.[1].split(',') ?? []
|
|
return parts.length === 4 ? Number(parts[3]) : 1
|
|
})
|
|
}
|
|
|
|
/** The surface is inside the window and nothing behind it shows through. */
|
|
async function solid(page: Page, surface: Locator) {
|
|
await expect(surface).toBeVisible()
|
|
const view = page.viewportSize()!
|
|
const box = (await surface.boundingBox())!
|
|
expect(box.x, 'left edge on screen').toBeGreaterThanOrEqual(0)
|
|
expect(box.y, 'top edge on screen').toBeGreaterThanOrEqual(0)
|
|
expect(box.x + box.width, 'right edge on screen').toBeLessThanOrEqual(view.width)
|
|
expect(box.y + box.height, 'bottom edge on screen').toBeLessThanOrEqual(view.height)
|
|
await expect.poll(() => ground(surface), { message: 'an opaque ground' }).toBe(1)
|
|
}
|
|
|
|
test.describe('the model under the composer', () => {
|
|
let base: string
|
|
let stop: () => Promise<void>
|
|
test.beforeAll(async () => {
|
|
const served = await serveExport()
|
|
base = served.url
|
|
stop = served.close
|
|
})
|
|
test.afterAll(async () => stop())
|
|
|
|
for (const size of SIZES) {
|
|
test(`at ${size.width}px it names the model picked, and its menu opens solid and on screen`, async ({ page }) => {
|
|
await page.setViewportSize(size)
|
|
await arrive(page, base, 'dev', FREE.id)
|
|
|
|
// A model kept by id is named by the catalog. Enso Free names no effort.
|
|
await expect(chip(page)).toHaveAttribute('aria-label', `Model: ${FREE.name}`)
|
|
await expect(chip(page)).toContainText(FREE.name)
|
|
await expect(effort(page)).toHaveCount(0)
|
|
|
|
await chip(page).click()
|
|
await solid(page, panel(page))
|
|
await row(page, PAID.id).click()
|
|
await expect(panel(page)).toHaveCount(0)
|
|
|
|
await expect(chip(page)).toHaveAttribute('aria-label', `Model: ${PAID.name}`)
|
|
await expect(chip(page)).toContainText(PAID.name)
|
|
await expect(chip(page)).not.toContainText('Enso')
|
|
|
|
// Back to the router: the word says Enso, and its effort stands beside it.
|
|
await chip(page).click()
|
|
await enso(page).click()
|
|
await expect(chip(page)).toHaveAttribute('aria-label', 'Model: Enso')
|
|
await expect(effort(page)).toHaveAttribute('aria-label', 'Effort: Medium')
|
|
|
|
// The search narrows the rows to the models it matches, and keeps the menu solid.
|
|
await chip(page).click()
|
|
await solid(page, panel(page))
|
|
await panel(page).getByPlaceholder('Search models').fill('zen')
|
|
await expect(row(page, PAID.id)).toBeVisible()
|
|
await expect(row(page, FREE.id)).toHaveCount(0)
|
|
await expect(enso(page), 'Enso stands for every ask, not a match').toHaveCount(0)
|
|
await page.keyboard.press('Escape')
|
|
await expect(panel(page)).toHaveCount(0)
|
|
await expect(chip(page)).toHaveAttribute('aria-label', 'Model: Enso')
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1)).toBe(false)
|
|
})
|
|
}
|
|
})
|