Spaces:
Sleeping
Sleeping
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)
- mirror.html +266 -159
- 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: #
|
| 24 |
--term-prompt: #79c0ff;
|
| 25 |
-
--term-assistant: #
|
| 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
|
| 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
|
| 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:
|
| 82 |
-
font-family: var(--mono);
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
.term-body::-webkit-scrollbar { width: 8px; }
|
| 85 |
.term-body::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
|
| 86 |
|
| 87 |
-
.turn { margin-bottom:
|
| 88 |
.turn.shown { opacity: 1; transform: none; }
|
| 89 |
-
.turn .
|
| 90 |
-
.turn.user .body { color: var(--
|
| 91 |
-
.turn.assistant .body { color: var(--term-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
.turn .annot {
|
| 93 |
-
display: block; margin-
|
| 94 |
-
color: var(--annot); font-size:
|
| 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.
|
| 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:
|
| 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
|
| 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:
|
| 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.
|
| 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
|
| 132 |
.ai-bar {
|
| 133 |
-
border-top: 1px solid var(--border); background: rgba(10,13,18,0.
|
| 134 |
-
padding:
|
| 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
|
| 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">
|
| 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="
|
| 297 |
-
<option value="
|
| 298 |
-
<option value="
|
| 299 |
-
<option value="
|
| 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
|
| 330 |
-
Spike on
|
| 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 |
-
|
| 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?",
|
|
|
|
| 399 |
emotions: mkEmos([["curious",0.061],["confident",0.052],["calm",0.044],["afraid",0.038]]) },
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
emotions: mkEmos([["confident",0.064],["calm",0.058],["curious",0.052]]) },
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
emotions: mkEmos([["
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
emotions: mkEmos([["angry",0.082],["hostile",0.071],["frustrated",0.066],["afraid",0.059]]) },
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
emotions: mkEmos([["confident",0.061],["calm",0.049],["reflective",0.041]]) },
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
emotions: mkEmos([["
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
emotions: mkEmos([["afraid",0.094],["angry",0.072],["hostile",0.066],["frustrated",0.059]]) },
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
emotions: mkEmos([["confident",0.077],["calm",0.064],["proud",0.040],["happy",0.038]]) },
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
emotions: mkEmos([["confident",0.060],["calm",0.050]]) },
|
| 422 |
-
|
|
|
|
|
|
|
| 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?",
|
|
|
|
| 427 |
emotions: mkEmos([["curious",0.072],["calm",0.055],["confident",0.048]]) },
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
emotions: mkEmos([["confident",0.062],["calm",0.054]]) },
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
emotions: mkEmos([["happy",0.054],["confident",0.049]]) },
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
emotions: mkEmos([["confident",0.058]]) },
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
| 443 |
emotions: mkEmos([["afraid",0.064],["frustrated",0.054],["nervous",0.048]]) },
|
| 444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
emotions: mkEmos([["reflective",0.054],["confident",0.048]]) },
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
|
|
|
| 450 |
],
|
|
|
|
| 451 |
"argilla-io/distilabel": [
|
| 452 |
-
{ role:"user", text:"my distilabel pipeline keeps OOM-ing on the generation step",
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
{ role:"
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
{ role:"user", text:"
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 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 |
-
|
| 598 |
-
|
| 599 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[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 => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[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(/("[^&]*?")/g, '<span class="s">$1</span>')
|
| 597 |
+
.replace(/(?<![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
|
| 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: #
|
| 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:
|
| 45 |
}
|
| 46 |
-
.hero h2 { font-size: 20px; margin: 0 0 6px; font-weight: 600;
|
| 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 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 71 |
-
|
| 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);
|
| 76 |
.out.muted { color: var(--term-muted); }
|
| 77 |
-
.step { opacity: 0; transform: translateY(
|
| 78 |
.step.shown { opacity: 1; transform: none; }
|
| 79 |
|
| 80 |
-
/*
|
| 81 |
.quest-panel {
|
| 82 |
-
margin-top:
|
| 83 |
-
background: rgba(13,17,23,0.
|
| 84 |
-
padding:
|
| 85 |
-
box-shadow: 0 0
|
| 86 |
color: var(--text);
|
| 87 |
-
font-family: var(--mono); font-size:
|
| 88 |
}
|
| 89 |
-
.quest-panel .qp-head { display: flex; justify-content: space-between; align-items: center; margin-bottom:
|
| 90 |
-
.quest-panel .qp-title { font-size:
|
| 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:
|
| 98 |
-
display: grid; grid-template-columns:
|
| 99 |
}
|
| 100 |
.qquest .icon {
|
| 101 |
-
width:
|
| 102 |
-
display: flex; align-items: center; justify-content: center;
|
| 103 |
-
font-size: 16px;
|
| 104 |
}
|
| 105 |
-
.qquest .body .title { font-size:
|
| 106 |
-
.qquest .body .desc { font-size:
|
| 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:
|
| 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:
|
| 116 |
font-family: inherit;
|
| 117 |
}
|
| 118 |
.qquest .accept:hover { filter: brightness(1.15); }
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
/*
|
| 121 |
-
.
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
| 125 |
}
|
| 126 |
-
.
|
| 127 |
-
.
|
| 128 |
-
|
| 129 |
-
border-radius: 6px; padding: 6px 10px; font-size: 12px;
|
| 130 |
}
|
| 131 |
-
.
|
| 132 |
-
|
| 133 |
-
|
| 134 |
}
|
| 135 |
-
.
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
/*
|
| 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 |
-
|
| 180 |
-
|
| 181 |
-
|
| 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
|
| 191 |
<div class="gap"></div>
|
| 192 |
-
<
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 210 |
-
repo's active maintainers. Re-
|
| 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;">
|
| 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
|
| 234 |
// ----------------------------------------------------------------------------
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
const REPOS = {
|
| 237 |
"huggingface/transformers": {
|
| 238 |
cwd: "~/Work/transformers",
|
| 239 |
detected_lang: "python",
|
| 240 |
active_maintainers: ["ArthurZ", "Lysandre", "sgugger"],
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
{
|
| 244 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
],
|
| 246 |
},
|
| 247 |
"huggingface/huggingface_hub": {
|
| 248 |
cwd: "~/Work/huggingface_hub",
|
| 249 |
detected_lang: "python",
|
| 250 |
active_maintainers: ["hanouticelina", "Wauplin", "julien-c"],
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
{
|
| 254 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
],
|
| 256 |
},
|
| 257 |
"huggingface/diffusers": {
|
| 258 |
cwd: "~/Work/diffusers",
|
| 259 |
detected_lang: "python",
|
| 260 |
active_maintainers: ["sayakpaul", "pcuenca", "yiyixuxu"],
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
{
|
| 264 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
],
|
| 266 |
},
|
| 267 |
"vllm-project/vllm": {
|
| 268 |
cwd: "~/Work/vllm",
|
| 269 |
detected_lang: "python/cuda",
|
| 270 |
active_maintainers: ["WoosukKwon", "zhuohan123", "simon-mo"],
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
{
|
| 274 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
],
|
| 276 |
},
|
| 277 |
};
|
| 278 |
|
| 279 |
-
// Universal badge pool (visible in the gallery
|
| 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 = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 320 |
-
|
| 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 => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
| 339 |
-
}
|
| 340 |
|
| 341 |
-
function appendStep(
|
| 342 |
const body = $("term-body");
|
| 343 |
const el = document.createElement("div");
|
| 344 |
el.className = "step";
|
| 345 |
-
|
| 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
|
| 356 |
-
const cfg = REPOS[repo];
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 368 |
-
|
| 369 |
-
|
|
|
|
| 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"
|
| 378 |
</div>
|
| 379 |
-
</div>
|
| 380 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[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">> </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>
|