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 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-05-27 22:15:51 +02:00
parent fe9c497308
commit 6ab67ec973
2 changed files with 148 additions and 12 deletions

View File

@ -6859,6 +6859,11 @@ const DC_ROLE_USE = {
window.openAddEngineDialog = function(role) { window.openAddEngineDialog = function(role) {
const dlg = document.getElementById('add-engine-dlg'); const dlg = document.getElementById('add-engine-dlg');
if (!dlg) return; 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'); const roleEl = document.getElementById('aed-role');
if (roleEl && role) roleEl.value = role; if (roleEl && role) roleEl.value = role;
['aed-name', 'aed-url', 'aed-container', 'aed-desc'].forEach(id => { ['aed-name', 'aed-url', 'aed-container', 'aed-desc'].forEach(id => {
@ -6867,11 +6872,29 @@ window.openAddEngineDialog = function(role) {
dlg.showModal(); 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() { (function initCustomEngineDialog() {
const dlg = document.getElementById('add-engine-dlg'); const dlg = document.getElementById('add-engine-dlg');
if (!dlg) return; if (!dlg) return;
document.getElementById('aed-cancel')?.addEventListener('click', () => dlg.close()); document.getElementById('aed-cancel')?.addEventListener('click', () => { delete dlg.dataset.editId; dlg.close(); });
dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); }); dlg.addEventListener('click', e => { if (e.target === dlg) { delete dlg.dataset.editId; dlg.close(); } });
document.getElementById('aed-save')?.addEventListener('click', () => { document.getElementById('aed-save')?.addEventListener('click', () => {
const name = document.getElementById('aed-name')?.value.trim(); const name = document.getElementById('aed-name')?.value.trim();
const role = document.getElementById('aed-role')?.value; const role = document.getElementById('aed-role')?.value;
@ -6880,6 +6903,19 @@ window.openAddEngineDialog = function(role) {
const desc = document.getElementById('aed-desc')?.value.trim() || ''; const desc = document.getElementById('aed-desc')?.value.trim() || '';
if (!name) { toast('Name is required', 'error'); return; } if (!name) { toast('Name is required', 'error'); return; }
if (!url) { toast('URL is required', 'error'); return; } if (!url) { toast('URL is required', 'error'); return; }
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 card = { id: 'custom-' + Date.now(), name, label: name, role, url, containerName, description: desc, isCustom: true };
const cards = loadCustomEngineCards(); const cards = loadCustomEngineCards();
cards.push(card); cards.push(card);
@ -6887,6 +6923,7 @@ window.openAddEngineDialog = function(role) {
dlg.close(); dlg.close();
loadLocalContainers(); loadLocalContainers();
toast(`"${name}" added`, 'success'); toast(`"${name}" added`, 'success');
}
}); });
})(); })();
@ -7003,9 +7040,15 @@ function renderLocalContainers(containers) {
? `<a class="llm-use-btn" href="${escHtml(c.repo)}" target="_blank" rel="noopener"><span class="mdi mdi-github"></span> View on GitHub</a>` ? `<a class="llm-use-btn" href="${escHtml(c.repo)}" target="_blank" rel="noopener"><span class="mdi mdi-github"></span> View on GitHub</a>`
: '')); : ''));
// Delete button for custom cards // Edit / Delete buttons for custom cards
const customId = escHtml(String(c.id || c.name));
const editBtn = isCustom
? `<button class="llm-use-btn dc-edit-btn" data-dc-custom-id="${customId}">
<span class="mdi mdi-pencil-outline"></span> Edit
</button>`
: '';
const deleteBtn = isCustom const deleteBtn = isCustom
? `<button class="llm-use-btn dc-delete-btn" data-dc-custom-id="${escHtml(String(c.id || c.name))}"> ? `<button class="llm-use-btn dc-delete-btn" data-dc-custom-id="${customId}">
<span class="mdi mdi-delete-outline"></span> Remove <span class="mdi mdi-delete-outline"></span> Remove
</button>` </button>`
: ''; : '';
@ -7034,7 +7077,7 @@ function renderLocalContainers(containers) {
</div> </div>
${metricsHtml} ${metricsHtml}
${c.description ? `<p class="llm-local-desc">${escHtml(c.description)}</p>` : ''}${urlRowHtml} ${c.description ? `<p class="llm-local-desc">${escHtml(c.description)}</p>` : ''}${urlRowHtml}
<div class="llm-local-actions">${dockerBtns}${useBtn}${deleteBtn}</div>${snippetHtml} <div class="llm-local-actions">${dockerBtns}${useBtn}${editBtn}${deleteBtn}</div>${snippetHtml}
</div>`; </div>`;
} }
@ -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 // Delete custom card
grid.querySelectorAll('.dc-delete-btn[data-dc-custom-id]').forEach(btn => { grid.querySelectorAll('.dc-delete-btn[data-dc-custom-id]').forEach(btn => {
btn.addEventListener('click', () => { 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'); 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 = { const LLM_USE_MAP = {
'llm-use-ollama-llm': { key: 'ollama', fallback: 'http://localhost:11434/v1' }, 'llm-use-ollama-llm': { key: 'ollama', fallback: 'http://localhost:11434/v1' },
'llm-use-vllm-llm': { key: 'vllm', fallback: 'http://localhost:8000/v1' }, 'llm-use-vllm-llm': { key: 'vllm', fallback: 'http://localhost:8000/v1' },
@ -7374,6 +7440,69 @@ function initLlmSnippets(root) {
initLlmSnippets(document); 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 =
`<span class="llm-local-url-label" title="Docker container name — enables Stop / Start / Restart"><span class="mdi mdi-docker"></span></span>` +
`<input class="llm-local-url-inp" type="text" placeholder="container name (optional)" spellcheck="false">`;
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,'&amp;').replace(/"/g,'&quot;');
dockerBtnsEl.innerHTML =
`<button class="llm-use-btn dc-btn" data-dc-action="start" data-dc-name="${esc}"><span class="mdi mdi-play"></span> Start</button>` +
`<button class="llm-use-btn dc-btn" data-dc-action="stop" data-dc-name="${esc}"><span class="mdi mdi-stop"></span> Stop</button>` +
`<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${esc}"><span class="mdi mdi-restart"></span> Restart</button>`;
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 ────────────────────────────────────────────────────── // ── Collapsible cards ──────────────────────────────────────────────────────
(function initCollapsibleCards() { (function initCollapsibleCards() {

View File

@ -497,6 +497,9 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
<input class="llm-local-url-inp" type="text" placeholder="localhost:10200" data-llm-local-key="piper" data-llm-local-default="localhost:10200" spellcheck="false"> <input class="llm-local-url-inp" type="text" placeholder="localhost:10200" data-llm-local-key="piper" data-llm-local-default="localhost:10200" spellcheck="false">
<button class="llm-local-ping" data-ping-key="piper" title="Test connection">Connect</button> <button class="llm-local-ping" data-ping-key="piper" title="Test connection">Connect</button>
</div> </div>
<div class="llm-local-actions">
<button class="llm-use-btn" id="llm-use-piper-tts" type="button"><span class="mdi mdi-content-copy"></span> Use as TTS</button>
</div>
<div class="llm-local-snippet"> <div class="llm-local-snippet">
<div class="llm-snippet-bar"> <div class="llm-snippet-bar">
<span>Docker</span> <span>Docker</span>
@ -562,6 +565,9 @@ docker run -p 8880:8880 --gpus all \
<input class="llm-local-url-inp" type="text" placeholder="http://localhost:8020/tts_to_audio" data-llm-local-key="xtts" data-llm-local-default="http://localhost:8020/tts_to_audio" spellcheck="false"> <input class="llm-local-url-inp" type="text" placeholder="http://localhost:8020/tts_to_audio" data-llm-local-key="xtts" data-llm-local-default="http://localhost:8020/tts_to_audio" spellcheck="false">
<button class="llm-local-ping" data-ping-key="xtts" title="Test connection">Connect</button> <button class="llm-local-ping" data-ping-key="xtts" title="Test connection">Connect</button>
</div> </div>
<div class="llm-local-actions">
<button class="llm-use-btn" id="llm-use-xtts-tts" type="button"><span class="mdi mdi-content-copy"></span> Use as TTS</button>
</div>
<div class="llm-local-snippet"> <div class="llm-local-snippet">
<div class="llm-snippet-bar"> <div class="llm-snippet-bar">
<span>Docker</span> <span>Docker</span>
@ -605,6 +611,7 @@ docker run -p 8880:8880 --gpus all \
-d '{"text":"Hello from VibeVoice"}' \ -d '{"text":"Hello from VibeVoice"}' \
--output vibevoice-test.wav</pre> --output vibevoice-test.wav</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/Mekopa/VibeVoice" target="_blank" rel="noopener">github.com/Mekopa/VibeVoice <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
</div> </div>