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) {
|
||||
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,6 +6903,19 @@ 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 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);
|
||||
@ -6887,6 +6923,7 @@ window.openAddEngineDialog = function(role) {
|
||||
dlg.close();
|
||||
loadLocalContainers();
|
||||
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>`
|
||||
: ''));
|
||||
|
||||
// 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
|
||||
? `<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
|
||||
</button>`
|
||||
: '';
|
||||
@ -7034,7 +7077,7 @@ function renderLocalContainers(containers) {
|
||||
</div>
|
||||
${metricsHtml}
|
||||
${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>`;
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
`<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 ──────────────────────────────────────────────────────
|
||||
|
||||
(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">
|
||||
<button class="llm-local-ping" data-ping-key="piper" title="Test connection">Connect</button>
|
||||
</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-snippet-bar">
|
||||
<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">
|
||||
<button class="llm-local-ping" data-ping-key="xtts" title="Test connection">Connect</button>
|
||||
</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-snippet-bar">
|
||||
<span>Docker</span>
|
||||
@ -605,6 +611,7 @@ docker run -p 8880:8880 --gpus all \
|
||||
-d '{"text":"Hello from VibeVoice"}' \
|
||||
--output vibevoice-test.wav</pre>
|
||||
</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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user