diff --git a/static/index.html b/static/index.html index 3e0f61c..26d6525 100644 --- a/static/index.html +++ b/static/index.html @@ -286,6 +286,86 @@ font-weight: 700; } + .runtimeWakeWordCard { + border-color: rgba(57,212,160,0.22); + background: + linear-gradient(135deg, rgba(57,212,160,0.09), rgba(255,138,42,0.08)), + var(--panel2); + } + + .runtimeWakeWordHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 14px; + } + + .runtimeWakeWordTitle { + display: flex; + align-items: flex-start; + gap: 12px; + } + + .runtimeWakeWordBadge { + flex: 0 0 auto; + display: grid; + place-items: center; + min-width: 68px; + height: 32px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid rgba(57,212,160,0.34); + background: rgba(57,212,160,0.1); + color: var(--ok); + font-size: 12px; + font-weight: 800; + } + + .runtimeWakeWordTitle h3 { + margin-bottom: 5px; + } + + .runtimeWakeWordTitle p { + max-width: 760px; + margin-bottom: 0; + font-size: 13px; + } + + .runtimeWakeWordLinks { + display: grid; + gap: 10px; + margin-top: 16px; + } + + .runtimeWakeWordItem { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + padding: 12px; + border-radius: 14px; + border: 1px solid rgba(255,255,255,0.08); + background: rgba(0,0,0,0.18); + } + + .runtimeWakeWordItem strong { + display: block; + margin-bottom: 4px; + } + + .runtimeWakeWordUrl { + display: block; + overflow: hidden; + color: var(--muted); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .runtimeWakeWordItem button { + white-space: nowrap; + } + .firmwareLayout { display: grid; grid-template-columns: 1fr; @@ -1109,6 +1189,17 @@ .firmwareActions button { width: 100%; } + .runtimeWakeWordHeader, + .runtimeWakeWordTitle { + flex-direction: column; + align-items: stretch; + } + .runtimeWakeWordItem { + grid-template-columns: 1fr; + } + .runtimeWakeWordItem button { + width: 100%; + } .studioPanelHeader, .capturedControlPanel { grid-template-columns: 1fr; @@ -1441,6 +1532,23 @@ +
+
+
+ 3.0.3+ +
+
Live Model Switching
+

Tater firmware 3.0.3 or higher can swap wake words live

+

No reflash is needed after training. Copy a trained wake word JSON URL below and paste it into your satellite's Home Assistant microWakeWord Model URL entity.

+
+
+ No reflash needed +
+ +
+
@@ -1601,7 +1709,7 @@ selectedFiles: [], captured: { items: [], captured_count: 0, negative_count: 0, personal_count: 0 }, samples: { personal: [], negative: [], personal_count: 0, negative_count: 0, activeBucket: "personal", pages: { personal: 0, negative: 0 } }, - firmware: { devices: [], templates: [], flashing: null, logLines: [], activeTemplateKey: "" }, + firmware: { devices: [], templates: [], wakeWords: [], flashing: null, logLines: [], activeTemplateKey: "" }, uploadBusy: false, reviewBusy: false, firmwareBusy: false, @@ -2388,6 +2496,8 @@ const templates = Array.isArray(payload?.templates) ? payload.templates : []; const previousTemplateKey = $("firmwareTemplate").value || uiState.firmware.activeTemplateKey || ""; uiState.firmware.templates = templates; + uiState.firmware.wakeWords = Array.isArray(payload?.wake_words) ? payload.wake_words : []; + renderRuntimeWakeWordLinks(); $("firmwareTemplate").innerHTML = templates.length ? templates.map((item) => ``).join("") : ``; @@ -2401,6 +2511,52 @@ applyFirmwareTemplateTarget(); } + function renderRuntimeWakeWordLinks() { + const container = $("runtimeWakeWordLinks"); + if (!container) return; + const wakeWords = Array.isArray(uiState.firmware.wakeWords) ? uiState.firmware.wakeWords : []; + const rows = wakeWords + .map((item) => ({ + label: String(item.label || item.wake_word || item.wake_word_name || item.key || "Trained wake word"), + url: String(item.json_url || "").trim(), + })) + .filter((item) => item.url); + + if (!rows.length) { + container.innerHTML = `
Train a wake word first, then its live model URL will appear here.
`; + return; + } + + container.innerHTML = rows.map((item) => ` +
+
+ ${escapeHtml(item.label)} + ${escapeHtml(item.url)} +
+ +
+ `).join(""); + } + + async function copyTextToClipboard(text) { + const value = String(text || ""); + if (!value) return false; + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return true; + } + const textarea = document.createElement("textarea"); + textarea.value = value; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + const copied = document.execCommand("copy"); + textarea.remove(); + return copied; + } + function renderFirmwareFields() { resetWakeSoundPreview(); const template = selectedFirmwareTemplate(); @@ -3198,6 +3354,25 @@ } }); + $("runtimeWakeWordLinks").addEventListener("click", async (event) => { + const target = event.target instanceof Element ? event.target : null; + const button = target?.closest("[data-runtime-wake-url]"); + if (!(button instanceof HTMLButtonElement)) return; + try { + const copied = await copyTextToClipboard(button.dataset.runtimeWakeUrl || ""); + if (!copied) throw new Error("Clipboard unavailable"); + const previousText = button.textContent || "Copy URL"; + button.textContent = "Copied"; + setPill($("firmwareStatus"), "Model URL copied", "ok"); + setTimeout(() => { + button.textContent = previousText; + }, 1200); + } catch (error) { + setPill($("firmwareStatus"), "Copy failed", "warn"); + alert("Copy failed: " + error.message); + } + }); + $("sampleFiles").addEventListener("change", () => { uiState.selectedFiles = Array.from($("sampleFiles").files || []); renderSelectedFiles();