fix(ui): reuse Home picker for Sound expansion (#12790)

* fix(ui): reuse Home picker for Sound expansion

Use the shared model chip and listbox for Sound expansion instead of
a native select. Keep explicit Sound selection and Backend default
independent from Home model preferences.

Preserve keyboard navigation and expose the selected value through an
accessible description. Hide runtime badges without loaded-model data.

Assisted-by: nib:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(ui): select Sound expansion through shared picker

Update media history coverage to use the model button and listbox.

Assisted-by: nib:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
mudler-agentandEttore Di Giacinto authored and GitHub committed 2026-10-20 18:08:37 +02:00
1 parent 560527e2e1
commit e684fdf084
6 files changed
+223 -55

No files matched your search

+12 -1
View File
@@ -174,6 +174,17 @@ test.describe('Home model chip', () => {
await expect(chip).toBeFocused()
})
test('keeps Home auto-selection and remembers an explicit model', async ({ page }) => {
await ready(page)
const chip = page.getByTestId('home-model-chip')
await expect(chip).toContainText('qwen3-8b-instruct')
await chip.click()
await page.getByRole('option', { name: /gemma-4-e4b/ }).click()
expect(await page.evaluate(() => localStorage.getItem('localai_last_model:FLAG_CHAT'))).toBe('gemma-4-e4b-it-qat-q4_0')
await page.reload()
await expect(chip).toContainText('gemma-4-e4b-it-qat-q4_0')
})
test('Escape closes the list and returns to the chip', async ({ page }) => {
await ready(page)
const chip = page.getByTestId('home-model-chip')
@@ -479,7 +490,7 @@ test.describe('Home first run', () => {
await mockFirstRun(page)
await ready(page)
const guide = page.getByTestId('home-first-run')
await expect(guide.getByRole('listitem')).toHaveCount(3)
await expect(guide.locator('.home-step')).toHaveCount(3)
await expect(guide.locator('.home-step[data-state="now"]')).toContainText('Browse the Model Gallery')
await expect(guide.locator('.home-step').nth(1).locator('.home-starters')).toContainText('tiny-chat')
await expect(guide.getByRole('button', { name: /browse model gallery/i })).toBeVisible()
+2 -1
View File
@@ -230,7 +230,8 @@ test.describe('Media History - Sound Generation', () => {
await page.goto('/app/sound')
await expect(page.getByRole('button', { name: 'test-sound-model' })).toBeVisible({ timeout: 10_000 })
await page.getByLabel('Prompt expansion model', { exact: true }).selectOption('__backend_default__')
await page.getByRole('button', { name: 'Prompt expansion model', exact: true }).click()
await page.getByRole('option', { name: 'Backend default', exact: true }).click()
await page.locator('.textarea').first().fill('upbeat jazz')
await page.locator('button[type="submit"]').click()
+141 -15
View File
@@ -6,7 +6,12 @@ const preference = 'localai_sound_expansion_model'
const chatPreference = 'localai_last_model:FLAG_CHAT'
const audio = { id: 'music', capabilities: ['FLAG_SOUND_GENERATION'] }
const chat = id => ({ id, capabilities: ['FLAG_CHAT'] })
const picker = page => page.getByLabel('Prompt expansion model', { exact: true })
const picker = page => page.getByRole('button', { name: 'Prompt expansion model', exact: true })
const choiceLabel = value => value === '__backend_default__' ? 'Backend default' : value || 'Choose a model or backend default'
async function choose(page, value) {
await picker(page).click()
await page.getByRole('listbox', { name: 'Prompt expansion model' }).getByRole('option').filter({ has: page.locator('code').getByText(choiceLabel(value), { exact: true }) }).click()
}
const submit = page => page.locator('[data-testid="ws-compose"] button[type="submit"]')
async function setup(page, models = [audio, chat('writer'), chat('poet')], saved) {
await page.route('**/api/**', route => route.fulfill({ json: {} }))
@@ -33,13 +38,15 @@ test('requires a choice, filters installed chat models and remembers only the So
const requests = await setup(page, [audio, chat('writer'), chat('poet'), { id: 'embedding', capabilities: ['FLAG_EMBEDDINGS'] }, { ...chat('disabled'), disabled: true }])
await prompt(page)
await expect(submit(page)).toBeDisabled()
await expect(picker(page).locator('option')).toHaveText(['Choose a model or backend default', 'Backend default', 'writer', 'poet'])
await picker(page).selectOption('poet')
await picker(page).click()
await expect(page.getByRole('listbox', { name: 'Prompt expansion model' }).getByRole('option').locator('code')).toHaveText(['Backend default', 'writer', 'poet'])
await page.keyboard.press('Escape')
await choose(page, 'poet')
await submit(page).click()
await expect.poll(() => requests.length).toBe(1)
expect(requests[0]).toMatchObject({ model_id: 'music', text: 'A quiet piano instrumental', instrumental: false, expansion_model: 'poet' })
await page.reload()
await expect(picker(page)).toHaveValue('poet')
await expect(picker(page)).toContainText(choiceLabel('poet'))
expect(await page.evaluate(key => localStorage.getItem(key), chatPreference)).toBe('general-chat')
expect(await page.evaluate(key => localStorage.getItem(key), preference)).toBe('poet')
})
@@ -47,7 +54,7 @@ for (const stale of ['removed', 'embedding']) {
test(`clears unavailable remembered model ${stale} and asks for a choice`, async ({ page }) => {
await setup(page, [audio, chat('writer'), { id: 'embedding', capabilities: ['FLAG_EMBEDDINGS'] }], stale)
await prompt(page)
await expect(picker(page)).toHaveValue('')
await expect(picker(page)).toContainText(choiceLabel(''))
await expect(submit(page)).toBeDisabled()
await expect(page.getByText('Choose a chat model or Backend default to continue.', { exact: true })).toBeVisible()
expect(await page.evaluate(key => localStorage.getItem(key), preference)).toBeNull()
@@ -55,10 +62,10 @@ for (const stale of ['removed', 'embedding']) {
}
test('revalidates a selected model when the installed list changes', async ({ page }) => {
await setup(page, [audio, chat('writer'), chat('poet')], 'writer')
await expect(picker(page)).toHaveValue('writer')
await expect(picker(page)).toContainText(choiceLabel('writer'))
await page.route('**/api/models/capabilities', route => route.fulfill({ json: { data: [audio, chat('poet')] } }))
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
await expect(picker(page)).toHaveValue('')
await expect(picker(page)).toContainText(choiceLabel(''))
await prompt(page)
await expect(submit(page)).toBeDisabled()
expect(await page.evaluate(key => localStorage.getItem(key), preference)).toBeNull()
@@ -68,17 +75,17 @@ test('Backend default works without chat models, omits expansion_model and survi
await prompt(page)
await expect(submit(page)).toBeDisabled()
await expect(page.getByText('No installed chat models. Choose Backend default to use the audio backend.', { exact: true })).toBeVisible()
await picker(page).selectOption('__backend_default__')
await choose(page, '__backend_default__')
await submit(page).click()
await expect.poll(() => requests.length).toBe(1)
expect(requests[0]).not.toHaveProperty('expansion_model')
await page.reload()
await expect(picker(page)).toHaveValue('__backend_default__')
await expect(picker(page)).toContainText(choiceLabel('__backend_default__'))
expect(await page.evaluate(key => localStorage.getItem(key), chatPreference)).toBe('general-chat')
})
test('Advanced preserves all fields and never sends the remembered expansion model', async ({ page }) => {
const requests = await setup(page, undefined, 'writer')
await expect(picker(page)).toHaveValue('writer')
await expect(picker(page)).toContainText(choiceLabel('writer'))
await page.getByRole('tab', { name: 'Advanced', exact: true }).click()
await expect(picker(page)).toHaveCount(0)
await page.locator('#sound-caption').fill('Warm acoustic folk')
@@ -117,15 +124,15 @@ test('Re-run with edits restores the original expansion choice and tracks edits'
contentType: 'audio/wav', headers: { 'Content-Disposition': 'attachment; filename="piano.wav"' }, body: silentWav(),
}))
await prompt(page)
await picker(page).selectOption('writer')
await choose(page, 'writer')
await submit(page).click()
const tile = page.getByTestId('media-history-item').first()
await expect(tile).toBeVisible()
await picker(page).selectOption('poet')
await choose(page, 'poet')
await tile.locator('.ws-tile__open').click()
await page.getByTestId('ws-rerun').click()
await expect(picker(page)).toHaveValue('writer')
await picker(page).selectOption('poet')
await expect(picker(page)).toContainText(choiceLabel('writer'))
await choose(page, 'poet')
await expect(page.getByTestId('ws-changes')).toContainText('Prompt expansion model writer to poet')
})
@@ -139,8 +146,127 @@ for (const historical of [undefined, 'removed']) {
await page.reload()
await page.getByTestId('media-history-item').first().locator('.ws-tile__open').click()
await page.getByTestId('ws-rerun').click()
await expect(picker(page)).toHaveValue(historical ? '' : '__backend_default__')
await expect(picker(page)).toContainText(choiceLabel(historical ? '' : '__backend_default__'))
if (historical) await expect(submit(page)).toBeDisabled()
else await expect(submit(page)).toBeEnabled()
})
}
test('shared chip supports keyboard, focus return and dismissal without choosing', async ({ page }) => {
await setup(page)
const chip = picker(page)
await expect(chip).toHaveAttribute('id', 'sound-expansion-model')
await expect(chip).toHaveAttribute('aria-describedby', /(?:^| )sound-expansion-help(?: |$)/)
await chip.press('ArrowDown')
const list = page.getByRole('listbox', { name: 'Prompt expansion model' })
await expect(list).toBeVisible()
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await expect(chip).toContainText('writer')
await expect(chip).toBeFocused()
await chip.click()
await page.keyboard.press('Escape')
await expect(list).toHaveCount(0)
await expect(chip).toBeFocused()
await chip.click()
await page.keyboard.press('Tab')
await expect(list).toHaveCount(0)
await chip.click()
await page.locator('[data-testid="ws-compose"] textarea').first().click()
await expect(list).toHaveCount(0)
await expect(chip).toContainText('writer')
})
for (const theme of ['dark', 'light']) {
test(`searches long IDs without clipping on a narrow ${theme} viewport`, async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await setup(page, [audio, ...Array.from({ length: 8 }, (_, i) => chat(`writer-${i}-${'long-model-name-'.repeat(5)}`))])
await page.evaluate(theme => document.documentElement.setAttribute('data-theme', theme), theme)
await picker(page).click()
const search = page.getByRole('combobox')
await search.fill('no-such-model')
await expect(page.getByRole('option')).toHaveCount(0)
await expect(page.getByText('No model matches.', { exact: false })).toBeVisible()
await search.fill('writer-7')
await expect(page.getByRole('option')).toHaveCount(1)
const menu = page.locator('.home-menu--models')
const box = await menu.boundingBox()
expect(box.x).toBeGreaterThanOrEqual(0)
expect(box.x + box.width).toBeLessThanOrEqual(390)
await page.keyboard.press('Enter')
await expect(picker(page)).toContainText('writer-7')
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390)
if (process.env.SOUND_PICKER_EVIDENCE) {
await picker(page).click()
await page.screenshot({ path: `${process.env.SOUND_PICKER_EVIDENCE}/sound-${theme}-narrow.png`, fullPage: true })
}
})
}
test('Backend default remains usable after a model list error', async ({ page }) => {
const requests = await setup(page)
await page.route('**/api/models/capabilities', route => route.fulfill({ status: 500, json: { error: 'offline' } }))
await page.route('**/v1/models', route => route.fulfill({ status: 500, json: { error: 'offline' } }))
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
await expect(page.locator('#sound-expansion-help')).toHaveText('Cannot load chat models. Choose Backend default or return to this tab to retry.')
await choose(page, '__backend_default__')
await prompt(page)
await expect(submit(page)).toBeEnabled()
await submit(page).click()
await expect.poll(() => requests.length).toBe(1)
expect(requests[0]).not.toHaveProperty('expansion_model')
})
test('initial model loading disables the picker until the response resolves', async ({ page }) => {
const requests = await setup(page, undefined, 'writer')
let release
const responseReady = new Promise(resolve => { release = resolve })
await page.route('**/api/models/capabilities', async route => {
await responseReady
await route.fulfill({ json: { data: [audio, chat('writer'), chat('poet')] } })
})
await page.reload()
try {
await expect(picker(page)).toBeDisabled()
await expect(page.locator('#sound-expansion-help')).toHaveText('Loading installed chat models…')
await prompt(page)
await expect(submit(page)).toBeDisabled()
} finally {
release()
}
await expect(picker(page)).toBeEnabled()
await expect(picker(page)).toContainText('writer')
await choose(page, 'poet')
await expect(submit(page)).toBeEnabled()
await submit(page).click()
await expect.poll(() => requests.length).toBe(1)
expect(requests[0]).toHaveProperty('expansion_model', 'poet')
})
test('Sound shows no unverified load status on any expansion model row or the chip', async ({ page }) => {
await setup(page)
await picker(page).click()
const options = page.getByRole('listbox', { name: 'Prompt expansion model' }).getByRole('option')
await expect(options.locator('code')).toHaveText(['Backend default', 'writer', 'poet'])
await expect(options.locator('.home-dot')).toHaveCount(0)
await expect(options.locator('small')).toHaveCount(0)
await page.keyboard.press('Escape')
await choose(page, 'writer')
await expect(picker(page).locator('.home-dot')).toHaveCount(0)
})
test('the stable picker label describes its current choice and retains the help association', async ({ page }) => {
await setup(page)
const chip = picker(page)
const help = 'A chat model expands your description into style and lyrics. Backend default uses the audio backend’s existing prompt behavior.'
await expect(chip).toHaveAccessibleName('Prompt expansion model')
await expect(chip).toHaveAccessibleDescription(`${choiceLabel('')} ${help}`)
await choose(page, 'writer')
await expect(chip).toHaveAccessibleName('Prompt expansion model')
await expect(chip).toHaveAccessibleDescription(`writer ${help}`)
await choose(page, '__backend_default__')
await expect(chip).toHaveAccessibleName('Prompt expansion model')
await expect(chip).toHaveAccessibleDescription(`Backend default ${help}`)
await expect(chip).toHaveAttribute('aria-describedby', /(?:^| )sound-expansion-help(?: |$)/)
})
@@ -1,5 +1,5 @@
// eslint-disable-next-line no-unused-vars
import { Fragment, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { Fragment, forwardRef, useCallback, useEffect, useImperativeHandle, useId, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useModels } from '../../hooks/useModels'
import { readLastModel, writeLastModel } from '../../utils/lastModel'
@@ -25,7 +25,9 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
{
value, onChange, capability, loadedIds, disabled = false, placeholder,
models: givenModels, loading: givenLoading, grouped = false, describe, onOpen, footer,
showWarm = true, labels,
showWarm = true, labels, autoSelect = true, rememberSelection = true,
defaultChoice, defaultSelected = false, onChooseDefault, id, describedBy,
},
ref,
) {
@@ -34,6 +36,10 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
const models = givenModels || own.models
const loading = givenModels ? !!givenLoading : own.loading
const names = useMemo(() => models.map(m => m.id), [models])
const instanceId = useId()
const menuId = `${instanceId}-menu`
const listId = `${instanceId}-list`
// A null row is a non-model action, never an installed model ID.
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const [active, setActive] = useState(0)
@@ -46,19 +52,19 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
// there, otherwise the first one. Auto-select is not a user choice, so it is
// not written back.
useEffect(() => {
if (names.length > 0 && (!value || !names.includes(value))) {
if (autoSelect && !defaultSelected && names.length > 0 && (!value || !names.includes(value))) {
const remembered = readLastModel(capability)
onChange(remembered && names.includes(remembered) ? remembered : names[0])
}
}, [names, value, onChange, capability])
}, [names, value, onChange, capability, autoSelect, defaultSelected])
// Loaded models first when the list is grouped; each group keeps its order.
const shown = useMemo(() => {
const q = query.trim().toLowerCase()
const found = q ? names.filter(n => n.toLowerCase().includes(q)) : names
if (!grouped) return found
return [...found.filter(n => loadedIds?.has(n)), ...found.filter(n => !loadedIds?.has(n))]
}, [names, query, grouped, loadedIds])
const ordered = grouped ? [...found.filter(n => loadedIds?.has(n)), ...found.filter(n => !loadedIds?.has(n))] : found
return defaultChoice && (!q || defaultChoice.toLowerCase().includes(q)) ? [null, ...ordered] : ordered
}, [names, query, grouped, loadedIds, defaultChoice])
const close = useCallback((restoreFocus = false) => {
setOpen(false)
@@ -67,12 +73,12 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
}, [])
const openMenu = useCallback(() => {
if (disabled || names.length === 0) return
setActive(Math.max(0, names.indexOf(value)))
if (disabled || (names.length === 0 && !defaultChoice)) return
setActive(defaultSelected ? 0 : Math.max(0, names.indexOf(value) + (defaultChoice ? 1 : 0)))
setQuery('')
setOpen(true)
onOpen?.()
}, [disabled, names, value, onOpen])
}, [disabled, names, value, onOpen, defaultChoice, defaultSelected])
useImperativeHandle(ref, () => ({ open: openMenu, focus: () => chipRef.current?.focus() }), [openMenu])
@@ -97,10 +103,13 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
}, [open, active, shown])
const pick = useCallback((name) => {
writeLastModel(capability, name)
onChange(name)
if (name === null) onChooseDefault?.()
else {
if (rememberSelection) writeLastModel(capability, name)
onChange(name)
}
close(true)
}, [capability, onChange, close])
}, [capability, onChange, close, rememberSelection, onChooseDefault])
const onMenuKey = (e) => {
const n = shown.length
@@ -108,24 +117,27 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
else if (e.key === 'ArrowUp') { e.preventDefault(); if (n) setActive(a => (a + n - 1) % n) }
else if (e.key === 'Home' && !query) { e.preventDefault(); setActive(0) }
else if (e.key === 'End' && !query) { e.preventDefault(); setActive(Math.max(0, n - 1)) }
else if (e.key === 'Enter') { e.preventDefault(); if (shown[active]) pick(shown[active]) }
else if (e.key === 'Enter') { e.preventDefault(); if (active < n) pick(shown[active]) }
else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(true) }
else if (e.key === 'Tab') close()
}
const isWarm = (name) => loadedIds?.has(name)
const label = value || placeholder || (loading ? t('picker.loading') : (labels?.none || t('picker.none')))
const activeId = shown[active] ? `home-model-opt-${active}` : undefined
const label = (defaultSelected ? defaultChoice : value) || placeholder || (loading ? t('picker.loading') : (labels?.none || t('picker.none')))
const activeId = active < shown.length ? `${instanceId}-opt-${active}` : undefined
return (
<div className="home-pick" ref={rootRef}>
<button
ref={chipRef}
id={id}
aria-label={labels?.label}
aria-describedby={[labels?.label && `${instanceId}-selection`, describedBy].filter(Boolean).join(' ') || undefined}
type="button"
className="home-chip home-chip--model"
aria-haspopup="listbox"
aria-expanded={open}
aria-controls="home-model-menu"
aria-controls={listId}
disabled={disabled}
title={labels?.title || t('picker.title')}
data-testid="home-model-chip"
@@ -133,12 +145,12 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
onKeyDown={(e) => { if (e.key === 'ArrowDown' && !open) { e.preventDefault(); openMenu() } }}
>
<Icon name="cube" />
{showWarm && <span className={`home-dot${value && isWarm(value) ? '' : ' home-dot--cold'}`} aria-hidden="true" />}
<span className="home-chip__text">{label}</span>
{showWarm && !defaultSelected && <span className={`home-dot${value && isWarm(value) ? '' : ' home-dot--cold'}`} aria-hidden="true" />}
<span id={labels?.label ? `${instanceId}-selection` : undefined} className="home-chip__text">{label}</span>
<Icon name="chevron-down" className="home-chip__caret" />
</button>
{open && (
<div className="home-menu home-menu--models" id="home-model-menu" onKeyDown={onMenuKey}>
<div className="home-menu home-menu--models" id={menuId} onKeyDown={onMenuKey}>
{names.length >= FILTER_FROM && (
<input
ref={filterRef}
@@ -150,13 +162,13 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
onChange={(e) => { setQuery(e.target.value); setActive(0) }}
role="combobox"
aria-expanded="true"
aria-controls="home-model-list"
aria-controls={listId}
aria-activedescendant={activeId}
/>
)}
<ul
ref={menuRef}
id="home-model-list"
id={listId}
className="home-menu__list"
role="listbox"
aria-label={labels?.label || t('picker.label')}
@@ -167,29 +179,30 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
{shown.length === 0 && <li className="home-menu__empty" role="presentation">{t('picker.noMatch')}</li>}
{shown.map((name, i) => {
const warm = isWarm(name)
const info = describe ? describe(name, warm) : null
const info = name !== null && describe ? describe(name, warm) : null
const selected = name === null ? defaultSelected : !defaultSelected && name === value
const first = grouped && (i === 0 || isWarm(shown[i - 1]) !== warm)
return (
<Fragment key={name}>
<Fragment key={name === null ? `${instanceId}-default` : name}>
{first && (
<li className="home-menu__head" role="presentation">
{warm ? t('picker.loadedNow') : t('picker.installed')}
</li>
)}
<li
id={`home-model-opt-${i}`}
id={`${instanceId}-opt-${i}`}
role="option"
aria-selected={name === value}
aria-selected={selected}
data-active={i === active}
className={`home-menu__item${info ? ' home-menu__item--rich' : ''}`}
onMouseMove={() => setActive(i)}
onClick={() => pick(name)}
>
{showWarm && <span className={`home-dot${warm ? '' : ' home-dot--cold'}`} aria-hidden="true" />}
{showWarm && name !== null && <span className={`home-dot${warm ? '' : ' home-dot--cold'}`} aria-hidden="true" />}
{info ? (
<>
<span className="home-menu__main">
<code>{name}</code>
<code>{name === null ? defaultChoice : name}</code>
{(info.size || info.vision) && (
<small className="home-menu__caps">
{info.size && <span>{info.size}</span>}
@@ -198,16 +211,16 @@ const HomeModelPicker = forwardRef(function HomeModelPicker(
)}
</span>
<span className="home-menu__state" data-tone={info.fit?.tone}>
<b>{name === value ? <Icon name="check" className="home-menu__check" /> : null}{warm ? t('picker.warm') : t('picker.cold')}</b>
<b>{selected ? <Icon name="check" className="home-menu__check" /> : null}{warm ? t('picker.warm') : t('picker.cold')}</b>
{info.fit && <small>{info.fit.text}</small>}
</span>
</>
) : (
<>
<code>{name}</code>
{name === value
<code>{name === null ? defaultChoice : name}</code>
{selected
? <Icon name="check" className="home-menu__check" />
: (showWarm && <small>{warm ? t('picker.warm') : t('picker.cold')}</small>)}
: (showWarm && name !== null && <small>{warm ? t('picker.warm') : t('picker.cold')}</small>)}
</>
)}
</li>
+19 -5
View File
@@ -1,5 +1,7 @@
import { useState } from 'react'
// eslint-disable-next-line no-unused-vars
import HomeModelPicker from '../components/home/HomeModelPicker'
// eslint-disable-next-line no-unused-vars
import RequestPanel from '../components/RequestPanel'
import { useParams, useOutletContext } from 'react-router-dom'
import { CAP_SOUND_GENERATION } from '../utils/capabilities'
@@ -218,11 +220,23 @@ export default function Sound() {
/>
{!text && <Starters type="sound" onPick={setText} />}
<Field label={t('sound.expansion.label')} htmlFor="sound-expansion-model" changed={changed.has('expansionModel')}>
<select id="sound-expansion-model" className="dk-input" value={expansion.value} onChange={e => expansion.choose(e.target.value)} disabled={expansion.loading} aria-describedby="sound-expansion-help">
<option value="" disabled>{t('sound.expansion.placeholder')}</option>
<option value={BACKEND_DEFAULT}>{t('sound.expansion.backendDefault')}</option>
{expansion.models.map(m => <option key={m.id} value={m.id}>{m.id}</option>)}
</select>
<HomeModelPicker
id="sound-expansion-model"
describedBy="sound-expansion-help"
labels={{ label: t('sound.expansion.label'), heading: t('sound.expansion.label') }}
placeholder={t('sound.expansion.placeholder')}
value={expansion.value === BACKEND_DEFAULT ? '' : expansion.value}
onChange={expansion.choose}
models={expansion.models}
loading={expansion.loading}
disabled={expansion.loading}
autoSelect={false}
showWarm={false}
rememberSelection={false}
defaultChoice={t('sound.expansion.backendDefault')}
defaultSelected={expansion.value === BACKEND_DEFAULT}
onChooseDefault={() => expansion.choose(BACKEND_DEFAULT)}
/>
<p id="sound-expansion-help" className="text-note" role="status">{expansion.loading
? t('sound.expansion.loading')
: expansion.error ? t('sound.expansion.error')