File size: 12,520 Bytes
d656a30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>tiny-llm demo</title>
<style>
:root {
  color-scheme: light;
  --page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink-2: #52514e; --muted: #898781;
  --grid: #e1e0d9; --ring: rgba(11,11,11,0.10); --accent: #2a78d6; --on-accent: #fff;
  --gen: #e8f1fc; --bad: #d03b3b; --warn: #fab219;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink-2: #c3c2b7; --muted: #898781;
    --grid: #2c2c2a; --ring: rgba(255,255,255,0.10); --accent: #3987e5; --gen: #12243a;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink-2: #c3c2b7; --muted: #898781;
  --grid: #2c2c2a; --ring: rgba(255,255,255,0.10); --accent: #3987e5; --gen: #12243a;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--page); color: var(--ink);
       font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
main { max-width: 760px; margin: 0 auto; padding: 28px 16px 48px; }
h1 { font-size: 24px; margin: 0; font-weight: 600; }
.lede { color: var(--ink-2); margin: 4px 0 20px; }
a { color: var(--accent); }
.panel { background: var(--surface); border: 1px solid var(--ring); border-radius: 12px; padding: 16px; }
label { font-size: 14px; color: var(--ink-2); }
textarea { width: 100%; min-height: 84px; resize: vertical; margin: 6px 0 4px; padding: 10px 12px;
           font: inherit; color: var(--ink); background: var(--page); border: 1px solid var(--ring); border-radius: 8px; }
textarea:focus-visible, button:focus-visible, input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.count { font-size: 12px; color: var(--muted); text-align: right; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 14px; }
.chip { font: inherit; font-size: 13px; padding: 4px 10px; border-radius: 999px; cursor: pointer;
        color: var(--ink-2); background: transparent; border: 1px solid var(--ring); }
.chip:hover { color: var(--ink); border-color: var(--muted); }
.controls { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 20px; }
.controls div { display: flex; flex-direction: column; }
.controls output { color: var(--ink); font-variant-numeric: tabular-nums; }
input[type=range] { accent-color: var(--accent); width: 100%; }
.actions { display: flex; gap: 10px; align-items: center; margin-top: 16px; flex-wrap: wrap; }
button.primary, button.secondary { font: inherit; font-size: 15px; padding: 8px 18px; border-radius: 8px; cursor: pointer; }
button.primary { background: var(--accent); color: var(--on-accent); border: 1px solid var(--accent); }
button.secondary { background: transparent; color: var(--ink); border: 1px solid var(--ring); }
button:disabled { opacity: 0.5; cursor: default; }
.hint { font-size: 13px; color: var(--muted); }
.story { margin-top: 16px; min-height: 120px; white-space: pre-wrap; font-size: 17px; line-height: 1.65; }
.story .p { color: var(--ink-2); }
.story .g { background: var(--gen); border-radius: 3px; }
.story .placeholder { color: var(--muted); }
.cursor { display: inline-block; width: 8px; height: 1.1em; vertical-align: text-bottom; background: var(--accent);
          margin-left: 1px; animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .cursor { animation: none; } }
.stats { display: flex; flex-wrap: wrap; gap: 6px 20px; margin-top: 12px; font-size: 13px; color: var(--ink-2);
         font-variant-numeric: tabular-nums; }
.stats b { color: var(--ink); font-weight: 600; }
.msg { display: none; margin-top: 12px; padding: 8px 12px; border-radius: 8px; font-size: 14px;
       border: 1px solid var(--ring); border-left: 4px solid var(--warn); }
.msg.show { display: block; }
.msg.error { border-left-color: var(--bad); }
footer { color: var(--ink-2); font-size: 13px; margin-top: 28px; }
footer p { margin: 6px 0; }
footer details { margin: 6px 0; }
footer summary { cursor: pointer; color: var(--ink); }
footer ul { margin: 6px 0; padding-left: 20px; }
@media (max-width: 520px) { .controls { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<main>
  <h1>tiny-llm</h1>
  <p class="lede">A small language model trained from scratch on children's stories.
    Start a story and it writes the rest.
    It knows simple stories, not facts, so it can't answer questions or follow instructions.</p>

  <section class="panel">
    <label for="prompt">Story opening</label>
    <textarea id="prompt" maxlength="600" spellcheck="true">Once upon a time, there was a little dog named Max.</textarea>
    <div class="count" id="count"></div>
    <div class="chips" id="chips" aria-label="Example openings"></div>
    <div class="controls">
      <div><label for="len">Length: <output id="len-out"></output> tokens</label>
        <input type="range" id="len" min="20" max="250" step="10" value="120"></div>
      <div><label for="temp">Temperature: <output id="temp-out"></output></label>
        <input type="range" id="temp" min="0" max="1.5" step="0.1" value="0.8"></div>
    </div>
    <div class="actions">
      <button class="primary" id="go">Write the story</button>
      <button class="secondary" id="stop" disabled>Stop</button>
      <span class="hint">Ctrl + Enter. Lower temperature: safer and more repetitive; higher: more surprising.</span>
    </div>
  </section>

  <div class="msg" id="msg" role="status"></div>
  <div class="story" id="story" aria-live="polite"><span class="placeholder">The story appears here, one token at a time.</span></div>
  <div class="stats" id="stats"></div>

  <footer>
    <p>Model: <span id="model">loading…</span> ·
      <a href="https://huggingface.co/CoolDip00/tiny-llm-tinystories-30m">model card</a> ·
      <a href="https://github.com/cooldip00/tiny-llm">source on GitHub</a> ·
      <a href="status">server status</a></p>
    <p>This demo runs on a shared CPU, so it is slower than the GPU numbers in the project README,
      and it can be busy or asleep for a minute.</p>
    <p>Privacy: your text isn't stored. The server keeps request metadata (timings, token counts and a
      salted hash of your address), never the opening or the story.</p>
    <details>
      <summary>What is logged</summary>
      <ul>
        <li>Per request: time, status, timings (queue wait, time to first token, total), token counts,
          whether it streamed, why it stopped, the error message if any, and how busy the server was.</li>
        <li>Your address only as a salted hash: it can tell repeat visits apart, not who you are.
          Raw addresses are never stored.</li>
        <li>Never the opening you type or the story the model writes.</li>
        <li>The log is kept in a private dataset that only the project owner can read, and is deleted after 30 days.</li>
      </ul>
    </details>
  </footer>
</main>
<script>
"use strict";
const EXAMPLES = [
  "Once upon a time, there was a little dog named Max.",
  "Lily found a shiny key under her bed.",
  "The sun was hot, and Tom wanted to swim.",
  "One day, a small bird could not fly.",
  "Mia and her mom baked a big cake.",
];
const MAX_CHARS = 600;
const $ = (id) => document.getElementById(id);
const prompt = $("prompt"), story = $("story"), stats = $("stats"), msg = $("msg");
let ctrl = null;

for (const ex of EXAMPLES) {
  const b = document.createElement("button");
  b.className = "chip"; b.type = "button"; b.textContent = ex;
  b.onclick = () => { prompt.value = ex; updateCount(); prompt.focus(); };
  $("chips").appendChild(b);
}
function updateCount() { $("count").textContent = `${prompt.value.length} / ${MAX_CHARS}`; }
function bindRange(id, fmt) {
  const r = $(id), o = $(id + "-out");
  const f = () => { o.textContent = fmt(r.value); };
  r.oninput = f; f();
}
bindRange("len", (v) => v);
bindRange("temp", (v) => Number(v).toFixed(1));
prompt.oninput = updateCount; updateCount();
prompt.onkeydown = (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); start(); } };
$("go").onclick = start;
$("stop").onclick = () => ctrl && ctrl.abort();

fetch("v1/models").then((r) => r.json()).then((d) => {
  const m = d.data && d.data[0];
  $("model").textContent = m ? (m.version || m.id) : "unknown";
}).catch(() => { $("model").textContent = "unavailable"; });

function say(text, kind) {
  msg.textContent = text;
  msg.className = "msg show" + (kind === "error" ? " error" : "");
}
function stat(label, value) {
  const s = document.createElement("span");
  const b = document.createElement("b"); b.textContent = value;
  s.append(document.createTextNode(label + " "), b);
  return s;
}
function showStats(t) {
  const items = [];
  if (t.ttft !== null) items.push(stat("First token", Math.round(t.ttft * 1000) + " ms"));
  if (t.tokens > 1 && t.genS > 0) items.push(stat("Speed", ((t.tokens - 1) / t.genS).toFixed(1) + " tokens/s"));
  items.push(stat("Tokens", String(t.tokens)));
  if (t.finish) items.push(stat("Stopped because", t.finish === "stop" ? "the story ended" :
                                t.finish === "length" ? "it reached the length" : t.finish));
  stats.replaceChildren(...items);
}
function errorText(status, body, retry) {
  if (status === 429) return `The server is busy right now. Please try again in ${retry || "a few"} seconds.`;
  if (status === 503) return "The model is still loading (or the server is waking up). Try again in a minute.";
  if (status === 504) return "That took too long and was stopped. Try a shorter length.";
  if (status === 400) return "The request wasn't accepted: " + ((body && body.error && body.error.message) || "bad input");
  return `Something went wrong on the server (${status}). Please try again.`;
}

async function start() {
  if (ctrl) return;
  const text = prompt.value.slice(0, MAX_CHARS);
  if (!text.trim()) { say("Write the start of a story first."); return; }
  msg.className = "msg";
  const pSpan = document.createElement("span"), gSpan = document.createElement("span"), cur = document.createElement("span");
  pSpan.className = "p"; gSpan.className = "g"; cur.className = "cursor";
  pSpan.textContent = text;
  story.replaceChildren(pSpan, gSpan, cur);
  stats.replaceChildren();
  ctrl = new AbortController();
  $("go").disabled = true; $("stop").disabled = false;
  const t = { t0: performance.now(), ttft: null, tFirst: 0, genS: 0, tokens: 0, finish: null };
  try {
    const r = await fetch("v1/completions", {
      method: "POST", headers: { "content-type": "application/json" }, signal: ctrl.signal,
      body: JSON.stringify({ prompt: text, max_tokens: Number($("len").value),
                             temperature: Number($("temp").value), stream: true }),
    });
    if (!r.ok) {
      let body = null; try { body = await r.json(); } catch (e) {}
      throw { status: r.status, body, retry: r.headers.get("retry-after") };
    }
    const reader = r.body.getReader(), dec = new TextDecoder();
    let buf = "";
    for (;;) {
      const { value, done } = await reader.read();
      if (done) break;
      buf += dec.decode(value, { stream: true });
      let i;
      while ((i = buf.indexOf("\n\n")) >= 0) {
        const line = buf.slice(0, i).trim(); buf = buf.slice(i + 2);
        if (!line.startsWith("data:")) continue;
        const data = line.slice(5).trim();
        if (data === "[DONE]") continue;
        const ev = JSON.parse(data);
        if (ev.error) throw { status: ev.error.code, body: ev };
        const ch = ev.choices[0], now = performance.now();
        if (t.ttft === null) { t.ttft = (now - t.t0) / 1000; t.tFirst = now; }
        t.genS = (now - t.tFirst) / 1000;
        if (ev.usage) t.tokens = ev.usage.completion_tokens; else t.tokens += 1;
        if (ch.text) gSpan.textContent += ch.text;
        if (ch.finish_reason) t.finish = ch.finish_reason;
        showStats(t);
      }
    }
  } catch (e) {
    if (e && e.name === "AbortError") { t.finish = "you pressed Stop"; showStats(t); }
    else if (e && e.status) say(errorText(e.status, e.body, e.retry), e.status === 429 || e.status === 503 ? "warn" : "error");
    else say("Can't reach the server. Check your connection, or try again in a minute.", "error");
  } finally {
    cur.remove();
    ctrl = null;
    $("go").disabled = false; $("stop").disabled = true;
  }
}
</script>
</body>
</html>