From 6ab67ec973856fb1909dea30b54792a0923736f5 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Wed, 27 May 2026 22:15:51 +0200 Subject: [PATCH] Complete engine card audit: edit mode, missing buttons, and docker management - Fix custom card dialog edit mode: save handler now updates existing card by id instead of always creating a new one; cancel/backdrop also clear editId - Add "Use as TTS" button to Piper TTS and XTTS v2 static cards - Add VibeVoice GitHub project link to VibeVoice card - Add Edit button to custom engine cards (pre-fills dialog for update flow) - Add initStaticDockerManagement(): injects optional docker container name field and Stop/Start/Restart buttons into all static llm-local-cards with no HTML changes Co-Authored-By: Claude Sonnet 4.6 --- static/app.js | 153 +++++++++++++++++++++++++++++++++--- static/sections/s-llms.html | 7 ++ 2 files changed, 148 insertions(+), 12 deletions(-) diff --git a/static/app.js b/static/app.js index 69c1031..8f8f000 100644 --- a/static/app.js +++ b/static/app.js @@ -6859,6 +6859,11 @@ const DC_ROLE_USE = { window.openAddEngineDialog = function(role) { const dlg = document.getElementById('add-engine-dlg'); if (!dlg) return; + delete dlg.dataset.editId; + const titleEl = dlg.querySelector('.add-engine-title'); + if (titleEl) titleEl.textContent = ' Add Custom Engine'; + const saveBtn = document.getElementById('aed-save'); + if (saveBtn) saveBtn.textContent = 'Add Card'; const roleEl = document.getElementById('aed-role'); if (roleEl && role) roleEl.value = role; ['aed-name', 'aed-url', 'aed-container', 'aed-desc'].forEach(id => { @@ -6867,11 +6872,29 @@ window.openAddEngineDialog = function(role) { dlg.showModal(); }; +function editCustomEngineCard(card) { + const dlg = document.getElementById('add-engine-dlg'); + if (!dlg) return; + dlg.dataset.editId = String(card.id || card.name); + const titleEl = dlg.querySelector('.add-engine-title'); + if (titleEl) titleEl.textContent = ' Edit Engine'; + const saveBtn = document.getElementById('aed-save'); + if (saveBtn) saveBtn.textContent = 'Save Changes'; + const roleEl = document.getElementById('aed-role'); + if (roleEl) roleEl.value = card.role || 'tts'; + const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val || ''; }; + setVal('aed-name', card.label || card.name); + setVal('aed-url', card.url); + setVal('aed-container', card.containerName); + setVal('aed-desc', card.description); + dlg.showModal(); +} + (function initCustomEngineDialog() { const dlg = document.getElementById('add-engine-dlg'); if (!dlg) return; - document.getElementById('aed-cancel')?.addEventListener('click', () => dlg.close()); - dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); }); + document.getElementById('aed-cancel')?.addEventListener('click', () => { delete dlg.dataset.editId; dlg.close(); }); + dlg.addEventListener('click', e => { if (e.target === dlg) { delete dlg.dataset.editId; dlg.close(); } }); document.getElementById('aed-save')?.addEventListener('click', () => { const name = document.getElementById('aed-name')?.value.trim(); const role = document.getElementById('aed-role')?.value; @@ -6880,13 +6903,27 @@ window.openAddEngineDialog = function(role) { const desc = document.getElementById('aed-desc')?.value.trim() || ''; if (!name) { toast('Name is required', 'error'); return; } if (!url) { toast('URL is required', 'error'); return; } - const card = { id: 'custom-' + Date.now(), name, label: name, role, url, containerName, description: desc, isCustom: true }; - const cards = loadCustomEngineCards(); - cards.push(card); - saveCustomEngineCards(cards); - dlg.close(); - loadLocalContainers(); - toast(`"${name}" added`, 'success'); + const editId = dlg.dataset.editId; + if (editId) { + const cards = loadCustomEngineCards().map(c => + String(c.id || c.name) === editId + ? { ...c, name, label: name, role, url, containerName, description: desc } + : c + ); + saveCustomEngineCards(cards); + delete dlg.dataset.editId; + dlg.close(); + loadLocalContainers(); + toast(`"${name}" updated`, 'success'); + } else { + const card = { id: 'custom-' + Date.now(), name, label: name, role, url, containerName, description: desc, isCustom: true }; + const cards = loadCustomEngineCards(); + cards.push(card); + saveCustomEngineCards(cards); + dlg.close(); + loadLocalContainers(); + toast(`"${name}" added`, 'success'); + } }); })(); @@ -7003,9 +7040,15 @@ function renderLocalContainers(containers) { ? ` View on GitHub` : '')); - // Delete button for custom cards + // Edit / Delete buttons for custom cards + const customId = escHtml(String(c.id || c.name)); + const editBtn = isCustom + ? `` + : ''; const deleteBtn = isCustom - ? `` : ''; @@ -7034,7 +7077,7 @@ function renderLocalContainers(containers) { ${metricsHtml} ${c.description ? `

${escHtml(c.description)}

` : ''}${urlRowHtml} -
${dockerBtns}${useBtn}${deleteBtn}
${snippetHtml} +
${dockerBtns}${useBtn}${editBtn}${deleteBtn}
${snippetHtml} `; } @@ -7140,6 +7183,15 @@ function renderLocalContainers(containers) { }); }); + // Edit custom card + grid.querySelectorAll('.dc-edit-btn[data-dc-custom-id]').forEach(btn => { + btn.addEventListener('click', () => { + const id = btn.dataset.dcCustomId; + const card = loadCustomEngineCards().find(c => String(c.id || c.name) === id); + if (card) editCustomEngineCard(card); + }); + }); + // Delete custom card grid.querySelectorAll('.dc-delete-btn[data-dc-custom-id]').forEach(btn => { btn.addEventListener('click', () => { @@ -7317,6 +7369,20 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl toast('VibeVoice URL saved → Settings. Pick "VibeVoice" in Try It Out → Backend or a routing rule.', 'success'); }); + $('llm-use-piper-tts')?.addEventListener('click', () => { + const url = document.querySelector('[data-llm-local-key="piper"]')?.value.trim() + || 'localhost:10200'; + applyAndSaveSettings({ tts_url: url }); + toast('Piper URL saved → tts_url. Note: Piper uses Wyoming protocol — pair with an OpenAI-compatible wrapper for full support.', 'success'); + }); + + $('llm-use-xtts-tts')?.addEventListener('click', () => { + const url = document.querySelector('[data-llm-local-key="xtts"]')?.value.trim() + || 'http://localhost:8020'; + applyAndSaveSettings({ tts_url: url }); + toast('XTTS v2 URL saved → tts_url in Settings.', 'success'); + }); + const LLM_USE_MAP = { 'llm-use-ollama-llm': { key: 'ollama', fallback: 'http://localhost:11434/v1' }, 'llm-use-vllm-llm': { key: 'vllm', fallback: 'http://localhost:8000/v1' }, @@ -7374,6 +7440,69 @@ function initLlmSnippets(root) { initLlmSnippets(document); +// ── Docker management for static llm-local-cards ────────────────────────── +// Finds every static card that has a [data-llm-local-key] URL input and +// appends an optional "container name" row + Stop/Start/Restart buttons. +// No HTML changes needed — the key is read from the existing input attribute. +(function initStaticDockerManagement() { + document.querySelectorAll('.llm-local-card:not([data-docker-init])').forEach(card => { + const urlInp = card.querySelector('[data-llm-local-key]'); + if (!urlInp) return; + card.dataset.dockerInit = '1'; + + const key = urlInp.dataset.llmLocalKey; + const saved = localStorage.getItem('llm-docker-name-' + key) || ''; + + // Inject the container name row right after the URL row + const urlRow = urlInp.closest('.llm-local-url'); + const dockerRow = document.createElement('div'); + dockerRow.className = 'llm-local-url'; + dockerRow.innerHTML = + `` + + ``; + const nameInp = dockerRow.querySelector('input'); + nameInp.value = saved; + if (urlRow) urlRow.after(dockerRow); else card.querySelector('.llm-local-actions')?.before(dockerRow); + + // Placeholder for the docker action buttons (appended into existing actions row) + const actionsEl = card.querySelector('.llm-local-actions'); + const dockerBtnsEl = document.createElement('span'); + actionsEl?.appendChild(dockerBtnsEl); + + function renderBtns() { + const name = nameInp.value.trim(); + if (!name) { dockerBtnsEl.innerHTML = ''; return; } + const esc = name.replace(/&/g,'&').replace(/"/g,'"'); + dockerBtnsEl.innerHTML = + `` + + `` + + ``; + dockerBtnsEl.querySelectorAll('.dc-btn').forEach(btn => { + btn.addEventListener('click', async () => { + const action = btn.dataset.dcAction; + const cname = btn.dataset.dcName; + const orig = btn.innerHTML; + btn.disabled = true; + btn.textContent = action === 'start' ? 'Starting…' : action === 'stop' ? 'Stopping…' : 'Restarting…'; + try { + const r = await fetch(`/api/local-containers/${encodeURIComponent(cname)}/${action}`, { method: 'POST' }); + const d = await r.json(); + if (d.ok) toast(`${cname}: ${action} OK`, 'success'); + else toast(d.error || `${action} failed`, 'error'); + } catch (e) { toast(`${action} failed: ${e.message}`, 'error'); } + btn.disabled = false; btn.innerHTML = orig; + }); + }); + } + + nameInp.addEventListener('input', () => { + localStorage.setItem('llm-docker-name-' + key, nameInp.value); + renderBtns(); + }); + renderBtns(); + }); +})(); + // ── Collapsible cards ────────────────────────────────────────────────────── (function initCollapsibleCards() { diff --git a/static/sections/s-llms.html b/static/sections/s-llms.html index e93ccdc..3fb991a 100644 --- a/static/sections/s-llms.html +++ b/static/sections/s-llms.html @@ -497,6 +497,9 @@ cd whisper.cpp && cmake -B build && cmake --build build -j +
+ +
Docker @@ -562,6 +565,9 @@ docker run -p 8880:8880 --gpus all \
+
+ +
Docker @@ -605,6 +611,7 @@ docker run -p 8880:8880 --gpus all \ -d '{"text":"Hello from VibeVoice"}' \ --output vibevoice-test.wav
+ github.com/Mekopa/VibeVoice