mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
A visual pass over the add-on pages found the same few problems on several of them. Fix them with one rule per problem instead of page by page. Titles: every add-on page takes the host page-title size. Talk and Tables no longer shrink it, Studio draws one "Studio" title above its tabs (the card of a generator is a section), and Voice keeps one title on all its tabs. The Text to speech tab gets a styled form. The "not available" page shows the add-on name, not the lowercase id, and sits at the same inset as an add-on page. One primary action: the empty state of a list carries the add button, so the header button waits until there is something in the list (Voice library, enrolling a speaker or a person, Add rows). The Add-ons page shows one way into Browse. A table with no columns says so once. Tables: Share is hidden when there are no named accounts. The active table tab is highlighted and an empty tab list leaves no gap before "New table". Studio: the tab strip stays on one row at 1280 wide, /studio/3d opens the 3D tab, and an input chip has a divider between its label and its placeholder. Fine-tune and Quantize: the Experimental badge sits beside the lede, not inside the h1; an open step shows its number instead of an empty circle; the sticky action bar keeps a gap above it. The footer shows nothing for the empty version of a source build, and the Voice Text to speech tab says where to get a model when none is installed. Assisted-by: Claude:claude-sonnet-5-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
67 lines
3.6 KiB
JavaScript
67 lines
3.6 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createRequire } from 'node:module'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { load } from './render.test.mjs'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
const require = createRequire(join(resolve(here, '../../../../core/http/react-ui'), 'package.json'))
|
|
const React = require('react')
|
|
const { renderToString } = require('react-dom/server')
|
|
|
|
test('speech: the first paint shows the title and the text box', async () => {
|
|
const { mod, sdk } = await load()
|
|
const addon = await mod.createAddon(sdk)
|
|
const html = renderToString(React.createElement(addon.Component, { page: 'voice', path: 'speech' }))
|
|
assert.match(html, /data-testid="voice-speech"/)
|
|
assert.match(html, /<textarea/)
|
|
})
|
|
|
|
test('speech: the request is POST /v1/audio/speech with the model, the text and the chosen voice', async () => {
|
|
const { buildSpeechRequest } = await import('../src/speechRequest.js')
|
|
assert.deepEqual(buildSpeechRequest({ model: 'piper', text: ' hello ', voice: 'amy', instructions: ' calm ' }), {
|
|
model: 'piper', input: 'hello', voice: 'amy', instructions: 'calm',
|
|
})
|
|
assert.deepEqual(buildSpeechRequest({ model: 'piper', text: 'hi', voice: '', instructions: '' }), { model: 'piper', input: 'hi' })
|
|
})
|
|
|
|
test('speech: nothing is sent without text or without a model', async () => {
|
|
const { buildSpeechRequest } = await import('../src/speechRequest.js')
|
|
assert.equal(buildSpeechRequest({ model: '', text: 'x' }), null)
|
|
assert.equal(buildSpeechRequest({ model: 'm', text: ' ' }), null)
|
|
})
|
|
|
|
test('speech: a failed answer surfaces the API message', async () => {
|
|
const { speak } = await import('../src/speechRequest.js')
|
|
const core = { fetch: async () => ({ ok: false, status: 403, json: async () => ({ error: { message: 'feature not enabled for your account' } }), blob: async () => null }) }
|
|
await assert.rejects(() => speak(core, { model: 'm', input: 'x' }), /feature not enabled for your account/)
|
|
})
|
|
|
|
test('speech: the tab draws the one Voice title, a styled form and no second title', async () => {
|
|
const { mod, sdk } = await load()
|
|
const addon = await mod.createAddon(sdk)
|
|
const html = renderToString(React.createElement(addon.Component, { page: 'voice', path: 'speech' }))
|
|
assert.deepEqual([...html.matchAll(/<h1[^>]*>(.*?)<\/h1>/g)].map(m => m[1]), ['Voices'])
|
|
assert.doesNotMatch(html, /Speak text/)
|
|
assert.match(html, /<form class="vc-speech__form dk-card"/)
|
|
assert.match(html, /placeholder="Optional speaker or voice ID"/)
|
|
})
|
|
|
|
test('library: the tab keeps the one Voice title and shows no count in it', async () => {
|
|
const { mod, sdk } = await load()
|
|
const addon = await mod.createAddon(sdk)
|
|
const html = renderToString(React.createElement(addon.Component, { page: 'voice', path: 'library' }))
|
|
assert.deepEqual([...html.matchAll(/<h1[^>]*>(.*?)<\/h1>/g)].map(m => m[1]), ['Voices'])
|
|
// While the list loads, or when it is empty, the empty state carries the one add button.
|
|
assert.doesNotMatch(html, /data-testid="voice-library-new"/)
|
|
})
|
|
|
|
test('off notice: a viewer without the feature gets the page title and a section heading, one h1', async () => {
|
|
const { mod, sdk } = await load({ viewer: { isAdmin: () => false, hasFeature: (f) => f !== 'voice_recognition' } })
|
|
const addon = await mod.createAddon(sdk)
|
|
const html = renderToString(React.createElement(addon.Component, { page: 'voice', path: 'recognition' }))
|
|
assert.deepEqual([...html.matchAll(/<h1[^>]*>(.*?)<\/h1>/g)].map(m => m[1]), ['Voices'])
|
|
assert.match(html, /<h2 class="idn-h2">/)
|
|
})
|