v0.11.0:1 - dashboard polish: tabs, collapsible endpoint, pill consistency
Three UX improvements, all client-side; no backend or behavior changes.
1. LLM / Audio tabs under the hardware section. The single long column got
split into two tabbed views:
* LLM -> model swap + download panel + spark-vllm-docker updates
* Audio -> Parakeet/Magpie services + speech-model patches
Selection persists in localStorage; default is LLM. The swap-panel
(in-flight LLM swap) sits ABOVE the tab strip so it stays visible
regardless of which tab is active.
2. Collapsible OpenAI-compatible Endpoint card. New chevron in the card
header collapses everything except the title. State persists per browser
via localStorage. Defaults to collapsed since you rarely need the URL/
model details visible (and the same info is one tab swap away).
3. Unified pill sizing. The .sm-pill class in speech-models was rendering
subtly larger than .tag pills on model cards. Dropped .sm-pill entirely
and reused .tag with semantic color modifiers (.tag.ok / .tag.warn /
.tag.bad). Same 11px / 2px×8px footprint everywhere now. Also added
explicit line-height: 1.5 + display: inline-block to .tag to lock down
vertical sizing.
No new endpoints, no new dependencies. Tested locally with node --check
and ast.parse(). Verified the tab DOM structure wraps the right sections
and the speech-models panel still self-shows/hides on data load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+66
-6
@@ -556,13 +556,13 @@ async function renderSpeechModels() {
|
||||
|
||||
let statusPill;
|
||||
if (status === 'in_sync') {
|
||||
statusPill = `<span class="sm-pill ok">patches in sync</span>`;
|
||||
statusPill = `<span class="tag ok">patches in sync</span>`;
|
||||
} else if (status === 'drift') {
|
||||
statusPill = `<span class="sm-pill warn">spark-control has newer patches</span>`;
|
||||
statusPill = `<span class="tag warn">spark-control has newer patches</span>`;
|
||||
} else if (status === 'missing') {
|
||||
statusPill = `<span class="sm-pill bad">patches missing in container</span>`;
|
||||
statusPill = `<span class="tag bad">patches missing in container</span>`;
|
||||
} else {
|
||||
statusPill = `<span class="sm-pill warn">unknown</span>`;
|
||||
statusPill = `<span class="tag warn">unknown</span>`;
|
||||
}
|
||||
|
||||
const asrLoaded = !!health.asr_loaded;
|
||||
@@ -599,12 +599,12 @@ async function renderSpeechModels() {
|
||||
<div class="sm-model-row">
|
||||
<span class="sm-model-kind">Parakeet ASR</span>
|
||||
<span class="sm-model-name">${asrModel}</span>
|
||||
<span class="sm-model-loaded">${asrLoaded ? '<span class="sm-pill ok">loaded</span>' : '<span class="sm-pill bad">not loaded</span>'}</span>
|
||||
<span class="sm-model-loaded">${asrLoaded ? '<span class="tag ok">loaded</span>' : '<span class="tag bad">not loaded</span>'}</span>
|
||||
</div>
|
||||
<div class="sm-model-row">
|
||||
<span class="sm-model-kind">Sortformer diarizer</span>
|
||||
<span class="sm-model-name">${diarModel}</span>
|
||||
<span class="sm-model-loaded">${diarLoaded ? '<span class="sm-pill ok">loaded</span>' : '<span class="sm-pill bad">not loaded</span>'}</span>
|
||||
<span class="sm-model-loaded">${diarLoaded ? '<span class="tag ok">loaded</span>' : '<span class="tag bad">not loaded</span>'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm-files">${fileRows}</div>
|
||||
@@ -768,6 +768,64 @@ function renderHealth(status) {
|
||||
|
||||
function renderBanner(status) {
|
||||
el('#setup-banner').classList.toggle('hidden', !!status.configured);
|
||||
// Dashboard tabs share the same "configured" gate as the rest of the
|
||||
// body — hidden until SSH is set up, then visible.
|
||||
const tabs = el('#dashboard-tabs');
|
||||
if (tabs) tabs.classList.toggle('hidden', !status.configured);
|
||||
}
|
||||
|
||||
// ===================== dashboard tabs (LLM / Audio) =====================
|
||||
|
||||
const TABS_STORAGE_KEY = 'sparkcontrol.dashboard.activeTab';
|
||||
|
||||
function setupDashboardTabs() {
|
||||
const buttons = $$('.dashboard-tab');
|
||||
if (!buttons.length) return;
|
||||
|
||||
// Restore the last-selected tab, default to "llm"
|
||||
let saved;
|
||||
try { saved = localStorage.getItem(TABS_STORAGE_KEY); } catch {}
|
||||
const initial = saved === 'audio' || saved === 'llm' ? saved : 'llm';
|
||||
|
||||
function selectTab(name) {
|
||||
buttons.forEach((b) => {
|
||||
const active = b.dataset.tab === name;
|
||||
b.classList.toggle('active', active);
|
||||
b.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
$$('.tab-content').forEach((c) => {
|
||||
c.classList.toggle('active', c.id === `tab-${name}`);
|
||||
});
|
||||
try { localStorage.setItem(TABS_STORAGE_KEY, name); } catch {}
|
||||
}
|
||||
|
||||
buttons.forEach((b) => {
|
||||
b.addEventListener('click', () => selectTab(b.dataset.tab));
|
||||
});
|
||||
selectTab(initial);
|
||||
}
|
||||
|
||||
// ===================== collapsible endpoint card =====================
|
||||
|
||||
const ENDPOINT_COLLAPSED_KEY = 'sparkcontrol.endpoint.collapsed';
|
||||
|
||||
function setupEndpointCollapse() {
|
||||
const panel = el('#endpoint-panel');
|
||||
const btn = el('#ep-collapse');
|
||||
if (!panel || !btn) return;
|
||||
// Default: collapsed (most of the time you don't need to see endpoint details)
|
||||
let collapsed = true;
|
||||
try {
|
||||
const v = localStorage.getItem(ENDPOINT_COLLAPSED_KEY);
|
||||
if (v === 'false') collapsed = false;
|
||||
else if (v === 'true') collapsed = true;
|
||||
} catch {}
|
||||
panel.classList.toggle('collapsed', collapsed);
|
||||
btn.addEventListener('click', () => {
|
||||
const nowCollapsed = !panel.classList.contains('collapsed');
|
||||
panel.classList.toggle('collapsed', nowCollapsed);
|
||||
try { localStorage.setItem(ENDPOINT_COLLAPSED_KEY, nowCollapsed ? 'true' : 'false'); } catch {}
|
||||
});
|
||||
}
|
||||
|
||||
function renderSwapPanel() {
|
||||
@@ -1800,6 +1858,8 @@ async function init() {
|
||||
a.classList.remove('hidden');
|
||||
}
|
||||
} catch {}
|
||||
setupDashboardTabs();
|
||||
setupEndpointCollapse();
|
||||
await loadModels();
|
||||
await pollStatus();
|
||||
await renderServices();
|
||||
|
||||
Reference in New Issue
Block a user