File size: 14,545 Bytes
5efc5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Masked-diffusion chat · LFM2.5 Encoder</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script>
  // Private Space auth: HF signs the embedded iframe URL with ?__sign=<token>.
  // Incognito / Safari block the third-party .hf.space cookie, so that token is
  // the only credential — carry it on same-origin API requests, else /api/*
  // returns 404 and the model-ready poll hangs at 92%.
  (() => {
    const sign = new URLSearchParams(location.search).get("__sign");
    if (!sign) return;
    const orig = self.fetch.bind(self);
    self.fetch = (input, init) => {
      try {
        const u = new URL(typeof input === "string" ? input : input.url, location.href);
        if (u.origin === location.origin && !u.searchParams.has("__sign")) {
          u.searchParams.set("__sign", sign);
          input = u.toString();
        }
      } catch (e) { /* non-URL input: pass through */ }
      return orig(input, init);
    };
  })();
</script>
</head>
<body>
<div class="wrap" id="app">
  <header class="site-intro">
    <h1>Masked-<em>diffusion</em> chat</h1>
    <p class="sub">Watch LFM2.5 Encoder turn masked tokens into an answer through iterative denoising.</p>
  </header>

  <section class="model-strip is-loading" id="model-strip" aria-label="Model status">
    <span class="model-logo-frame" aria-hidden="true">
      <img id="model-logo" class="model-logo" width="56" height="59" alt="" src="lliquid.gif">
    </span>
    <div class="model-copy">
      <div class="model-name-row">
        <a href="https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M" target="_blank" rel="noopener">LFM2.5 Encoder</a>
        <span class="model-variant">350M parameters</span>
      </div>
    </div>
    <div class="load-block">
      <div class="load-meta">
        <span id="status" role="status" aria-live="polite">Connecting to server…</span>
        <span id="load-percent">0%</span>
      </div>
      <div class="load-track" role="progressbar" aria-label="Model loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span id="load-progress"></span></div>
    </div>
  </section>

  <main>
    <section class="diffusion-workspace" id="workspace" aria-label="Masked-diffusion generator">
      <article class="workspace-card prompt-panel">
        <header class="panel-header">
          <div><h2>Prompt</h2><p>Enter an instruction or question</p></div>
        </header>

        <form id="composer">
          <label class="sr-only" for="prompt">Prompt</label>
          <div class="prompt-stage">
            <textarea id="prompt" rows="5" placeholder="Ask LFM2.5 Encoder…">Give three tips for staying healthy.</textarea>
            <div class="prompt-scan" aria-hidden="true"></div>
          </div>
          <div class="composer-row">
            <span>Enter to diffuse · Shift + Enter for a new line</span>
            <button id="go" type="submit" disabled><span>Diffuse</span><i aria-hidden="true">→</i></button>
          </div>
        </form>

        <div class="settings-heading"><h3>Generation settings</h3><span>Customize the denoising path</span></div>
        <div class="settings-grid">
          <label class="range-control" for="steps"><span>Step budget</span><output id="stepsV" for="steps">32</output><input type="range" id="steps" min="8" max="128" step="8" value="32"></label>
          <label class="range-control" for="mnt"><span>Max tokens</span><output id="mntV" for="mnt">64</output><input type="range" id="mnt" min="16" max="192" step="16" value="64"></label>
          <label class="range-control" for="blk"><span>Block size</span><output id="blkV" for="blk">16</output><input type="range" id="blk" min="8" max="64" step="8" value="16"></label>
          <label class="range-control" for="temp"><span>Temperature</span><output id="tempV" for="temp">0.00</output><input type="range" id="temp" min="0" max="100" step="5" value="0"></label>
          <label class="range-control range-wide" for="tau"><span>Confidence reveal</span><output id="tauV" for="tau">0.90</output><input type="range" id="tau" min="50" max="100" step="5" value="90"></label>
        </div>
      </article>

      <article class="workspace-card response-panel">
        <header class="panel-header response-heading">
          <div><h2>Response</h2></div>
          <span class="activity-pill" id="activity"><i></i><span id="activityText">Ready</span></span>
        </header>

        <div class="response-stage" id="responseStage" aria-live="polite">
          <div class="response-scan" aria-hidden="true"></div>
          <div class="empty-state" id="emptyState">
            <div class="idle-orbit" aria-hidden="true"><span></span><i></i></div>
            <strong>Ready to denoise</strong>
            <p>Your answer will emerge here, token by token.</p>
            <div class="idle-masks" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span></div>
          </div>
          <article class="assistant-message" id="assistantMessage" hidden>
            <div id="canvas" class="canvas"></div>
            <div class="generation-meta">
              <div class="step-track"><span id="stepProgress"></span></div>
              <div class="stats" id="live"></div>
            </div>
          </article>
        </div>
      </article>
    </section>

    <section class="examples-panel" aria-label="Example prompts">
      <span class="examples-label">Examples</span>
      <div class="example-list">
        <button type="button" class="ex active" data-q="Give three tips for staying healthy.">Health tips</button>
        <button type="button" class="ex" data-q="Explain what a black hole is to a five-year-old.">Black hole</button>
        <button type="button" class="ex" data-q="Write a short poem about the ocean.">Ocean poem</button>
        <button type="button" class="ex" data-q="What are the primary colors?">Primary colors</button>
        <button type="button" class="ex" data-q="Give me a concise guide to brewing green tea.">Green tea</button>
        <button type="button" class="ex" data-q="Explain why this loop never stops: while (x > 0) { x++; }">Debug a loop</button>
      </div>
    </section>
  </main>
</div>

<script type="module">
// The model runs on the server (CPU). This script only drives the UI and
// streams denoising frames from /api/generate as newline-delimited JSON.

const MASK = null;   // the server sends null for a still-masked position
const $ = (id) => document.getElementById(id);

const setLoadProgress = (percent) => {
  const value = Math.max(0, Math.min(100, percent));
  $("load-progress").style.width = `${value}%`;
  $("load-percent").textContent = `${Math.round(value)}%`;
  document.querySelector(".load-track").setAttribute("aria-valuenow", String(Math.round(value)));
};
const setStatus = (message, state = "loading") => {
  $("status").textContent = message;
  const badge = $("model-strip");
  badge.classList.toggle("is-loading", state === "loading");
  badge.classList.toggle("is-preparing", state === "preparing");
  badge.classList.toggle("is-ready", state === "ready");
  badge.classList.toggle("is-error", state === "error");
};
function updateRangeFill(el) {
  const pct = ((+el.value - +el.min) / (+el.max - +el.min)) * 100;
  el.style.setProperty("--pct", `${pct}%`);
}
for (const [s, l] of [["steps","stepsV"],["mnt","mntV"],["blk","blkV"]])
  $(s).oninput = () => { updateRangeFill($(s)); $(l).textContent = $(s).value; };
$("temp").oninput = () => { updateRangeFill($("temp")); $("tempV").textContent = ($("temp").value / 100).toFixed(2); };
$("tau").oninput = () => { updateRangeFill($("tau")); $("tauV").textContent = ($("tau").value / 100).toFixed(2); };
document.querySelectorAll("input[type=range]").forEach(updateRangeFill);

// ---------- wait for the server-side model to be ready ----------
async function waitForServer() {
  setStatus("Connecting to server…", "loading");
  setLoadProgress(12);
  for (let attempt = 0; attempt < 900; attempt++) {
    try {
      const r = await fetch("api/ready", { cache: "no-store" });
      if (r.ok && (await r.json()).ready) return true;
    } catch (e) { /* server still starting */ }
    setStatus("Warming up model on the server…", "preparing");
    setLoadProgress(Math.min(92, 12 + attempt * 4));
    await new Promise((r) => setTimeout(r, 1000));
  }
  return false;
}

(async () => {
  const ok = await waitForServer();
  if (ok) {
    setLoadProgress(100);
    setStatus("Model ready", "ready");
    $("go").disabled = false;
  } else {
    setStatus("Model unavailable", "error");
    setLoadProgress(100);
    $("load-percent").textContent = "Error";
  }
})();

// ---------- rendering ----------
const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;");
function render(tokens, revealed) {
  const revealedSet = new Set(revealed);
  const parts = [];
  for (let i = 0; i < tokens.length; i++) {
    if (tokens[i] === MASK) {
      parts.push(`<span class="mask" style="--token-index:${i}" aria-label="masked token">&lt;MASK&gt;</span>`);
      continue;
    }
    const esc = escapeHtml(tokens[i] || "·");
    parts.push(`<span class="tok${revealedSet.has(i) ? " new" : ""}" style="--token-index:${i}">${esc}</span>`);
  }
  $("canvas").innerHTML = parts.join("");
}

// ---------- streamed generation ----------
async function generate(userText) {
  const maxNew = +$("mnt").value;
  const params = new URLSearchParams({
    prompt: userText,
    mnt: $("mnt").value, steps: $("steps").value, blk: $("blk").value,
    temp: $("temp").value, tau: $("tau").value,
  });
  const t0 = performance.now();
  const resp = await fetch("api/generate?" + params.toString());
  if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`);

  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "", steps = 0, finalText = null;
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let nl;
    while ((nl = buffer.indexOf("\n")) >= 0) {
      const line = buffer.slice(0, nl).trim();
      buffer = buffer.slice(nl + 1);
      if (!line) continue;
      const msg = JSON.parse(line);
      if (msg.error) throw new Error(msg.error);
      if (msg.done) { finalText = msg.text; continue; }
      steps = msg.step;
      render(msg.tokens, msg.revealed);
      const doneCount = msg.tokens.filter((t) => t !== MASK).length;
      const dt = (performance.now() - t0) / 1000;
      const progress = doneCount / maxNew * 100;
      $("stepProgress").style.width = `${progress}%`;
      $("activityText").textContent = `Denoising · ${Math.round(progress)}%`;
      $("live").textContent = `Step ${msg.step} · ${doneCount}/${maxNew} tokens · ${dt.toFixed(2)}s · ${(doneCount / Math.max(dt, .01)).toFixed(0)} tok/s`;
      await new Promise((r) => requestAnimationFrame(r));
    }
  }
  const totalS = (performance.now() - t0) / 1000;
  return { text: finalText ?? "", totalS, steps, msPerStep: totalS * 1000 / Math.max(steps, 1), tokPerS: maxNew / totalS };
}

$("composer").addEventListener("submit", async (event) => {
  event.preventDefault();
  const userText = $("prompt").value.trim();
  if (!userText || $("go").disabled) return;

  $("assistantMessage").hidden = false;
  $("emptyState").hidden = true;
  $("canvas").innerHTML = Array.from({ length: +$("mnt").value }, (_, i) => `<span class="mask" style="--token-index:${i}" aria-label="masked token">&lt;MASK&gt;</span>`).join("");
  $("live").textContent = "Starting diffusion…";
  $("stepProgress").style.width = "0%";
  $("assistantMessage").setAttribute("aria-busy", "true");
  $("workspace").classList.add("is-generating");
  $("responseStage").classList.add("is-denoising");
  $("responseStage").classList.remove("is-complete");
  $("activity").className = "activity-pill is-running";
  $("activityText").textContent = "Building canvas";
  $("go").disabled = true;
  $("go").querySelector("span").textContent = "Denoising";
  try {
    const r = await generate(userText);
    $("canvas").textContent = r.text || "(empty)";
    $("stepProgress").style.width = "100%";
    $("live").textContent = `${r.steps} steps · ${r.totalS.toFixed(2)}s · ${r.msPerStep.toFixed(0)} ms/step · ${r.tokPerS.toFixed(0)} tok/s`;
    $("activity").className = "activity-pill is-done";
    $("activityText").textContent = "Complete";
    $("responseStage").classList.add("is-complete");
    window.__LAST_GEN = r;
  } catch (e) {
    $("canvas").textContent = "Error: " + (e.message || e);
    $("activity").className = "activity-pill is-error";
    $("activityText").textContent = "Could not finish";
    console.error(e);
  }
  $("assistantMessage").removeAttribute("aria-busy");
  $("workspace").classList.remove("is-generating");
  $("responseStage").classList.remove("is-denoising");
  $("go").disabled = false;
  $("go").querySelector("span").textContent = "Diffuse";
  setTimeout(() => $("responseStage").classList.remove("is-complete"), 900);
});

$("prompt").addEventListener("keydown", (event) => {
  if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
    event.preventDefault();
    $("composer").requestSubmit();
  }
});

document.querySelector(".example-list").addEventListener("click", (event) => {
  const option = event.target.closest(".ex");
  if (!option) return;
  document.querySelectorAll(".ex").forEach((el) => el.classList.toggle("active", el === option));
  $("prompt").value = option.dataset.q || "";
  $("prompt").dispatchEvent(new Event("input", { bubbles: true }));
  document.querySelector(".prompt-panel").classList.remove("example-changed");
  requestAnimationFrame(() => document.querySelector(".prompt-panel").classList.add("example-changed"));
});

$("prompt").addEventListener("input", () => {
  const active = document.querySelector(".ex.active");
  if (!active?.dataset.q || active.dataset.q !== $("prompt").value) {
    document.querySelectorAll(".ex").forEach((el) => el.classList.remove("active"));
  }
});
</script>
</body>
</html>