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:
parent
fe9c497308
commit
6ab67ec973
139
static/app.js
139
static/app.js
@ -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,'&').replace(/"/g,'"');
|
||||||
|
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() {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user