Implement replay mode enhancements and run-locally instructions
Browse files- Updated `app.js` to allow sending prompts in replay mode while providing a hint for running the server locally.
- Added a new `run-locally` section in `index.html` with instructions and buttons for copying commands and dismissing the hint.
- Enhanced `style.css` to style the run-locally callout and added visual cues for replay-locked buttons.
- Improved user experience by keeping the textarea editable in replay mode while disabling actual submissions.
- web/app.js +42 -8
- web/index.html +13 -1
- web/style.css +36 -1
web/app.js
CHANGED
|
@@ -81,11 +81,18 @@ function setMode(mode) {
|
|
| 81 |
ui.connection.textContent = "replay";
|
| 82 |
ui.connection.classList.remove("offline");
|
| 83 |
ui.connection.classList.add("replay");
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
ui.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
setBanner(
|
| 88 |
-
"REPLAY MODE — this is a pre-recorded session.
|
| 89 |
"replay-banner",
|
| 90 |
);
|
| 91 |
if (ui.speed) ui.speed.style.display = "";
|
|
@@ -412,15 +419,42 @@ async function sendPrompt(prompt) {
|
|
| 412 |
}
|
| 413 |
}
|
| 414 |
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
const p = ui.prompt.value;
|
| 418 |
await sendPrompt(p);
|
| 419 |
await new Promise(r => setTimeout(r, 200));
|
| 420 |
await sendPrompt(p);
|
| 421 |
-
});
|
| 422 |
ui.prompt.addEventListener("keydown", (e) => {
|
| 423 |
-
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") sendPrompt(e.target.value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
});
|
| 425 |
|
| 426 |
if (ui.speed) ui.speed.addEventListener("change", () => {
|
|
|
|
| 81 |
ui.connection.textContent = "replay";
|
| 82 |
ui.connection.classList.remove("offline");
|
| 83 |
ui.connection.classList.add("replay");
|
| 84 |
+
// Keep textarea editable — feels alive — but the Send buttons can't
|
| 85 |
+
// really submit, so they open the run-locally hint instead.
|
| 86 |
+
ui.send.disabled = false;
|
| 87 |
+
ui.sendTwice.disabled = false;
|
| 88 |
+
ui.prompt.disabled = false;
|
| 89 |
+
ui.send.classList.add("replay-locked");
|
| 90 |
+
ui.sendTwice.classList.add("replay-locked");
|
| 91 |
+
ui.send.title = "Replay mode — click to see how to run live locally";
|
| 92 |
+
ui.sendTwice.title = ui.send.title;
|
| 93 |
+
ui.prompt.placeholder = "Recorded demo — typing won't submit. Run the server locally to use your own prompts.";
|
| 94 |
setBanner(
|
| 95 |
+
"REPLAY MODE — this is a pre-recorded session. Send buttons show local-run instructions instead.",
|
| 96 |
"replay-banner",
|
| 97 |
);
|
| 98 |
if (ui.speed) ui.speed.style.display = "";
|
|
|
|
| 419 |
}
|
| 420 |
}
|
| 421 |
|
| 422 |
+
function showRunLocally() {
|
| 423 |
+
const el = $("run-locally");
|
| 424 |
+
if (!el) return;
|
| 425 |
+
el.hidden = false;
|
| 426 |
+
el.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
function trySend(handler) {
|
| 430 |
+
if (state.mode !== "live") { showRunLocally(); return; }
|
| 431 |
+
handler();
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
ui.send.addEventListener("click", () => trySend(() => sendPrompt(ui.prompt.value)));
|
| 435 |
+
ui.sendTwice.addEventListener("click", () => trySend(async () => {
|
| 436 |
const p = ui.prompt.value;
|
| 437 |
await sendPrompt(p);
|
| 438 |
await new Promise(r => setTimeout(r, 200));
|
| 439 |
await sendPrompt(p);
|
| 440 |
+
}));
|
| 441 |
ui.prompt.addEventListener("keydown", (e) => {
|
| 442 |
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") trySend(() => sendPrompt(e.target.value));
|
| 443 |
+
});
|
| 444 |
+
|
| 445 |
+
// Run-locally callout controls.
|
| 446 |
+
const rlClose = $("rl-close");
|
| 447 |
+
if (rlClose) rlClose.addEventListener("click", () => { $("run-locally").hidden = true; });
|
| 448 |
+
const rlCopy = $("rl-copy");
|
| 449 |
+
if (rlCopy) rlCopy.addEventListener("click", async () => {
|
| 450 |
+
const cmd = $("run-locally").querySelector("pre").innerText.trim();
|
| 451 |
+
try {
|
| 452 |
+
await navigator.clipboard.writeText(cmd);
|
| 453 |
+
rlCopy.textContent = "Copied ✓";
|
| 454 |
+
setTimeout(() => { rlCopy.textContent = "Copy command"; }, 1500);
|
| 455 |
+
} catch {
|
| 456 |
+
rlCopy.textContent = "Copy failed";
|
| 457 |
+
}
|
| 458 |
});
|
| 459 |
|
| 460 |
if (ui.speed) ui.speed.addEventListener("change", () => {
|
web/index.html
CHANGED
|
@@ -58,6 +58,18 @@
|
|
| 58 |
<button id="restart" style="display:none" class="ghost">Restart</button>
|
| 59 |
</span>
|
| 60 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
</section>
|
| 62 |
|
| 63 |
<main>
|
|
@@ -94,7 +106,7 @@
|
|
| 94 |
</main>
|
| 95 |
|
| 96 |
<footer>
|
| 97 |
-
Subscribed to <code>/engine/events</code> · Source on <a href="https://github.com/surajsharan/tiny_vLLM" target="_blank" rel="noopener">github.com/surajsharan/tiny_vLLM</a>
|
| 98 |
</footer>
|
| 99 |
|
| 100 |
<script src="app.js"></script>
|
|
|
|
| 58 |
<button id="restart" style="display:none" class="ghost">Restart</button>
|
| 59 |
</span>
|
| 60 |
</div>
|
| 61 |
+
|
| 62 |
+
<div id="run-locally" class="run-locally" hidden>
|
| 63 |
+
<div class="rl-head">This is a <b>recorded</b> session — to send live prompts, run the server locally.</div>
|
| 64 |
+
<pre><code>git clone https://github.com/surajsharan/tiny_vLLM
|
| 65 |
+
cd tiny_vLLM && pip install -r requirements.txt
|
| 66 |
+
python -m tiny_vllm.server --model Qwen/Qwen2.5-0.5B-Instruct
|
| 67 |
+
# then open http://localhost:8000</code></pre>
|
| 68 |
+
<div class="rl-foot">
|
| 69 |
+
<button id="rl-copy" class="ghost">Copy command</button>
|
| 70 |
+
<button id="rl-close" class="ghost">Dismiss</button>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
</section>
|
| 74 |
|
| 75 |
<main>
|
|
|
|
| 106 |
</main>
|
| 107 |
|
| 108 |
<footer>
|
| 109 |
+
Subscribed to <code>/engine/events</code> · Source on <a href="https://github.com/surajsharan/tiny_vLLM" target="_blank" rel="noopener">github.com/surajsharan/tiny_vLLM</a>
|
| 110 |
</footer>
|
| 111 |
|
| 112 |
<script src="app.js"></script>
|
web/style.css
CHANGED
|
@@ -183,7 +183,12 @@ button {
|
|
| 183 |
transition: all 0.12s ease;
|
| 184 |
}
|
| 185 |
button:hover { background: var(--accent-dim); border-color: var(--accent-dim); }
|
| 186 |
-
button:disabled { opacity: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
button.ghost {
|
| 188 |
background: transparent;
|
| 189 |
border: 1px solid var(--border-strong);
|
|
@@ -198,6 +203,36 @@ button.ghost:hover { border-color: var(--accent); color: var(--accent); }
|
|
| 198 |
|
| 199 |
textarea:disabled { opacity: 0.5; }
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
.replay-controls { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
| 202 |
.replay-controls select {
|
| 203 |
background: var(--bg); color: var(--fg);
|
|
|
|
| 183 |
transition: all 0.12s ease;
|
| 184 |
}
|
| 185 |
button:hover { background: var(--accent-dim); border-color: var(--accent-dim); }
|
| 186 |
+
button:disabled { opacity: 0.55; cursor: not-allowed; background: var(--bg-elev); color: var(--muted); border-color: var(--border); }
|
| 187 |
+
/* Replay-mode "soft disable": looks clickable, but Send opens the run-locally
|
| 188 |
+
* hint instead of submitting. We mark it with a class so the cursor and
|
| 189 |
+
* tooltip read as intentional, not broken. */
|
| 190 |
+
button.replay-locked { cursor: help; }
|
| 191 |
+
button.replay-locked::after { content: " ✦"; color: var(--purple); }
|
| 192 |
button.ghost {
|
| 193 |
background: transparent;
|
| 194 |
border: 1px solid var(--border-strong);
|
|
|
|
| 203 |
|
| 204 |
textarea:disabled { opacity: 0.5; }
|
| 205 |
|
| 206 |
+
/* ---------- run-locally callout (replay mode) ---------- */
|
| 207 |
+
.run-locally {
|
| 208 |
+
margin-top: 4px;
|
| 209 |
+
background: var(--bg);
|
| 210 |
+
border: 1px solid var(--border-strong);
|
| 211 |
+
border-left: 2px solid var(--purple);
|
| 212 |
+
border-radius: var(--radius);
|
| 213 |
+
padding: 12px 14px;
|
| 214 |
+
font-family: var(--mono);
|
| 215 |
+
font-size: 12px;
|
| 216 |
+
}
|
| 217 |
+
.run-locally[hidden] { display: none; }
|
| 218 |
+
.run-locally .rl-head {
|
| 219 |
+
color: var(--fg);
|
| 220 |
+
font-size: 12px;
|
| 221 |
+
margin-bottom: 8px;
|
| 222 |
+
}
|
| 223 |
+
.run-locally .rl-head b { color: var(--purple); font-weight: 600; }
|
| 224 |
+
.run-locally pre {
|
| 225 |
+
margin: 0 0 10px;
|
| 226 |
+
padding: 10px 12px;
|
| 227 |
+
background: var(--bg-elev2);
|
| 228 |
+
border: 1px solid var(--border);
|
| 229 |
+
border-radius: var(--radius);
|
| 230 |
+
font-family: var(--mono); font-size: 12px;
|
| 231 |
+
color: var(--accent);
|
| 232 |
+
overflow-x: auto;
|
| 233 |
+
}
|
| 234 |
+
.run-locally .rl-foot { display: flex; gap: 8px; }
|
| 235 |
+
|
| 236 |
.replay-controls { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
| 237 |
.replay-controls select {
|
| 238 |
background: var(--bg); color: var(--fg);
|