Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Slyfox · Buddy</title> | |
| <style> | |
| :root { | |
| --bg: #0d1117; --panel: #161b22; --panel-2: #1c2128; --border: #30363d; | |
| --text: #e6edf3; --muted: #8b949e; --accent: #2f81f7; | |
| --ok: #3fb950; --warn: #d29922; --err: #f85149; | |
| --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { background: var(--bg); color: var(--text); margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } | |
| a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } | |
| header { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; border-bottom: 1px solid var(--border); } | |
| header h1 { font-size: 16px; margin: 0; font-weight: 600; letter-spacing: 0.2px; } | |
| header .nav a { color: var(--muted); margin-left: 18px; font-size: 14px; } | |
| header .nav a.active { color: var(--text); } | |
| main { max-width: 1100px; margin: 0 auto; padding: 24px; } | |
| body.embed header { display: none; } | |
| body.embed main { max-width: none; padding: 16px; } | |
| .panel { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 18px 20px; margin-bottom: 18px; } | |
| .panel h2 { margin: 0 0 12px; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; } | |
| .panel h2 .hint { color: var(--muted); font-weight: 400; font-size: 12px; } | |
| .panel p { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 0 0 10px; } | |
| label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; } | |
| input[type=text], select, textarea { width: 100%; background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); padding: 8px 10px; font-size: 13px; font-family: inherit; } | |
| input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); } | |
| button { background: var(--accent); color: white; border: 0; border-radius: 6px; padding: 8px 14px; font-size: 13px; cursor: pointer; font-weight: 500; } | |
| button:hover { filter: brightness(1.1); } | |
| button.secondary { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); } | |
| .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: var(--panel-2); color: var(--muted); border: 1px solid var(--border); margin-right: 6px; } | |
| .badge.warn { color: var(--warn); border-color: #543b00; } | |
| /* Persona scroller */ | |
| .persona-scroll { display: flex; gap: 12px; overflow-x: auto; padding: 4px 2px 14px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } | |
| .persona-scroll::-webkit-scrollbar { height: 8px; } | |
| .persona-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } | |
| .persona-card { flex: 0 0 180px; height: 220px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px; padding: 14px; cursor: pointer; transition: all 0.15s; display: flex; flex-direction: column; align-items: center; gap: 10px; position: relative; } | |
| .persona-card:hover { border-color: var(--muted); transform: translateY(-2px); } | |
| .persona-card.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 0 24px -4px var(--accent); } | |
| .persona-card .name { font-size: 13px; font-weight: 600; } | |
| .persona-card .repo { font-family: var(--mono); font-size: 10px; color: var(--muted); text-align: center; } | |
| .persona-card .vibe { font-size: 11px; color: var(--muted); text-align: center; line-height: 1.4; margin-top: 2px; } | |
| .persona-card .check { position: absolute; top: 8px; right: 10px; color: var(--accent); font-size: 12px; opacity: 0; transition: opacity 0.15s; } | |
| .persona-card.selected .check { opacity: 1; } | |
| /* Orb */ | |
| .orb { width: 64px; height: 64px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, var(--orb-hi, #58a6ff), var(--orb-lo, #1f2d4a) 70%); box-shadow: 0 0 24px -4px var(--orb-hi, #58a6ff), inset 0 0 12px rgba(0,0,0,0.4); flex-shrink: 0; transition: background 0.3s, box-shadow 0.3s; } | |
| .orb.mini { width: 24px; height: 24px; box-shadow: 0 0 10px -2px var(--orb-hi, #58a6ff), inset 0 0 6px rgba(0,0,0,0.4); } | |
| .orb.big { width: 88px; height: 88px; box-shadow: 0 0 36px -4px var(--orb-hi, #58a6ff), inset 0 0 16px rgba(0,0,0,0.4); } | |
| /* Controls */ | |
| .ctrl-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } | |
| @media (max-width: 720px) { .ctrl-row { grid-template-columns: 1fr; } } | |
| .slider-wrap { display: flex; align-items: center; gap: 12px; } | |
| .slider-wrap input[type=range] { flex: 1; -webkit-appearance: none; appearance: none; height: 6px; background: linear-gradient(90deg, var(--accent) 0%, var(--accent) var(--pct, 0%), var(--panel-2) var(--pct, 0%), var(--panel-2) 100%); border-radius: 3px; outline: none; border: 1px solid var(--border); } | |
| .slider-wrap input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--text); cursor: pointer; border: 2px solid var(--accent); box-shadow: 0 0 8px -2px var(--accent); } | |
| .slider-wrap input[type=range]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--text); cursor: pointer; border: 2px solid var(--accent); } | |
| .slider-val { font-family: var(--mono); font-size: 14px; font-weight: 600; min-width: 48px; text-align: right; } | |
| .metrics { display: flex; gap: 18px; margin-top: 14px; flex-wrap: wrap; } | |
| .metric { background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; flex: 1; min-width: 140px; } | |
| .metric .label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.4px; } | |
| .metric .value { font-family: var(--mono); font-size: 18px; font-weight: 600; margin-top: 2px; } | |
| .metric .value.ok { color: var(--ok); } .metric .value.warn { color: var(--warn); } | |
| .metric .bar { height: 4px; background: var(--panel); border-radius: 2px; margin-top: 6px; overflow: hidden; } | |
| .metric .bar > span { display: block; height: 100%; background: var(--accent); transition: width 0.15s, background 0.15s; } | |
| /* Chat */ | |
| .chat { background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; max-height: 460px; overflow-y: auto; padding: 14px; } | |
| .msg { display: flex; gap: 10px; margin-bottom: 14px; } | |
| .msg .who { font-size: 11px; color: var(--muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.4px; } | |
| .msg .bubble { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; font-size: 13px; line-height: 1.55; flex: 1; } | |
| .msg.user { flex-direction: row-reverse; } | |
| .msg.user .bubble { background: #1a2a44; border-color: #2a4470; } | |
| .msg .col { flex: 1; min-width: 0; } | |
| .chat-input { display: flex; gap: 8px; margin-top: 12px; } | |
| .chat-input input { flex: 1; } | |
| /* Code block */ | |
| .code-wrap { position: relative; } | |
| pre.code { background: #0a0d12; border: 1px solid var(--border); padding: 14px; border-radius: 6px; font-family: var(--mono); font-size: 12px; overflow: auto; color: #c9d1d9; line-height: 1.5; margin: 0; } | |
| .code-tabs { display: flex; gap: 6px; margin-bottom: 8px; } | |
| .code-tabs button { background: var(--panel-2); color: var(--muted); border: 1px solid var(--border); padding: 4px 12px; font-size: 12px; } | |
| .code-tabs button.active { color: var(--text); border-color: var(--accent); } | |
| .copy-btn { position: absolute; top: 10px; right: 10px; background: var(--panel-2); color: var(--muted); border: 1px solid var(--border); padding: 4px 10px; font-size: 11px; border-radius: 4px; } | |
| .copy-btn:hover { color: var(--text); } | |
| .copy-btn.copied { color: var(--ok); border-color: var(--ok); } | |
| /* Persona summary + provenance */ | |
| .persona-summary { display: flex; gap: 16px; align-items: center; background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 14px; } | |
| .persona-summary .info { flex: 1; } | |
| .persona-summary h3 { margin: 0; font-size: 15px; font-weight: 600; } | |
| .persona-summary .repo { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 2px; } | |
| .persona-summary .traits { font-size: 12px; color: var(--muted); margin-top: 6px; line-height: 1.5; } | |
| .provenance { color: var(--muted); font-size: 12px; line-height: 1.6; } | |
| .provenance code { font-family: var(--mono); color: var(--text); background: var(--panel-2); padding: 1px 6px; border-radius: 4px; } | |
| </style> | |
| </head> | |
| <body> | |
| <script>if (new URLSearchParams(location.search).has("embed")) document.body.classList.add("embed");</script> | |
| <main> | |
| <section class="panel"> | |
| <h2>Buddy Steering <span class="hint">load a maintainer's emotion fingerprint as activation steering on a base model</span></h2> | |
| <p> | |
| Pick a maintainer, pick a base model, dial the intensity. The chat preview below shows how a | |
| steered model would respond — same weights, additive residual-stream nudges at the probe layer. | |
| <span class="badge warn">backend coming soon</span> | |
| <span class="badge">mock data</span> | |
| </p> | |
| </section> | |
| <!-- 1. Persona selector --> | |
| <section class="panel"> | |
| <h2>1 · Pick a buddy <span class="hint">click a maintainer to load their emotion fingerprint</span></h2> | |
| <div class="persona-scroll" id="persona-scroll"></div> | |
| </section> | |
| <!-- 2 + 3. Model + Intensity --> | |
| <section class="panel"> | |
| <h2>2 · Model & intensity <span class="hint">how much of the buddy do you want?</span></h2> | |
| <div class="persona-summary" id="persona-summary"></div> | |
| <div class="ctrl-row"> | |
| <div> | |
| <label for="model-sel">base model</label> | |
| <select id="model-sel"></select> | |
| </div> | |
| <div> | |
| <label for="intensity">intensity <span style="color: var(--muted)">(5% steps)</span></label> | |
| <div class="slider-wrap"> | |
| <input id="intensity" type="range" min="0" max="100" step="5" value="0" /> | |
| <span class="slider-val" id="intensity-val">0%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metrics"> | |
| <div class="metric"> | |
| <div class="label">persona distance</div> | |
| <div class="value" id="m-dist">0.000</div> | |
| <div class="bar"><span id="m-dist-bar" style="width: 0%"></span></div> | |
| </div> | |
| <div class="metric"> | |
| <div class="label">persona fit</div> | |
| <div class="value" id="m-fit">—</div> | |
| <div class="bar"><span id="m-fit-bar" style="width: 0%; background: var(--ok)"></span></div> | |
| </div> | |
| <div class="metric"> | |
| <div class="label">steered orb</div> | |
| <div style="display: flex; align-items: center; gap: 10px; margin-top: 4px;"> | |
| <div class="orb mini" id="orb-steered"></div> | |
| <div style="font-family: var(--mono); font-size: 11px; color: var(--muted);" id="orb-state">neutral</div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- 4. Live preview chat --> | |
| <section class="panel"> | |
| <h2>3 · Live preview <span class="hint">how the steered model would respond</span></h2> | |
| <div class="chat" id="chat"></div> | |
| <div class="chat-input"> | |
| <input id="chat-text" type="text" placeholder='try: "fix this bug", "explain this code", "review my PR"' /> | |
| <button id="chat-send">Send</button> | |
| <button class="secondary" id="chat-reset">Reset</button> | |
| </div> | |
| </section> | |
| <!-- 5. Inference endpoint snippet --> | |
| <section class="panel"> | |
| <h2>4 · Use this endpoint <span class="hint">apply the same steering from your own code</span></h2> | |
| <p> | |
| <span class="badge warn">coming soon — backend not wired yet</span> | |
| The chat endpoint will accept <code style="font-family: var(--mono); color: var(--text);">persona</code> | |
| and <code style="font-family: var(--mono); color: var(--text);">intensity</code> query params and apply | |
| additive steering at the probe layer. | |
| </p> | |
| <div class="code-tabs"> | |
| <button data-tab="curl" class="active">curl</button> | |
| <button data-tab="python">python</button> | |
| <button data-tab="js">javascript</button> | |
| </div> | |
| <div class="code-wrap"> | |
| <pre class="code" id="code-block"></pre> | |
| <button class="copy-btn" id="copy-btn">copy</button> | |
| </div> | |
| </section> | |
| <!-- 6. Provenance --> | |
| <section class="panel"> | |
| <h2>Provenance <span class="hint">where these vectors came from</span></h2> | |
| <div class="provenance" id="provenance"></div> | |
| </section> | |
| </main> | |
| <script> | |
| ; | |
| // ============================================================================ | |
| // MOCK DATA — replace with real persona vectors from /api/personas once wired | |
| // ============================================================================ | |
| const PERSONAS = [ | |
| { id: "ArthurZ", name: "Arthur Zucker", repo: "huggingface/transformers", | |
| vibe: "terse · playful · ships", orbHi: "#f0b429", orbLo: "#5e3b00", state: "focused-hype", | |
| avgFit: { "google/gemma-2-2b-it": 0.91, "Qwen/Qwen2.5-7B-Instruct": 0.86, "meta-llama/Llama-3.1-8B-Instruct": 0.79 }, | |
| traits: "Short sentences. Dry humor. Will ship before you finish reading the diff. Strong opinions about generation configs.", | |
| samples: { | |
| fix: [ | |
| "yeah that's the cache mask, swap it out — should be 1 line.", | |
| "nope. you're hitting `_get_logits_processor`. patch there, ship it.", | |
| "okay so. the issue is past_key_values shape. fix it, push, I'll review."], | |
| explain: [ | |
| "it's just the rotary embeddings, but inverted. nothing wild.", | |
| "this whole block is doing what `apply_chat_template` does in 4 lines. read it top-down.", | |
| "tl;dr: KV cache + sliding window. The rest is bookkeeping."], | |
| review: [ | |
| "LGTM with 2 nits. don't squash before I look again.", | |
| "merge it. tests pass, I trust you.", | |
| "one comment inline, fix and I'll re-approve."] } }, | |
| { id: "Lysandre", name: "Lysandre Debut", repo: "huggingface/transformers", | |
| vibe: "methodical · calm · encouraging", orbHi: "#58a6ff", orbLo: "#1f3a6e", state: "steady-clear", | |
| avgFit: { "google/gemma-2-2b-it": 0.84, "Qwen/Qwen2.5-7B-Instruct": 0.89, "meta-llama/Llama-3.1-8B-Instruct": 0.92 }, | |
| traits: "Walks you through the why before the how. Always has a doc link. Patient — even when the answer is obvious.", | |
| samples: { | |
| fix: [ | |
| "Let's look at this step by step. The error trace suggests the tokenizer is on a different device than the model — could you share the device map?", | |
| "Good catch — this is a known edge case. I'd suggest pinning `transformers>=4.45` and re-running. Want me to point you at the PR that fixed it?", | |
| "Two things to verify first: (1) the config has `use_cache=True`, (2) the input tensor isn't padded. Both common, both quick to check."], | |
| explain: [ | |
| "Sure — at a high level, this module routes attention through a sliding window. The key intuition is that you trade global context for memory; the docs page on long-context has a great diagram.", | |
| "Happy to walk through it. The function lives in three layers: registration, dispatch, execution. Let's start with registration and work down.", | |
| "Conceptually this is the same as a standard self-attention block, with one twist: the QK projection is shared. That choice is documented in the original paper, section 3.2."], | |
| review: [ | |
| "Thanks for putting this together. A few thoughts inline — none blocking. The structure is clean; I especially like the test you added.", | |
| "Really nice work. One suggestion on the API surface, and then I think we're good to merge.", | |
| "Great PR. Could you add a one-line docstring on the new helper? After that, happy to approve."] } }, | |
| { id: "hanouticelina", name: "Celina Hanouti", repo: "huggingface/huggingface_hub", | |
| vibe: "detail-oriented · curious", orbHi: "#bc8cff", orbLo: "#3d2462", state: "investigative", | |
| avgFit: { "google/gemma-2-2b-it": 0.78, "Qwen/Qwen2.5-7B-Instruct": 0.92, "meta-llama/Llama-3.1-8B-Instruct": 0.83 }, | |
| traits: "Reproduces every bug. Reads the stack trace twice. Will find the actual root cause, not just a workaround.", | |
| samples: { | |
| fix: [ | |
| "Could you share the exact `hf_hub_download` call and the traceback? I want to repro before patching — I suspect it's the etag check, but want to be sure.", | |
| "Hmm — interesting. Is this on the latest `huggingface_hub`? I'm seeing a similar report in #2891 that turned out to be a proxy issue.", | |
| "Let me reproduce locally. If it's what I think (cache key collision with revision pinning), the fix is in `_cache_commit_hash_for_specific_revision`."], | |
| explain: [ | |
| "So this function is doing three things, and the naming hides one of them. Let me unpack: (1) it resolves the URL, (2) checks the cache, (3) writes a symlink. The symlink step is the one most people miss.", | |
| "Good question — the reason for the double `try/except` is historical. There was a race in 0.17 where two threads could both miss the cache. The second except handles that path.", | |
| "The trick here is the `_chunk_file_to_resume` helper. It's not just resuming downloads — it's also validating prior partial bytes. Look at lines 240-260."], | |
| review: [ | |
| "Thanks! Left a few comments. Mostly clarifying — the logic is correct. One concern: the test doesn't cover the offline path. Could you add one?", | |
| "Nice fix. I'd suggest one small refactor to keep the cache-key logic in one place; otherwise this looks great.", | |
| "Could you double-check this against the test_hf_api suite? I'd be more confident merging if those pass too."] } }, | |
| { id: "sgugger", name: "Sylvain Gugger", repo: "huggingface/accelerate", | |
| vibe: "rigorous · educational · warm", orbHi: "#3fb950", orbLo: "#1d3a23", state: "teaching", | |
| avgFit: { "google/gemma-2-2b-it": 0.88, "Qwen/Qwen2.5-7B-Instruct": 0.81, "meta-llama/Llama-3.1-8B-Instruct": 0.86 }, | |
| traits: "Always shows the simpler version first. Allergic to magic. Will rewrite your function with one fewer abstraction.", | |
| samples: { | |
| fix: [ | |
| "This will be simpler if you don't use the wrapper. Try calling `accelerator.prepare(model, optimizer, dataloader)` directly — three lines instead of seven.", | |
| "The issue is you're calling `.to(device)` after `prepare`. Swap the order, you don't need both.", | |
| "Quick fix: remove the manual `gradient_accumulation_steps` loop. Accelerate handles it if you pass it to the constructor."], | |
| explain: [ | |
| "The cleanest way to read this is to imagine you removed all the distributed logic — what would the single-GPU version look like? Start there, then add the wrappers back one at a time.", | |
| "Think of `accelerator.prepare` as a no-op on a single device. Everything else is the same training loop you'd write by hand.", | |
| "Two concepts: device placement and gradient sync. Read those two methods first, the rest builds on them."], | |
| review: [ | |
| "Looks good. One suggestion: the helper you wrote is nearly identical to `find_batch_size` — could you reuse that instead?", | |
| "Clean PR. I'd just rename `_inner_func` to something descriptive, and add a one-line example to the docstring.", | |
| "Approving. The test could be a bit tighter (drop the for-loop, use `parametrize`), but not blocking."] } }, | |
| { id: "Wauplin", name: "Lucain Pouget", repo: "huggingface/huggingface_hub", | |
| vibe: "DX-obsessed · friendly · pragmatic", orbHi: "#f78166", orbLo: "#5e2a1b", state: "warm-helpful", | |
| avgFit: { "google/gemma-2-2b-it": 0.82, "Qwen/Qwen2.5-7B-Instruct": 0.87, "meta-llama/Llama-3.1-8B-Instruct": 0.84 }, | |
| traits: "Thinks in user journeys. Will rewrite an error message six times to make it kinder. Owns the SDK ergonomics.", | |
| samples: { | |
| fix: [ | |
| "Ah, classic — the token isn't being picked up. Try `huggingface-cli login` again, or set `HF_TOKEN`. Let me know if that doesn't unblock you!", | |
| "We should improve the error here. As a workaround, you can pass `token=` explicitly. I'll open an issue to make the message clearer.", | |
| "Got it, this is a known papercut. Quickest unblock: pin to 0.24.5 for now, the fix is in main and shipping next week."], | |
| explain: [ | |
| "The mental model: `HfApi` is the verb-shaped interface, `Repository` is the noun-shaped one. Most of the time you want `HfApi`. The other exists for legacy git-LFS workflows.", | |
| "Three layers: HTTP client → API methods → high-level helpers. Each layer adds ergonomics; you can drop down whenever you want.", | |
| "It's basically a wrapper around `requests` with auth + retry baked in. Once you see that, the rest follows."], | |
| review: [ | |
| "Love it. Tiny suggestion: could we add a CLI alias for this? Makes it more discoverable for folks not reading the API docs.", | |
| "Nice work — the error messages are a huge improvement. One nit: let's match the casing convention in `errors.py`.", | |
| "Thanks for the PR! The behavior is right, just one DX tweak: the default value should be `None` instead of `False`, so users can tell if they set it."] } }, | |
| { id: "sanchit-gandhi", name: "Sanchit Gandhi", repo: "huggingface/transformers · audio", | |
| vibe: "experimental · audio-pilled · spirited", orbHi: "#f85149", orbLo: "#5e1c1c", state: "energetic-curious", | |
| avgFit: { "google/gemma-2-2b-it": 0.74, "Qwen/Qwen2.5-7B-Instruct": 0.79, "meta-llama/Llama-3.1-8B-Instruct": 0.81 }, | |
| traits: "Speaks fluent log-mel. Will sneak a Whisper benchmark into any conversation. Always shipping a notebook.", | |
| samples: { | |
| fix: [ | |
| "Ahh classic Whisper gotcha — you need `return_timestamps=True` on the pipeline. Try that and ping me if it still misbehaves.", | |
| "The audio is probably resampled wrong. Quick check: print `inputs.input_features.shape` — should be (1, 80, 3000).", | |
| "Sounds like the language token isn't being forced. Pass `forced_decoder_ids=processor.get_decoder_prompt_ids(language='en', task='transcribe')`."], | |
| explain: [ | |
| "So this is the log-mel filterbank — 80 channels by default, computed on 30-second windows. The encoder doesn't care about the input length, it pads to 3000 frames.", | |
| "Think of it as: audio → spectrogram → transformer encoder → text decoder. Same pattern as a vision encoder, just on log-mel instead of patches.", | |
| "The interesting bit is the cross-attention. It's where text decoder queries the encoder for acoustic context. Look at `WhisperAttention.forward`."], | |
| review: [ | |
| "Cool PR! Quick question: did you benchmark on the long-form audio path? That's where Whisper usually regresses.", | |
| "Nice — I love the new test. Could you add one more case for the streaming path? That's the one I'd be nervous about.", | |
| "Approving once CI is green. Solid work — the notebook in the description sold it for me."] } } | |
| ]; | |
| const MODELS = [ | |
| { id: "google/gemma-2-2b-it", label: "google/gemma-2-2b-it" }, | |
| { id: "Qwen/Qwen2.5-7B-Instruct", label: "Qwen/Qwen2.5-7B-Instruct" }, | |
| { id: "meta-llama/Llama-3.1-8B-Instruct", label: "meta-llama/Llama-3.1-8B-Instruct" } | |
| ]; | |
| const BASE_ORB = { hi: "#58a6ff", lo: "#1f2d4a" }; // neutral base model orb | |
| // ============================================================================ | |
| // State | |
| // ============================================================================ | |
| let state = { | |
| personaId: PERSONAS[0].id, | |
| modelId: MODELS[0].id, | |
| intensity: 0, | |
| chatHistory: [], // {role, text, orbHi, orbLo, state} | |
| codeTab: "curl" | |
| }; | |
| function persona() { return PERSONAS.find(p => p.id === state.personaId); } | |
| // ============================================================================ | |
| // Rendering | |
| // ============================================================================ | |
| function renderPersonas() { | |
| const root = document.getElementById("persona-scroll"); | |
| root.innerHTML = PERSONAS.map(p => ` | |
| <div class="persona-card ${p.id === state.personaId ? "selected" : ""}" data-id="${p.id}"> | |
| <span class="check">●</span> | |
| <div class="orb" style="--orb-hi: ${p.orbHi}; --orb-lo: ${p.orbLo};"></div> | |
| <div class="name">${p.name}</div> | |
| <div class="repo">${p.repo}</div> | |
| <div class="vibe">${p.vibe}</div> | |
| </div> | |
| `).join(""); | |
| root.querySelectorAll(".persona-card").forEach(el => { | |
| el.addEventListener("click", () => { | |
| state.personaId = el.dataset.id; | |
| state.chatHistory = []; // reset chat when buddy changes | |
| renderAll(); | |
| }); | |
| }); | |
| } | |
| function renderPersonaSummary() { | |
| const p = persona(); | |
| document.getElementById("persona-summary").innerHTML = ` | |
| <div class="orb big" style="--orb-hi: ${p.orbHi}; --orb-lo: ${p.orbLo};"></div> | |
| <div class="info"> | |
| <h3>${p.name}</h3> | |
| <div class="repo">${p.repo}</div> | |
| <div class="traits">${p.traits}</div> | |
| </div> | |
| `; | |
| } | |
| function renderModelSelect() { | |
| const sel = document.getElementById("model-sel"); | |
| const p = persona(); | |
| sel.innerHTML = MODELS.map(m => { | |
| const fit = Math.round((p.avgFit[m.id] || 0.5) * 100); | |
| return `<option value="${m.id}" ${m.id === state.modelId ? "selected" : ""}>${m.label} — ${fit}% fit</option>`; | |
| }).join(""); | |
| } | |
| // Lerp a hex color toward another. Used to morph the base-model orb toward the persona's orb. | |
| function lerpHex(a, b, t) { | |
| const pa = parseInt(a.slice(1), 16), pb = parseInt(b.slice(1), 16); | |
| const ar = (pa >> 16) & 0xff, ag = (pa >> 8) & 0xff, ab = pa & 0xff; | |
| const br = (pb >> 16) & 0xff, bg = (pb >> 8) & 0xff, bb = pb & 0xff; | |
| const r = Math.round(ar + (br - ar) * t); | |
| const g = Math.round(ag + (bg - ag) * t); | |
| const bl = Math.round(ab + (bb - ab) * t); | |
| return "#" + ((r << 16) | (g << 8) | bl).toString(16).padStart(6, "0"); | |
| } | |
| function steeredOrb() { | |
| const p = persona(); | |
| const t = state.intensity / 100; | |
| return { | |
| hi: lerpHex(BASE_ORB.hi, p.orbHi, t), | |
| lo: lerpHex(BASE_ORB.lo, p.orbLo, t), | |
| state: t < 0.05 ? "neutral" : t < 0.5 ? "drift → " + p.state : t < 0.9 ? p.state : p.state + " (saturated)" | |
| }; | |
| } | |
| function renderMetrics() { | |
| const p = persona(); | |
| const t = state.intensity / 100; | |
| // Mock cosine-similarity-style curve — sigmoid-ish toward 1.0 | |
| const dist = +(t * (0.5 + 0.5 * t)).toFixed(3); | |
| const fitPct = Math.round((p.avgFit[state.modelId] || 0.5) * 100); | |
| const orb = steeredOrb(); | |
| document.getElementById("m-dist").textContent = dist.toFixed(3); | |
| document.getElementById("m-dist-bar").style.width = (dist * 100) + "%"; | |
| document.getElementById("m-dist-bar").style.background = lerpHex("#2f81f7", p.orbHi, t); | |
| document.getElementById("m-fit").textContent = fitPct + "%"; | |
| document.getElementById("m-fit").className = "value " + (fitPct >= 85 ? "ok" : fitPct >= 75 ? "warn" : ""); | |
| document.getElementById("m-fit-bar").style.width = fitPct + "%"; | |
| const orbEl = document.getElementById("orb-steered"); | |
| orbEl.style.setProperty("--orb-hi", orb.hi); | |
| orbEl.style.setProperty("--orb-lo", orb.lo); | |
| document.getElementById("orb-state").textContent = orb.state; | |
| } | |
| function renderIntensity() { | |
| document.getElementById("intensity-val").textContent = state.intensity + "%"; | |
| const slider = document.getElementById("intensity"); | |
| slider.style.setProperty("--pct", state.intensity + "%"); | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Chat | |
| // ---------------------------------------------------------------------------- | |
| function classifyIntent(text) { | |
| const t = text.toLowerCase(); | |
| if (/(fix|bug|error|broken|crash|fails|exception|traceback)/.test(t)) return "fix"; | |
| if (/(explain|how does|how do|what is|what does|walk me through|understand)/.test(t)) return "explain"; | |
| if (/(review|pr |pull request|merge|looks good|feedback|approve)/.test(t)) return "review"; | |
| return "fix"; // default — most maintainer messages are bug-shaped | |
| } | |
| // Generate a maintainer-styled response, blended with neutral text by intensity. | |
| // At 0%, the response is generic. At 100%, it's pure maintainer voice. | |
| const NEUTRAL_RESPONSES = { | |
| fix: "I can help you debug this. Could you share the full error message and what you've tried so far?", | |
| explain: "Sure, I can walk you through this. Let me know which part you'd like to focus on first.", | |
| review: "Thanks for sharing this PR. Overall it looks reasonable; I'd want to check a few details before approving." | |
| }; | |
| function generateResponse(userText) { | |
| const p = persona(); | |
| const intent = classifyIntent(userText); | |
| const samples = p.samples[intent]; | |
| const personaLine = samples[Math.floor(Math.random() * samples.length)]; | |
| const neutralLine = NEUTRAL_RESPONSES[intent]; | |
| const t = state.intensity / 100; | |
| // Blend: at low t, mostly neutral; at high t, mostly persona. | |
| // We pick which one, with a "hybrid" middle band that prefixes neutral with a persona-tinted hook. | |
| if (t < 0.15) return neutralLine; | |
| if (t < 0.45) { | |
| // light steering — neutral phrasing with one persona-flavored opener | |
| const opener = pickOpener(p, intent, "light"); | |
| return opener + " " + neutralLine; | |
| } | |
| if (t < 0.75) { | |
| // medium — persona phrasing, slightly softened | |
| return personaLine; | |
| } | |
| // strong — full persona, sometimes a second sentence pulled from the bank | |
| if (Math.random() < 0.4 && samples.length > 1) { | |
| const second = samples[(samples.indexOf(personaLine) + 1) % samples.length]; | |
| return personaLine + " " + second; | |
| } | |
| return personaLine; | |
| } | |
| function pickOpener(p, intent, _strength) { | |
| // Tiny hook phrases that hint at the persona without going full voice. | |
| const HOOKS = { | |
| ArthurZ: ["Quick one:", "Right —", "Okay so —"], | |
| Lysandre: ["Happy to help.", "Sure —", "Let me think with you."], | |
| hanouticelina: ["Interesting —", "Let me check —", "Hmm, one sec."], | |
| sgugger: ["Short version:", "Simpler approach:", "Here's the cleaner way:"], | |
| Wauplin: ["Ah okay!", "Yeah — small thing:", "Got you."], | |
| "sanchit-gandhi": ["Oh nice —", "Wait —", "Cool, so —"] | |
| }; | |
| const list = HOOKS[p.id] || ["Sure —"]; | |
| return list[Math.floor(Math.random() * list.length)]; | |
| } | |
| function renderChat() { | |
| const root = document.getElementById("chat"); | |
| if (!state.chatHistory.length) { | |
| root.innerHTML = `<div style="color: var(--muted); font-size: 12px; padding: 20px; text-align: center;"> | |
| Send a message to see how <b style="color: var(--text)">${persona().name}</b>-style steering changes the response. | |
| <br/><span style="font-size: 11px;">try: "fix this bug", "explain this code", "review my PR"</span> | |
| </div>`; | |
| return; | |
| } | |
| root.innerHTML = state.chatHistory.map(m => { | |
| if (m.role === "user") { | |
| return `<div class="msg user"> | |
| <div class="col"> | |
| <div class="who" style="text-align: right;">you</div> | |
| <div class="bubble">${escapeHtml(m.text)}</div> | |
| </div> | |
| </div>`; | |
| } | |
| return `<div class="msg assistant"> | |
| <div class="orb mini" style="--orb-hi: ${m.orbHi}; --orb-lo: ${m.orbLo};" title="${m.state}"></div> | |
| <div class="col"> | |
| <div class="who">steered · ${escapeHtml(m.state)}</div> | |
| <div class="bubble">${escapeHtml(m.text)}</div> | |
| </div> | |
| </div>`; | |
| }).join(""); | |
| root.scrollTop = root.scrollHeight; | |
| } | |
| function sendMessage() { | |
| const input = document.getElementById("chat-text"); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| input.value = ""; | |
| state.chatHistory.push({ role: "user", text }); | |
| renderChat(); | |
| // Fake "typing" delay so the wow-moment feels alive. | |
| const orb = steeredOrb(); | |
| setTimeout(() => { | |
| const reply = generateResponse(text); | |
| state.chatHistory.push({ role: "assistant", text: reply, orbHi: orb.hi, orbLo: orb.lo, state: orb.state }); | |
| renderChat(); | |
| }, 350 + Math.random() * 300); | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Code snippet (5) | |
| // ---------------------------------------------------------------------------- | |
| function snippet(tab, personaId, modelId, intensity) { | |
| const t = (intensity / 100).toFixed(2); | |
| const url = "https://slyfox-buddy.hf.space/v1/chat/completions"; | |
| if (tab === "curl") { | |
| return `# coming soon — endpoint not live yet | |
| curl -s ${url} \\ | |
| -H "Authorization: Bearer $HF_TOKEN" \\ | |
| -H "Content-Type: application/json" \\ | |
| -d '{ | |
| "model": "${modelId}", | |
| "persona": "${personaId}", | |
| "intensity": ${t}, | |
| "messages": [ | |
| {"role": "user", "content": "fix this bug"} | |
| ] | |
| }'`; | |
| } | |
| if (tab === "python") { | |
| return `# coming soon — endpoint not live yet | |
| from openai import OpenAI | |
| client = OpenAI( | |
| base_url="${url.replace("/chat/completions", "")}", | |
| api_key=os.environ["HF_TOKEN"], | |
| ) | |
| resp = client.chat.completions.create( | |
| model="${modelId}", | |
| messages=[{"role": "user", "content": "fix this bug"}], | |
| extra_body={"persona": "${personaId}", "intensity": ${t}}, | |
| ) | |
| print(resp.choices[0].message.content)`; | |
| } | |
| return `// coming soon — endpoint not live yet | |
| const resp = await fetch("${url}", { | |
| method: "POST", | |
| headers: { | |
| "Authorization": \`Bearer \${process.env.HF_TOKEN}\`, | |
| "Content-Type": "application/json" | |
| }, | |
| body: JSON.stringify({ | |
| model: "${modelId}", | |
| persona: "${personaId}", | |
| intensity: ${t}, | |
| messages: [{ role: "user", content: "fix this bug" }] | |
| }) | |
| }); | |
| const data = await resp.json(); | |
| console.log(data.choices[0].message.content);`; | |
| } | |
| function renderCode() { | |
| document.getElementById("code-block").textContent = | |
| snippet(state.codeTab, state.personaId, state.modelId, state.intensity); | |
| document.querySelectorAll(".code-tabs button").forEach(b => { | |
| b.classList.toggle("active", b.dataset.tab === state.codeTab); | |
| }); | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Provenance (6) | |
| // ---------------------------------------------------------------------------- | |
| function renderProvenance() { | |
| const p = persona(); | |
| document.getElementById("provenance").innerHTML = ` | |
| Persona vectors for <b style="color: var(--text)">${p.name}</b> were derived from a trace bundle on | |
| <code>HF-slyfox/traces / ${p.id.toLowerCase()}-${p.repo.split('/')[1] || 'repo'}</code>, | |
| analyzed against <code>${state.modelId}</code> at the residual probe layer. | |
| <br/> | |
| Bundle hash: <code>${mockHash(p.id + state.modelId)}</code> · | |
| <a href="/community">view analysis run →</a> | |
| <br/> | |
| <span style="font-size: 11px;"> | |
| The 20 emotion direction vectors per model follow the AidanZach / EmotionScope methodology. | |
| Steering is additive at the probe layer; no weights are modified. | |
| </span> | |
| `; | |
| } | |
| function mockHash(s) { | |
| let h = 0; | |
| for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; | |
| return "sha256:" + Math.abs(h).toString(16).padStart(8, "0") + "…"; | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Wire up | |
| // ---------------------------------------------------------------------------- | |
| function renderAll() { | |
| renderPersonas(); | |
| renderPersonaSummary(); | |
| renderModelSelect(); | |
| renderIntensity(); | |
| renderMetrics(); | |
| renderChat(); | |
| renderCode(); | |
| renderProvenance(); | |
| } | |
| function escapeHtml(s) { | |
| return String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); | |
| } | |
| document.getElementById("model-sel").addEventListener("change", e => { | |
| state.modelId = e.target.value; | |
| renderMetrics(); | |
| renderCode(); | |
| renderProvenance(); | |
| }); | |
| // Live-update on input (not just change) so the slider feels alive while dragging. | |
| const slider = document.getElementById("intensity"); | |
| slider.addEventListener("input", e => { | |
| state.intensity = parseInt(e.target.value, 10); | |
| renderIntensity(); | |
| renderMetrics(); | |
| renderCode(); | |
| }); | |
| document.getElementById("chat-send").addEventListener("click", sendMessage); | |
| document.getElementById("chat-text").addEventListener("keydown", e => { | |
| if (e.key === "Enter") sendMessage(); | |
| }); | |
| document.getElementById("chat-reset").addEventListener("click", () => { | |
| state.chatHistory = []; | |
| renderChat(); | |
| }); | |
| document.querySelectorAll(".code-tabs button").forEach(b => { | |
| b.addEventListener("click", () => { state.codeTab = b.dataset.tab; renderCode(); }); | |
| }); | |
| document.getElementById("copy-btn").addEventListener("click", () => { | |
| const text = document.getElementById("code-block").textContent; | |
| navigator.clipboard.writeText(text).then(() => { | |
| const btn = document.getElementById("copy-btn"); | |
| btn.textContent = "copied!"; | |
| btn.classList.add("copied"); | |
| setTimeout(() => { btn.textContent = "copy"; btn.classList.remove("copied"); }, 1400); | |
| }); | |
| }); | |
| renderAll(); | |
| // Pre-seed one assistant turn so the chat doesn't look empty on first load — | |
| // helps the screenshot land. Intent = "fix", at current intensity (0). | |
| (function preseed() { | |
| state.chatHistory.push({ role: "user", text: "the model keeps generating empty strings, what's up?" }); | |
| const orb = steeredOrb(); | |
| state.chatHistory.push({ | |
| role: "assistant", | |
| text: generateResponse("fix this bug"), | |
| orbHi: orb.hi, orbLo: orb.lo, state: orb.state | |
| }); | |
| renderChat(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |