*, *::before, *::after {
box-sizing: border-box;
}
:root {
--page: #eef2f7;
--card: #ffffff;
--text: #0f172a;
--muted: #5b6678;
--line: #d5dce6;
--soft: #eef2f7;
--accent: #2563eb;
--error: #b91c1c;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
background: var(--page);
color: var(--text);
}
.page {
display: grid;
grid-template-columns: minmax(0, 640px);
justify-content: center;
align-content: center;
min-height: 100vh;
padding: 24px 16px;
}
.stream {
padding: 20px;
border-radius: 18px;
background: var(--card);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05), 0 12px 32px -12px rgba(15, 23, 42, 0.2);
}
.ask {
display: flex;
gap: 8px;
}
.ask input {
flex: 1;
min-width: 0;
min-height: 44px;
padding: 8px 14px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--card);
color: var(--text);
font: inherit;
}
.btn {
min-width: 76px;
min-height: 44px;
padding: 8px 18px;
border: 1px solid var(--text);
border-radius: 12px;
background: var(--text);
color: var(--card);
font: inherit;
font-weight: 600;
cursor: pointer;
}
.btn:hover {
opacity: 0.88;
}
.btn.is-stop {
border-color: var(--line);
background: transparent;
color: var(--text);
}
.server {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 0 16px;
font-size: 0.875rem;
color: var(--muted);
}
.server select {
min-height: 34px;
padding: 4px 8px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--card);
color: var(--text);
font: inherit;
}
.ask input:focus-visible,
.btn:focus-visible,
.server select:focus-visible,
.reply:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.reply {
height: clamp(180px, 45vh, 280px);
overflow-y: auto;
padding: 16px;
border-radius: 12px;
background: var(--soft);
line-height: 1.6;
}
.reply p {
margin: 0 0 12px;
}
.reply p:last-child {
margin-bottom: 0;
}
.reply .placeholder {
color: var(--muted);
}
.reply code {
padding: 1px 5px;
border-radius: 5px;
background: var(--card);
font-family: ui-monospace, Consolas, monospace;
font-size: 0.875em;
}
.reply pre {
margin: 0 0 12px;
padding: 14px 16px;
overflow-x: auto;
border-radius: 10px;
background: #0f172a;
color: #e2e8f0;
}
.reply pre code {
padding: 0;
background: none;
font-size: 0.8125rem;
line-height: 1.6;
}
.caret {
display: inline-block;
width: 8px;
height: 1.1em;
margin-left: 2px;
border-radius: 2px;
background: var(--accent);
vertical-align: text-bottom;
animation: blink 1s steps(1) infinite;
}
pre .caret {
background: #93c5fd;
}
.error-note {
color: var(--error);
font-weight: 600;
}
.meta {
min-height: 1.5em;
margin: 12px 2px 0;
font-size: 0.8125rem;
font-variant-numeric: tabular-nums;
color: var(--muted);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@keyframes blink {
50% {
opacity: 0;
}
}
@media (max-width: 420px) {
.stream {
padding: 16px 12px;
}
.reply {
padding: 12px;
}
}
@media (pointer: coarse) {
.server select {
min-height: 44px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--page: #0b1120;
--card: #151e30;
--text: #e2e8f0;
--muted: #a3afc2;
--line: #334155;
--soft: #0f172a;
--accent: #60a5fa;
--error: #fca5a5;
}
.reply pre {
background: #020617;
}
}
@media (prefers-reduced-motion: reduce) {
.caret {
animation: none;
}
}
const ANSWER = [
"A debounce waits until the input goes quiet for a set time, then runs the function once. Each keystroke clears the previous timer with `clearTimeout`, so only the last one fires.",
"",
"```js",
"function debounce(fn, delay) {",
" let timer;",
" return function(...args) {",
" clearTimeout(timer);",
" timer = setTimeout(() => fn.apply(this, args), delay);",
" };",
"}",
"",
"const search = debounce(function(e) {",
" runSearch(e.target.value);",
"}, 300);",
"",
"input.addEventListener('input', search);",
"```",
"",
"With a 300 ms delay, typing a whole word quickly runs the search once, 300 ms after the last key."
].join("\n");
const form = document.getElementById("ask");
const question = document.getElementById("question");
const sendBtn = document.getElementById("send");
const scenario = document.getElementById("scenario");
const reply = document.getElementById("reply");
const meta = document.getElementById("meta");
const status = document.getElementById("status");
let controller = null;
let text = "";
let frame = 0;
// A stand-in for fetch that streams server-sent events
function fakeFetch(url, options) {
const mode = scenario.value;
const signal = options.signal;
return new Promise(function(resolve, reject) {
const firstByte = setTimeout(function() {
if (mode === "500") {
resolve(new Response(JSON.stringify({ error: "The model is overloaded" }), { status: 500, headers: { "Content-Type": "application/json" } }));
return;
}
const pieces = ANSWER.match(/\S*\s*/g).filter(Boolean);
const events = pieces.map(function(piece) {
return "data: " + JSON.stringify({ text: piece }) + "\n\n";
}).join("") + "data: [DONE]\n\n";
const bytes = new TextEncoder().encode(events);
let offset = 0;
let timer = null;
const body = new ReadableStream({
start: function(stream) {
signal.addEventListener("abort", function() {
clearTimeout(timer);
stream.error(signal.reason);
});
function push() {
if (mode === "drop" && offset > bytes.length * 0.45) {
stream.error(new TypeError("network error"));
return;
}
const size = 16 + Math.floor(Math.random() * 64);
stream.enqueue(bytes.slice(offset, offset + size));
offset += size;
if (offset >= bytes.length) {
stream.close();
} else {
timer = setTimeout(push, 20 + Math.random() * 40);
}
}
push();
}
});
resolve(new Response(body, { headers: { "Content-Type": "text/event-stream" } }));
}, 400);
signal.addEventListener("abort", function() {
clearTimeout(firstByte);
reject(signal.reason);
});
});
}
// Read the stream and pass on the text of each event
async function streamAnswer(prompt, signal, onText, stats) {
const res = await fakeFetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: prompt }),
signal: signal
});
if (!res.ok) {
const data = await res.json().catch(function() { return {}; });
throw new Error("The server answered " + res.status + ". " + (data.error || ""));
}
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) return;
stats.chunks++;
// Turn CRLF and CR into LF, but keep a trailing CR for later
buffer = (buffer + value).replace(/\r\n|\r(?!$)/g, "\n");
const events = buffer.split("\n\n");
buffer = events.pop();
for (const event of events) {
const data = event.split("\n").filter(function(line) {
return line.startsWith("data:");
}).map(function(line) {
return line.replace(/^data: ?/, "");
}).join("\n");
if (!data) continue;
if (data === "[DONE]") return;
const piece = JSON.parse(data).text;
if (typeof piece === "string") onText(piece);
}
}
}
function el(tag, className) {
const node = document.createElement(tag);
if (className) node.className = className;
return node;
}
// Split the text into paragraphs and code blocks
function toBlocks(source) {
const blocks = [];
let current = null;
source.split("\n").forEach(function(line) {
if (line.startsWith("```")) {
current = current && current.type === "code" ? null : { type: "code", lines: [] };
if (current) blocks.push(current);
return;
}
if (current && current.type === "code") {
current.lines.push(line);
return;
}
if (!line.trim()) {
current = null;
return;
}
if (!current) {
current = { type: "p", lines: [] };
blocks.push(current);
}
current.lines.push(line);
});
return blocks;
}
function render(streaming) {
const stick = reply.scrollHeight - reply.scrollTop - reply.clientHeight < 40;
reply.textContent = "";
let last = null;
toBlocks(text).forEach(function(block) {
if (block.type === "code") {
const pre = el("pre");
last = el("code");
last.textContent = block.lines.join("\n");
pre.append(last);
reply.append(pre);
} else {
last = el("p");
block.lines.join(" ").split("`").forEach(function(part, i) {
if (!part) return;
if (i % 2) {
const code = el("code");
code.textContent = part;
last.append(code);
} else {
last.append(part);
}
});
reply.append(last);
}
});
if (streaming) {
if (!last) {
last = el("p");
reply.append(last);
}
last.append(el("span", "caret"));
}
if (stick) reply.scrollTop = reply.scrollHeight;
}
function scheduleRender() {
if (frame) return;
frame = requestAnimationFrame(function() {
frame = 0;
render(true);
});
}
function setBusy(busy) {
sendBtn.textContent = busy ? "Stop" : "Send";
sendBtn.classList.toggle("is-stop", busy);
reply.setAttribute("aria-busy", String(busy));
scenario.disabled = busy;
}
async function ask() {
const stats = { chunks: 0 };
const started = performance.now();
const current = new AbortController();
controller = current;
text = "";
setBusy(true);
render(true);
meta.textContent = "Waiting for the first chunk";
status.textContent = "Reply started";
try {
await streamAnswer(question.value, current.signal, function(piece) {
text += piece;
meta.textContent = "Received " + stats.chunks + " chunks, " + text.length + " characters";
scheduleRender();
}, stats);
const seconds = ((performance.now() - started) / 1000).toFixed(1);
meta.textContent = "Done. " + stats.chunks + " chunks, " + text.length + " characters in " + seconds + " s";
status.textContent = "Reply finished";
} catch (err) {
const note = el("p", "error-note");
if (err.name === "AbortError") {
meta.textContent = "Stopped after " + stats.chunks + " chunks";
} else if (err instanceof TypeError && stats.chunks > 0) {
note.textContent = "The connection dropped partway through, so this reply is incomplete.";
meta.textContent = "Connection lost after " + stats.chunks + " chunks";
} else if (err instanceof TypeError) {
note.textContent = "The server could not be reached. Check the connection and send it again.";
meta.textContent = "Request failed";
} else {
note.textContent = err.message.trim();
meta.textContent = "Request failed";
}
status.textContent = note.textContent || meta.textContent;
cancelAnimationFrame(frame);
frame = 0;
render(false);
if (note.textContent) reply.append(note);
} finally {
cancelAnimationFrame(frame);
frame = 0;
if (!reply.querySelector(".error-note")) render(false);
controller = null;
setBusy(false);
}
}
form.addEventListener("submit", function(e) {
e.preventDefault();
if (controller) {
controller.abort();
} else if (!question.value.trim()) {
meta.textContent = "Type a question first";
question.focus();
} else {
ask();
}
});
// Ignore the second click of a double click
sendBtn.addEventListener("click", function(e) {
if (e.detail > 1) e.preventDefault();
});
setTimeout(function() {
if (!controller) ask();
}, 600);