134 lines
7.7 KiB
JavaScript
134 lines
7.7 KiB
JavaScript
(() => {
|
|
const root = document.querySelector('[data-lumi-page="lumi-transcription"]');
|
|
if (!root) return;
|
|
const button = root.querySelector("[data-create-pairing]");
|
|
const status = root.querySelector("[data-status]");
|
|
const activeList = root.querySelector("[data-active-devices]");
|
|
const revokedList = root.querySelector("[data-revoked-devices]");
|
|
root.querySelectorAll("[data-device-view]").forEach((button) => button.addEventListener("click", () => {
|
|
const revoked = button.dataset.deviceView === "revoked";
|
|
activeList.hidden = revoked; revokedList.hidden = !revoked;
|
|
root.querySelectorAll("[data-device-view]").forEach((choice) => {
|
|
const selected = choice === button; choice.setAttribute("aria-pressed", String(selected)); choice.classList.toggle("subtle", !selected);
|
|
});
|
|
}));
|
|
|
|
const latencyStops = [[0, "#6f9f82"], [750, "#6f9f82"], [1250, "#6f91a3"], [2000, "#c0a65b"], [3000, "#c18152"], [4500, "#b5686b"]];
|
|
const confidenceStops = [[0, "#b5686b"], [.45, "#c18152"], [.65, "#c0a65b"], [.8, "#6f91a3"], [.92, "#6f9f82"], [1, "#6f9f82"]];
|
|
const colorAt = (value, stops) => {
|
|
const bounded = Math.max(stops[0][0], Math.min(stops.at(-1)[0], Number(value)));
|
|
let upper = stops.findIndex(([point]) => point >= bounded);
|
|
if (upper <= 0) return stops[0][1];
|
|
const [lowPoint, lowColor] = stops[upper - 1]; const [highPoint, highColor] = stops[upper];
|
|
const ratio = highPoint === lowPoint ? 0 : (bounded - lowPoint) / (highPoint - lowPoint);
|
|
const rgb = [1, 3, 5].map((offset) => Math.round(parseInt(lowColor.slice(offset, offset + 2), 16) + (parseInt(highColor.slice(offset, offset + 2), 16) - parseInt(lowColor.slice(offset, offset + 2), 16)) * ratio));
|
|
return `rgb(${rgb.join(",")})`;
|
|
};
|
|
const paintTest = (test, mode) => {
|
|
test.dataset.analysisMode = mode;
|
|
test.querySelectorAll("[data-analysis-select]").forEach((button) => {
|
|
const selected = button.dataset.analysisSelect === mode;
|
|
button.setAttribute("aria-pressed", String(selected)); button.classList.toggle("subtle", !selected);
|
|
});
|
|
test.querySelectorAll(".benchmark-word").forEach((word) => {
|
|
const latency = colorAt(word.dataset.latency, latencyStops);
|
|
const confidence = word.dataset.confidence === "" ? "#aeb8bc" : colorAt(word.dataset.confidence, confidenceStops);
|
|
word.style.background = mode === "latency" ? latency : mode === "confidence" ? confidence : `linear-gradient(to bottom, ${latency} 0 60%, ${confidence} 60% 100%)`;
|
|
});
|
|
};
|
|
root.querySelectorAll("[data-benchmark-test]").forEach((test) => {
|
|
paintTest(test, test.dataset.analysisMode || "combined");
|
|
test.addEventListener("click", (event) => {
|
|
const choice = event.target.closest("[data-analysis-select]");
|
|
if (choice) paintTest(test, choice.dataset.analysisSelect);
|
|
});
|
|
});
|
|
button?.addEventListener("click", async () => {
|
|
button.disabled = true;
|
|
status.textContent = "Creating a one-time pairing package…";
|
|
try {
|
|
const response = await fetch("/plugins/lumi_transcription/api/companion/download", { method: "POST", headers: { Accept: "application/json" } });
|
|
if (!response.ok) throw new Error((await response.json()).error || "Pairing package could not be created.");
|
|
const blob = await response.blob();
|
|
const disposition = response.headers.get("content-disposition") || "";
|
|
const filename = /filename="?([^";]+)"?/i.exec(disposition)?.[1] || "Lumi-Companion-paired.zip";
|
|
const link = document.createElement("a");
|
|
link.href = URL.createObjectURL(blob); link.download = filename; link.click();
|
|
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
|
status.textContent = "Companion package created. Extract it and run Lumi Companion Setup within 15 minutes; the included pairing key works once.";
|
|
} catch (error) { status.textContent = error.message; }
|
|
finally { button.disabled = false; }
|
|
});
|
|
|
|
root.addEventListener("click", async (event) => {
|
|
const revoke = event.target.closest("[data-revoke-device]");
|
|
if (!revoke) return;
|
|
if (!window.LumiConfirm?.destructiveFetch) {
|
|
status.textContent = "Timed confirmation is unavailable. Reload Lumi and try again.";
|
|
return;
|
|
}
|
|
const action = `/plugins/lumi_transcription/api/devices/${encodeURIComponent(revoke.dataset.revokeDevice)}/revoke`;
|
|
revoke.disabled = true;
|
|
try {
|
|
const response = await window.LumiConfirm.destructiveFetch(action, {
|
|
method: "POST",
|
|
headers: { Accept: "application/json" }
|
|
}, {
|
|
title: "Revoke Companion access",
|
|
text: `Permanently revoke ${revoke.dataset.deviceName || "this device"}? Its credential will stop working immediately and pairing it again will require a new download.`,
|
|
label: "Revoke access"
|
|
});
|
|
if (!response) return;
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) throw new Error(payload.error || "Device access could not be revoked.");
|
|
status.textContent = "Companion access revoked. The device must be paired again before it can reconnect.";
|
|
setTimeout(() => window.location.reload(), 500);
|
|
} catch (error) {
|
|
status.textContent = error.message;
|
|
} finally {
|
|
revoke.disabled = false;
|
|
}
|
|
});
|
|
|
|
root.addEventListener("click", async (event) => {
|
|
const runtime = event.target.closest("[data-install-runtime]");
|
|
if (!runtime) return;
|
|
const size = Number(runtime.dataset.runtimeSize) || 0;
|
|
if (!window.confirm(`Download and activate ${runtime.dataset.runtimeLabel}${size ? ` (${size} MiB)` : ""} on the Lumi host? Active transcription tests must be stopped first.`)) return;
|
|
runtime.disabled = true;
|
|
status.textContent = "Downloading, verifying, and activating the inference runtime…";
|
|
try {
|
|
const response = await fetch(`/plugins/lumi_transcription/api/runtime/${encodeURIComponent(runtime.dataset.installRuntime)}/install`, {
|
|
method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
body: JSON.stringify({ confirmed: true })
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) throw new Error(result.error || "The inference runtime could not be installed.");
|
|
status.textContent = "Inference runtime verified and activated.";
|
|
setTimeout(() => window.location.reload(), 700);
|
|
} catch (error) { status.textContent = error.message; runtime.disabled = false; }
|
|
});
|
|
|
|
root.addEventListener("click", async (event) => {
|
|
const download = event.target.closest("[data-download-model]");
|
|
const load = event.target.closest("[data-load-model]");
|
|
if (!download && !load) return;
|
|
const modelId = download?.dataset.downloadModel || load?.dataset.loadModel;
|
|
if (download && !window.confirm(`Download ${download.dataset.modelLabel} (${download.dataset.modelSize} MiB) to the Lumi host?`)) return;
|
|
const action = download ? "download" : "load";
|
|
const target = download || load;
|
|
target.disabled = true;
|
|
status.textContent = download ? "Downloading and verifying the model…" : "Loading the model into whisper.cpp…";
|
|
try {
|
|
const response = await fetch(`/plugins/lumi_transcription/api/models/${encodeURIComponent(modelId)}/${action}`, {
|
|
method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
body: download ? JSON.stringify({ confirmed: true }) : "{}"
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) throw new Error(result.error || `Model ${action} failed.`);
|
|
status.textContent = download ? "Model verified. You can load it now." : "Model loaded. Run the host benchmark before live use.";
|
|
setTimeout(() => window.location.reload(), 700);
|
|
} catch (error) { status.textContent = error.message; target.disabled = false; }
|
|
});
|
|
})();
|