fix: popup empty on cold load — lazy-load gradio client so UI never waits on the CDN
Browse filesThe static esm.sh import blocked module execution until the CDN resolved, so on a
cold load the popup rendered with empty text. Load @gradio/client via dynamic import
(ensureClient, awaited in send/sendWatch) and seed the popup with default text in HTML.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- frontend/app.js +18 -6
- frontend/index.html +4 -4
frontend/app.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
import { Client, handle_file } from "https://esm.sh/@gradio/client";
|
| 2 |
-
|
| 3 |
const $ = (id) => document.getElementById(id);
|
| 4 |
const cam = $("cam"), canvas = $("canvas"), statusEl = $("status"),
|
| 5 |
answerEl = $("answer"), hintEl = $("hint"), langBtn = $("lang"),
|
|
@@ -114,8 +112,19 @@ function stopRec() {
|
|
| 114 |
});
|
| 115 |
}
|
| 116 |
|
| 117 |
-
// ---- backend ----
|
| 118 |
-
let client = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
async function send(frame, audio, qtext = "") {
|
| 121 |
if (busy) return;
|
|
@@ -123,6 +132,8 @@ async function send(frame, audio, qtext = "") {
|
|
| 123 |
answerEl.textContent = "";
|
| 124 |
setState("thinking", t("thinking"));
|
| 125 |
try {
|
|
|
|
|
|
|
| 126 |
const payload = { image: handle_file(frame), lang };
|
| 127 |
if (audio) payload.audio = handle_file(audio);
|
| 128 |
if (qtext) payload.qtext = qtext;
|
|
@@ -415,6 +426,8 @@ async function sendWatch(frame, hint = "") {
|
|
| 415 |
if (busy) return;
|
| 416 |
busy = true;
|
| 417 |
try {
|
|
|
|
|
|
|
| 418 |
const result = await client.predict("/watch", { image: handle_file(frame), prev: history.join(" · "), lang, hint });
|
| 419 |
const out = Array.isArray(result.data) ? result.data[0] : result.data;
|
| 420 |
if (out && out.speak && out.answer && !history.some((h) => tooSimilar(h, out.answer))) {
|
|
@@ -546,6 +559,5 @@ function updateLabels() {
|
|
| 546 |
setState("", t("idle"));
|
| 547 |
showIntro(); // welcome popup on every open (the camera starts when it closes)
|
| 548 |
loadDetector(); // preload the object detector in the background (non-blocking)
|
| 549 |
-
|
| 550 |
-
catch (e) { console.error("connect:", e); setState("", t("err")); }
|
| 551 |
})();
|
|
|
|
|
|
|
|
|
|
| 1 |
const $ = (id) => document.getElementById(id);
|
| 2 |
const cam = $("cam"), canvas = $("canvas"), statusEl = $("status"),
|
| 3 |
answerEl = $("answer"), hintEl = $("hint"), langBtn = $("lang"),
|
|
|
|
| 112 |
});
|
| 113 |
}
|
| 114 |
|
| 115 |
+
// ---- backend (gradio client loaded lazily, so the UI never waits on the CDN) ----
|
| 116 |
+
let Client, handle_file, client = null, clientReady = null;
|
| 117 |
+
function ensureClient() {
|
| 118 |
+
if (!clientReady) {
|
| 119 |
+
clientReady = (async () => {
|
| 120 |
+
const mod = await import("https://esm.sh/@gradio/client");
|
| 121 |
+
Client = mod.Client; handle_file = mod.handle_file;
|
| 122 |
+
client = await Client.connect(window.location.origin);
|
| 123 |
+
console.log("Iris connected");
|
| 124 |
+
})().catch((e) => { console.error("connect:", e); clientReady = null; });
|
| 125 |
+
}
|
| 126 |
+
return clientReady;
|
| 127 |
+
}
|
| 128 |
|
| 129 |
async function send(frame, audio, qtext = "") {
|
| 130 |
if (busy) return;
|
|
|
|
| 132 |
answerEl.textContent = "";
|
| 133 |
setState("thinking", t("thinking"));
|
| 134 |
try {
|
| 135 |
+
await ensureClient();
|
| 136 |
+
if (!client) throw new Error("client not ready");
|
| 137 |
const payload = { image: handle_file(frame), lang };
|
| 138 |
if (audio) payload.audio = handle_file(audio);
|
| 139 |
if (qtext) payload.qtext = qtext;
|
|
|
|
| 426 |
if (busy) return;
|
| 427 |
busy = true;
|
| 428 |
try {
|
| 429 |
+
await ensureClient();
|
| 430 |
+
if (!client) return;
|
| 431 |
const result = await client.predict("/watch", { image: handle_file(frame), prev: history.join(" · "), lang, hint });
|
| 432 |
const out = Array.isArray(result.data) ? result.data[0] : result.data;
|
| 433 |
if (out && out.speak && out.answer && !history.some((h) => tooSimilar(h, out.answer))) {
|
|
|
|
| 559 |
setState("", t("idle"));
|
| 560 |
showIntro(); // welcome popup on every open (the camera starts when it closes)
|
| 561 |
loadDetector(); // preload the object detector in the background (non-blocking)
|
| 562 |
+
ensureClient(); // connect in the background; the UI never waits on it
|
|
|
|
| 563 |
})();
|
frontend/index.html
CHANGED
|
@@ -71,14 +71,14 @@
|
|
| 71 |
</svg>
|
| 72 |
</button>
|
| 73 |
<h2 id="intro-title">Iris</h2>
|
| 74 |
-
<p id="intro-body"></p>
|
| 75 |
<div class="intro-langs" role="group" aria-label="Language">
|
| 76 |
<button id="intro-en" class="intro-lang">EN</button>
|
| 77 |
<button id="intro-pt" class="intro-lang">PT</button>
|
| 78 |
</div>
|
| 79 |
-
<a id="intro-video" class="intro-video" href="
|
| 80 |
-
<button id="intro-close" class="intro-close"></button>
|
| 81 |
-
<p id="intro-hint" class="intro-hint"></p>
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
|
|
|
|
| 71 |
</svg>
|
| 72 |
</button>
|
| 73 |
<h2 id="intro-title">Iris</h2>
|
| 74 |
+
<p id="intro-body">This is Iris, your eyes by voice. Point your phone and it tells you what is in front of you, and reads your money, your bills, and your medicine, out loud.</p>
|
| 75 |
<div class="intro-langs" role="group" aria-label="Language">
|
| 76 |
<button id="intro-en" class="intro-lang">EN</button>
|
| 77 |
<button id="intro-pt" class="intro-lang">PT</button>
|
| 78 |
</div>
|
| 79 |
+
<a id="intro-video" class="intro-video" href="https://huggingface.co/spaces/build-small-hackathon/iris" target="_blank" rel="noopener">Watch the demo</a>
|
| 80 |
+
<button id="intro-close" class="intro-close">Close</button>
|
| 81 |
+
<p id="intro-hint" class="intro-hint">Say "close" or tap the button</p>
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
|