Add URL inputs, Use-as buttons, and custom engine cards to Engines section
- Add URL input row to all docker container cards (defaulting to host.docker.internal:{port}), with Connect button, localStorage persistence, and green card border when reachable
- Map each docker container to its settings key (tts_url, nvidia_tts_url, nvidia_asr_url, tts_stream_url) via DC_USE_MAP; add "Use as TTS/STT" button that applies the URL to settings
- Add "+ Add Custom" button to LLM / STT / TTS sub-page headers, opening a dialog with Name, Role, URL, optional Docker container name, and Description fields
- Custom cards are saved to localStorage, rendered in dc-grid-llm/stt/tts alongside docker cards, have URL input + Use-as + Remove buttons; container name field enables Stop/Start/Restart
- Expose applyAndSaveSettings and probeUrl as window globals so docker card bindings can call them after renderLocalContainers
- Add dialog CSS and .dc-delete-btn red variant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6808db3c59
commit
fe9c497308
222
static/app.js
222
static/app.js
@ -6831,35 +6831,94 @@ loadVoiceLibrary().then(renderIntegrationSnippets).catch(e => status('Voice libr
|
|||||||
// ── Engines section: load containers on startup ────────────────────────────
|
// ── Engines section: load containers on startup ────────────────────────────
|
||||||
loadLocalContainers();
|
loadLocalContainers();
|
||||||
|
|
||||||
|
// ── Custom engine card storage helpers ────────────────────────────────────
|
||||||
|
function loadCustomEngineCards() {
|
||||||
|
try { return JSON.parse(localStorage.getItem('engines-custom-cards') || '[]'); }
|
||||||
|
catch (e) { return []; }
|
||||||
|
}
|
||||||
|
function saveCustomEngineCards(cards) {
|
||||||
|
localStorage.setItem('engines-custom-cards', JSON.stringify(cards));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings key per docker container name; role fallback for custom cards
|
||||||
|
const DC_USE_MAP = {
|
||||||
|
'faster-qwen3-tts-voiceclone': { settingKey: 'tts_url', label: 'Use as TTS' },
|
||||||
|
'faster-qwen3-tts-voicedesign': { settingKey: 'tts_url', label: 'Use as TTS' },
|
||||||
|
'faster-qwen3-tts-customvoice': { settingKey: 'tts_url', label: 'Use as TTS' },
|
||||||
|
'faster-qwen3-tts-streaming': { settingKey: 'tts_stream_url', label: 'Use as Streaming TTS' },
|
||||||
|
'parakeet-asr': { settingKey: 'nvidia_asr_url', label: 'Use as STT' },
|
||||||
|
'magpie-tts': { settingKey: 'nvidia_tts_url', label: 'Use as TTS' },
|
||||||
|
'parakeet-rnnt-nim': { settingKey: 'nvidia_asr_url', label: 'Use as STT' },
|
||||||
|
};
|
||||||
|
const DC_ROLE_USE = {
|
||||||
|
tts: { settingKey: 'tts_url', label: 'Use as TTS' },
|
||||||
|
stt: { settingKey: 'faster_whisper_url', label: 'Use as STT' },
|
||||||
|
llm: { settingKey: 'llm_url', label: 'Use as LLM' },
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openAddEngineDialog = function(role) {
|
||||||
|
const dlg = document.getElementById('add-engine-dlg');
|
||||||
|
if (!dlg) return;
|
||||||
|
const roleEl = document.getElementById('aed-role');
|
||||||
|
if (roleEl && role) roleEl.value = role;
|
||||||
|
['aed-name', 'aed-url', 'aed-container', 'aed-desc'].forEach(id => {
|
||||||
|
const el = document.getElementById(id); if (el) el.value = '';
|
||||||
|
});
|
||||||
|
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-save')?.addEventListener('click', () => {
|
||||||
|
const name = document.getElementById('aed-name')?.value.trim();
|
||||||
|
const role = document.getElementById('aed-role')?.value;
|
||||||
|
const url = document.getElementById('aed-url')?.value.trim();
|
||||||
|
const containerName = document.getElementById('aed-container')?.value.trim() || '';
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// ── Local Docker container management ─────────────────────────────────────
|
// ── Local Docker container management ─────────────────────────────────────
|
||||||
|
|
||||||
async function loadLocalContainers() {
|
async function loadLocalContainers() {
|
||||||
const gridTts = $('dc-grid-tts');
|
const gridTts = $('dc-grid-tts');
|
||||||
const gridStt = $('dc-grid-stt');
|
const gridStt = $('dc-grid-stt');
|
||||||
if (!gridTts && !gridStt) return;
|
const gridLlm = $('dc-grid-llm');
|
||||||
|
if (!gridTts && !gridStt && !gridLlm) return;
|
||||||
const loading = '<div class="dc-loading">Checking container status…</div>';
|
const loading = '<div class="dc-loading">Checking container status…</div>';
|
||||||
if (gridTts) gridTts.innerHTML = loading;
|
if (gridTts) gridTts.innerHTML = loading;
|
||||||
if (gridStt) gridStt.innerHTML = loading;
|
if (gridStt) gridStt.innerHTML = loading;
|
||||||
|
if (gridLlm) gridLlm.innerHTML = '';
|
||||||
|
const custom = loadCustomEngineCards();
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/local-containers');
|
const r = await fetch('/api/local-containers');
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
renderLocalContainers(d.containers || []);
|
renderLocalContainers([...(d.containers || []), ...custom]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = `<div class="dc-loading dc-load-err">Could not reach server: ${escHtml(e.message)}</div>`;
|
const err = `<div class="dc-loading dc-load-err">Could not reach server: ${escHtml(e.message)}</div>`;
|
||||||
if (gridTts) gridTts.innerHTML = err;
|
if (gridTts) gridTts.innerHTML = err;
|
||||||
if (gridStt) gridStt.innerHTML = err;
|
if (gridStt) gridStt.innerHTML = err;
|
||||||
|
if (custom.length) renderLocalContainers(custom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLocalContainers(containers) {
|
function renderLocalContainers(containers) {
|
||||||
const gridTts = $('dc-grid-tts');
|
const gridTts = $('dc-grid-tts');
|
||||||
const gridStt = $('dc-grid-stt');
|
const gridStt = $('dc-grid-stt');
|
||||||
if (!gridTts && !gridStt) return;
|
const gridLlm = $('dc-grid-llm');
|
||||||
if (!containers.length) {
|
if (!gridTts && !gridStt && !gridLlm) return;
|
||||||
if (gridTts) gridTts.innerHTML = '';
|
|
||||||
if (gridStt) gridStt.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROLE_LABEL = { tts: 'TTS', stt: 'STT', 'stt+tts': 'STT · TTS', llm: 'LLM' };
|
const ROLE_LABEL = { tts: 'TTS', stt: 'STT', 'stt+tts': 'STT · TTS', llm: 'LLM' };
|
||||||
const DC_ICONS = {
|
const DC_ICONS = {
|
||||||
@ -6887,12 +6946,13 @@ function renderLocalContainers(containers) {
|
|||||||
const roleIcon = { tts: '<span class="mdi mdi-speaker-outline"></span>', stt: '<span class="mdi mdi-microphone-variant"></span>', 'stt+tts': '<span class="mdi mdi-swap-horizontal"></span>', llm: '<span class="mdi mdi-robot-outline"></span>' };
|
const roleIcon = { tts: '<span class="mdi mdi-speaker-outline"></span>', stt: '<span class="mdi mdi-microphone-variant"></span>', 'stt+tts': '<span class="mdi mdi-swap-horizontal"></span>', llm: '<span class="mdi mdi-robot-outline"></span>' };
|
||||||
|
|
||||||
function buildCardHtml(c) {
|
function buildCardHtml(c) {
|
||||||
const st = c.status || 'not_found';
|
const isCustom = !!c.isCustom;
|
||||||
|
const st = isCustom ? 'custom' : (c.status || 'not_found');
|
||||||
const running = st === 'running';
|
const running = st === 'running';
|
||||||
const stopped = st === 'exited' || st === 'stopped';
|
const stopped = st === 'exited' || st === 'stopped';
|
||||||
const absent = st === 'not_found';
|
const absent = st === 'not_found';
|
||||||
const stLabel = running ? 'Running' : stopped ? 'Stopped' : 'Not installed';
|
const stLabel = isCustom ? 'Custom' : running ? 'Running' : stopped ? 'Stopped' : 'Not installed';
|
||||||
const stCls = running ? 'llm-compat-running' : stopped ? 'llm-compat-stopped' : '';
|
const stCls = isCustom ? '' : running ? 'llm-compat-running' : stopped ? 'llm-compat-stopped' : '';
|
||||||
const cardCls = running ? ' llm-local-card-running' : stopped ? ' llm-local-card-offline' : '';
|
const cardCls = running ? ' llm-local-card-running' : stopped ? ' llm-local-card-offline' : '';
|
||||||
const roleBadge = ROLE_LABEL[c.role] || c.role || '';
|
const roleBadge = ROLE_LABEL[c.role] || c.role || '';
|
||||||
const portStr = c.port ? ` :${c.port}` : '';
|
const portStr = c.port ? ` :${c.port}` : '';
|
||||||
@ -6902,7 +6962,38 @@ function renderLocalContainers(containers) {
|
|||||||
.map(([em, txt]) => `<span class="llm-metric-chip">${em} ${escHtml(txt)}</span>`).join('');
|
.map(([em, txt]) => `<span class="llm-metric-chip">${em} ${escHtml(txt)}</span>`).join('');
|
||||||
const metricsHtml = metricChips ? `<div class="llm-local-metrics">${metricChips}</div>` : '';
|
const metricsHtml = metricChips ? `<div class="llm-local-metrics">${metricChips}</div>` : '';
|
||||||
const n = escHtml(c.name);
|
const n = escHtml(c.name);
|
||||||
const actions = installed
|
|
||||||
|
// URL row (docker containers use port-based default; custom cards use their stored url)
|
||||||
|
const defaultUrl = isCustom
|
||||||
|
? (c.url || '')
|
||||||
|
: (c.port ? `http://host.docker.internal:${c.port}` : '');
|
||||||
|
const urlRowHtml = `
|
||||||
|
<div class="llm-local-url">
|
||||||
|
<span class="llm-local-url-label">URL</span>
|
||||||
|
<input class="llm-local-url-inp dc-url-inp" type="text" spellcheck="false"
|
||||||
|
placeholder="${escHtml(defaultUrl)}"
|
||||||
|
data-dc-url-key="${n}"
|
||||||
|
data-dc-url-default="${escHtml(defaultUrl)}">
|
||||||
|
<button class="llm-local-ping dc-connect-btn" data-dc-url-key="${n}">Connect</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Use-as button
|
||||||
|
const useEntry = DC_USE_MAP[c.name] || DC_ROLE_USE[c.role];
|
||||||
|
const useBtn = useEntry
|
||||||
|
? `<button class="llm-use-btn dc-use-btn" data-dc-use-key="${escHtml(useEntry.settingKey)}" data-dc-name="${n}">
|
||||||
|
<span class="mdi mdi-check-circle-outline"></span> ${escHtml(useEntry.label)}
|
||||||
|
</button>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Docker stop/start/restart buttons
|
||||||
|
const dockerContainerName = isCustom ? escHtml(c.containerName || '') : n;
|
||||||
|
const dockerBtns = isCustom
|
||||||
|
? (c.containerName
|
||||||
|
? `<button class="llm-use-btn dc-btn" data-dc-action="start" data-dc-name="${dockerContainerName}"><span class="mdi mdi-play"></span> Start</button>
|
||||||
|
<button class="llm-use-btn dc-btn" data-dc-action="stop" data-dc-name="${dockerContainerName}"><span class="mdi mdi-stop"></span> Stop</button>
|
||||||
|
<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${dockerContainerName}"><span class="mdi mdi-restart"></span> Restart</button>`
|
||||||
|
: '')
|
||||||
|
: (installed
|
||||||
? (running
|
? (running
|
||||||
? `<button class="llm-use-btn dc-btn" data-dc-action="stop" data-dc-name="${n}"><span class="mdi mdi-stop"></span> Stop</button>
|
? `<button class="llm-use-btn dc-btn" data-dc-action="stop" data-dc-name="${n}"><span class="mdi mdi-stop"></span> Stop</button>
|
||||||
<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${n}"><span class="mdi mdi-restart"></span> Restart</button>`
|
<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${n}"><span class="mdi mdi-restart"></span> Restart</button>`
|
||||||
@ -6910,13 +7001,22 @@ function renderLocalContainers(containers) {
|
|||||||
<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${n}"><span class="mdi mdi-restart"></span> Restart</button>`)
|
<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${n}"><span class="mdi mdi-restart"></span> Restart</button>`)
|
||||||
: (c.repo
|
: (c.repo
|
||||||
? `<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
|
||||||
|
const deleteBtn = isCustom
|
||||||
|
? `<button class="llm-use-btn dc-delete-btn" data-dc-custom-id="${escHtml(String(c.id || c.name))}">
|
||||||
|
<span class="mdi mdi-delete-outline"></span> Remove
|
||||||
|
</button>`
|
||||||
|
: '';
|
||||||
|
|
||||||
const portNum = c.port || '';
|
const portNum = c.port || '';
|
||||||
|
const snippetBaseUrl = isCustom ? (c.url || 'http://localhost') : `http://localhost:${portNum}`;
|
||||||
const snippetCode = c.role === 'tts'
|
const snippetCode = c.role === 'tts'
|
||||||
? `curl -s "http://localhost:${portNum}/v1/audio/speech" \\\n -H "Authorization: Bearer dummy" \\\n -H "Content-Type: application/json" \\\n -d '{"model":"tts-1","voice":"default","input":"Hello world","response_format":"wav"}' \\\n --output test.wav`
|
? `curl -s "${snippetBaseUrl}/v1/audio/speech" \\\n -H "Authorization: Bearer dummy" \\\n -H "Content-Type: application/json" \\\n -d '{"model":"tts-1","voice":"default","input":"Hello world","response_format":"wav"}' \\\n --output test.wav`
|
||||||
: `curl -s "http://localhost:${portNum}/v1/audio/transcriptions" \\\n -F "file=@audio.wav" \\\n -F "model=whisper-1"`;
|
: `curl -s "${snippetBaseUrl}/v1/audio/transcriptions" \\\n -F "file=@audio.wav" \\\n -F "model=whisper-1"`;
|
||||||
const snippetCopy = escHtml(snippetCode);
|
const snippetCopy = escHtml(snippetCode);
|
||||||
const snippetHtml = portNum ? `
|
const snippetHtml = (portNum || isCustom) ? `
|
||||||
<div class="llm-local-snippet">
|
<div class="llm-local-snippet">
|
||||||
<div class="llm-snippet-bar">
|
<div class="llm-snippet-bar">
|
||||||
<span>API test</span>
|
<span>API test</span>
|
||||||
@ -6924,6 +7024,7 @@ function renderLocalContainers(containers) {
|
|||||||
</div>
|
</div>
|
||||||
<pre>${escHtml(snippetCode)}</pre>
|
<pre>${escHtml(snippetCode)}</pre>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
return `<div class="llm-local-card${cardCls}">
|
return `<div class="llm-local-card${cardCls}">
|
||||||
<div class="llm-local-head">
|
<div class="llm-local-head">
|
||||||
<span class="llm-local-icon">${icon}</span>
|
<span class="llm-local-icon">${icon}</span>
|
||||||
@ -6932,19 +7033,23 @@ function renderLocalContainers(containers) {
|
|||||||
<span class="llm-local-compat ${stCls}">${escHtml(stLabel)}</span>
|
<span class="llm-local-compat ${stCls}">${escHtml(stLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
${metricsHtml}
|
${metricsHtml}
|
||||||
${c.description ? `<p class="llm-local-desc">${escHtml(c.description)}</p>` : ''}
|
${c.description ? `<p class="llm-local-desc">${escHtml(c.description)}</p>` : ''}${urlRowHtml}
|
||||||
<div class="llm-local-actions">${actions}</div>${snippetHtml}
|
<div class="llm-local-actions">${dockerBtns}${useBtn}${deleteBtn}</div>${snippetHtml}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTts = c => c.role === 'tts';
|
const isTts = c => c.role === 'tts';
|
||||||
const isStt = c => c.role === 'stt' || c.role === 'stt+tts';
|
const isStt = c => c.role === 'stt' || c.role === 'stt+tts';
|
||||||
|
const isLlm = c => c.role === 'llm';
|
||||||
|
|
||||||
if (gridTts) gridTts.innerHTML = containers.filter(isTts).map(buildCardHtml).join('');
|
if (gridTts) gridTts.innerHTML = containers.filter(isTts).map(buildCardHtml).join('');
|
||||||
if (gridStt) gridStt.innerHTML = containers.filter(isStt).map(buildCardHtml).join('');
|
if (gridStt) gridStt.innerHTML = containers.filter(isStt).map(buildCardHtml).join('');
|
||||||
|
if (gridLlm) gridLlm.innerHTML = containers.filter(isLlm).map(buildCardHtml).join('');
|
||||||
|
|
||||||
[gridTts, gridStt].forEach(grid => {
|
[gridTts, gridStt, gridLlm].forEach(grid => {
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
|
// Stop / Start / Restart docker containers
|
||||||
grid.querySelectorAll('.dc-btn[data-dc-action]').forEach(btn => {
|
grid.querySelectorAll('.dc-btn[data-dc-action]').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const action = btn.dataset.dcAction;
|
const action = btn.dataset.dcAction;
|
||||||
@ -6961,6 +7066,8 @@ function renderLocalContainers(containers) {
|
|||||||
await loadLocalContainers();
|
await loadLocalContainers();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Copy snippet buttons
|
||||||
grid.querySelectorAll('.dc-copy-btn').forEach(btn => {
|
grid.querySelectorAll('.dc-copy-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const text = (btn.dataset.copy || '').replace(/ /g, '\n').replace(/"/g, '"').replace(/&/g, '&');
|
const text = (btn.dataset.copy || '').replace(/ /g, '\n').replace(/"/g, '"').replace(/&/g, '&');
|
||||||
@ -6968,6 +7075,81 @@ function renderLocalContainers(containers) {
|
|||||||
toast('Copied', 'success');
|
toast('Copied', 'success');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// URL input — load from localStorage, auto-save, restore connected state
|
||||||
|
grid.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(inp => {
|
||||||
|
const key = inp.dataset.dcUrlKey;
|
||||||
|
const saved = localStorage.getItem('dc-url-' + key);
|
||||||
|
if (saved) inp.value = saved;
|
||||||
|
inp.addEventListener('input', () => localStorage.setItem('dc-url-' + key, inp.value));
|
||||||
|
if (localStorage.getItem('dc-con-' + key) === '1') {
|
||||||
|
const card = inp.closest('.llm-local-card');
|
||||||
|
const btn = grid.querySelector(`.dc-connect-btn[data-dc-url-key="${CSS.escape(key)}"]`);
|
||||||
|
if (card) card.classList.add('llm-local-card-online');
|
||||||
|
if (btn) { btn.textContent = '✓ Connected'; btn.className = 'llm-local-ping dc-connect-btn ok'; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect / Disconnect button in docker card URL row
|
||||||
|
grid.querySelectorAll('.dc-connect-btn[data-dc-url-key]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const key = btn.dataset.dcUrlKey;
|
||||||
|
const inp = grid.querySelector(`.dc-url-inp[data-dc-url-key="${CSS.escape(key)}"]`);
|
||||||
|
const rawUrl = inp?.value.trim() || inp?.placeholder;
|
||||||
|
if (!rawUrl) return;
|
||||||
|
const card = btn.closest('.llm-local-card');
|
||||||
|
btn.disabled = true; btn.textContent = 'Connecting…';
|
||||||
|
try {
|
||||||
|
if (window.probeUrl) {
|
||||||
|
const d = await window.probeUrl(rawUrl);
|
||||||
|
if (d.ok) {
|
||||||
|
btn.textContent = '✓ Connected'; btn.className = 'llm-local-ping dc-connect-btn ok';
|
||||||
|
if (card) card.classList.add('llm-local-card-online');
|
||||||
|
localStorage.setItem('dc-con-' + key, '1');
|
||||||
|
} else {
|
||||||
|
btn.textContent = 'Connect'; btn.className = 'llm-local-ping dc-connect-btn';
|
||||||
|
if (card) card.classList.remove('llm-local-card-online');
|
||||||
|
localStorage.removeItem('dc-con-' + key);
|
||||||
|
toast('Cannot reach ' + rawUrl + ': ' + (d.error || 'No response'), 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.textContent = 'Connect'; btn.className = 'llm-local-ping dc-connect-btn';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
btn.textContent = 'Connect'; btn.className = 'llm-local-ping dc-connect-btn';
|
||||||
|
if (card) card.classList.remove('llm-local-card-online');
|
||||||
|
localStorage.removeItem('dc-con-' + key);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use as TTS / STT / LLM
|
||||||
|
grid.querySelectorAll('.dc-use-btn[data-dc-use-key]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const settingKey = btn.dataset.dcUseKey;
|
||||||
|
const urlKey = btn.dataset.dcName;
|
||||||
|
const inp = grid.querySelector(`.dc-url-inp[data-dc-url-key="${CSS.escape(urlKey)}"]`);
|
||||||
|
const url = inp?.value.trim() || inp?.placeholder || '';
|
||||||
|
if (!url) { toast('Enter a URL first', 'error'); return; }
|
||||||
|
if (window.applyAndSaveSettings) {
|
||||||
|
await window.applyAndSaveSettings({ [settingKey]: url });
|
||||||
|
toast(`${settingKey.replace(/_/g, ' ')} → ${url}`, 'success');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete custom card
|
||||||
|
grid.querySelectorAll('.dc-delete-btn[data-dc-custom-id]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const id = btn.dataset.dcCustomId;
|
||||||
|
const cards = loadCustomEngineCards().filter(c => String(c.id || c.name) !== id);
|
||||||
|
saveCustomEngineCards(cards);
|
||||||
|
loadLocalContainers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
initLlmSnippets(grid);
|
initLlmSnippets(grid);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -7104,6 +7286,8 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl
|
|||||||
await refreshSttBackends();
|
await refreshSttBackends();
|
||||||
} catch (e) { toast('Apply failed: ' + e.message, 'error'); }
|
} catch (e) { toast('Apply failed: ' + e.message, 'error'); }
|
||||||
}
|
}
|
||||||
|
window.applyAndSaveSettings = applyAndSaveSettings;
|
||||||
|
window.probeUrl = probeUrl;
|
||||||
|
|
||||||
$('llm-use-faster-whisper-stt')?.addEventListener('click', () => {
|
$('llm-use-faster-whisper-stt')?.addEventListener('click', () => {
|
||||||
const url = document.querySelector('[data-llm-local-key="faster-whisper"]')?.value.trim()
|
const url = document.querySelector('[data-llm-local-key="faster-whisper"]')?.value.trim()
|
||||||
|
|||||||
@ -14,8 +14,13 @@
|
|||||||
<h3 class="llm-sec-title">Language Models</h3>
|
<h3 class="llm-sec-title">Language Models</h3>
|
||||||
<p class="llm-sec-note">Used for persona rewriting and transcription refinement. Click <strong>Use as LLM</strong> to apply a URL to this app’s LLM setting.</p>
|
<p class="llm-sec-note">Used for persona rewriting and transcription refinement. Click <strong>Use as LLM</strong> to apply a URL to this app’s LLM setting.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn-secondary" type="button" onclick="openAddEngineDialog('llm')"><span class="mdi mdi-plus"></span> Add Custom</button>
|
||||||
<span class="llm-free-badge llm-free-local">Local first</span>
|
<span class="llm-free-badge llm-free-local">Local first</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dc-grid-llm" class="dc-grid" style="margin-bottom:14px"></div>
|
||||||
|
|
||||||
<h4 class="llm-local-cat">Local</h4>
|
<h4 class="llm-local-cat">Local</h4>
|
||||||
<div class="llm-local-grid">
|
<div class="llm-local-grid">
|
||||||
@ -279,6 +284,7 @@ Check "Enable CORS" for browser access</pre>
|
|||||||
<p class="llm-sec-note">Your Docker stack containers appear at the top. Local runners and cloud APIs below.</p>
|
<p class="llm-sec-note">Your Docker stack containers appear at the top. Local runners and cloud APIs below.</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;align-items:center">
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn-secondary" type="button" onclick="openAddEngineDialog('stt')"><span class="mdi mdi-plus"></span> Add Custom</button>
|
||||||
<button class="btn-secondary dc-refresh-btn" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
|
<button class="btn-secondary dc-refresh-btn" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
|
||||||
<span class="llm-free-badge llm-free-local">100% Local</span>
|
<span class="llm-free-badge llm-free-local">100% Local</span>
|
||||||
</div>
|
</div>
|
||||||
@ -462,6 +468,7 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
|
|||||||
<p class="llm-sec-note">Your Docker stack containers appear at the top. Click <strong>Use as TTS</strong> to apply a URL to this app’s backend settings.</p>
|
<p class="llm-sec-note">Your Docker stack containers appear at the top. Click <strong>Use as TTS</strong> to apply a URL to this app’s backend settings.</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;align-items:center">
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn-secondary" type="button" onclick="openAddEngineDialog('tts')"><span class="mdi mdi-plus"></span> Add Custom</button>
|
||||||
<button class="btn-secondary dc-refresh-btn" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
|
<button class="btn-secondary dc-refresh-btn" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
|
||||||
<span class="llm-free-badge llm-free-local">100% Local</span>
|
<span class="llm-free-badge llm-free-local">100% Local</span>
|
||||||
</div>
|
</div>
|
||||||
@ -687,3 +694,38 @@ docker run -p 8880:8880 --gpus all \
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /tts -->
|
</div><!-- /tts -->
|
||||||
|
|
||||||
|
<!-- ── Add Custom Engine dialog ─────────────────────────────── -->
|
||||||
|
<dialog id="add-engine-dlg" class="add-engine-dlg">
|
||||||
|
<div class="add-engine-form">
|
||||||
|
<h3 class="add-engine-title"><span class="mdi mdi-plus-circle-outline"></span> Add Custom Engine</h3>
|
||||||
|
<div class="add-engine-field">
|
||||||
|
<label for="aed-name">Name</label>
|
||||||
|
<input id="aed-name" type="text" placeholder="My TTS Server" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div class="add-engine-field">
|
||||||
|
<label for="aed-role">Role</label>
|
||||||
|
<select id="aed-role">
|
||||||
|
<option value="tts">Text to Speech (TTS)</option>
|
||||||
|
<option value="stt">Speech to Text (STT)</option>
|
||||||
|
<option value="llm">Language Model (LLM)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="add-engine-field">
|
||||||
|
<label for="aed-url">URL</label>
|
||||||
|
<input id="aed-url" type="text" placeholder="http://192.168.1.100:8080" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div class="add-engine-field">
|
||||||
|
<label for="aed-container">Docker container name <span class="add-engine-opt">(optional — enables Stop / Start / Restart)</span></label>
|
||||||
|
<input id="aed-container" type="text" placeholder="my-tts-container" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div class="add-engine-field">
|
||||||
|
<label for="aed-desc">Description <span class="add-engine-opt">(optional)</span></label>
|
||||||
|
<input id="aed-desc" type="text" placeholder="Short description">
|
||||||
|
</div>
|
||||||
|
<div class="add-engine-actions">
|
||||||
|
<button type="button" class="btn-secondary" id="aed-cancel">Cancel</button>
|
||||||
|
<button type="button" class="btn-primary" id="aed-save">Add Card</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|||||||
@ -1601,6 +1601,31 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.llm-howto-body p { margin: 0; color: var(--subtext); }
|
.llm-howto-body p { margin: 0; color: var(--subtext); }
|
||||||
.llm-howto-body em { font-style: normal; font-weight: 600; color: var(--text); }
|
.llm-howto-body em { font-style: normal; font-weight: 600; color: var(--text); }
|
||||||
|
|
||||||
|
/* ── Add Custom Engine dialog ─────────────────────────────────────────────── */
|
||||||
|
.add-engine-dlg {
|
||||||
|
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
padding: 0; width: min(460px, 94vw); box-shadow: 0 8px 32px rgba(0,0,0,.28);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.add-engine-dlg::backdrop { background: rgba(0,0,0,.45); }
|
||||||
|
.add-engine-form { padding: 22px 24px; display: flex; flex-direction: column; gap: 14px; }
|
||||||
|
.add-engine-title { margin: 0 0 2px; font-size: 15px; font-weight: 700; }
|
||||||
|
.add-engine-field { display: flex; flex-direction: column; gap: 5px; }
|
||||||
|
.add-engine-field label { font-size: 12px; font-weight: 600; color: var(--subtext); }
|
||||||
|
.add-engine-opt { font-weight: 400; opacity: .65; }
|
||||||
|
.add-engine-field input, .add-engine-field select {
|
||||||
|
padding: 7px 10px; border: 1px solid var(--border); border-radius: 5px;
|
||||||
|
background: var(--bg); color: var(--text); font-family: inherit; font-size: 13px;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.add-engine-field input:focus, .add-engine-field select:focus {
|
||||||
|
outline: none; border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.add-engine-actions { display: flex; gap: 8px; justify-content: flex-end; padding-top: 4px; }
|
||||||
|
/* Delete button variant */
|
||||||
|
.dc-delete-btn { border-color: rgba(220,38,38,.35) !important; color: var(--red, #dc2626) !important; background: rgba(220,38,38,.05) !important; }
|
||||||
|
.dc-delete-btn:hover { background: rgba(220,38,38,.12) !important; border-color: var(--red, #dc2626) !important; }
|
||||||
|
|
||||||
/* ── ElevenLabs Voice Browser ──────────────────────────────────────────────── */
|
/* ── ElevenLabs Voice Browser ──────────────────────────────────────────────── */
|
||||||
.el-browser { padding: 0; overflow: hidden; }
|
.el-browser { padding: 0; overflow: hidden; }
|
||||||
.el-head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 14px; padding: 16px 20px 14px; border-bottom: 1px solid var(--border); }
|
.el-head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 14px; padding: 16px 20px 14px; border-bottom: 1px solid var(--border); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user