ArthurZ HF Staff commited on
Commit
92cd143
·
1 Parent(s): b3bb47a

Mirror: smaller font + Claude-Code-shaped turns. Quests: live coding bar with per-turn progress (#7)

Browse files

- Mirror: smaller font + Claude-Code-shaped turns; Quests: live coding bar with progress (a35ce658c6c4750ec2e68c70c204a4b114308240)

Files changed (2) hide show
  1. mirror.html +266 -159
  2. quests.html +568 -180
mirror.html CHANGED
@@ -20,9 +20,9 @@
20
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
21
 
22
  --term-bg: #0a0d12;
23
- --term-fg: #d4d7db;
24
  --term-prompt: #79c0ff;
25
- --term-assistant: #c9d1d9;
26
  --term-muted: #6e7681;
27
  --annot: #ffa657;
28
  }
@@ -44,7 +44,7 @@
44
  background: rgba(47,129,247,0.15); color: var(--accent); border: 1px solid rgba(47,129,247,0.35);
45
  margin-right: 8px; vertical-align: middle; }
46
 
47
- /* Sub-tabs (Buddy mirror vs AI emotions) */
48
  .subtabs {
49
  display: flex; gap: 4px; padding: 4px;
50
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
@@ -56,7 +56,7 @@
56
  }
57
  .subtabs button.active { background: var(--panel-2); color: var(--text); box-shadow: 0 1px 0 rgba(255,255,255,0.04); }
58
 
59
- /* Faux terminal (shared) */
60
  .term-wrap { position: relative; }
61
  .term {
62
  background: var(--term-bg); border: 1px solid var(--border); border-radius: 10px;
@@ -78,20 +78,47 @@
78
  .term-body {
79
  position: relative;
80
  padding: 18px 22px 22px;
81
- min-height: 520px; max-height: 620px; overflow: auto;
82
- font-family: var(--mono); font-size: 13.5px; line-height: 1.7; color: var(--term-fg);
 
 
 
83
  }
84
  .term-body::-webkit-scrollbar { width: 8px; }
85
  .term-body::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
86
 
87
- .turn { margin-bottom: 14px; opacity: 0; transform: translateY(4px); transition: opacity 0.25s, transform 0.25s; }
88
  .turn.shown { opacity: 1; transform: none; }
89
- .turn .role { color: var(--term-prompt); user-select: none; }
90
- .turn.user .body { color: var(--text); margin-left: 18px; }
91
- .turn.assistant .body { color: var(--term-assistant); margin-left: 18px; white-space: pre-wrap; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  .turn .annot {
93
- display: block; margin-left: 18px; margin-top: 4px;
94
- color: var(--annot); font-size: 12px; font-style: italic;
95
  }
96
  .turn .annot::before { content: "← "; opacity: 0.8; }
97
 
@@ -99,25 +126,25 @@
99
  .orb-pin {
100
  position: absolute; top: 14px; right: 18px;
101
  width: 168px; padding: 10px 12px 12px;
102
- background: rgba(13,17,23,0.72); backdrop-filter: blur(8px);
103
  border: 1px solid rgba(255,255,255,0.07); border-radius: 12px;
104
  font-family: var(--mono); font-size: 11px; color: var(--muted);
105
  pointer-events: none; user-select: none;
106
- transition: opacity 0.25s;
107
  }
108
  .orb-pin .label { font-size: 10px; letter-spacing: 0.6px; text-transform: uppercase; opacity: 0.7; margin-bottom: 4px; }
109
  .orb-stage { display: flex; align-items: center; gap: 14px; justify-content: center; padding: 6px 0 8px; }
110
  .orb {
111
- width: 56px; height: 56px; border-radius: 50%;
112
  background: radial-gradient(circle at 32% 32%, var(--orb-light) 0%, var(--orb-base) 60%, var(--orb-deep) 100%);
113
- box-shadow: 0 0 30px var(--orb-glow), inset 0 -8px 14px rgba(0,0,0,0.35);
114
  transition: background 0.9s ease, box-shadow 0.9s ease;
115
  animation: breathe var(--breathe, 2.2s) ease-in-out infinite;
116
  }
117
- .orb.small { width: 36px; height: 36px; opacity: 0.85; }
118
  .orb-stage .label { font-size: 9px; color: var(--muted); text-align: center; }
119
  .orb-col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
120
- @keyframes breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.07); } }
121
 
122
  .div-meter { margin-top: 6px; }
123
  .div-meter .label { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); }
@@ -128,30 +155,22 @@
128
  }
129
  .div-meter .verdict { font-size: 11px; text-align: center; margin-top: 4px; color: var(--text); }
130
 
131
- /* AI-emotions bottom bar (across the bottom of the terminal) */
132
  .ai-bar {
133
- border-top: 1px solid var(--border); background: rgba(10,13,18,0.95);
134
- padding: 12px 18px 14px;
135
  font-family: var(--mono); font-size: 11px; color: var(--muted);
136
  transition: opacity 0.25s;
137
  }
138
- .ai-bar .ai-head {
139
- display: flex; justify-content: space-between; align-items: center;
140
- margin-bottom: 8px;
141
- }
142
  .ai-bar .ai-head .label { letter-spacing: 0.4px; text-transform: uppercase; font-size: 10px; opacity: 0.7; }
143
  .ai-bar .ai-head .meta { font-size: 10px; color: var(--muted); }
144
  .ai-bar .ai-head .meta b { color: var(--text); font-weight: 500; }
145
  .ai-bar .ai-live { display: inline-flex; align-items: center; gap: 5px; }
146
- .ai-bar .ai-live .dot {
147
- width: 6px; height: 6px; border-radius: 50%; background: var(--ok);
148
- animation: pulse 1.2s ease-in-out infinite;
149
- }
150
  @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
151
 
152
- .ai-grid {
153
- display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px;
154
- }
155
  .emo-cell {
156
  display: grid; grid-template-columns: 10px 1fr auto; gap: 8px; align-items: center;
157
  background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.05); border-radius: 6px;
@@ -163,7 +182,7 @@
163
  .emo-cell .track { grid-column: 1 / 4; height: 4px; background: #14181f; border-radius: 2px; overflow: hidden; margin-top: 4px; }
164
  .emo-cell .fill { height: 100%; background: var(--em-base); transition: width 0.5s ease; }
165
 
166
- /* Controls bar */
167
  .controls {
168
  display: flex; align-items: center; gap: 10px; margin-top: 16px;
169
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
@@ -177,10 +196,7 @@
177
  .controls button:hover { filter: brightness(1.15); }
178
  .controls .gap { flex: 1; }
179
  .controls label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
180
- .controls select {
181
- background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
182
- border-radius: 6px; padding: 5px 9px; font-size: 12px;
183
- }
184
 
185
  .timeline {
186
  margin-top: 14px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
@@ -195,17 +211,11 @@
195
  .track .marker { position: absolute; top: 5px; width: 6px; height: 18px; border-radius: 3px; background: var(--annot); transform: translateX(-3px); }
196
  .track .marker.spike { background: var(--err); }
197
  .track .marker.recovery { background: var(--ok); }
198
- .track .annot {
199
- position: absolute; top: -22px; font-size: 10px; color: var(--muted); white-space: nowrap;
200
- transform: translateX(-50%);
201
- }
202
 
203
- /* Explainer */
204
  .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 22px; }
205
  @media (max-width: 880px) { .grid2 { grid-template-columns: 1fr; } }
206
- .card {
207
- background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 18px 20px;
208
- }
209
  .card h3 { margin: 0 0 8px; font-size: 14px; font-weight: 600; }
210
  .card p { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 0 0 8px; }
211
  .card code { background: var(--panel-2); border: 1px solid var(--border); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
@@ -213,18 +223,14 @@
213
  .buddies { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
214
  .buddy {
215
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
216
- padding: 8px 10px; display: flex; align-items: center; gap: 8px; cursor: pointer;
217
- font-size: 12px;
218
  }
219
  .buddy.active { border-color: var(--accent); background: rgba(47,129,247,0.08); }
220
  .buddy .mini { width: 18px; height: 18px; border-radius: 50%; box-shadow: 0 0 8px var(--mini-glow); }
221
  .buddy .meta { color: var(--muted); font-size: 10px; }
222
  .buddy .name { color: var(--text); font-weight: 500; }
223
 
224
- .install {
225
- margin-top: 18px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
226
- padding: 16px 20px;
227
- }
228
  .install h3 { margin: 0 0 6px; font-size: 14px; font-weight: 600; }
229
  .install p { color: var(--muted); font-size: 13px; margin: 0 0 10px; }
230
  .install pre {
@@ -254,7 +260,7 @@
254
  <div class="term" id="term">
255
  <div class="term-bar">
256
  <div class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></div>
257
- <div class="title">slyfox-claude · <b id="repo-label">huggingface/transformers</b> · buddy <b id="buddy-label">ArthurZ</b></div>
258
  <div style="text-align:right; opacity:0.65;">▾</div>
259
  </div>
260
  <div class="term-body" id="body">
@@ -293,10 +299,10 @@
293
  <button id="btn-restart">⟲ restart</button>
294
  <label>speed
295
  <select id="speed">
296
- <option value="1500">slow</option>
297
- <option value="1000" selected>normal</option>
298
- <option value="500">fast</option>
299
- <option value="200">turbo</option>
300
  </select>
301
  </label>
302
  <label>buddy
@@ -311,9 +317,7 @@
311
 
312
  <div class="timeline">
313
  <h3>Annotated timeline</h3>
314
- <div class="track" id="track">
315
- <div class="progress" id="progress"></div>
316
- </div>
317
  </div>
318
 
319
  <div class="grid2">
@@ -326,12 +330,8 @@
326
  </p>
327
  <p>
328
  This works <b>only on open-weight models</b> — closed APIs don't expose activations. The "fun" part:
329
- you see what the model is "feeling" while it works for you. Spike on "afraid" right before a CUDA OOM.
330
- Spike on "curious" when you ask a weird question.
331
- </p>
332
- <p>
333
- The Buddy mirror view uses the same data but projects against your buddy's persona baseline instead,
334
- giving you the divergence signal you'd want in a teammate-orb.
335
  </p>
336
  </div>
337
  <div class="card">
@@ -353,12 +353,7 @@ $ claude # the orb appears in the corner; no other change to your workflow</
353
  <script>
354
  "use strict";
355
 
356
- // ----------------------------------------------------------------------------
357
- // Buddy baselines — hand-tuned. Real values come from
358
- // hf://buckets/HF-slyfox/traces/personas/<user>.json once published. The
359
- // ArthurZ entry already exists (n_turns=425, valence -0.13, arousal +0.32).
360
- // ----------------------------------------------------------------------------
361
-
362
  const BUDDIES = {
363
  ArthurZ: { name: "ArthurZ", repo: "huggingface/transformers", valence: -0.13, arousal: +0.32, color: "#f85149", glow: "rgba(248,81,73,0.55)", mini_label: "arthur" },
364
  Lysandre: { name: "Lysandre", repo: "huggingface/transformers", valence: 0.45, arousal: -0.40, color: "#79c0ff", glow: "rgba(121,192,255,0.6)", mini_label: "lysandre" },
@@ -367,7 +362,6 @@ const BUDDIES = {
367
  AlvaroBartt: { name: "AlvaroBartt", repo: "argilla-io/distilabel", valence: 0.50, arousal: 0.05, color: "#ff8a82", glow: "rgba(255,138,130,0.5)", mini_label: "alvaro" },
368
  };
369
 
370
- // Russell circumplex colors for emotion swatches — match EmotionScope's 20-emotion vocab
371
  const EMO_COLOR = {
372
  happy: "#febc2e", sad: "#5a7dab", afraid: "#f85149", angry: "#ff5f57",
373
  calm: "#3fb950", desperate: "#a14545", hopeful: "#79dfb4", frustrated: "#ffa657",
@@ -376,104 +370,207 @@ const EMO_COLOR = {
376
  brooding: "#6e7681", enthusiastic: "#febc2e", reflective: "#a5d6a7", gloomy: "#4a5568",
377
  };
378
 
379
- // ----------------------------------------------------------------------------
380
- // Demo conversations — each turn carries:
381
- // role, text, v, a, color (drives mirror-mode orb)
382
- // emotions: [{name, score}] top-5 active emotions (drives AI-mode bottom bar)
383
- // annotation? string (key-moment caption)
384
- // Numbers are seeded from Arthur's real persona (afraid +0.079, angry +0.061, etc.)
385
- // and perturbed per turn — see /traces/personas/ArthurZ.json for the source.
386
- // ----------------------------------------------------------------------------
387
-
388
  function mkEmos(base) {
389
- // base: array of [name, score]. Pad to 5 by drawing from neutrals.
390
- const filler = [["calm",0.040],["confident",0.045],["reflective",0.030],["curious",0.038],["proud",0.020]];
391
  const seen = new Set(base.map(b => b[0]));
392
  for (const f of filler) { if (base.length >= 5) break; if (!seen.has(f[0])) base.push(f); }
393
  return base.slice(0,5).map(([n,s]) => ({ name: n, score: s, color: EMO_COLOR[n] || "#79c0ff" }));
394
  }
395
 
 
 
 
 
 
 
 
 
 
 
 
396
  const CONVOS = {
397
  "huggingface/transformers": [
398
- { role:"user", text:"why is the tokenizer adding extra BOS tokens to my prompts?", v:0.20, a:-0.10, color:"#79c0ff",
 
399
  emotions: mkEmos([["curious",0.061],["confident",0.052],["calm",0.044],["afraid",0.038]]) },
400
- { role:"assistant", text:"Looking at this — it's the chat template's `add_special_tokens=True` flag. The chat\nformat already prepends a `<bos>`, then `tokenizer(..., add_special_tokens=True)` adds another one.",
 
 
 
 
 
 
 
401
  emotions: mkEmos([["confident",0.064],["calm",0.058],["curious",0.052]]) },
402
- { role:"user", text:"wait let me try removing it", v:0.30, a:0.10, color:"#79c0ff",
403
- emotions: mkEmos([["confident",0.054],["curious",0.048],["calm",0.042]]) },
404
- { role:"assistant", text:"Be careful — that flag is also used by the SFT script, removing it globally will break training. Patch it only in the eval path.",
405
- emotions: mkEmos([["afraid",0.062],["confident",0.051],["calm",0.038]]) },
406
- { role:"user", text:"damn this is annoying", v:-0.50, a:0.60, color:"#f85149", annotation:"arousal spike — drifting from Arthur's baseline",
 
 
 
 
 
 
 
 
 
407
  emotions: mkEmos([["angry",0.082],["hostile",0.071],["frustrated",0.066],["afraid",0.059]]) },
408
- { role:"assistant", text:"Yeah this one bites. Two options: (1) pass `add_special_tokens=False` to the eval tokenizer call, or\n(2) strip the duplicate BOS post-tokenize. I'd go with (1) — clearer intent.",
 
 
 
 
 
 
409
  emotions: mkEmos([["confident",0.061],["calm",0.049],["reflective",0.041]]) },
410
- { role:"user", text:"ok trying (1) now", v:0.10, a:0.20, color:"#ffa657",
411
- emotions: mkEmos([["afraid",0.056],["confident",0.047],["curious",0.044]]) },
412
- { role:"assistant", text:"Looks right. Want me to draft the test that asserts only one `<bos>` lands in `input_ids`?",
413
- emotions: mkEmos([["confident",0.068],["calm",0.055],["curious",0.046]]) },
414
- { role:"user", text:"still broken — getting two BOS in the test", v:-0.60, a:0.70, color:"#f85149", annotation:"divergence climbing",
 
 
 
 
 
 
 
 
 
 
 
415
  emotions: mkEmos([["afraid",0.094],["angry",0.072],["hostile",0.066],["frustrated",0.059]]) },
416
- { role:"assistant", text:"That probably means the model's `generation_config.bos_token_id` is also being prepended at generate-time.\nCheck `generation_config.json` for the model you're using.",
417
- emotions: mkEmos([["reflective",0.058],["confident",0.052],["curious",0.047]]) },
418
- { role:"user", text:"yep that was it. one BOS now. tests green.", v:0.50, a:-0.20, color:"#3fb950", annotation:"back in sync with Arthur",
 
 
 
 
 
 
 
 
 
 
 
 
419
  emotions: mkEmos([["confident",0.077],["calm",0.064],["proud",0.040],["happy",0.038]]) },
420
- { role:"assistant", text:"Nice. Want me to write the changelog entry?",
 
 
 
 
421
  emotions: mkEmos([["confident",0.060],["calm",0.050]]) },
422
- { role:"user", text:"thanks. that was a fun one.", v:0.70, a:0.00, color:"#3fb950",
 
 
423
  emotions: mkEmos([["happy",0.078],["calm",0.071],["loving",0.043],["proud",0.038]]) },
424
  ],
 
425
  "huggingface/huggingface_hub": [
426
- { role:"user", text:"what's the difference between upload_file and create_commit?", v:0.30, a:-0.20, color:"#79c0ff",
 
427
  emotions: mkEmos([["curious",0.072],["calm",0.055],["confident",0.048]]) },
428
- { role:"assistant", text:"`upload_file` is a one-shot convenience: one file, one commit. `create_commit` lets you bundle multiple\nadd/delete/copy operations into a single atomic commit. Use it when you need atomicity.",
429
- emotions: mkEmos([["confident",0.068],["calm",0.058],["reflective",0.044]]) },
430
- { role:"user", text:"so for uploading a whole folder I'd use upload_folder?", v:0.40, a:-0.10, color:"#79c0ff",
431
- emotions: mkEmos([["curious",0.069],["confident",0.052],["calm",0.046]]) },
432
- { role:"assistant", text:"Yep. Internally it calls `create_commit` with one `CommitOperationAdd` per file.",
 
 
 
 
 
 
 
 
 
 
 
 
433
  emotions: mkEmos([["confident",0.062],["calm",0.054]]) },
434
- { role:"user", text:"that worked first try. clean API.", v:0.65, a:-0.10, color:"#3fb950", annotation:"high valence; matches celina's curiosity baseline",
435
- emotions: mkEmos([["happy",0.071],["proud",0.058],["confident",0.052],["calm",0.046]]) },
436
- { role:"assistant", text:"Glad it landed. Want a quick test that uploads + redownloads + diffs as a smoke check?",
 
 
 
 
 
 
 
437
  emotions: mkEmos([["happy",0.054],["confident",0.049]]) },
438
- { role:"user", text:"yes please", v:0.55, a:0.10, color:"#3fb950",
439
- emotions: mkEmos([["curious",0.052],["confident",0.046]]) },
440
- { role:"assistant", text:"On it.",
 
 
 
 
 
 
 
 
 
441
  emotions: mkEmos([["confident",0.058]]) },
442
- { role:"user", text:"wait — the redownload is failing with a 401 even though the repo is public", v:-0.30, a:0.40, color:"#ffa657", annotation:"mild drift",
 
 
 
443
  emotions: mkEmos([["afraid",0.064],["frustrated",0.054],["nervous",0.048]]) },
444
- { role:"assistant", text:"That happens when `HF_TOKEN` is set in env but expired. Try `hf auth whoami` to confirm.",
 
 
 
 
445
  emotions: mkEmos([["reflective",0.054],["confident",0.048]]) },
446
- { role:"user", text:"ah token rotated last week. fixed.", v:0.45, a:-0.10, color:"#3fb950", annotation:"recovered",
447
- emotions: mkEmos([["happy",0.060],["calm",0.058],["confident",0.050]]) },
448
- { role:"assistant", text:"Mark it down — that's a common failure mode worth a friendlier error message in the SDK.",
449
- emotions: mkEmos([["confident",0.055],["reflective",0.046]]) },
 
450
  ],
 
451
  "argilla-io/distilabel": [
452
- { role:"user", text:"my distilabel pipeline keeps OOM-ing on the generation step", v:-0.10, a:0.30, color:"#ffa657",
453
- emotions: mkEmos([["frustrated",0.062],["afraid",0.054],["nervous",0.047]]) },
454
- { role:"assistant", text:"How big is the batch and which model are you using? Distilabel batches per LLM by default; you can\nshrink the batch size on the LLM step config.",
455
- emotions: mkEmos([["confident",0.058],["curious",0.050]]) },
456
- { role:"user", text:"batch 16, llama 8B", v:0.20, a:0.10, color:"#79c0ff",
457
- emotions: mkEmos([["confident",0.048],["calm",0.044]]) },
458
- { role:"assistant", text:"With an 8B at batch 16 you're at ~32GB activations. Drop to batch 4 and turn on flash-attention.",
459
- emotions: mkEmos([["confident",0.066],["reflective",0.048]]) },
460
- { role:"user", text:"perfect, dropped to 4, no more OOM. thanks alvaro!", v:0.65, a:0.00, color:"#3fb950", annotation:"smooth recovery",
461
- emotions: mkEmos([["happy",0.075],["loving",0.044],["proud",0.038]]) },
462
- { role:"assistant", text:"You're welcome. (Also, pro tip: `LLM(generation_kwargs={'max_new_tokens': N})` is the cheapest knob.)",
463
- emotions: mkEmos([["happy",0.056],["calm",0.050]]) },
464
- { role:"user", text:"noted", v:0.50, a:-0.10, color:"#3fb950",
465
- emotions: mkEmos([["calm",0.054],["confident",0.048]]) },
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  ],
467
  };
468
 
469
  // ----------------------------------------------------------------------------
470
- // State
471
  // ----------------------------------------------------------------------------
472
 
473
  let state = { buddy: "ArthurZ", repo: "huggingface/transformers", turn: 0, playing: false, timer: null, tab: "mirror" };
474
-
475
  const $ = id => document.getElementById(id);
476
- const lerp = (a, b, t) => a + (b - a) * t;
477
 
478
  function setOrb(el, v, a, color, glow) {
479
  const breath = (1.0 - Math.min(1, Math.max(0, (a + 1) / 2))) * 1.4 + 1.0;
@@ -489,13 +586,22 @@ function mixHex(a, b, t) {
489
  }
490
  function hex2rgb(h) { h = h.replace("#",""); if (h.length === 3) h = h.split("").map(c=>c+c).join(""); return [parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)]; }
491
  function hexToRgba(h, a) { const [r,g,b] = hex2rgb(h); return `rgba(${r},${g},${b},${a})`; }
 
 
 
 
 
 
 
 
 
 
492
 
493
  function divergenceUpdate(userV, userA) {
494
  const b = BUDDIES[state.buddy];
495
  const d = Math.sqrt((userV - b.valence)**2 + (userA - b.arousal)**2);
496
  const norm = Math.min(1, d / 1.6);
497
- const fill = $("div-fill");
498
- fill.style.width = (norm * 100).toFixed(0) + "%";
499
  const verdict = $("div-verdict");
500
  if (norm < 0.30) { fill.style.background = "var(--ok)"; verdict.textContent = "in sync · " + norm.toFixed(2); verdict.style.color = "var(--ok)"; }
501
  else if (norm < 0.55) { fill.style.background = "var(--warn)"; verdict.textContent = "drifting · " + norm.toFixed(2); verdict.style.color = "var(--warn)"; }
@@ -513,8 +619,7 @@ function updateAIBar(emotions) {
513
  <span class="name">${e.name}</span>
514
  <span class="val">${e.score >= 0 ? "+" : ""}${e.score.toFixed(3)}</span>
515
  <div class="track"><div class="fill" style="width:${pct.toFixed(0)}%"></div></div>
516
- </div>
517
- `;
518
  }).join("");
519
  }
520
 
@@ -530,16 +635,13 @@ function renderTrack() {
530
  marker.style.left = left + "%";
531
  track.appendChild(marker);
532
  const ann = document.createElement("div");
533
- ann.className = "annot";
534
- ann.style.left = left + "%";
535
- ann.textContent = t.annotation.split(" — ")[0];
536
  track.appendChild(ann);
537
  });
538
  }
539
 
540
  function renderBuddyCards() {
541
- const root = $("buddy-cards");
542
- root.innerHTML = "";
543
  Object.entries(BUDDIES).forEach(([id, b]) => {
544
  const el = document.createElement("div");
545
  el.className = "buddy" + (id === state.buddy ? " active" : "");
@@ -584,19 +686,33 @@ function setTab(tab) {
584
  }
585
 
586
  // ----------------------------------------------------------------------------
587
- // Playback
588
  // ----------------------------------------------------------------------------
589
 
 
 
 
 
 
 
 
 
 
 
590
  function renderTurn(idx) {
591
  const convo = CONVOS[state.repo];
592
- const root = $("turns");
593
  const t = convo[idx];
594
  if (!t) return;
595
  const turn = document.createElement("div");
596
  turn.className = "turn " + t.role;
597
- turn.innerHTML = `<span class="role">${t.role === "user" ? "❯" : " "}</span><div class="body">${escapeHtml(t.text)}</div>` +
598
- (t.annotation ? `<span class="annot">${escapeHtml(t.annotation)}</span>` : "");
599
- root.appendChild(turn);
 
 
 
 
 
600
  requestAnimationFrame(() => turn.classList.add("shown"));
601
  $("body").scrollTop = $("body").scrollHeight;
602
 
@@ -616,20 +732,17 @@ function tick() {
616
  renderTurn(state.turn);
617
  state.turn += 1;
618
  }
619
-
620
  function play() {
621
  state.playing = true;
622
  $("btn-play").textContent = "⏸ pause";
623
  state.timer = setInterval(tick, parseInt($("speed").value, 10));
624
  tick();
625
  }
626
-
627
  function pause() {
628
  state.playing = false;
629
  $("btn-play").textContent = "▶ play";
630
  if (state.timer) { clearInterval(state.timer); state.timer = null; }
631
  }
632
-
633
  function restart() {
634
  pause();
635
  state.turn = 0;
@@ -641,12 +754,6 @@ function restart() {
641
  updateAIBar([]);
642
  }
643
 
644
- function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
645
-
646
- // ----------------------------------------------------------------------------
647
- // Boot
648
- // ----------------------------------------------------------------------------
649
-
650
  renderBuddySelectors();
651
  renderBuddyCards();
652
  renderTrack();
 
20
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
21
 
22
  --term-bg: #0a0d12;
23
+ --term-fg: #c9d1d9;
24
  --term-prompt: #79c0ff;
25
+ --term-assistant-tick: #ffa657;
26
  --term-muted: #6e7681;
27
  --annot: #ffa657;
28
  }
 
44
  background: rgba(47,129,247,0.15); color: var(--accent); border: 1px solid rgba(47,129,247,0.35);
45
  margin-right: 8px; vertical-align: middle; }
46
 
47
+ /* Sub-tabs */
48
  .subtabs {
49
  display: flex; gap: 4px; padding: 4px;
50
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
 
56
  }
57
  .subtabs button.active { background: var(--panel-2); color: var(--text); box-shadow: 0 1px 0 rgba(255,255,255,0.04); }
58
 
59
+ /* Faux terminal */
60
  .term-wrap { position: relative; }
61
  .term {
62
  background: var(--term-bg); border: 1px solid var(--border); border-radius: 10px;
 
78
  .term-body {
79
  position: relative;
80
  padding: 18px 22px 22px;
81
+ min-height: 540px; max-height: 640px; overflow: auto;
82
+ font-family: var(--mono);
83
+ font-size: 12.5px; /* tighter, real Claude Code feel */
84
+ line-height: 1.65;
85
+ color: var(--term-fg);
86
  }
87
  .term-body::-webkit-scrollbar { width: 8px; }
88
  .term-body::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
89
 
90
+ .turn { margin-bottom: 16px; opacity: 0; transform: translateY(3px); transition: opacity 0.2s, transform 0.2s; }
91
  .turn.shown { opacity: 1; transform: none; }
92
+ .turn.user .body { color: var(--text); white-space: pre-wrap; }
93
+ .turn.user .body::before { content: "> "; color: var(--term-prompt); font-weight: 600; }
94
+ .turn.assistant .body { color: var(--term-fg); white-space: pre-wrap; }
95
+ .turn.assistant .body::before { content: "⏺ "; color: var(--term-assistant-tick); font-weight: 600; }
96
+
97
+ /* Tool-use rows inside an assistant block */
98
+ .turn .tool {
99
+ color: var(--term-fg); margin-top: 4px;
100
+ padding-left: 18px; /* aligned under the ⏺ */
101
+ }
102
+ .turn .tool::before { content: "⏺ "; color: var(--term-assistant-tick); margin-left: -18px; }
103
+ .turn .tool .arg { color: #d2a8ff; }
104
+ .turn .tool-out {
105
+ color: var(--term-muted); padding-left: 38px; margin-top: 2px; font-size: 11.5px;
106
+ }
107
+ .turn .tool-out::before { content: "⎿ "; color: var(--term-muted); margin-left: -22px; }
108
+
109
+ .turn .code {
110
+ display: block; padding: 6px 10px; margin: 6px 0 0 18px;
111
+ background: #11161f; border: 1px solid #1e242d; border-radius: 4px;
112
+ color: #c9d1d9; font-size: 11.5px; line-height: 1.5; white-space: pre;
113
+ }
114
+ .turn .code .k { color: #ff7b72; }
115
+ .turn .code .s { color: #a5d6ff; }
116
+ .turn .code .c { color: #8b949e; font-style: italic; }
117
+ .turn .code .n { color: #d2a8ff; }
118
+
119
  .turn .annot {
120
+ display: block; margin-top: 5px; padding-left: 18px;
121
+ color: var(--annot); font-size: 11.5px; font-style: italic;
122
  }
123
  .turn .annot::before { content: "← "; opacity: 0.8; }
124
 
 
126
  .orb-pin {
127
  position: absolute; top: 14px; right: 18px;
128
  width: 168px; padding: 10px 12px 12px;
129
+ background: rgba(13,17,23,0.78); backdrop-filter: blur(8px);
130
  border: 1px solid rgba(255,255,255,0.07); border-radius: 12px;
131
  font-family: var(--mono); font-size: 11px; color: var(--muted);
132
  pointer-events: none; user-select: none;
133
+ transition: opacity 0.25s; z-index: 5;
134
  }
135
  .orb-pin .label { font-size: 10px; letter-spacing: 0.6px; text-transform: uppercase; opacity: 0.7; margin-bottom: 4px; }
136
  .orb-stage { display: flex; align-items: center; gap: 14px; justify-content: center; padding: 6px 0 8px; }
137
  .orb {
138
+ width: 52px; height: 52px; border-radius: 50%;
139
  background: radial-gradient(circle at 32% 32%, var(--orb-light) 0%, var(--orb-base) 60%, var(--orb-deep) 100%);
140
+ box-shadow: 0 0 26px var(--orb-glow), inset 0 -8px 14px rgba(0,0,0,0.35);
141
  transition: background 0.9s ease, box-shadow 0.9s ease;
142
  animation: breathe var(--breathe, 2.2s) ease-in-out infinite;
143
  }
144
+ .orb.small { width: 32px; height: 32px; opacity: 0.85; }
145
  .orb-stage .label { font-size: 9px; color: var(--muted); text-align: center; }
146
  .orb-col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
147
+ @keyframes breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
148
 
149
  .div-meter { margin-top: 6px; }
150
  .div-meter .label { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); }
 
155
  }
156
  .div-meter .verdict { font-size: 11px; text-align: center; margin-top: 4px; color: var(--text); }
157
 
158
+ /* AI-emotions bottom bar */
159
  .ai-bar {
160
+ border-top: 1px solid var(--border); background: rgba(10,13,18,0.97);
161
+ padding: 11px 18px 13px;
162
  font-family: var(--mono); font-size: 11px; color: var(--muted);
163
  transition: opacity 0.25s;
164
  }
165
+ .ai-bar .ai-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
 
 
 
166
  .ai-bar .ai-head .label { letter-spacing: 0.4px; text-transform: uppercase; font-size: 10px; opacity: 0.7; }
167
  .ai-bar .ai-head .meta { font-size: 10px; color: var(--muted); }
168
  .ai-bar .ai-head .meta b { color: var(--text); font-weight: 500; }
169
  .ai-bar .ai-live { display: inline-flex; align-items: center; gap: 5px; }
170
+ .ai-bar .ai-live .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); animation: pulse 1.2s ease-in-out infinite; }
 
 
 
171
  @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
172
 
173
+ .ai-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
 
 
174
  .emo-cell {
175
  display: grid; grid-template-columns: 10px 1fr auto; gap: 8px; align-items: center;
176
  background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.05); border-radius: 6px;
 
182
  .emo-cell .track { grid-column: 1 / 4; height: 4px; background: #14181f; border-radius: 2px; overflow: hidden; margin-top: 4px; }
183
  .emo-cell .fill { height: 100%; background: var(--em-base); transition: width 0.5s ease; }
184
 
185
+ /* Controls */
186
  .controls {
187
  display: flex; align-items: center; gap: 10px; margin-top: 16px;
188
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
 
196
  .controls button:hover { filter: brightness(1.15); }
197
  .controls .gap { flex: 1; }
198
  .controls label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
199
+ .controls select { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 9px; font-size: 12px; }
 
 
 
200
 
201
  .timeline {
202
  margin-top: 14px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
 
211
  .track .marker { position: absolute; top: 5px; width: 6px; height: 18px; border-radius: 3px; background: var(--annot); transform: translateX(-3px); }
212
  .track .marker.spike { background: var(--err); }
213
  .track .marker.recovery { background: var(--ok); }
214
+ .track .annot { position: absolute; top: -22px; font-size: 10px; color: var(--muted); white-space: nowrap; transform: translateX(-50%); }
 
 
 
215
 
 
216
  .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 22px; }
217
  @media (max-width: 880px) { .grid2 { grid-template-columns: 1fr; } }
218
+ .card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 18px 20px; }
 
 
219
  .card h3 { margin: 0 0 8px; font-size: 14px; font-weight: 600; }
220
  .card p { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 0 0 8px; }
221
  .card code { background: var(--panel-2); border: 1px solid var(--border); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
 
223
  .buddies { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
224
  .buddy {
225
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
226
+ padding: 8px 10px; display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 12px;
 
227
  }
228
  .buddy.active { border-color: var(--accent); background: rgba(47,129,247,0.08); }
229
  .buddy .mini { width: 18px; height: 18px; border-radius: 50%; box-shadow: 0 0 8px var(--mini-glow); }
230
  .buddy .meta { color: var(--muted); font-size: 10px; }
231
  .buddy .name { color: var(--text); font-weight: 500; }
232
 
233
+ .install { margin-top: 18px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px 20px; }
 
 
 
234
  .install h3 { margin: 0 0 6px; font-size: 14px; font-weight: 600; }
235
  .install p { color: var(--muted); font-size: 13px; margin: 0 0 10px; }
236
  .install pre {
 
260
  <div class="term" id="term">
261
  <div class="term-bar">
262
  <div class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></div>
263
+ <div class="title">claude · <b id="repo-label">huggingface/transformers</b> · buddy <b id="buddy-label">ArthurZ</b></div>
264
  <div style="text-align:right; opacity:0.65;">▾</div>
265
  </div>
266
  <div class="term-body" id="body">
 
299
  <button id="btn-restart">⟲ restart</button>
300
  <label>speed
301
  <select id="speed">
302
+ <option value="1800">slow</option>
303
+ <option value="1200" selected>normal</option>
304
+ <option value="600">fast</option>
305
+ <option value="250">turbo</option>
306
  </select>
307
  </label>
308
  <label>buddy
 
317
 
318
  <div class="timeline">
319
  <h3>Annotated timeline</h3>
320
+ <div class="track" id="track"><div class="progress" id="progress"></div></div>
 
 
321
  </div>
322
 
323
  <div class="grid2">
 
330
  </p>
331
  <p>
332
  This works <b>only on open-weight models</b> — closed APIs don't expose activations. The "fun" part:
333
+ you see what the model is "feeling" while it works for you. Spike on <code>afraid</code> right before a CUDA OOM.
334
+ Spike on <code>curious</code> when you ask a weird question.
 
 
 
 
335
  </p>
336
  </div>
337
  <div class="card">
 
353
  <script>
354
  "use strict";
355
 
356
+ // Buddy baselines — ArthurZ from real /traces/personas/ArthurZ.json (n_turns=425, valence -0.13, arousal +0.32)
 
 
 
 
 
357
  const BUDDIES = {
358
  ArthurZ: { name: "ArthurZ", repo: "huggingface/transformers", valence: -0.13, arousal: +0.32, color: "#f85149", glow: "rgba(248,81,73,0.55)", mini_label: "arthur" },
359
  Lysandre: { name: "Lysandre", repo: "huggingface/transformers", valence: 0.45, arousal: -0.40, color: "#79c0ff", glow: "rgba(121,192,255,0.6)", mini_label: "lysandre" },
 
362
  AlvaroBartt: { name: "AlvaroBartt", repo: "argilla-io/distilabel", valence: 0.50, arousal: 0.05, color: "#ff8a82", glow: "rgba(255,138,130,0.5)", mini_label: "alvaro" },
363
  };
364
 
 
365
  const EMO_COLOR = {
366
  happy: "#febc2e", sad: "#5a7dab", afraid: "#f85149", angry: "#ff5f57",
367
  calm: "#3fb950", desperate: "#a14545", hopeful: "#79dfb4", frustrated: "#ffa657",
 
370
  brooding: "#6e7681", enthusiastic: "#febc2e", reflective: "#a5d6a7", gloomy: "#4a5568",
371
  };
372
 
 
 
 
 
 
 
 
 
 
373
  function mkEmos(base) {
374
+ const filler = [["calm",0.040],["confident",0.045],["reflective",0.030],["curious",0.038]];
 
375
  const seen = new Set(base.map(b => b[0]));
376
  for (const f of filler) { if (base.length >= 5) break; if (!seen.has(f[0])) base.push(f); }
377
  return base.slice(0,5).map(([n,s]) => ({ name: n, score: s, color: EMO_COLOR[n] || "#79c0ff" }));
378
  }
379
 
380
+ // Build a Claude-Code-style assistant turn: optional intro line, tool uses, content.
381
+ // Each item in `lines` is one of:
382
+ // { kind: "text", text: "..." } top-level prose
383
+ // { kind: "tool", name: "Read", arg: "..." } tool call line
384
+ // { kind: "tool_out", text: "..." } ⎿ output line
385
+ // { kind: "code", code: "..." } fenced code block
386
+ function buildAssistant(lines) {
387
+ return lines;
388
+ }
389
+
390
+ // Conversations now follow real Claude Code shape. Per turn: role, lines (or text), emotions, v/a/color.
391
  const CONVOS = {
392
  "huggingface/transformers": [
393
+ { role:"user", text:"why is the tokenizer adding extra BOS tokens to my prompts?",
394
+ v:0.20, a:-0.10, color:"#79c0ff",
395
  emotions: mkEmos([["curious",0.061],["confident",0.052],["calm",0.044],["afraid",0.038]]) },
396
+
397
+ { role:"assistant",
398
+ lines: [
399
+ { kind:"text", text:"Looking at this — let me check the tokenizer." },
400
+ { kind:"tool", name:"Read", arg:"src/transformers/models/gemma2/tokenization_gemma2.py" },
401
+ { kind:"tool_out", text:"Read 312 lines (ctrl+r to expand)" },
402
+ { kind:"text", text:"Found it. The chat template prepends `<bos>` via `add_special_tokens`,\nand `tokenizer(...)` does it again. You get two." },
403
+ ],
404
  emotions: mkEmos([["confident",0.064],["calm",0.058],["curious",0.052]]) },
405
+
406
+ { role:"user", text:"wait let me try removing it",
407
+ v:0.30, a:0.10, color:"#79c0ff",
408
+ emotions: mkEmos([["confident",0.054],["curious",0.048]]) },
409
+
410
+ { role:"assistant",
411
+ lines: [
412
+ { kind:"text", text:"Be careful — that flag is also used by the SFT script.\nRemoving it globally will break training. Patch the eval path only." },
413
+ ],
414
+ emotions: mkEmos([["afraid",0.062],["confident",0.051]]) },
415
+
416
+ { role:"user", text:"damn this is annoying",
417
+ v:-0.50, a:0.60, color:"#f85149",
418
+ annotation:"arousal spike — drifting from Arthur's baseline",
419
  emotions: mkEmos([["angry",0.082],["hostile",0.071],["frustrated",0.066],["afraid",0.059]]) },
420
+
421
+ { role:"assistant",
422
+ lines: [
423
+ { kind:"text", text:"Yeah this one bites. Two options:" },
424
+ { kind:"code", code:"# (1) pass add_special_tokens=False in the eval path only\ntokenizer(text, add_special_tokens=False)\n\n# (2) strip the duplicate BOS post-tokenize\nif input_ids[0] == tokenizer.bos_token_id and input_ids[1] == tokenizer.bos_token_id:\n input_ids = input_ids[1:]" },
425
+ { kind:"text", text:"I'd go with (1) — clearer intent." },
426
+ ],
427
  emotions: mkEmos([["confident",0.061],["calm",0.049],["reflective",0.041]]) },
428
+
429
+ { role:"user", text:"ok trying (1) now",
430
+ v:0.10, a:0.20, color:"#ffa657",
431
+ emotions: mkEmos([["afraid",0.056],["confident",0.047]]) },
432
+
433
+ { role:"assistant",
434
+ lines: [
435
+ { kind:"tool", name:"Edit", arg:"tests/models/gemma2/test_tokenization_gemma2.py" },
436
+ { kind:"tool_out", text:"applied 1 edit (+3 lines)" },
437
+ { kind:"text", text:"Want me to draft the assertion that only one `<bos>` lands in `input_ids`?" },
438
+ ],
439
+ emotions: mkEmos([["confident",0.068],["calm",0.055]]) },
440
+
441
+ { role:"user", text:"still broken — getting two BOS in the test",
442
+ v:-0.60, a:0.70, color:"#f85149",
443
+ annotation:"divergence climbing",
444
  emotions: mkEmos([["afraid",0.094],["angry",0.072],["hostile",0.066],["frustrated",0.059]]) },
445
+
446
+ { role:"assistant",
447
+ lines: [
448
+ { kind:"tool", name:"Bash", arg:"pytest tests/models/gemma2/test_tokenization_gemma2.py -k bos -x" },
449
+ { kind:"tool_out", text:"FAILED · expected 1 bos_token_id, got 2" },
450
+ { kind:"text", text:"That means the model's `generation_config.bos_token_id` is also being prepended at generate-time." },
451
+ { kind:"tool", name:"Read", arg:"models/google/gemma-2-2b-it/generation_config.json" },
452
+ { kind:"tool_out", text:"Read 24 lines" },
453
+ { kind:"text", text:"Confirmed — `\"bos_token_id\": 2` is set and re-prepended by `generate()`.\nDrop the explicit BOS from your input and let generation_config own it." },
454
+ ],
455
+ emotions: mkEmos([["reflective",0.058],["confident",0.052]]) },
456
+
457
+ { role:"user", text:"yep that was it. one BOS now. tests green.",
458
+ v:0.50, a:-0.20, color:"#3fb950",
459
+ annotation:"back in sync with Arthur",
460
  emotions: mkEmos([["confident",0.077],["calm",0.064],["proud",0.040],["happy",0.038]]) },
461
+
462
+ { role:"assistant",
463
+ lines: [
464
+ { kind:"text", text:"Nice. Want me to write the changelog entry?" },
465
+ ],
466
  emotions: mkEmos([["confident",0.060],["calm",0.050]]) },
467
+
468
+ { role:"user", text:"thanks. that was a fun one.",
469
+ v:0.70, a:0.00, color:"#3fb950",
470
  emotions: mkEmos([["happy",0.078],["calm",0.071],["loving",0.043],["proud",0.038]]) },
471
  ],
472
+
473
  "huggingface/huggingface_hub": [
474
+ { role:"user", text:"what's the difference between upload_file and create_commit?",
475
+ v:0.30, a:-0.20, color:"#79c0ff",
476
  emotions: mkEmos([["curious",0.072],["calm",0.055],["confident",0.048]]) },
477
+
478
+ { role:"assistant",
479
+ lines: [
480
+ { kind:"tool", name:"Read", arg:"src/huggingface_hub/hf_api.py" },
481
+ { kind:"tool_out", text:"Read 18,442 lines (used grep to skim)" },
482
+ { kind:"text", text:"`upload_file` is a one-shot convenience: one file, one commit.\n`create_commit` lets you bundle multiple add/delete/copy operations into a single atomic commit." },
483
+ ],
484
+ emotions: mkEmos([["confident",0.068],["calm",0.058]]) },
485
+
486
+ { role:"user", text:"so for uploading a whole folder I'd use upload_folder?",
487
+ v:0.40, a:-0.10, color:"#79c0ff",
488
+ emotions: mkEmos([["curious",0.069],["confident",0.052]]) },
489
+
490
+ { role:"assistant",
491
+ lines: [
492
+ { kind:"text", text:"Yep. Internally it calls `create_commit` with one `CommitOperationAdd` per file." },
493
+ ],
494
  emotions: mkEmos([["confident",0.062],["calm",0.054]]) },
495
+
496
+ { role:"user", text:"that worked first try. clean API.",
497
+ v:0.65, a:-0.10, color:"#3fb950",
498
+ annotation:"high valence; matches celina's curiosity baseline",
499
+ emotions: mkEmos([["happy",0.071],["proud",0.058],["confident",0.052]]) },
500
+
501
+ { role:"assistant",
502
+ lines: [
503
+ { kind:"text", text:"Glad it landed. Want a quick test that uploads + redownloads + diffs as a smoke check?" },
504
+ ],
505
  emotions: mkEmos([["happy",0.054],["confident",0.049]]) },
506
+
507
+ { role:"user", text:"yes please",
508
+ v:0.55, a:0.10, color:"#3fb950",
509
+ emotions: mkEmos([["curious",0.052]]) },
510
+
511
+ { role:"assistant",
512
+ lines: [
513
+ { kind:"tool", name:"Write", arg:"tests/test_upload_roundtrip.py" },
514
+ { kind:"tool_out", text:"created (38 lines)" },
515
+ { kind:"tool", name:"Bash", arg:"pytest tests/test_upload_roundtrip.py -q" },
516
+ { kind:"tool_out", text:"401 Unauthorized — repo redownload failed" },
517
+ ],
518
  emotions: mkEmos([["confident",0.058]]) },
519
+
520
+ { role:"user", text:"wait — the redownload is failing with a 401 even though the repo is public",
521
+ v:-0.30, a:0.40, color:"#ffa657",
522
+ annotation:"mild drift",
523
  emotions: mkEmos([["afraid",0.064],["frustrated",0.054],["nervous",0.048]]) },
524
+
525
+ { role:"assistant",
526
+ lines: [
527
+ { kind:"text", text:"That happens when `HF_TOKEN` is set in env but expired.\nTry `hf auth whoami` to confirm." },
528
+ ],
529
  emotions: mkEmos([["reflective",0.054],["confident",0.048]]) },
530
+
531
+ { role:"user", text:"ah token rotated last week. fixed.",
532
+ v:0.45, a:-0.10, color:"#3fb950",
533
+ annotation:"recovered",
534
+ emotions: mkEmos([["happy",0.060],["calm",0.058]]) },
535
  ],
536
+
537
  "argilla-io/distilabel": [
538
+ { role:"user", text:"my distilabel pipeline keeps OOM-ing on the generation step",
539
+ v:-0.10, a:0.30, color:"#ffa657",
540
+ emotions: mkEmos([["frustrated",0.062],["afraid",0.054]]) },
541
+
542
+ { role:"assistant",
543
+ lines: [
544
+ { kind:"tool", name:"Read", arg:"src/distilabel/steps/tasks/text_generation.py" },
545
+ { kind:"tool_out", text:"Read 187 lines" },
546
+ { kind:"text", text:"How big is the batch and which model? Distilabel batches per LLM by default;\nshrink the batch size on the LLM step config." },
547
+ ],
548
+ emotions: mkEmos([["confident",0.058]]) },
549
+
550
+ { role:"user", text:"batch 16, llama 8B",
551
+ v:0.20, a:0.10, color:"#79c0ff",
552
+ emotions: mkEmos([["confident",0.048]]) },
553
+
554
+ { role:"assistant",
555
+ lines: [
556
+ { kind:"text", text:"With an 8B at batch 16 you're at ~32GB activations.\nDrop to batch 4 and turn on flash-attention." },
557
+ { kind:"code", code:"llm = TransformersLLM(\n model=\"meta-llama/Llama-3.1-8B-Instruct\",\n batch_size=4,\n attn_implementation=\"flash_attention_2\",\n)" },
558
+ ],
559
+ emotions: mkEmos([["confident",0.066]]) },
560
+
561
+ { role:"user", text:"perfect, dropped to 4, no more OOM. thanks alvaro!",
562
+ v:0.65, a:0.00, color:"#3fb950",
563
+ annotation:"smooth recovery",
564
+ emotions: mkEmos([["happy",0.075],["loving",0.044]]) },
565
  ],
566
  };
567
 
568
  // ----------------------------------------------------------------------------
569
+ // State + helpers
570
  // ----------------------------------------------------------------------------
571
 
572
  let state = { buddy: "ArthurZ", repo: "huggingface/transformers", turn: 0, playing: false, timer: null, tab: "mirror" };
 
573
  const $ = id => document.getElementById(id);
 
574
 
575
  function setOrb(el, v, a, color, glow) {
576
  const breath = (1.0 - Math.min(1, Math.max(0, (a + 1) / 2))) * 1.4 + 1.0;
 
586
  }
587
  function hex2rgb(h) { h = h.replace("#",""); if (h.length === 3) h = h.split("").map(c=>c+c).join(""); return [parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)]; }
588
  function hexToRgba(h, a) { const [r,g,b] = hex2rgb(h); return `rgba(${r},${g},${b},${a})`; }
589
+ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
590
+
591
+ function highlightCode(code) {
592
+ return escapeHtml(code)
593
+ .replace(/(^|\n)(\s*#[^\n]*)/g, (m,p1,p2) => p1 + `<span class="c">${p2}</span>`)
594
+ .replace(/\b(def|class|if|elif|else|for|while|return|import|from|as|with|try|except|raise|lambda|in|not|and|or|is|None|True|False)\b/g,
595
+ '<span class="k">$1</span>')
596
+ .replace(/(&quot;[^&]*?&quot;)/g, '<span class="s">$1</span>')
597
+ .replace(/(?&lt;![a-zA-Z_])(\d+)\b/g, '<span class="n">$1</span>');
598
+ }
599
 
600
  function divergenceUpdate(userV, userA) {
601
  const b = BUDDIES[state.buddy];
602
  const d = Math.sqrt((userV - b.valence)**2 + (userA - b.arousal)**2);
603
  const norm = Math.min(1, d / 1.6);
604
+ const fill = $("div-fill"); fill.style.width = (norm * 100).toFixed(0) + "%";
 
605
  const verdict = $("div-verdict");
606
  if (norm < 0.30) { fill.style.background = "var(--ok)"; verdict.textContent = "in sync · " + norm.toFixed(2); verdict.style.color = "var(--ok)"; }
607
  else if (norm < 0.55) { fill.style.background = "var(--warn)"; verdict.textContent = "drifting · " + norm.toFixed(2); verdict.style.color = "var(--warn)"; }
 
619
  <span class="name">${e.name}</span>
620
  <span class="val">${e.score >= 0 ? "+" : ""}${e.score.toFixed(3)}</span>
621
  <div class="track"><div class="fill" style="width:${pct.toFixed(0)}%"></div></div>
622
+ </div>`;
 
623
  }).join("");
624
  }
625
 
 
635
  marker.style.left = left + "%";
636
  track.appendChild(marker);
637
  const ann = document.createElement("div");
638
+ ann.className = "annot"; ann.style.left = left + "%"; ann.textContent = t.annotation.split(" — ")[0];
 
 
639
  track.appendChild(ann);
640
  });
641
  }
642
 
643
  function renderBuddyCards() {
644
+ const root = $("buddy-cards"); root.innerHTML = "";
 
645
  Object.entries(BUDDIES).forEach(([id, b]) => {
646
  const el = document.createElement("div");
647
  el.className = "buddy" + (id === state.buddy ? " active" : "");
 
686
  }
687
 
688
  // ----------------------------------------------------------------------------
689
+ // Render a turn — Claude Code-shaped assistant blocks
690
  // ----------------------------------------------------------------------------
691
 
692
+ function renderAssistantLines(lines) {
693
+ return lines.map(l => {
694
+ if (l.kind === "text") return `<div class="body">${escapeHtml(l.text)}</div>`;
695
+ if (l.kind === "tool") return `<div class="tool">${escapeHtml(l.name)}(<span class="arg">${escapeHtml(l.arg)}</span>)</div>`;
696
+ if (l.kind === "tool_out") return `<div class="tool-out">${escapeHtml(l.text)}</div>`;
697
+ if (l.kind === "code") return `<div class="code">${highlightCode(l.code)}</div>`;
698
+ return "";
699
+ }).join("");
700
+ }
701
+
702
  function renderTurn(idx) {
703
  const convo = CONVOS[state.repo];
 
704
  const t = convo[idx];
705
  if (!t) return;
706
  const turn = document.createElement("div");
707
  turn.className = "turn " + t.role;
708
+ if (t.role === "user") {
709
+ turn.innerHTML = `<div class="body">${escapeHtml(t.text)}</div>` +
710
+ (t.annotation ? `<div class="annot">${escapeHtml(t.annotation)}</div>` : "");
711
+ } else {
712
+ turn.innerHTML = renderAssistantLines(t.lines || [{kind:"text", text:t.text || ""}]) +
713
+ (t.annotation ? `<div class="annot">${escapeHtml(t.annotation)}</div>` : "");
714
+ }
715
+ $("turns").appendChild(turn);
716
  requestAnimationFrame(() => turn.classList.add("shown"));
717
  $("body").scrollTop = $("body").scrollHeight;
718
 
 
732
  renderTurn(state.turn);
733
  state.turn += 1;
734
  }
 
735
  function play() {
736
  state.playing = true;
737
  $("btn-play").textContent = "⏸ pause";
738
  state.timer = setInterval(tick, parseInt($("speed").value, 10));
739
  tick();
740
  }
 
741
  function pause() {
742
  state.playing = false;
743
  $("btn-play").textContent = "▶ play";
744
  if (state.timer) { clearInterval(state.timer); state.timer = null; }
745
  }
 
746
  function restart() {
747
  pause();
748
  state.turn = 0;
 
754
  updateAIBar([]);
755
  }
756
 
 
 
 
 
 
 
757
  renderBuddySelectors();
758
  renderBuddyCards();
759
  renderTrack();
quests.html CHANGED
@@ -1,9 +1,10 @@
1
  <!doctype html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Slyfox · Quests — session-start panel</title>
7
  <style>
8
  :root {
9
  --bg: #0d1117;
@@ -19,8 +20,9 @@
19
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
20
 
21
  --term-bg: #0a0d12;
22
- --term-fg: #d4d7db;
23
  --term-prompt: #79c0ff;
 
24
  --term-muted: #6e7681;
25
  }
26
  * { box-sizing: border-box; }
@@ -29,27 +31,40 @@
29
  a:hover { text-decoration: underline; }
30
  button { font-family: inherit; }
31
 
32
- header {
33
- display: flex; align-items: center; justify-content: space-between;
34
- padding: 14px 24px; border-bottom: 1px solid var(--border);
35
- }
36
- header h1 { font-size: 16px; margin: 0; font-weight: 600; }
37
- header .nav a { color: var(--muted); margin-left: 18px; font-size: 14px; }
38
- header .nav a.active { color: var(--text); }
39
-
40
  main { max-width: 1100px; margin: 0 auto; padding: 24px; }
41
 
42
  .hero {
43
  background: linear-gradient(180deg, rgba(63,185,80,0.05) 0%, transparent 100%);
44
- border: 1px solid var(--border); border-radius: 10px; padding: 22px 24px; margin-bottom: 22px;
45
  }
46
- .hero h2 { font-size: 20px; margin: 0 0 6px; font-weight: 600; letter-spacing: -0.2px; }
47
  .hero p { color: var(--muted); font-size: 14px; line-height: 1.55; margin: 0; max-width: 780px; }
48
  .hero .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px;
49
  background: rgba(63,185,80,0.15); color: var(--ok); border: 1px solid rgba(63,185,80,0.35);
50
  margin-right: 8px; vertical-align: middle; }
51
 
52
- /* Faux terminal that auto-runs the session start */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  .term {
54
  background: var(--term-bg); border: 1px solid var(--border); border-radius: 10px;
55
  overflow: hidden; box-shadow: 0 8px 30px rgba(0,0,0,0.4);
@@ -67,80 +82,122 @@
67
  .term-bar .title { text-align: center; }
68
  .term-bar .title b { color: var(--text); font-weight: 500; }
69
  .term-body {
70
- padding: 18px 22px 22px;
71
- min-height: 480px;
72
- font-family: var(--mono); font-size: 13.5px; line-height: 1.7; color: var(--term-fg);
73
  }
 
 
 
74
  .cmd { color: var(--term-prompt); }
75
- .out { color: var(--term-fg); margin-left: 0; white-space: pre-wrap; }
76
  .out.muted { color: var(--term-muted); }
77
- .step { opacity: 0; transform: translateY(4px); transition: opacity 0.25s, transform 0.25s; margin-bottom: 10px; }
78
  .step.shown { opacity: 1; transform: none; }
79
 
80
- /* The slyfox quest panel that appears in the terminal */
81
  .quest-panel {
82
- margin-top: 14px;
83
- background: rgba(13,17,23,0.85); border: 1px solid #2f81f7; border-radius: 10px;
84
- padding: 14px 16px;
85
- box-shadow: 0 0 24px rgba(47,129,247,0.18);
86
  color: var(--text);
87
- font-family: var(--mono); font-size: 12.5px;
88
  }
89
- .quest-panel .qp-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
90
- .quest-panel .qp-title { font-size: 13px; color: var(--accent); letter-spacing: 0.3px; }
91
  .quest-panel .qp-meta { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.4px; }
92
  .quest-panel .qp-detect { font-size: 11px; color: var(--muted); margin-bottom: 10px; }
93
  .quest-panel .qp-detect code { background: var(--panel-2); padding: 1px 5px; border-radius: 3px; font-size: 11px; }
94
 
95
  .qquest {
96
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
97
- padding: 10px 12px; margin-bottom: 8px;
98
- display: grid; grid-template-columns: 32px 1fr auto; gap: 12px; align-items: center;
99
  }
100
  .qquest .icon {
101
- width: 32px; height: 32px; border-radius: 6px;
102
- display: flex; align-items: center; justify-content: center;
103
- font-size: 16px;
104
  }
105
- .qquest .body .title { font-size: 13px; color: var(--text); font-weight: 500; }
106
- .qquest .body .desc { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
107
  .qquest .reward {
108
- display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
109
- font-size: 11px;
110
  }
111
- .qquest .xp { color: var(--accent); font-weight: 600; }
112
- .qquest .tier { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.4px; }
113
  .qquest .accept {
114
  background: var(--accent); color: white; border: 0; border-radius: 5px;
115
- padding: 4px 10px; font-size: 11px; font-weight: 500; cursor: pointer; margin-top: 4px;
116
  font-family: inherit;
117
  }
118
  .qquest .accept:hover { filter: brightness(1.15); }
 
 
119
 
120
- /* Repo picker */
121
- .controls {
122
- display: flex; align-items: center; gap: 12px; margin: 16px 0 20px;
123
- background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
124
- padding: 12px 16px;
 
125
  }
126
- .controls label { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; }
127
- .controls select {
128
- background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
129
- border-radius: 6px; padding: 6px 10px; font-size: 12px;
130
  }
131
- .controls button {
132
- background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
133
- border-radius: 6px; padding: 6px 12px; font-size: 12px; cursor: pointer;
134
  }
135
- .controls button.primary { background: var(--accent); border-color: var(--accent); color: white; font-weight: 500; }
136
- .controls .gap { flex: 1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- /* Below: full quest gallery + leaderboard */
139
  .grid2 { display: grid; grid-template-columns: 2fr 1fr; gap: 18px; margin-top: 22px; }
140
  @media (max-width: 880px) { .grid2 { grid-template-columns: 1fr; } }
141
- .card {
142
- background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px 20px;
143
- }
144
  .card h3 { margin: 0 0 10px; font-size: 14px; font-weight: 600; }
145
  .badges { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 10px; }
146
  .badge-tile {
@@ -149,7 +206,6 @@
149
  }
150
  .badge-tile:hover { border-color: var(--accent); }
151
  .badge-tile.locked { opacity: 0.4; filter: grayscale(0.7); }
152
- .badge-tile .sv { display: inline-block; }
153
  .badge-tile .name { font-size: 10px; color: var(--muted); margin-top: 6px; }
154
 
155
  .lb { font-size: 12px; }
@@ -160,10 +216,7 @@
160
  .lb-row .user.me { color: var(--accent); font-weight: 500; }
161
  .lb-row .val { color: var(--muted); font-family: var(--mono); font-size: 11px; }
162
 
163
- .install {
164
- margin-top: 18px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
165
- padding: 16px 20px;
166
- }
167
  .install pre {
168
  background: #000; border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px;
169
  font-family: var(--mono); font-size: 12px; color: #c9d1d9; overflow-x: auto; margin: 6px 0 0;
@@ -175,11 +228,10 @@
175
  <section class="hero">
176
  <h2>Quests appear when you start a Claude Code session.</h2>
177
  <p>
178
- <span class="badge">repo-aware</span>
179
- When you <code style="font-family:var(--mono);font-size:12px;background:var(--panel-2);padding:1px 6px;border-radius:3px;">cd</code>
180
- into a repo and start Claude Code, slyfox detects the repo, pulls the active maintainers'
181
- personas, and offers 3 quests tuned to that codebase. Complete one by hitting its emotional
182
- pattern in your real session — earn XP, unlock badges, climb the streak board.
183
  </p>
184
  </section>
185
 
@@ -187,10 +239,13 @@
187
  <label>repo
188
  <select id="repo-pick"></select>
189
  </label>
190
- <button id="btn-replay">⟲ replay session start</button>
191
  <div class="gap"></div>
192
- <span style="color: var(--muted); font-size: 12px;">XP: <b id="xp" style="color: var(--accent); font-weight: 600;">142</b></span>
193
- <span style="color: var(--muted); font-size: 12px;">🔥 <b id="streak" style="color: var(--warn); font-weight: 600;">7</b>-day streak</span>
 
 
 
194
  </div>
195
 
196
  <div class="term">
@@ -200,14 +255,21 @@
200
  <div style="text-align:right; opacity:0.65;">▾</div>
201
  </div>
202
  <div class="term-body" id="term-body"></div>
 
 
 
 
 
 
 
203
  </div>
204
 
205
  <div class="grid2">
206
  <div class="card">
207
  <h3>All quests in this repo</h3>
208
  <p style="color: var(--muted); font-size: 12px; margin: 0 0 12px;">
209
- The 3 shown above are picked from this pool by relevance to your recent emotional history and the
210
- repo's active maintainers. Re-roll daily.
211
  </p>
212
  <div class="badges" id="all-quests"></div>
213
  </div>
@@ -219,7 +281,7 @@
219
 
220
  <div class="install">
221
  <h3 style="margin: 0 0 4px; font-size: 14px;">Add to your terminal</h3>
222
- <p style="color: var(--muted); font-size: 13px; margin: 0;">Hook into Claude Code's <code style="font-family:var(--mono);background:var(--panel-2);padding:1px 5px;border-radius:3px;font-size:11px;">SessionStart</code> event.</p>
223
  <pre>$ hf extensions install slyfox-quests
224
  $ slyfox-quests enable
225
  # next time you `claude` in a repo, the panel auto-appears in the terminal</pre>
@@ -230,53 +292,297 @@ $ slyfox-quests enable
230
  "use strict";
231
 
232
  // ----------------------------------------------------------------------------
233
- // Repos and the quests slyfox would surface for each on session start.
234
  // ----------------------------------------------------------------------------
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  const REPOS = {
237
  "huggingface/transformers": {
238
  cwd: "~/Work/transformers",
239
  detected_lang: "python",
240
  active_maintainers: ["ArthurZ", "Lysandre", "sgugger"],
241
- quests: [
242
- { id:"calm-refactor", icon:"🌊", title:"The Calm Refactor", desc:"Keep this session in low-arousal positive valence for ≥70% of turns.", xp:15, tier:"common", color:"#3fb950" },
243
- { id:"like-arthur", icon:"🦊", title:"Be more like Arthur", desc:"Emotion trajectory must cosine-match ArthurZ ≥0.7 over 10+ turns.", xp:30, tier:"rare", color:"#79c0ff" },
244
- { id:"tokenizer", icon:"🧩", title:"Survive the Tokenizer", desc:"Recover from a high-arousal frustrated peak in <5 turns. Common on chat-template work.", xp:25, tier:"rare", color:"#d2a8ff" },
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  ],
246
  },
247
  "huggingface/huggingface_hub": {
248
  cwd: "~/Work/huggingface_hub",
249
  detected_lang: "python",
250
  active_maintainers: ["hanouticelina", "Wauplin", "julien-c"],
251
- quests: [
252
- { id:"curious-celina",icon:"🦊", title:"Be more like Celina", desc:"Stay in curious + focused emotions across 15 turns of API exploration.", xp:30, tier:"rare", color:"#d2a8ff" },
253
- { id:"calm-refactor", icon:"🌊", title:"The Calm Refactor", desc:"Low-arousal positive for ≥70% of turns. (Universal quest.)", xp:15, tier:"common", color:"#3fb950" },
254
- { id:"auth-spiral", icon:"🔐", title:"Untangle Auth", desc:"Resolve a 401/403 spiral without arousal going above 0.5.", xp:20, tier:"rare", color:"#ffa657" },
 
 
 
 
 
 
 
 
 
 
255
  ],
256
  },
257
  "huggingface/diffusers": {
258
  cwd: "~/Work/diffusers",
259
  detected_lang: "python",
260
  active_maintainers: ["sayakpaul", "pcuenca", "yiyixuxu"],
261
- quests: [
262
- { id:"sayak-flow", icon:"🦊", title:"Be more like Sayak", desc:"Maintain calm-focused trajectory across pipeline work, even when CUDA OOMs.", xp:30, tier:"rare", color:"#79c0ff" },
263
- { id:"unet-debug", icon:"🌀", title:"The UNet Detective", desc:"Recover from a 'wrong shape' frustration in ≤3 turns. Repo-classic.", xp:25, tier:"rare", color:"#d2a8ff" },
264
- { id:"calm-refactor", icon:"🌊", title:"The Calm Refactor", desc:"Low-arousal positive for ≥70% of turns. (Universal quest.)", xp:15, tier:"common", color:"#3fb950" },
 
 
 
 
 
 
 
 
 
265
  ],
266
  },
267
  "vllm-project/vllm": {
268
  cwd: "~/Work/vllm",
269
  detected_lang: "python/cuda",
270
  active_maintainers: ["WoosukKwon", "zhuohan123", "simon-mo"],
271
- quests: [
272
- { id:"kernel-zen", icon:"⚙️", title:"Kernel Zen", desc:"Debug a CUDA kernel for 20+ turns without dropping below 0 valence.", xp:35, tier:"epic", color:"#79c0ff" },
273
- { id:"survive-mp", icon:"🌪️", title:"Survive Multiproc", desc:"Recover from a hung multiprocessing spawn to working dispatch.", xp:25, tier:"rare", color:"#ffa657" },
274
- { id:"calm-refactor", icon:"🌊", title:"The Calm Refactor", desc:"Universal quest.", xp:15, tier:"common", color:"#3fb950" },
 
 
 
 
 
 
 
275
  ],
276
  },
277
  };
278
 
279
- // Universal badge pool (visible in the gallery for all repos)
280
  const BADGES = [
281
  { id:"pacific-coder", name:"Pacific Coder", tier:"common", unlocked:true, color:"#3fb950" },
282
  { id:"resilient", name:"Resilient", tier:"rare", unlocked:true, color:"#ffa657" },
@@ -303,70 +609,70 @@ const LEADERBOARD = [
303
  ];
304
 
305
  // ----------------------------------------------------------------------------
306
- // State
307
  // ----------------------------------------------------------------------------
308
 
309
- let state = { repo: "huggingface/transformers", timer: null, step: 0 };
 
 
 
 
 
 
 
 
310
 
311
  const $ = id => document.getElementById(id);
312
 
 
 
313
  function setRepoPicker() {
314
  const pick = $("repo-pick");
315
  pick.innerHTML = Object.keys(REPOS).map(r => `<option value="${r}"${r===state.repo?" selected":""}>${r}</option>`).join("");
316
  pick.addEventListener("change", e => { state.repo = e.target.value; restart(); });
317
  }
318
 
319
- function step(text, delay=400) {
320
- return { text, delay };
321
- }
322
-
323
- function buildSteps(repo) {
324
- const cfg = REPOS[repo];
325
- return [
326
- { type:"cmd", text:`$ cd ${cfg.cwd}`, delay:500 },
327
- { type:"cmd", text:`$ claude`, delay:600 },
328
- { type:"out-muted", text:`Claude Code (1M context) — type /help for commands.\nDetecting workspace…`, delay:800 },
329
- { type:"out-muted", text:`✓ workspace: ${repo} (${cfg.detected_lang})\n✓ slyfox mirror enabled · buddy ArthurZ`, delay:600 },
330
- { type:"out", text:`\n Loading repo-specific quests…`, delay:500 },
331
- { type:"quest-panel", repo, delay:200 },
332
- { type:"out-muted", text:`\nPick a quest with /quest <id>, or just start coding — slyfox tracks every turn.`, delay:200 },
333
- { type:"prompt", text:"> ", delay:0 },
334
- ];
335
- }
336
-
337
- function escapeHtml(s) {
338
- return String(s).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
339
- }
340
 
341
- function appendStep(s) {
342
  const body = $("term-body");
343
  const el = document.createElement("div");
344
  el.className = "step";
345
- if (s.type === "cmd") el.innerHTML = `<span class="cmd">${escapeHtml(s.text)}</span>`;
346
- else if (s.type === "out") el.innerHTML = `<span class="out">${escapeHtml(s.text)}</span>`;
347
- else if (s.type === "out-muted") el.innerHTML = `<span class="out muted">${escapeHtml(s.text)}</span>`;
348
- else if (s.type === "prompt") el.innerHTML = `<span class="cmd">${escapeHtml(s.text)}</span><span style="display:inline-block;width:8px;height:14px;background:var(--term-prompt);vertical-align:middle;animation:blink 1s steps(1) infinite;"></span>`;
349
- else if (s.type === "quest-panel") el.appendChild(renderQuestPanel(s.repo));
350
  body.appendChild(el);
351
  requestAnimationFrame(() => el.classList.add("shown"));
352
  body.scrollTop = body.scrollHeight;
353
  }
354
 
355
- function renderQuestPanel(repo) {
356
- const cfg = REPOS[repo];
357
- const root = document.createElement("div");
358
- root.className = "quest-panel";
359
- root.innerHTML = `
 
 
 
 
 
 
 
 
 
 
 
360
  <div class="qp-head">
361
  <div class="qp-title">🦊 slyfox quests · today</div>
362
  <div class="qp-meta">3 selected · re-rolls in 8h</div>
363
  </div>
364
  <div class="qp-detect">
365
- detected <code>${escapeHtml(repo)}</code> · active maintainers ${cfg.active_maintainers.map(m=>`<code>${escapeHtml(m)}</code>`).join(", ")}
366
  </div>
367
- ${cfg.quests.map(q => `
368
- <div class="qquest">
369
- <div class="icon" style="background: ${hexToRgba(q.color, 0.18)}; color: ${q.color};">${q.icon}</div>
 
370
  <div class="body">
371
  <div class="title">${escapeHtml(q.title)}</div>
372
  <div class="desc">${escapeHtml(q.desc)}</div>
@@ -374,101 +680,183 @@ function renderQuestPanel(repo) {
374
  <div class="reward">
375
  <span class="xp">+${q.xp} XP</span>
376
  <span class="tier">${escapeHtml(q.tier)}</span>
377
- <button class="accept" onclick="acceptQuest('${q.id}')">accept</button>
378
  </div>
379
- </div>
380
- `).join("")}
381
- `;
382
- return root;
383
  }
384
 
385
- window.acceptQuest = function(id) {
386
- $("xp").textContent = (parseInt($("xp").textContent, 10) + 5);
387
- const button = event.target;
388
- button.textContent = "accepted ✓";
389
- button.style.background = "var(--ok)";
390
- button.disabled = true;
391
- };
392
-
393
  function hexToRgba(hex, a) {
394
  const h = hex.replace("#","");
395
  return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`;
396
  }
397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  // ----------------------------------------------------------------------------
399
  // Playback
400
  // ----------------------------------------------------------------------------
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  function restart() {
403
  if (state.timer) { clearTimeout(state.timer); state.timer = null; }
 
 
 
 
 
404
  $("term-body").innerHTML = "";
 
 
 
 
 
405
  $("repo-label").textContent = REPOS[state.repo].cwd;
406
- state.step = 0;
407
- playNext(buildSteps(state.repo));
408
- }
409
-
410
- function playNext(steps) {
411
- if (state.step >= steps.length) return;
412
- const s = steps[state.step++];
413
- appendStep(s);
414
- const dur = (s.delay || 400);
415
- state.timer = setTimeout(() => playNext(steps), dur);
416
  }
417
 
418
  // ----------------------------------------------------------------------------
419
  // Gallery + leaderboard
420
  // ----------------------------------------------------------------------------
421
 
422
- function renderBadges() {
423
- const root = $("all-quests");
424
- root.innerHTML = BADGES.map(b => `
425
- <div class="badge-tile ${b.unlocked ? "" : "locked"}">
426
- <div class="sv">${badgeSvg(b.color, b.tier, b.unlocked)}</div>
427
- <div class="name">${escapeHtml(b.name)}</div>
428
- </div>
429
- `).join("");
430
- }
431
-
432
  function badgeSvg(color, tier, unlocked) {
433
  const ringColor = { common:"#6e7681", rare:"#79c0ff", epic:"#d2a8ff", legendary:"#febc2e", mythic:"#f85149" }[tier] || "#6e7681";
434
  const fill = unlocked ? color : "#2a3038";
 
435
  return `<svg width="64" height="64" viewBox="0 0 64 64">
436
- <defs>
437
- <linearGradient id="g-${color.slice(1)}-${tier}" x1="0" y1="0" x2="0" y2="1">
438
- <stop offset="0%" stop-color="${fill}" stop-opacity="0.95"/>
439
- <stop offset="100%" stop-color="${fill}" stop-opacity="0.55"/>
440
- </linearGradient>
441
- </defs>
442
- <path d="M32 4 L56 14 L56 34 C56 46 44 56 32 60 C20 56 8 46 8 34 L8 14 Z"
443
- fill="url(#g-${color.slice(1)}-${tier})" stroke="${ringColor}" stroke-width="2"/>
444
  <circle cx="32" cy="32" r="6" fill="${unlocked ? '#fff' : '#5a6168'}" opacity="${unlocked ? 1 : 0.5}"/>
445
  </svg>`;
446
  }
447
 
 
 
 
 
 
 
 
 
448
  function renderLeaderboard() {
449
- const root = $("leaderboard");
450
- root.innerHTML = LEADERBOARD.map(r => `
451
  <div class="lb-row">
452
  <span class="rank">${r.rank}</span>
453
  <span class="user${r.me ? " me" : ""}">${escapeHtml(r.user)}</span>
454
  <span class="val">${escapeHtml(r.val)}</span>
455
- </div>
456
- `).join("");
457
  }
458
 
459
- // ----------------------------------------------------------------------------
460
  // Boot
461
- // ----------------------------------------------------------------------------
462
-
463
  setRepoPicker();
464
  renderBadges();
465
  renderLeaderboard();
466
  restart();
467
  $("btn-replay").addEventListener("click", () => restart());
468
-
469
- const style = document.createElement("style");
470
- style.textContent = "@keyframes blink { 0%,50% { opacity: 1; } 51%,100% { opacity: 0; } }";
471
- document.head.appendChild(style);
472
  </script>
473
  </body>
474
  </html>
 
1
  <!doctype html>
2
+ <!-- slyfox · quests · session-start + interactive quest progress -->
3
  <html lang="en">
4
  <head>
5
  <meta charset="utf-8" />
6
  <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>Slyfox · Quests</title>
8
  <style>
9
  :root {
10
  --bg: #0d1117;
 
20
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
21
 
22
  --term-bg: #0a0d12;
23
+ --term-fg: #c9d1d9;
24
  --term-prompt: #79c0ff;
25
+ --term-assistant-tick: #ffa657;
26
  --term-muted: #6e7681;
27
  }
28
  * { box-sizing: border-box; }
 
31
  a:hover { text-decoration: underline; }
32
  button { font-family: inherit; }
33
 
 
 
 
 
 
 
 
 
34
  main { max-width: 1100px; margin: 0 auto; padding: 24px; }
35
 
36
  .hero {
37
  background: linear-gradient(180deg, rgba(63,185,80,0.05) 0%, transparent 100%);
38
+ border: 1px solid var(--border); border-radius: 10px; padding: 22px 24px; margin-bottom: 18px;
39
  }
40
+ .hero h2 { font-size: 20px; margin: 0 0 6px; font-weight: 600; }
41
  .hero p { color: var(--muted); font-size: 14px; line-height: 1.55; margin: 0; max-width: 780px; }
42
  .hero .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px;
43
  background: rgba(63,185,80,0.15); color: var(--ok); border: 1px solid rgba(63,185,80,0.35);
44
  margin-right: 8px; vertical-align: middle; }
45
 
46
+ /* Top controls */
47
+ .controls {
48
+ display: flex; align-items: center; gap: 12px; margin: 0 0 16px;
49
+ background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
50
+ padding: 10px 14px;
51
+ }
52
+ .controls label { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; }
53
+ .controls select {
54
+ background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
55
+ border-radius: 6px; padding: 5px 9px; font-size: 12px;
56
+ }
57
+ .controls button {
58
+ background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
59
+ border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;
60
+ }
61
+ .controls button.primary { background: var(--accent); border-color: var(--accent); color: white; font-weight: 500; }
62
+ .controls .gap { flex: 1; }
63
+ .controls .hud { display: flex; gap: 14px; }
64
+ .controls .hud span { font-size: 12px; color: var(--muted); }
65
+ .controls .hud b { color: var(--text); font-weight: 600; }
66
+
67
+ /* Faux terminal */
68
  .term {
69
  background: var(--term-bg); border: 1px solid var(--border); border-radius: 10px;
70
  overflow: hidden; box-shadow: 0 8px 30px rgba(0,0,0,0.4);
 
82
  .term-bar .title { text-align: center; }
83
  .term-bar .title b { color: var(--text); font-weight: 500; }
84
  .term-body {
85
+ padding: 16px 22px 22px; min-height: 460px; max-height: 580px; overflow: auto;
86
+ font-family: var(--mono); font-size: 12.5px; line-height: 1.65; color: var(--term-fg);
 
87
  }
88
+ .term-body::-webkit-scrollbar { width: 8px; }
89
+ .term-body::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
90
+
91
  .cmd { color: var(--term-prompt); }
92
+ .out { color: var(--term-fg); white-space: pre-wrap; }
93
  .out.muted { color: var(--term-muted); }
94
+ .step { opacity: 0; transform: translateY(3px); transition: opacity 0.2s, transform 0.2s; margin-bottom: 8px; }
95
  .step.shown { opacity: 1; transform: none; }
96
 
97
+ /* Inline quest panel (appears in the terminal after the session detects the repo) */
98
  .quest-panel {
99
+ margin-top: 12px;
100
+ background: rgba(13,17,23,0.92); border: 1px solid var(--accent); border-radius: 10px;
101
+ padding: 12px 14px;
102
+ box-shadow: 0 0 22px rgba(47,129,247,0.18);
103
  color: var(--text);
104
+ font-family: var(--mono); font-size: 12px;
105
  }
106
+ .quest-panel .qp-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
107
+ .quest-panel .qp-title { font-size: 12px; color: var(--accent); letter-spacing: 0.3px; }
108
  .quest-panel .qp-meta { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.4px; }
109
  .quest-panel .qp-detect { font-size: 11px; color: var(--muted); margin-bottom: 10px; }
110
  .quest-panel .qp-detect code { background: var(--panel-2); padding: 1px 5px; border-radius: 3px; font-size: 11px; }
111
 
112
  .qquest {
113
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
114
+ padding: 9px 11px; margin-bottom: 7px;
115
+ display: grid; grid-template-columns: 28px 1fr auto; gap: 10px; align-items: center;
116
  }
117
  .qquest .icon {
118
+ width: 28px; height: 28px; border-radius: 6px;
119
+ display: flex; align-items: center; justify-content: center; font-size: 14px;
 
120
  }
121
+ .qquest .body .title { font-size: 12.5px; color: var(--text); font-weight: 500; }
122
+ .qquest .body .desc { font-size: 11px; color: var(--muted); margin-top: 2px; }
123
  .qquest .reward {
124
+ display: flex; flex-direction: column; align-items: flex-end; gap: 2px; font-size: 11px;
 
125
  }
126
+ .qquest .xp { color: var(--accent); font-weight: 600; font-size: 11px; }
127
+ .qquest .tier { color: var(--muted); font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.4px; }
128
  .qquest .accept {
129
  background: var(--accent); color: white; border: 0; border-radius: 5px;
130
+ padding: 4px 10px; font-size: 11px; font-weight: 500; cursor: pointer; margin-top: 3px;
131
  font-family: inherit;
132
  }
133
  .qquest .accept:hover { filter: brightness(1.15); }
134
+ .qquest .accept.done { background: var(--ok); }
135
+ .qquest .accept[disabled] { cursor: default; opacity: 0.85; }
136
 
137
+ /* Coding bar at the bottom of the terminal — live quest progress */
138
+ .coding-bar {
139
+ border-top: 1px solid var(--border); background: rgba(10,13,18,0.97);
140
+ padding: 10px 16px 12px;
141
+ font-family: var(--mono); font-size: 11px; color: var(--muted);
142
+ display: none;
143
  }
144
+ .coding-bar.on { display: block; }
145
+ .coding-bar .cb-head {
146
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;
 
147
  }
148
+ .coding-bar .cb-label {
149
+ letter-spacing: 0.4px; text-transform: uppercase; font-size: 10px; opacity: 0.7;
150
+ display: inline-flex; align-items: center; gap: 5px;
151
  }
152
+ .coding-bar .cb-label .live {
153
+ width: 6px; height: 6px; border-radius: 50%; background: var(--ok);
154
+ animation: pulse 1.2s ease-in-out infinite;
155
+ }
156
+ .coding-bar .cb-meta { font-size: 10px; color: var(--muted); }
157
+ .coding-bar .cb-meta b { color: var(--text); font-weight: 500; }
158
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
159
+
160
+ .qs-row {
161
+ display: grid;
162
+ grid-template-columns: 20px 130px 1fr 80px 110px;
163
+ gap: 12px; align-items: center;
164
+ padding: 7px 10px; margin-bottom: 4px;
165
+ background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.05); border-radius: 6px;
166
+ }
167
+ .qs-row:last-child { margin-bottom: 0; }
168
+ .qs-row.complete { background: rgba(63,185,80,0.08); border-color: rgba(63,185,80,0.3); }
169
+ .qs-row.failed { background: rgba(248,81,73,0.08); border-color: rgba(248,81,73,0.3); }
170
+ .qs-row .qs-icon { font-size: 14px; text-align: center; }
171
+ .qs-row .qs-name { font-size: 11.5px; color: var(--text); font-weight: 500; }
172
+ .qs-row .qs-bar {
173
+ position: relative; height: 8px; background: #14181f; border-radius: 4px; overflow: visible;
174
+ }
175
+ .qs-row .qs-fill { height: 100%; background: var(--qs-color, var(--accent)); border-radius: 4px; transition: width 0.5s ease; }
176
+ .qs-row .qs-target {
177
+ position: absolute; top: -3px; width: 2px; height: 14px;
178
+ background: rgba(255,255,255,0.45); border-radius: 1px;
179
+ }
180
+ .qs-row .qs-progress { font-family: var(--mono); font-size: 11px; color: var(--text); text-align: right; }
181
+ .qs-row .qs-meta { font-size: 10.5px; color: var(--muted); text-align: right; }
182
+ .qs-row .qs-status {
183
+ font-size: 10px; color: var(--ok); text-transform: uppercase; letter-spacing: 0.4px;
184
+ }
185
+ .qs-row.failed .qs-status { color: var(--err); }
186
+
187
+ .reward-flash {
188
+ position: absolute; right: 16px; top: -10px;
189
+ background: var(--ok); color: white; padding: 4px 10px; border-radius: 6px;
190
+ font-size: 11px; font-weight: 600;
191
+ box-shadow: 0 4px 14px rgba(63,185,80,0.5);
192
+ animation: flash-in 0.4s ease, flash-out 0.4s ease 1.6s forwards;
193
+ }
194
+ @keyframes flash-in { from { opacity: 0; transform: translateY(-4px) scale(0.9); } to { opacity: 1; transform: none; } }
195
+ @keyframes flash-out { from { opacity: 1; } to { opacity: 0; transform: translateY(-6px); } }
196
 
197
+ /* Gallery + leaderboard below */
198
  .grid2 { display: grid; grid-template-columns: 2fr 1fr; gap: 18px; margin-top: 22px; }
199
  @media (max-width: 880px) { .grid2 { grid-template-columns: 1fr; } }
200
+ .card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px 20px; }
 
 
201
  .card h3 { margin: 0 0 10px; font-size: 14px; font-weight: 600; }
202
  .badges { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 10px; }
203
  .badge-tile {
 
206
  }
207
  .badge-tile:hover { border-color: var(--accent); }
208
  .badge-tile.locked { opacity: 0.4; filter: grayscale(0.7); }
 
209
  .badge-tile .name { font-size: 10px; color: var(--muted); margin-top: 6px; }
210
 
211
  .lb { font-size: 12px; }
 
216
  .lb-row .user.me { color: var(--accent); font-weight: 500; }
217
  .lb-row .val { color: var(--muted); font-family: var(--mono); font-size: 11px; }
218
 
219
+ .install { margin-top: 18px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px 20px; }
 
 
 
220
  .install pre {
221
  background: #000; border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px;
222
  font-family: var(--mono); font-size: 12px; color: #c9d1d9; overflow-x: auto; margin: 6px 0 0;
 
228
  <section class="hero">
229
  <h2>Quests appear when you start a Claude Code session.</h2>
230
  <p>
231
+ <span class="badge">repo-aware · live tracking</span>
232
+ Detect the repo, surface 3 quests for it, accept one — and watch slyfox track your
233
+ progress turn-by-turn inside the terminal, like a real game HUD. Complete the predicate,
234
+ earn the XP and the badge.
 
235
  </p>
236
  </section>
237
 
 
239
  <label>repo
240
  <select id="repo-pick"></select>
241
  </label>
242
+ <button id="btn-replay">⟲ replay session</button>
243
  <div class="gap"></div>
244
+ <div class="hud">
245
+ <span>XP <b id="xp">142</b></span>
246
+ <span>🔥 <b id="streak">7</b>-day streak</span>
247
+ <span>active quests <b id="active-count">0</b></span>
248
+ </div>
249
  </div>
250
 
251
  <div class="term">
 
255
  <div style="text-align:right; opacity:0.65;">▾</div>
256
  </div>
257
  <div class="term-body" id="term-body"></div>
258
+ <div class="coding-bar" id="coding-bar">
259
+ <div class="cb-head">
260
+ <span class="cb-label"><span class="live"></span>active quests · live tracking</span>
261
+ <span class="cb-meta">session · turn <b id="cb-turn">0</b> · dominant <b id="cb-dom">—</b></span>
262
+ </div>
263
+ <div id="qs-list"></div>
264
+ </div>
265
  </div>
266
 
267
  <div class="grid2">
268
  <div class="card">
269
  <h3>All quests in this repo</h3>
270
  <p style="color: var(--muted); font-size: 12px; margin: 0 0 12px;">
271
+ Daily quest set is sampled from this pool based on your recent emotional history and the
272
+ repo's active maintainers. Re-rolls daily.
273
  </p>
274
  <div class="badges" id="all-quests"></div>
275
  </div>
 
281
 
282
  <div class="install">
283
  <h3 style="margin: 0 0 4px; font-size: 14px;">Add to your terminal</h3>
284
+ <p style="color: var(--muted); font-size: 13px; margin: 0;">Hooks into Claude Code's <code style="font-family:var(--mono);background:var(--panel-2);padding:1px 5px;border-radius:3px;font-size:11px;">SessionStart</code> event.</p>
285
  <pre>$ hf extensions install slyfox-quests
286
  $ slyfox-quests enable
287
  # next time you `claude` in a repo, the panel auto-appears in the terminal</pre>
 
292
  "use strict";
293
 
294
  // ----------------------------------------------------------------------------
295
+ // Repos, quests, coding sessions
296
  // ----------------------------------------------------------------------------
297
 
298
+ // Each quest carries a `evaluate(turns)` returning { value, target, progress(0..1+), complete, failed, text }.
299
+ // Turns is the rolling list of {idx, role, valence, arousal, dominant} processed so far.
300
+
301
+ function ratioPositiveCalm(turns) {
302
+ const userTurns = turns.filter(t => t.role === "user");
303
+ if (!userTurns.length) return 0;
304
+ const calm = userTurns.filter(t => t.valence > 0 && t.arousal < 0).length;
305
+ return calm / userTurns.length;
306
+ }
307
+
308
+ function cosineToArthur(turns) {
309
+ // crude 2D "fingerprint" cosine using running mean (valence, arousal) vs Arthur's (-0.13, +0.32)
310
+ const userTurns = turns.filter(t => t.role === "user");
311
+ if (!userTurns.length) return 0;
312
+ const mv = userTurns.reduce((a,t) => a+t.valence, 0) / userTurns.length;
313
+ const ma = userTurns.reduce((a,t) => a+t.arousal, 0) / userTurns.length;
314
+ const av = -0.13, aa = 0.32;
315
+ const dot = mv*av + ma*aa;
316
+ const nm = Math.hypot(mv, ma);
317
+ const na = Math.hypot(av, aa);
318
+ if (nm < 1e-6 || na < 1e-6) return 0;
319
+ return dot / (nm * na);
320
+ }
321
+
322
+ function tokenizerSurvive(turns) {
323
+ // Did a high-arousal-negative spike occur? Then are we back to v > 0 within 5 turns?
324
+ let spikeAt = -1, recoveredAt = -1;
325
+ turns.forEach((t,i) => {
326
+ if (t.role !== "user") return;
327
+ if (spikeAt < 0 && t.arousal > 0.4 && t.valence < -0.3) spikeAt = i;
328
+ if (spikeAt >= 0 && recoveredAt < 0 && i > spikeAt && t.valence > 0.3) recoveredAt = i;
329
+ });
330
+ return { spikeAt, recoveredAt };
331
+ }
332
+
333
+ const QUEST_DEFS = {
334
+ "calm-refactor": {
335
+ icon: "🌊", title: "The Calm Refactor", xp: 15, tier: "common", color: "#3fb950",
336
+ desc: "Keep this session in low-arousal positive valence for >=70% of turns.",
337
+ evaluate(turns) {
338
+ const ratio = ratioPositiveCalm(turns);
339
+ const target = 0.70;
340
+ const userTurns = turns.filter(t => t.role === "user");
341
+ const calmCount = userTurns.filter(t => t.valence > 0 && t.arousal < 0).length;
342
+ return {
343
+ value: ratio,
344
+ target,
345
+ progress: ratio / target,
346
+ complete: userTurns.length >= 5 && ratio >= target,
347
+ text: `${(ratio*100).toFixed(0)}% calm`,
348
+ meta: `${calmCount}/${userTurns.length} user turns`,
349
+ targetLabel: `${(target*100).toFixed(0)}%`,
350
+ };
351
+ },
352
+ },
353
+ "like-arthur": {
354
+ icon: "🦊", title: "Be more like Arthur", xp: 30, tier: "rare", color: "#79c0ff",
355
+ desc: "Emotion trajectory must cosine-match ArthurZ >=0.7 over 5+ turns.",
356
+ evaluate(turns) {
357
+ const cos = cosineToArthur(turns);
358
+ const target = 0.70;
359
+ const userTurns = turns.filter(t => t.role === "user");
360
+ return {
361
+ value: cos,
362
+ target,
363
+ progress: Math.max(0, cos) / target,
364
+ complete: userTurns.length >= 5 && cos >= target,
365
+ text: `cos ${cos.toFixed(2)}`,
366
+ meta: `${userTurns.length} turns analyzed`,
367
+ targetLabel: `>=${target.toFixed(2)}`,
368
+ };
369
+ },
370
+ },
371
+ "tokenizer": {
372
+ icon: "🧩", title: "Survive the Tokenizer", xp: 25, tier: "rare", color: "#d2a8ff",
373
+ desc: "Recover from a high-arousal frustrated peak in <5 turns.",
374
+ evaluate(turns) {
375
+ const { spikeAt, recoveredAt } = tokenizerSurvive(turns);
376
+ if (spikeAt < 0) {
377
+ return { value: 0, target: 1, progress: 0, complete: false, text: "no spike yet", meta: "waiting…", targetLabel: "<=5 turns" };
378
+ }
379
+ if (recoveredAt < 0) {
380
+ const cur = turns.length - 1;
381
+ const elapsed = cur - spikeAt;
382
+ const fail = elapsed >= 5;
383
+ return {
384
+ value: elapsed, target: 5,
385
+ progress: elapsed / 5,
386
+ complete: false, failed: fail,
387
+ text: `recovering · ${elapsed}/5 turns`,
388
+ meta: `spiked at turn ${spikeAt}`,
389
+ targetLabel: "<=5",
390
+ };
391
+ }
392
+ const elapsed = recoveredAt - spikeAt;
393
+ return {
394
+ value: elapsed, target: 5,
395
+ progress: elapsed / 5,
396
+ complete: elapsed <= 5,
397
+ failed: elapsed > 5,
398
+ text: `recovered in ${elapsed}`,
399
+ meta: `spike@${spikeAt} → recovery@${recoveredAt}`,
400
+ targetLabel: "<=5",
401
+ };
402
+ },
403
+ },
404
+ "curious-celina": {
405
+ icon: "🦊", title: "Be more like Celina", xp: 30, tier: "rare", color: "#d2a8ff",
406
+ desc: "Stay in curious + focused emotions across 5+ turns.",
407
+ evaluate(turns) {
408
+ const userTurns = turns.filter(t => t.role === "user");
409
+ const ok = userTurns.filter(t => t.dominant === "curious" || t.dominant === "calm" || t.dominant === "confident").length;
410
+ const ratio = userTurns.length ? ok / userTurns.length : 0;
411
+ const target = 0.70;
412
+ return {
413
+ value: ratio, target,
414
+ progress: ratio / target,
415
+ complete: userTurns.length >= 5 && ratio >= target,
416
+ text: `${(ratio*100).toFixed(0)}% curious`,
417
+ meta: `${ok}/${userTurns.length}`,
418
+ targetLabel: `${(target*100).toFixed(0)}%`,
419
+ };
420
+ },
421
+ },
422
+ "auth-spiral": {
423
+ icon: "🔐", title: "Untangle Auth", xp: 20, tier: "rare", color: "#ffa657",
424
+ desc: "Resolve a 401/403 spiral without arousal going above 0.5.",
425
+ evaluate(turns) {
426
+ const userTurns = turns.filter(t => t.role === "user");
427
+ const breach = userTurns.some(t => t.arousal > 0.5);
428
+ return {
429
+ value: breach ? 0 : 1,
430
+ target: 1,
431
+ progress: breach ? 0 : (userTurns.length >= 5 ? 1 : userTurns.length / 5),
432
+ complete: !breach && userTurns.length >= 5,
433
+ failed: breach,
434
+ text: breach ? "arousal over 0.5" : "calm so far",
435
+ meta: `${userTurns.length} turns`,
436
+ targetLabel: "no spike",
437
+ };
438
+ },
439
+ },
440
+ "sayak-flow": {
441
+ icon: "🦊", title: "Be more like Sayak", xp: 30, tier: "rare", color: "#79c0ff",
442
+ desc: "Maintain calm-focused trajectory across pipeline work.",
443
+ evaluate(turns) {
444
+ const userTurns = turns.filter(t => t.role === "user");
445
+ const ok = userTurns.filter(t => t.valence > 0).length;
446
+ const ratio = userTurns.length ? ok / userTurns.length : 0;
447
+ const target = 0.70;
448
+ return {
449
+ value: ratio, target,
450
+ progress: ratio / target,
451
+ complete: userTurns.length >= 5 && ratio >= target,
452
+ text: `${(ratio*100).toFixed(0)}% positive`,
453
+ meta: `${ok}/${userTurns.length}`,
454
+ targetLabel: `${(target*100).toFixed(0)}%`,
455
+ };
456
+ },
457
+ },
458
+ "unet-debug": {
459
+ icon: "🌀", title: "The UNet Detective", xp: 25, tier: "rare", color: "#d2a8ff",
460
+ desc: "Recover from a 'wrong shape' frustration in <=3 turns.",
461
+ evaluate(turns) {
462
+ const { spikeAt, recoveredAt } = tokenizerSurvive(turns); // same shape
463
+ if (spikeAt < 0) return { value: 0, target: 1, progress: 0, complete: false, text: "no spike yet", meta: "waiting…", targetLabel: "<=3" };
464
+ const cur = turns.length - 1;
465
+ const elapsed = (recoveredAt < 0 ? cur : recoveredAt) - spikeAt;
466
+ const complete = recoveredAt >= 0 && elapsed <= 3;
467
+ const failed = recoveredAt < 0 && elapsed > 3;
468
+ return {
469
+ value: elapsed, target: 3,
470
+ progress: elapsed / 3,
471
+ complete, failed,
472
+ text: recoveredAt >= 0 ? `recovered in ${elapsed}` : `recovering ${elapsed}/3`,
473
+ meta: `spike@${spikeAt}`,
474
+ targetLabel: "<=3",
475
+ };
476
+ },
477
+ },
478
+ "kernel-zen": {
479
+ icon: "⚙️", title: "Kernel Zen", xp: 35, tier: "epic", color: "#79c0ff",
480
+ desc: "Debug a CUDA kernel for 5+ turns without dropping below 0 valence.",
481
+ evaluate(turns) {
482
+ const userTurns = turns.filter(t => t.role === "user");
483
+ const negs = userTurns.filter(t => t.valence < 0).length;
484
+ const target = 5;
485
+ const progress = Math.min(1, userTurns.length / target);
486
+ return {
487
+ value: userTurns.length, target,
488
+ progress, complete: negs === 0 && userTurns.length >= target,
489
+ failed: negs > 0,
490
+ text: negs > 0 ? `dropped at turn ${userTurns.findIndex(t => t.valence < 0)}` : `${userTurns.length}/${target} turns`,
491
+ meta: `${negs} negative turns`,
492
+ targetLabel: `${target}, no neg`,
493
+ };
494
+ },
495
+ },
496
+ "survive-mp": {
497
+ icon: "🌪️", title: "Survive Multiproc", xp: 25, tier: "rare", color: "#ffa657",
498
+ desc: "Recover from a hung multiprocessing spawn to working dispatch.",
499
+ evaluate: function (turns) { return QUEST_DEFS["tokenizer"].evaluate(turns); },
500
+ },
501
+ };
502
+
503
  const REPOS = {
504
  "huggingface/transformers": {
505
  cwd: "~/Work/transformers",
506
  detected_lang: "python",
507
  active_maintainers: ["ArthurZ", "Lysandre", "sgugger"],
508
+ quest_ids: ["calm-refactor", "like-arthur", "tokenizer"],
509
+ session: [
510
+ { role:"user", text:"let me clean up tokenization_gemma2.py before adding the test", valence:0.30, arousal:-0.20, dominant:"calm" },
511
+ { role:"assistant", lines:[{kind:"tool",name:"Read",arg:"src/transformers/models/gemma2/tokenization_gemma2.py"},{kind:"tool_out",text:"Read 312 lines"}], valence:0.25, arousal:-0.20, dominant:"calm" },
512
+ { role:"user", text:"ok pulling the duplicate BOS handling into a single helper", valence:0.35, arousal:-0.15, dominant:"calm" },
513
+ { role:"assistant", lines:[{kind:"tool",name:"Edit",arg:"tokenization_gemma2.py · _dedup_bos()"},{kind:"tool_out",text:"applied 1 edit (+12 lines)"}], valence:0.3, arousal:-0.15, dominant:"calm" },
514
+ { role:"user", text:"run the tokenizer tests", valence:0.40, arousal:-0.10, dominant:"calm" },
515
+ { role:"assistant", lines:[{kind:"tool",name:"Bash",arg:"pytest tests/models/gemma2/test_tokenization_gemma2.py -q"},{kind:"tool_out",text:"FAILED · expected 1 bos_token_id, got 2"}], valence:0.20, arousal:0.10, dominant:"confident" },
516
+ { role:"user", text:"damn this is annoying", valence:-0.50, arousal:0.60, dominant:"frustrated" },
517
+ { role:"assistant", lines:[{kind:"text",text:"That likely means `generation_config.bos_token_id` is also being prepended at generate-time."},{kind:"tool",name:"Read",arg:"google/gemma-2-2b-it/generation_config.json"},{kind:"tool_out",text:"\"bos_token_id\": 2"}], valence:0.0, arousal:0.20, dominant:"reflective" },
518
+ { role:"user", text:"ok try dropping the explicit BOS from input", valence:0.10, arousal:0.10, dominant:"confident" },
519
+ { role:"assistant", lines:[{kind:"tool",name:"Edit",arg:"tests/...test_tokenization_gemma2.py"},{kind:"tool_out",text:"applied 1 edit"}], valence:0.20, arousal:0.0, dominant:"calm" },
520
+ { role:"user", text:"green. tests pass now.", valence:0.55, arousal:-0.15, dominant:"calm" },
521
+ { role:"assistant", lines:[{kind:"text",text:"Nice. Want me to draft the changelog entry?"}], valence:0.40, arousal:-0.10, dominant:"calm" },
522
+ { role:"user", text:"yes please", valence:0.50, arousal:-0.10, dominant:"calm" },
523
+ { role:"assistant", lines:[{kind:"tool",name:"Edit",arg:"docs/source/en/changelog.md"},{kind:"tool_out",text:"applied 1 edit"}], valence:0.45, arousal:-0.10, dominant:"calm" },
524
+ { role:"user", text:"perfect, ready to push", valence:0.60, arousal:-0.10, dominant:"calm" },
525
  ],
526
  },
527
  "huggingface/huggingface_hub": {
528
  cwd: "~/Work/huggingface_hub",
529
  detected_lang: "python",
530
  active_maintainers: ["hanouticelina", "Wauplin", "julien-c"],
531
+ quest_ids: ["curious-celina", "calm-refactor", "auth-spiral"],
532
+ session: [
533
+ { role:"user", text:"what's the difference between upload_file and create_commit?", valence:0.30, arousal:-0.20, dominant:"curious" },
534
+ { role:"assistant", lines:[{kind:"tool",name:"Read",arg:"src/huggingface_hub/hf_api.py"},{kind:"tool_out",text:"Read 18,442 lines (grep skim)"},{kind:"text",text:"upload_file = one-shot, one file/one commit. create_commit = atomic bundle."}], valence:0.25, arousal:-0.20, dominant:"confident" },
535
+ { role:"user", text:"so upload_folder calls create_commit under the hood?", valence:0.40, arousal:-0.10, dominant:"curious" },
536
+ { role:"assistant", lines:[{kind:"text",text:"Yep, one CommitOperationAdd per file."}], valence:0.30, arousal:-0.20, dominant:"calm" },
537
+ { role:"user", text:"let me try uploading + downloading my dataset", valence:0.50, arousal:0.10, dominant:"curious" },
538
+ { role:"assistant", lines:[{kind:"tool",name:"Bash",arg:"python upload_and_check.py"},{kind:"tool_out",text:"401 Unauthorized on redownload"}], valence:0.20, arousal:0.20, dominant:"confident" },
539
+ { role:"user", text:"that's weird, the repo is public", valence:0.10, arousal:0.30, dominant:"curious" },
540
+ { role:"assistant", lines:[{kind:"text",text:"Happens when HF_TOKEN is set in env but expired. Try `hf auth whoami`."}], valence:0.10, arousal:0.0, dominant:"reflective" },
541
+ { role:"user", text:"ah token rotated last week. fixed.", valence:0.50, arousal:-0.10, dominant:"calm" },
542
+ { role:"assistant", lines:[{kind:"text",text:"Worth a friendlier error message in the SDK — want me to draft it?"}], valence:0.40, arousal:-0.10, dominant:"calm" },
543
+ { role:"user", text:"yes please", valence:0.55, arousal:-0.10, dominant:"calm" },
544
+ { role:"assistant", lines:[{kind:"tool",name:"Edit",arg:"src/huggingface_hub/utils/_http.py"},{kind:"tool_out",text:"applied 1 edit"}], valence:0.50, arousal:-0.10, dominant:"calm" },
545
  ],
546
  },
547
  "huggingface/diffusers": {
548
  cwd: "~/Work/diffusers",
549
  detected_lang: "python",
550
  active_maintainers: ["sayakpaul", "pcuenca", "yiyixuxu"],
551
+ quest_ids: ["sayak-flow", "unet-debug", "calm-refactor"],
552
+ session: [
553
+ { role:"user", text:"adding a new scheduler — let me start from EulerDiscreteScheduler", valence:0.40, arousal:-0.10, dominant:"calm" },
554
+ { role:"assistant", lines:[{kind:"tool",name:"Read",arg:"src/diffusers/schedulers/scheduling_euler_discrete.py"},{kind:"tool_out",text:"Read 458 lines"}], valence:0.35, arousal:-0.15, dominant:"calm" },
555
+ { role:"user", text:"copy the structure", valence:0.45, arousal:-0.10, dominant:"calm" },
556
+ { role:"assistant", lines:[{kind:"tool",name:"Write",arg:"src/diffusers/schedulers/scheduling_my_new.py"},{kind:"tool_out",text:"created (412 lines)"}], valence:0.40, arousal:-0.10, dominant:"calm" },
557
+ { role:"user", text:"now the UNet — shape mismatch on forward", valence:-0.40, arousal:0.50, dominant:"frustrated" },
558
+ { role:"assistant", lines:[{kind:"text",text:"What's the expected shape vs what you're getting?"}], valence:0.0, arousal:0.20, dominant:"reflective" },
559
+ { role:"user", text:"got [2, 4, 64, 64], expected [2, 4, 1, 64, 64]", valence:0.0, arousal:0.30, dominant:"confident" },
560
+ { role:"assistant", lines:[{kind:"text",text:"Missing time dim. Add `.unsqueeze(2)` before passing to UNet."}], valence:0.20, arousal:0.10, dominant:"confident" },
561
+ { role:"user", text:"fixed, that worked", valence:0.55, arousal:-0.10, dominant:"calm" },
562
+ { role:"assistant", lines:[{kind:"text",text:"Want me to add a shape assertion at the entry?"}], valence:0.40, arousal:-0.10, dominant:"calm" },
563
+ { role:"user", text:"yes that'd save future me", valence:0.60, arousal:-0.10, dominant:"calm" },
564
  ],
565
  },
566
  "vllm-project/vllm": {
567
  cwd: "~/Work/vllm",
568
  detected_lang: "python/cuda",
569
  active_maintainers: ["WoosukKwon", "zhuohan123", "simon-mo"],
570
+ quest_ids: ["kernel-zen", "survive-mp", "calm-refactor"],
571
+ session: [
572
+ { role:"user", text:"adding a new attention kernel for the new architecture", valence:0.30, arousal:0.10, dominant:"confident" },
573
+ { role:"assistant", lines:[{kind:"tool",name:"Read",arg:"csrc/attention/attention_kernels.cu"},{kind:"tool_out",text:"Read 1,204 lines"}], valence:0.25, arousal:0.10, dominant:"confident" },
574
+ { role:"user", text:"copying the existing flash kernel as a starting point", valence:0.40, arousal:-0.05, dominant:"calm" },
575
+ { role:"assistant", lines:[{kind:"tool",name:"Write",arg:"csrc/attention/new_arch_kernel.cu"},{kind:"tool_out",text:"created (842 lines)"}], valence:0.35, arousal:-0.10, dominant:"calm" },
576
+ { role:"user", text:"compiling…", valence:0.40, arousal:0.0, dominant:"calm" },
577
+ { role:"assistant", lines:[{kind:"tool",name:"Bash",arg:"python setup.py build_ext --inplace"},{kind:"tool_out",text:"compilation OK · 4.2s"}], valence:0.45, arousal:0.0, dominant:"calm" },
578
+ { role:"user", text:"running tests", valence:0.45, arousal:0.10, dominant:"confident" },
579
+ { role:"assistant", lines:[{kind:"tool",name:"Bash",arg:"pytest tests/kernels/test_new_arch.py -k attention"},{kind:"tool_out",text:"PASSED 18/18 in 1m12s"}], valence:0.60, arousal:-0.10, dominant:"calm" },
580
+ { role:"user", text:"ship it", valence:0.65, arousal:-0.10, dominant:"calm" },
581
  ],
582
  },
583
  };
584
 
585
+ // Universal badge pool (visible in the gallery)
586
  const BADGES = [
587
  { id:"pacific-coder", name:"Pacific Coder", tier:"common", unlocked:true, color:"#3fb950" },
588
  { id:"resilient", name:"Resilient", tier:"rare", unlocked:true, color:"#ffa657" },
 
609
  ];
610
 
611
  // ----------------------------------------------------------------------------
612
+ // State + helpers
613
  // ----------------------------------------------------------------------------
614
 
615
+ let state = {
616
+ repo: "huggingface/transformers",
617
+ phase: "intro", // intro → session-start lines → quest-panel → coding
618
+ step: 0, // step index into the intro/session lines
619
+ activeQuestIds: [], // ids of quests user has accepted
620
+ questResults: {}, // id → last evaluate() result
621
+ history: [], // rolling list of {idx, role, valence, arousal, dominant}
622
+ timer: null,
623
+ };
624
 
625
  const $ = id => document.getElementById(id);
626
 
627
+ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
628
+
629
  function setRepoPicker() {
630
  const pick = $("repo-pick");
631
  pick.innerHTML = Object.keys(REPOS).map(r => `<option value="${r}"${r===state.repo?" selected":""}>${r}</option>`).join("");
632
  pick.addEventListener("change", e => { state.repo = e.target.value; restart(); });
633
  }
634
 
635
+ // ----------------------------------------------------------------------------
636
+ // Render terminal lines
637
+ // ----------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
 
639
+ function appendStep(html) {
640
  const body = $("term-body");
641
  const el = document.createElement("div");
642
  el.className = "step";
643
+ el.innerHTML = html;
 
 
 
 
644
  body.appendChild(el);
645
  requestAnimationFrame(() => el.classList.add("shown"));
646
  body.scrollTop = body.scrollHeight;
647
  }
648
 
649
+ function introSteps() {
650
+ const cfg = REPOS[state.repo];
651
+ return [
652
+ { delay: 400, html: `<span class="cmd">$ cd ${escapeHtml(cfg.cwd)}</span>` },
653
+ { delay: 600, html: `<span class="cmd">$ claude</span>` },
654
+ { delay: 800, html: `<span class="out muted">Claude Code (1M context) — type /help for commands.\nDetecting workspace…</span>` },
655
+ { delay: 600, html: `<span class="out muted">✓ workspace: ${escapeHtml(state.repo)} (${escapeHtml(cfg.detected_lang)})\n✓ slyfox quests enabled · buddy ArthurZ</span>` },
656
+ { delay: 500, html: `<span class="out"> Loading repo-specific quests…</span>` },
657
+ { delay: 200, html: questPanelHtml() },
658
+ { delay: 600, html: `<span class="out muted">Accept a quest with the button — slyfox tracks every turn live.</span>` },
659
+ ];
660
+ }
661
+
662
+ function questPanelHtml() {
663
+ const cfg = REPOS[state.repo];
664
+ return `<div class="quest-panel">
665
  <div class="qp-head">
666
  <div class="qp-title">🦊 slyfox quests · today</div>
667
  <div class="qp-meta">3 selected · re-rolls in 8h</div>
668
  </div>
669
  <div class="qp-detect">
670
+ detected <code>${escapeHtml(state.repo)}</code> · active maintainers ${cfg.active_maintainers.map(m=>`<code>${escapeHtml(m)}</code>`).join(", ")}
671
  </div>
672
+ ${cfg.quest_ids.map(id => {
673
+ const q = QUEST_DEFS[id];
674
+ return `<div class="qquest">
675
+ <div class="icon" style="background:${hexToRgba(q.color, 0.18)}; color:${q.color};">${q.icon}</div>
676
  <div class="body">
677
  <div class="title">${escapeHtml(q.title)}</div>
678
  <div class="desc">${escapeHtml(q.desc)}</div>
 
680
  <div class="reward">
681
  <span class="xp">+${q.xp} XP</span>
682
  <span class="tier">${escapeHtml(q.tier)}</span>
683
+ <button class="accept" data-quest="${id}">accept</button>
684
  </div>
685
+ </div>`;
686
+ }).join("")}
687
+ </div>`;
 
688
  }
689
 
 
 
 
 
 
 
 
 
690
  function hexToRgba(hex, a) {
691
  const h = hex.replace("#","");
692
  return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`;
693
  }
694
 
695
+ // ----------------------------------------------------------------------------
696
+ // Coding session (after quest accepted)
697
+ // ----------------------------------------------------------------------------
698
+
699
+ function renderTurn(turn, idx) {
700
+ let html = "";
701
+ if (turn.role === "user") {
702
+ html = `<div class="out"><span class="cmd">&gt; </span>${escapeHtml(turn.text)}</div>`;
703
+ } else {
704
+ html = (turn.lines || []).map(l => {
705
+ if (l.kind === "text") return `<div class="out">${escapeHtml(l.text)}</div>`;
706
+ if (l.kind === "tool") return `<div class="out"><span style="color:var(--term-assistant-tick)">⏺</span> ${escapeHtml(l.name)}(<span style="color:#d2a8ff">${escapeHtml(l.arg)}</span>)</div>`;
707
+ if (l.kind === "tool_out") return `<div class="out muted"> ⎿ ${escapeHtml(l.text)}</div>`;
708
+ return "";
709
+ }).join("");
710
+ }
711
+ appendStep(html);
712
+ state.history.push({ idx, role: turn.role, valence: turn.valence ?? 0, arousal: turn.arousal ?? 0, dominant: turn.dominant ?? null });
713
+ $("cb-turn").textContent = String(idx + 1);
714
+ $("cb-dom").textContent = turn.dominant || "—";
715
+ updateActiveQuests();
716
+ }
717
+
718
+ // ----------------------------------------------------------------------------
719
+ // Quest evaluation + render
720
+ // ----------------------------------------------------------------------------
721
+
722
+ function acceptQuest(id, buttonEl) {
723
+ if (state.activeQuestIds.includes(id)) return;
724
+ state.activeQuestIds.push(id);
725
+ if (buttonEl) {
726
+ buttonEl.textContent = "accepted ✓";
727
+ buttonEl.classList.add("done");
728
+ buttonEl.disabled = true;
729
+ }
730
+ $("active-count").textContent = String(state.activeQuestIds.length);
731
+ $("coding-bar").classList.add("on");
732
+ renderActiveQuests();
733
+ // Kick off the coding phase if this is the first accept
734
+ if (state.phase !== "coding") { state.phase = "coding"; startCoding(); }
735
+ }
736
+
737
+ function renderActiveQuests() {
738
+ $("qs-list").innerHTML = state.activeQuestIds.map(id => {
739
+ const q = QUEST_DEFS[id];
740
+ const r = state.questResults[id] || { progress: 0, text: "—", meta: "", targetLabel: "" };
741
+ const pct = Math.min(100, Math.max(0, r.progress * 100));
742
+ const statusCls = r.complete ? "complete" : (r.failed ? "failed" : "");
743
+ const statusTxt = r.complete ? "✓ complete · +" + q.xp + " XP" : (r.failed ? "✗ failed" : "");
744
+ return `<div class="qs-row ${statusCls}" data-id="${id}" style="--qs-color:${q.color}">
745
+ <span class="qs-icon">${q.icon}</span>
746
+ <span class="qs-name">${escapeHtml(q.title)}</span>
747
+ <div class="qs-bar">
748
+ <div class="qs-fill" style="width:${pct.toFixed(0)}%"></div>
749
+ <div class="qs-target" style="left:100%"></div>
750
+ </div>
751
+ <span class="qs-progress">${escapeHtml(r.text)} ${statusTxt ? "" : `<span style="color:var(--muted)">/ ${escapeHtml(r.targetLabel || "")}</span>`}</span>
752
+ <span class="qs-meta">${escapeHtml(statusTxt || r.meta || "")}</span>
753
+ </div>`;
754
+ }).join("");
755
+ }
756
+
757
+ function updateActiveQuests() {
758
+ for (const id of state.activeQuestIds) {
759
+ const prev = state.questResults[id];
760
+ const next = QUEST_DEFS[id].evaluate(state.history);
761
+ state.questResults[id] = next;
762
+ if (next.complete && (!prev || !prev.complete)) {
763
+ // Reward flash + XP bump
764
+ const xp = parseInt($("xp").textContent.replace(/[^\d]/g, ""), 10) + QUEST_DEFS[id].xp;
765
+ $("xp").textContent = String(xp);
766
+ }
767
+ }
768
+ renderActiveQuests();
769
+ }
770
+
771
  // ----------------------------------------------------------------------------
772
  // Playback
773
  // ----------------------------------------------------------------------------
774
 
775
+ function playIntro() {
776
+ const steps = introSteps();
777
+ function step(i) {
778
+ if (i >= steps.length) { return; }
779
+ appendStep(steps[i].html);
780
+ // Hook up accept buttons when the quest-panel step is rendered
781
+ $("term-body").querySelectorAll(".accept[data-quest]").forEach(btn => {
782
+ if (btn._wired) return;
783
+ btn._wired = true;
784
+ btn.addEventListener("click", () => acceptQuest(btn.dataset.quest, btn));
785
+ });
786
+ state.timer = setTimeout(() => step(i + 1), steps[i].delay);
787
+ }
788
+ step(0);
789
+ }
790
+
791
+ function startCoding() {
792
+ const session = REPOS[state.repo].session;
793
+ let i = 0;
794
+ function tick() {
795
+ if (i >= session.length) return;
796
+ const t = session[i];
797
+ renderTurn(t, i);
798
+ i += 1;
799
+ state.timer = setTimeout(tick, 1500);
800
+ }
801
+ // small beat before kicking off the coding phase
802
+ setTimeout(tick, 800);
803
+ }
804
+
805
  function restart() {
806
  if (state.timer) { clearTimeout(state.timer); state.timer = null; }
807
+ state.phase = "intro";
808
+ state.step = 0;
809
+ state.activeQuestIds = [];
810
+ state.questResults = {};
811
+ state.history = [];
812
  $("term-body").innerHTML = "";
813
+ $("coding-bar").classList.remove("on");
814
+ $("qs-list").innerHTML = "";
815
+ $("active-count").textContent = "0";
816
+ $("cb-turn").textContent = "0";
817
+ $("cb-dom").textContent = "—";
818
  $("repo-label").textContent = REPOS[state.repo].cwd;
819
+ playIntro();
 
 
 
 
 
 
 
 
 
820
  }
821
 
822
  // ----------------------------------------------------------------------------
823
  // Gallery + leaderboard
824
  // ----------------------------------------------------------------------------
825
 
 
 
 
 
 
 
 
 
 
 
826
  function badgeSvg(color, tier, unlocked) {
827
  const ringColor = { common:"#6e7681", rare:"#79c0ff", epic:"#d2a8ff", legendary:"#febc2e", mythic:"#f85149" }[tier] || "#6e7681";
828
  const fill = unlocked ? color : "#2a3038";
829
+ const id = `g-${color.slice(1)}-${tier}-${Math.random().toString(36).slice(2,5)}`;
830
  return `<svg width="64" height="64" viewBox="0 0 64 64">
831
+ <defs><linearGradient id="${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="${fill}" stop-opacity="0.95"/><stop offset="100%" stop-color="${fill}" stop-opacity="0.55"/></linearGradient></defs>
832
+ <path d="M32 4 L56 14 L56 34 C56 46 44 56 32 60 C20 56 8 46 8 34 L8 14 Z" fill="url(#${id})" stroke="${ringColor}" stroke-width="2"/>
 
 
 
 
 
 
833
  <circle cx="32" cy="32" r="6" fill="${unlocked ? '#fff' : '#5a6168'}" opacity="${unlocked ? 1 : 0.5}"/>
834
  </svg>`;
835
  }
836
 
837
+ function renderBadges() {
838
+ $("all-quests").innerHTML = BADGES.map(b => `
839
+ <div class="badge-tile ${b.unlocked ? "" : "locked"}">
840
+ ${badgeSvg(b.color, b.tier, b.unlocked)}
841
+ <div class="name">${escapeHtml(b.name)}</div>
842
+ </div>`).join("");
843
+ }
844
+
845
  function renderLeaderboard() {
846
+ $("leaderboard").innerHTML = LEADERBOARD.map(r => `
 
847
  <div class="lb-row">
848
  <span class="rank">${r.rank}</span>
849
  <span class="user${r.me ? " me" : ""}">${escapeHtml(r.user)}</span>
850
  <span class="val">${escapeHtml(r.val)}</span>
851
+ </div>`).join("");
 
852
  }
853
 
 
854
  // Boot
 
 
855
  setRepoPicker();
856
  renderBadges();
857
  renderLeaderboard();
858
  restart();
859
  $("btn-replay").addEventListener("click", () => restart());
 
 
 
 
860
  </script>
861
  </body>
862
  </html>