Spaces:
Running
Sandbox card layout, sticky-nav breadcrumb, drop §2/§3 score buttons
Browse files- Sandbox: refactor into stacked cards (sb-card with eyebrow/title/hint
headers, Input → Output) plus a Connected-to strip up top, replacing
the flat wall of controls.
- Sticky nav: add a CARBON / huggingfacebio-carbon-3b breadcrumb on the
left of the sticky strip so the "you're on the Carbon page" cue stays
visible after the hero scrolls out.
- §2 (VEP) and §3 (Track): drop the manual ▶ score / score all buttons.
§3 tracks are precomputed (logprobs ship in data/genes.json), so the
toolbar is just a gene selector now; §2 will auto-score on select.
- Misc: app.py, precompute.py, controls.css, layout.css, section-tree.css,
banner.js and data/genes.json picked up along the way.
- Add social-banner.html.
Co-authored-by: Cursor <cursoragent@cursor.com>
- app.py +9 -0
- assets/js/banner.js +11 -11
- assets/js/sections/sandbox.js +6 -1
- assets/js/sections/track.js +10 -52
- assets/js/sections/vep.js +8 -79
- assets/styles/banner.css +75 -12
- assets/styles/controls.css +8 -4
- assets/styles/layout.css +6 -4
- assets/styles/sandbox.css +97 -14
- assets/styles/section-tree.css +5 -11
- data/genes.json +0 -0
- demo.html +110 -75
- scripts/precompute.py +10 -5
- social-banner.html +299 -0
|
@@ -146,6 +146,15 @@ def sandbox_only():
|
|
| 146 |
return FileResponse(os.path.join(HERE, "index.html"))
|
| 147 |
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
@app.get("/config")
|
| 150 |
def config():
|
| 151 |
return {"model": MODEL_NAME, "endpoint": ENDPOINT_URL}
|
|
|
|
| 146 |
return FileResponse(os.path.join(HERE, "index.html"))
|
| 147 |
|
| 148 |
|
| 149 |
+
@app.get("/social-banner")
|
| 150 |
+
def social_banner():
|
| 151 |
+
# Standalone hero — wordmark + subtitle + specs + animated DNA helix,
|
| 152 |
+
# sized to fit common social-media canvases (Twitter / OG / LinkedIn /
|
| 153 |
+
# HF). Used to grab cover-art screenshots without firing up the full
|
| 154 |
+
# demo page.
|
| 155 |
+
return FileResponse(os.path.join(HERE, "social-banner.html"))
|
| 156 |
+
|
| 157 |
+
|
| 158 |
@app.get("/config")
|
| 159 |
def config():
|
| 160 |
return {"model": MODEL_NAME, "endpoint": ENDPOINT_URL}
|
|
@@ -27,7 +27,7 @@
|
|
| 27 |
// Numbers tuned for a hero that *dominates* the right half of the banner:
|
| 28 |
// big amplitude, thick ribbons, oversized ATCG glyphs.
|
| 29 |
const helix = {
|
| 30 |
-
startY: 0, endY: 1100, centerX: 220, amplitude:
|
| 31 |
cycles: 4.0, speed: 0.00015,
|
| 32 |
rungCount: 26,
|
| 33 |
// Dense sampling: each strand section is rendered as a continuous
|
|
@@ -37,14 +37,14 @@
|
|
| 37 |
// 4 fills + 8 stroke calls per frame total (one fill per back/front
|
| 38 |
// section per strand, plus the 4 edge polylines).
|
| 39 |
segmentCount: 512,
|
| 40 |
-
// Strand half-thickness in viewBox units. Trimmed 14 → 11 so the
|
| 41 |
-
// reads slimmer/more technical and the saturated green edge gets
|
| 42 |
-
// the heavy lifting rather than the cream body fill.
|
| 43 |
-
bodyRadius:
|
| 44 |
rungInset: 16, glyphGap: 30,
|
| 45 |
};
|
| 46 |
-
// Helix bbox in viewBox coords. Width gives room for amplitude (±
|
| 47 |
-
// centerX=220 → wave reaches x=
|
| 48 |
const VB = { x: 0, y: -30, w: 440, h: 1160 };
|
| 49 |
|
| 50 |
const COLORS = {
|
|
@@ -311,11 +311,11 @@
|
|
| 311 |
edgePath(points, from, to, -(helix.bodyRadius - 0.2));
|
| 312 |
ctx.stroke();
|
| 313 |
|
| 314 |
-
// Dark forest edge at the outer rim — the primary depth cue.
|
| 315 |
-
//
|
| 316 |
-
// outline
|
| 317 |
ctx.strokeStyle = grads.edge;
|
| 318 |
-
ctx.lineWidth = px(
|
| 319 |
edgePath(points, from, to, helix.bodyRadius + 0.4);
|
| 320 |
ctx.stroke();
|
| 321 |
edgePath(points, from, to, -(helix.bodyRadius + 0.4));
|
|
|
|
| 27 |
// Numbers tuned for a hero that *dominates* the right half of the banner:
|
| 28 |
// big amplitude, thick ribbons, oversized ATCG glyphs.
|
| 29 |
const helix = {
|
| 30 |
+
startY: 0, endY: 1100, centerX: 220, amplitude: 150,
|
| 31 |
cycles: 4.0, speed: 0.00015,
|
| 32 |
rungCount: 26,
|
| 33 |
// Dense sampling: each strand section is rendered as a continuous
|
|
|
|
| 37 |
// 4 fills + 8 stroke calls per frame total (one fill per back/front
|
| 38 |
// section per strand, plus the 4 edge polylines).
|
| 39 |
segmentCount: 512,
|
| 40 |
+
// Strand half-thickness in viewBox units. Trimmed 14 → 11 → 8 so the
|
| 41 |
+
// ribbon reads slimmer/more technical and the saturated green edge gets
|
| 42 |
+
// to do the heavy lifting rather than the cream body fill.
|
| 43 |
+
bodyRadius: 8,
|
| 44 |
rungInset: 16, glyphGap: 30,
|
| 45 |
};
|
| 46 |
+
// Helix bbox in viewBox coords. Width gives room for amplitude (±138 around
|
| 47 |
+
// centerX=220 → wave reaches x=82..358, fits comfortably in 440-wide VB).
|
| 48 |
const VB = { x: 0, y: -30, w: 440, h: 1160 };
|
| 49 |
|
| 50 |
const COLORS = {
|
|
|
|
| 311 |
edgePath(points, from, to, -(helix.bodyRadius - 0.2));
|
| 312 |
ctx.stroke();
|
| 313 |
|
| 314 |
+
// Dark forest edge at the outer rim — the primary depth cue. Kept on
|
| 315 |
+
// the bolder side (1.6 → 2.0 → 1.6) so even with the slimmer ribbon
|
| 316 |
+
// the green outline still reads as a deliberate stroke.
|
| 317 |
ctx.strokeStyle = grads.edge;
|
| 318 |
+
ctx.lineWidth = px(1.6);
|
| 319 |
edgePath(points, from, to, helix.bodyRadius + 0.4);
|
| 320 |
ctx.stroke();
|
| 321 |
edgePath(points, from, to, -(helix.bodyRadius + 0.4));
|
|
@@ -312,7 +312,12 @@
|
|
| 312 |
}
|
| 313 |
function setStatus(text, mode = "") {
|
| 314 |
els.statusText.textContent = text;
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
|
| 318 |
els.modeBtns.querySelectorAll(".sb-mode-btn").forEach(b => {
|
|
|
|
| 312 |
}
|
| 313 |
function setStatus(text, mode = "") {
|
| 314 |
els.statusText.textContent = text;
|
| 315 |
+
// Hide the pill outright in the idle/empty state — same "no idle UI"
|
| 316 |
+
// pattern used by the other demos in §1–§5. The pill comes back as
|
| 317 |
+
// soon as setStatus is called with a meaningful state ("connecting…",
|
| 318 |
+
// "streaming", "done", an error message, etc.).
|
| 319 |
+
const hide = !text || text === "idle";
|
| 320 |
+
els.status.className = "sb-status" + (mode ? " " + mode : "") + (hide ? " is-hidden" : "");
|
| 321 |
}
|
| 322 |
|
| 323 |
els.modeBtns.querySelectorAll(".sb-mode-btn").forEach(b => {
|
|
@@ -1,5 +1,10 @@
|
|
| 1 |
// =========================================================================
|
| 2 |
// §3 — Likelihood track over a real gene
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
// =========================================================================
|
| 4 |
(function initDemo3() {
|
| 5 |
const els = {
|
|
@@ -8,9 +13,6 @@
|
|
| 8 |
track: document.getElementById("d3-track"),
|
| 9 |
chart: document.getElementById("d3-chart"),
|
| 10 |
bpLabel: document.getElementById("d3-bp-label"),
|
| 11 |
-
go: document.getElementById("d3-go"),
|
| 12 |
-
status: document.getElementById("d3-status"),
|
| 13 |
-
statusText: document.querySelector("#d3-status span:last-child"),
|
| 14 |
meanExon: document.getElementById("d3-mean-exon"),
|
| 15 |
meanIntron: document.getElementById("d3-mean-intron"),
|
| 16 |
delta: document.getElementById("d3-delta"),
|
|
@@ -20,16 +22,9 @@
|
|
| 20 |
|
| 21 |
let gene = null;
|
| 22 |
let scoreData = null; // { tokens, token_logprobs, scoredLength }
|
| 23 |
-
const cache = {}; //
|
| 24 |
const MAX_WINDOW = 6000;
|
| 25 |
|
| 26 |
-
function setStatus(text, mode = "") {
|
| 27 |
-
els.statusText.textContent = text;
|
| 28 |
-
// See §1 for the "no idle pill" rationale.
|
| 29 |
-
const hide = !text || text === "idle";
|
| 30 |
-
els.status.className = "status" + (mode ? " " + mode : "") + (hide ? " is-hidden" : "");
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
function renderTrack(scoredLen) {
|
| 34 |
const W = 1000, H = 28;
|
| 35 |
if (!gene) { els.track.innerHTML = ""; return; }
|
|
@@ -49,7 +44,7 @@
|
|
| 49 |
function renderChart() {
|
| 50 |
const W = 1000, H = 140, padT = 6, padB = 16;
|
| 51 |
if (!scoreData || !gene) {
|
| 52 |
-
els.chart.innerHTML = `<text x="${W/2}" y="${H/2}" text-anchor="middle" font-family="JetBrains Mono" font-size="11" fill="#bbb">—
|
| 53 |
return;
|
| 54 |
}
|
| 55 |
const tokens = scoreData.tokens;
|
|
@@ -188,53 +183,18 @@
|
|
| 188 |
[els.meanExon, els.meanIntron, els.delta, els.tokens, els.mean].forEach(e => e.classList.remove("muted"));
|
| 189 |
}
|
| 190 |
|
| 191 |
-
async function score() {
|
| 192 |
-
if (!gene) return;
|
| 193 |
-
const cached = cache[gene.symbol];
|
| 194 |
-
if (cached) {
|
| 195 |
-
scoreData = cached;
|
| 196 |
-
renderTrack(scoreData.scoredLength);
|
| 197 |
-
renderChart();
|
| 198 |
-
updateStats();
|
| 199 |
-
setStatus("cached");
|
| 200 |
-
return;
|
| 201 |
-
}
|
| 202 |
-
setStatus("scoring (cold endpoint takes ~30s)…", "streaming");
|
| 203 |
-
els.go.disabled = true;
|
| 204 |
-
try {
|
| 205 |
-
const seq = gene.seq.slice(0, MAX_WINDOW);
|
| 206 |
-
const r = await fetch("/score", {
|
| 207 |
-
method: "POST",
|
| 208 |
-
headers: { "Content-Type": "application/json" },
|
| 209 |
-
body: JSON.stringify({ sequence: seq, max_window: MAX_WINDOW }),
|
| 210 |
-
});
|
| 211 |
-
const data = await r.json();
|
| 212 |
-
if (data.error) throw new Error(data.error);
|
| 213 |
-
data.scoredLength = seq.length;
|
| 214 |
-
cache[gene.symbol] = data;
|
| 215 |
-
scoreData = data;
|
| 216 |
-
renderTrack(data.scoredLength);
|
| 217 |
-
renderChart();
|
| 218 |
-
updateStats();
|
| 219 |
-
setStatus("done");
|
| 220 |
-
} catch (e) {
|
| 221 |
-
setStatus(e.message, "error");
|
| 222 |
-
} finally {
|
| 223 |
-
els.go.disabled = false;
|
| 224 |
-
}
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
function selectGene(symbol) {
|
| 228 |
const g = GENES.find(x => x.symbol === symbol);
|
| 229 |
if (!g) return;
|
| 230 |
gene = g;
|
| 231 |
els.pills.querySelectorAll(".pill").forEach(p => p.classList.toggle("active", p.dataset.gene === symbol));
|
| 232 |
-
|
|
|
|
|
|
|
| 233 |
scoreData = cache[symbol] || null;
|
| 234 |
renderTrack(scoreData ? scoreData.scoredLength : Math.min(gene.length, MAX_WINDOW));
|
| 235 |
renderChart();
|
| 236 |
updateStats();
|
| 237 |
-
setStatus(scoreData ? "cached" : "idle");
|
| 238 |
}
|
| 239 |
|
| 240 |
loadGenes().then(genes => {
|
|
@@ -257,7 +217,5 @@
|
|
| 257 |
});
|
| 258 |
selectGene(genes[0].symbol);
|
| 259 |
});
|
| 260 |
-
|
| 261 |
-
els.go.addEventListener("click", score);
|
| 262 |
})();
|
| 263 |
|
|
|
|
| 1 |
// =========================================================================
|
| 2 |
// §3 — Likelihood track over a real gene
|
| 3 |
+
//
|
| 4 |
+
// All tracks are precomputed (each gene in data/genes.json ships with
|
| 5 |
+
// its token logprobs under `track`), so this section is read-only: pick
|
| 6 |
+
// a gene, the cached track is rendered instantly. There is no "score"
|
| 7 |
+
// button — rescoring would just replay numbers we already have.
|
| 8 |
// =========================================================================
|
| 9 |
(function initDemo3() {
|
| 10 |
const els = {
|
|
|
|
| 13 |
track: document.getElementById("d3-track"),
|
| 14 |
chart: document.getElementById("d3-chart"),
|
| 15 |
bpLabel: document.getElementById("d3-bp-label"),
|
|
|
|
|
|
|
|
|
|
| 16 |
meanExon: document.getElementById("d3-mean-exon"),
|
| 17 |
meanIntron: document.getElementById("d3-mean-intron"),
|
| 18 |
delta: document.getElementById("d3-delta"),
|
|
|
|
| 22 |
|
| 23 |
let gene = null;
|
| 24 |
let scoreData = null; // { tokens, token_logprobs, scoredLength }
|
| 25 |
+
const cache = {}; // by gene symbol — hydrated from precomputed tracks
|
| 26 |
const MAX_WINDOW = 6000;
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
function renderTrack(scoredLen) {
|
| 29 |
const W = 1000, H = 28;
|
| 30 |
if (!gene) { els.track.innerHTML = ""; return; }
|
|
|
|
| 44 |
function renderChart() {
|
| 45 |
const W = 1000, H = 140, padT = 6, padB = 16;
|
| 46 |
if (!scoreData || !gene) {
|
| 47 |
+
els.chart.innerHTML = `<text x="${W/2}" y="${H/2}" text-anchor="middle" font-family="JetBrains Mono" font-size="11" fill="#bbb">— no precomputed track for this gene —</text>`;
|
| 48 |
return;
|
| 49 |
}
|
| 50 |
const tokens = scoreData.tokens;
|
|
|
|
| 183 |
[els.meanExon, els.meanIntron, els.delta, els.tokens, els.mean].forEach(e => e.classList.remove("muted"));
|
| 184 |
}
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
function selectGene(symbol) {
|
| 187 |
const g = GENES.find(x => x.symbol === symbol);
|
| 188 |
if (!g) return;
|
| 189 |
gene = g;
|
| 190 |
els.pills.querySelectorAll(".pill").forEach(p => p.classList.toggle("active", p.dataset.gene === symbol));
|
| 191 |
+
const scoredBp = Math.min(gene.length, MAX_WINDOW).toLocaleString("en-US");
|
| 192 |
+
const totalBp = gene.length.toLocaleString("en-US");
|
| 193 |
+
els.info.innerHTML = `<strong>${gene.symbol}</strong> · ${gene.blurb} · <span style="color:#888">${scoredBp} bp scored${gene.length > MAX_WINDOW ? ` (of ${totalBp})` : ""}</span>`;
|
| 194 |
scoreData = cache[symbol] || null;
|
| 195 |
renderTrack(scoreData ? scoreData.scoredLength : Math.min(gene.length, MAX_WINDOW));
|
| 196 |
renderChart();
|
| 197 |
updateStats();
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
loadGenes().then(genes => {
|
|
|
|
| 217 |
});
|
| 218 |
selectGene(genes[0].symbol);
|
| 219 |
});
|
|
|
|
|
|
|
| 220 |
})();
|
| 221 |
|
|
@@ -1,5 +1,11 @@
|
|
| 1 |
// =========================================================================
|
| 2 |
// §2 — VEP: ref vs alt allele likelihood
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
// =========================================================================
|
| 4 |
(function initDemo2() {
|
| 5 |
const els = {
|
|
@@ -8,23 +14,12 @@
|
|
| 8 |
window: document.getElementById("d2-window"),
|
| 9 |
result: document.getElementById("d2-result"),
|
| 10 |
bars: document.getElementById("d2-bars"),
|
| 11 |
-
go: document.getElementById("d2-go"),
|
| 12 |
-
all: document.getElementById("d2-all"),
|
| 13 |
-
status: document.getElementById("d2-status"),
|
| 14 |
-
statusText: document.querySelector("#d2-status span:last-child"),
|
| 15 |
};
|
| 16 |
|
| 17 |
let VARIANTS = null;
|
| 18 |
let selected = null;
|
| 19 |
const cache = {}; // by rs id → { refSum, altSum, refLps, altLps }
|
| 20 |
|
| 21 |
-
function setStatus(text, mode = "") {
|
| 22 |
-
els.statusText.textContent = text;
|
| 23 |
-
// See §1 for the "no idle pill" rationale.
|
| 24 |
-
const hide = !text || text === "idle";
|
| 25 |
-
els.status.className = "status" + (mode ? " " + mode : "") + (hide ? " is-hidden" : "");
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
function altWindow(v) {
|
| 29 |
return v.ref_window.slice(0, v.var_offset) + v.alt + v.ref_window.slice(v.var_offset + 1);
|
| 30 |
}
|
|
@@ -42,7 +37,7 @@
|
|
| 42 |
if (!v) { els.result.innerHTML = ""; return; }
|
| 43 |
const c = cache[v.rs];
|
| 44 |
if (!c) {
|
| 45 |
-
els.result.innerHTML = `<div style="grid-column:1/-1;color:#aaa;font-style:italic">
|
| 46 |
return;
|
| 47 |
}
|
| 48 |
// Map sums to a common scale: take min/max across both for visual ratio.
|
|
@@ -81,7 +76,7 @@
|
|
| 81 |
els.bars.setAttribute("height", H);
|
| 82 |
|
| 83 |
if (!scored.length) {
|
| 84 |
-
els.bars.innerHTML = `<text x="${W/2}" y="${H/2}" text-anchor="middle" font-family="JetBrains Mono" font-size="11" fill="#bbb">—
|
| 85 |
return;
|
| 86 |
}
|
| 87 |
const absMax = Math.max(2, ...scored.map(x => Math.abs(x.d)));
|
|
@@ -171,69 +166,6 @@
|
|
| 171 |
els.bars.innerHTML = svg;
|
| 172 |
}
|
| 173 |
|
| 174 |
-
async function scoreOne(v) {
|
| 175 |
-
if (cache[v.rs]) return cache[v.rs];
|
| 176 |
-
const ref = v.ref_window;
|
| 177 |
-
const alt = altWindow(v);
|
| 178 |
-
// Score both in parallel
|
| 179 |
-
const [refResp, altResp] = await Promise.all([
|
| 180 |
-
fetch("/score", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ sequence: ref }) }).then(r => r.json()),
|
| 181 |
-
fetch("/score", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ sequence: alt }) }).then(r => r.json()),
|
| 182 |
-
]);
|
| 183 |
-
if (refResp.error) throw new Error("ref: " + refResp.error);
|
| 184 |
-
if (altResp.error) throw new Error("alt: " + altResp.error);
|
| 185 |
-
const sumLp = (lps) => {
|
| 186 |
-
let s = 0, n = 0;
|
| 187 |
-
for (const lp of lps) {
|
| 188 |
-
if (lp != null && !isNaN(lp)) { s += lp; n++; }
|
| 189 |
-
}
|
| 190 |
-
return { sum: s, n };
|
| 191 |
-
};
|
| 192 |
-
const r = sumLp(refResp.token_logprobs);
|
| 193 |
-
const a = sumLp(altResp.token_logprobs);
|
| 194 |
-
const result = {
|
| 195 |
-
refSum: r.sum, altSum: a.sum, n: r.n,
|
| 196 |
-
refLps: refResp.token_logprobs, altLps: altResp.token_logprobs,
|
| 197 |
-
};
|
| 198 |
-
cache[v.rs] = result;
|
| 199 |
-
return result;
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
async function scoreSelected() {
|
| 203 |
-
if (!selected) return;
|
| 204 |
-
setStatus(`scoring ${selected.name}…`, "streaming");
|
| 205 |
-
els.go.disabled = true; els.all.disabled = true;
|
| 206 |
-
try {
|
| 207 |
-
await scoreOne(selected);
|
| 208 |
-
renderResult(selected);
|
| 209 |
-
renderForestBars();
|
| 210 |
-
setStatus("done");
|
| 211 |
-
} catch (e) {
|
| 212 |
-
setStatus(e.message, "error");
|
| 213 |
-
} finally {
|
| 214 |
-
els.go.disabled = false; els.all.disabled = false;
|
| 215 |
-
}
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
async function scoreAll() {
|
| 219 |
-
setStatus("scoring all…", "streaming");
|
| 220 |
-
els.go.disabled = true; els.all.disabled = true;
|
| 221 |
-
try {
|
| 222 |
-
// Sequential to be polite to the endpoint and to allow incremental UI updates.
|
| 223 |
-
for (const v of VARIANTS) {
|
| 224 |
-
if (cache[v.rs]) continue;
|
| 225 |
-
await scoreOne(v);
|
| 226 |
-
renderForestBars();
|
| 227 |
-
}
|
| 228 |
-
if (selected) renderResult(selected);
|
| 229 |
-
setStatus("done");
|
| 230 |
-
} catch (e) {
|
| 231 |
-
setStatus(e.message, "error");
|
| 232 |
-
} finally {
|
| 233 |
-
els.go.disabled = false; els.all.disabled = false;
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
function selectVariant(rs) {
|
| 238 |
const v = VARIANTS.find(x => x.rs === rs);
|
| 239 |
if (!v) return;
|
|
@@ -269,8 +201,5 @@
|
|
| 269 |
}).catch(e => {
|
| 270 |
els.info.textContent = "failed to load variants: " + e.message;
|
| 271 |
});
|
| 272 |
-
|
| 273 |
-
els.go.addEventListener("click", scoreSelected);
|
| 274 |
-
els.all.addEventListener("click", scoreAll);
|
| 275 |
})();
|
| 276 |
|
|
|
|
| 1 |
// =========================================================================
|
| 2 |
// §2 — VEP: ref vs alt allele likelihood
|
| 3 |
+
//
|
| 4 |
+
// This section runs entirely off the precomputed scores baked into
|
| 5 |
+
// data/variants.json (each variant ships with ref/alt logprobs already).
|
| 6 |
+
// There is no "score" button or live /score call here: rescoring would
|
| 7 |
+
// just replay numbers we already have, so the toolbar is just the
|
| 8 |
+
// variant pills and the bars/result panes render straight from cache.
|
| 9 |
// =========================================================================
|
| 10 |
(function initDemo2() {
|
| 11 |
const els = {
|
|
|
|
| 14 |
window: document.getElementById("d2-window"),
|
| 15 |
result: document.getElementById("d2-result"),
|
| 16 |
bars: document.getElementById("d2-bars"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
};
|
| 18 |
|
| 19 |
let VARIANTS = null;
|
| 20 |
let selected = null;
|
| 21 |
const cache = {}; // by rs id → { refSum, altSum, refLps, altLps }
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
function altWindow(v) {
|
| 24 |
return v.ref_window.slice(0, v.var_offset) + v.alt + v.ref_window.slice(v.var_offset + 1);
|
| 25 |
}
|
|
|
|
| 37 |
if (!v) { els.result.innerHTML = ""; return; }
|
| 38 |
const c = cache[v.rs];
|
| 39 |
if (!c) {
|
| 40 |
+
els.result.innerHTML = `<div style="grid-column:1/-1;color:#aaa;font-style:italic">no precomputed score for this variant</div>`;
|
| 41 |
return;
|
| 42 |
}
|
| 43 |
// Map sums to a common scale: take min/max across both for visual ratio.
|
|
|
|
| 76 |
els.bars.setAttribute("height", H);
|
| 77 |
|
| 78 |
if (!scored.length) {
|
| 79 |
+
els.bars.innerHTML = `<text x="${W/2}" y="${H/2}" text-anchor="middle" font-family="JetBrains Mono" font-size="11" fill="#bbb">— no precomputed scores available —</text>`;
|
| 80 |
return;
|
| 81 |
}
|
| 82 |
const absMax = Math.max(2, ...scored.map(x => Math.abs(x.d)));
|
|
|
|
| 166 |
els.bars.innerHTML = svg;
|
| 167 |
}
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
function selectVariant(rs) {
|
| 170 |
const v = VARIANTS.find(x => x.rs === rs);
|
| 171 |
if (!v) return;
|
|
|
|
| 201 |
}).catch(e => {
|
| 202 |
els.info.textContent = "failed to load variants: " + e.message;
|
| 203 |
});
|
|
|
|
|
|
|
|
|
|
| 204 |
})();
|
| 205 |
|
|
@@ -112,7 +112,22 @@
|
|
| 112 |
letter-spacing: 0.18em;
|
| 113 |
text-transform: uppercase;
|
| 114 |
color: #8a8a85;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
/* --- Headline: oversized wordmark + tagline. Vertically centered in the
|
| 118 |
middle row of the grid so it sits dead-center between the identity row
|
|
@@ -164,16 +179,14 @@
|
|
| 164 |
margin-left: 0.15em;
|
| 165 |
align-self: stretch;
|
| 166 |
background: var(--green);
|
| 167 |
-
/*
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
margin-top: 0.15em;
|
| 176 |
-
transform: translateY(-0.08em);
|
| 177 |
animation: cb-cursor-blink 1.05s steps(1) infinite;
|
| 178 |
}
|
| 179 |
@keyframes cb-cursor-blink {
|
|
@@ -337,12 +350,38 @@
|
|
| 337 |
.sticky-nav__inner {
|
| 338 |
display: flex;
|
| 339 |
align-items: stretch;
|
|
|
|
|
|
|
| 340 |
max-width: 1200px;
|
| 341 |
/* Match the container.wide horizontal padding so the strip's tabs sit
|
| 342 |
at the same left edge as the content column underneath. */
|
| 343 |
margin: 0 auto;
|
| 344 |
padding: 0 32px;
|
| 345 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
.sticky-nav .tab {
|
| 347 |
position: relative;
|
| 348 |
display: flex;
|
|
@@ -425,7 +464,7 @@
|
|
| 425 |
pointer-events: none;
|
| 426 |
/* Slight leftward shift so the helix sits closer to the wordmark instead
|
| 427 |
of hugging the right edge of the banner. */
|
| 428 |
-
transform: translateX(-25px) rotate(
|
| 429 |
transform-origin: 60% 50%;
|
| 430 |
}
|
| 431 |
|
|
@@ -445,7 +484,7 @@
|
|
| 445 |
.cb-helix-canvas {
|
| 446 |
top: -40px; right: 0; bottom: -40px;
|
| 447 |
width: 100%; height: calc(100% + 80px);
|
| 448 |
-
transform: rotate(
|
| 449 |
transform-origin: center;
|
| 450 |
}
|
| 451 |
.banner-wordmark { font-size: clamp(56px, 16vw, 96px); }
|
|
@@ -453,4 +492,28 @@
|
|
| 453 |
without horizontal scroll. */
|
| 454 |
.banner-tabs { width: 100%; }
|
| 455 |
.banner-tabs .tab { width: auto; flex: 1 1 0; padding: 14px 12px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
}
|
|
|
|
| 112 |
letter-spacing: 0.18em;
|
| 113 |
text-transform: uppercase;
|
| 114 |
color: #8a8a85;
|
| 115 |
+
text-decoration: none;
|
| 116 |
+
transition: color 120ms ease;
|
| 117 |
+
}
|
| 118 |
+
a.banner-path,
|
| 119 |
+
a.banner-path:link,
|
| 120 |
+
a.banner-path:visited,
|
| 121 |
+
a.banner-path:hover,
|
| 122 |
+
a.banner-path:active,
|
| 123 |
+
a.banner-path:focus,
|
| 124 |
+
a.banner-path:focus-visible {
|
| 125 |
+
text-decoration: none;
|
| 126 |
}
|
| 127 |
+
a.banner-path { cursor: pointer; }
|
| 128 |
+
a.banner-path:hover,
|
| 129 |
+
a.banner-path:focus-visible { color: var(--green); }
|
| 130 |
+
a.banner-path:focus-visible { outline: none; }
|
| 131 |
|
| 132 |
/* --- Headline: oversized wordmark + tagline. Vertically centered in the
|
| 133 |
middle row of the grid so it sits dead-center between the identity row
|
|
|
|
| 179 |
margin-left: 0.15em;
|
| 180 |
align-self: stretch;
|
| 181 |
background: var(--green);
|
| 182 |
+
/* Caret stretches to the wordmark's flex line, then drops a hair below
|
| 183 |
+
the baseline via a small negative bottom margin — reads as a
|
| 184 |
+
deliberate block caret that hangs slightly under the N rather than
|
| 185 |
+
being trapped inside the cap-height box. The top sits flush with
|
| 186 |
+
the flex line (margin-top: 0) so it no longer overshoots the N's
|
| 187 |
+
apex. */
|
| 188 |
+
margin-top: 0;
|
| 189 |
+
margin-bottom: -0.015em;
|
|
|
|
|
|
|
| 190 |
animation: cb-cursor-blink 1.05s steps(1) infinite;
|
| 191 |
}
|
| 192 |
@keyframes cb-cursor-blink {
|
|
|
|
| 350 |
.sticky-nav__inner {
|
| 351 |
display: flex;
|
| 352 |
align-items: stretch;
|
| 353 |
+
/* Brand to the left, tab cluster pinned to the right. */
|
| 354 |
+
justify-content: space-between;
|
| 355 |
max-width: 1200px;
|
| 356 |
/* Match the container.wide horizontal padding so the strip's tabs sit
|
| 357 |
at the same left edge as the content column underneath. */
|
| 358 |
margin: 0 auto;
|
| 359 |
padding: 0 32px;
|
| 360 |
}
|
| 361 |
+
/* Left-side identity: stacked title + breadcrumb path, mirroring the
|
| 362 |
+
in-banner .banner-breadcrumb verbatim so the two reads as the same
|
| 363 |
+
model-card identity. Anchored as a link back to the top of the page. */
|
| 364 |
+
.sticky-nav__brand {
|
| 365 |
+
display: flex;
|
| 366 |
+
flex-direction: column;
|
| 367 |
+
justify-content: center;
|
| 368 |
+
gap: 2px;
|
| 369 |
+
line-height: 1.2;
|
| 370 |
+
text-decoration: none;
|
| 371 |
+
color: inherit;
|
| 372 |
+
/* Sits flush with the tab tops — push the brand down by the same 7px
|
| 373 |
+
as .sticky-nav padding-top so it's vertically centred against the
|
| 374 |
+
full tab card, not against the headroom above them. */
|
| 375 |
+
padding-bottom: 7px;
|
| 376 |
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
| 377 |
+
}
|
| 378 |
+
.sticky-nav__brand .banner-title { font-size: 13px; }
|
| 379 |
+
.sticky-nav__brand .banner-path { font-size: 10.5px; }
|
| 380 |
+
.sticky-nav__brand:hover .banner-title { color: var(--green); }
|
| 381 |
+
.sticky-nav__tabs {
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: stretch;
|
| 384 |
+
}
|
| 385 |
.sticky-nav .tab {
|
| 386 |
position: relative;
|
| 387 |
display: flex;
|
|
|
|
| 464 |
pointer-events: none;
|
| 465 |
/* Slight leftward shift so the helix sits closer to the wordmark instead
|
| 466 |
of hugging the right edge of the banner. */
|
| 467 |
+
transform: translateX(-25px) rotate(5deg);
|
| 468 |
transform-origin: 60% 50%;
|
| 469 |
}
|
| 470 |
|
|
|
|
| 484 |
.cb-helix-canvas {
|
| 485 |
top: -40px; right: 0; bottom: -40px;
|
| 486 |
width: 100%; height: calc(100% + 80px);
|
| 487 |
+
transform: rotate(8deg);
|
| 488 |
transform-origin: center;
|
| 489 |
}
|
| 490 |
.banner-wordmark { font-size: clamp(56px, 16vw, 96px); }
|
|
|
|
| 492 |
without horizontal scroll. */
|
| 493 |
.banner-tabs { width: 100%; }
|
| 494 |
.banner-tabs .tab { width: auto; flex: 1 1 0; padding: 14px 12px; }
|
| 495 |
+
|
| 496 |
+
/* Sticky nav goes mobile too: drop the CARBON/huggingfacebio brand on
|
| 497 |
+
the left (the in-banner hero has already scrolled away by the time
|
| 498 |
+
this strip appears, but on narrow viewports the brand + 3×150px tabs
|
| 499 |
+
simply won't fit on one line). Tabs then expand to fill the row
|
| 500 |
+
evenly, mirroring the in-banner mobile treatment above. */
|
| 501 |
+
.sticky-nav__inner {
|
| 502 |
+
padding: 0 18px;
|
| 503 |
+
}
|
| 504 |
+
.sticky-nav__brand {
|
| 505 |
+
display: none;
|
| 506 |
+
}
|
| 507 |
+
.sticky-nav__tabs {
|
| 508 |
+
flex: 1 1 auto;
|
| 509 |
+
width: 100%;
|
| 510 |
+
}
|
| 511 |
+
.sticky-nav .tab {
|
| 512 |
+
width: auto;
|
| 513 |
+
flex: 1 1 0;
|
| 514 |
+
padding: 14px 12px;
|
| 515 |
+
/* Slightly tighter tracking so 'SANDBOX' still fits comfortably at
|
| 516 |
+
narrow widths without truncating. */
|
| 517 |
+
letter-spacing: 0.12em;
|
| 518 |
+
}
|
| 519 |
}
|
|
@@ -16,10 +16,15 @@
|
|
| 16 |
}
|
| 17 |
.demo-toolbar .spacer { flex: 1; }
|
| 18 |
|
| 19 |
-
/* --- Buttons ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
button.action, .pill {
|
| 21 |
font-family: "JetBrains Mono", monospace;
|
| 22 |
-
font-size:
|
| 23 |
padding: 5px 11px; border: 1px solid #ccc; border-radius: 3px;
|
| 24 |
background: #fff; color: #555; cursor: pointer;
|
| 25 |
text-transform: uppercase; letter-spacing: 1.5px;
|
|
@@ -31,8 +36,7 @@ button.action.primary:hover { background: #000; }
|
|
| 31 |
button.action:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 32 |
button.action.primary:disabled { background: #888; border-color: #888; }
|
| 33 |
.pill.active { background: #1f1f1d; color: #fff; border-color: #1f1f1d; }
|
| 34 |
-
.pills { display: inline-flex; gap:
|
| 35 |
-
.pills .pill { font-size: 9px; padding: 4px 8px; }
|
| 36 |
|
| 37 |
/* --- Stub placeholder for unbuilt demos --- */
|
| 38 |
.stub {
|
|
|
|
| 16 |
}
|
| 17 |
.demo-toolbar .spacer { flex: 1; }
|
| 18 |
|
| 19 |
+
/* --- Buttons ---
|
| 20 |
+
`.pill` size is the canonical pill size used across every demo (the
|
| 21 |
+
§7 Species tree toolbar set the precedent: 11px mono / 5px 11px
|
| 22 |
+
padding / 6px gap reads cleanly without dominating the toolbar).
|
| 23 |
+
Demo-specific stylesheets only override colour or text-transform,
|
| 24 |
+
never the size, so the pill rhythm stays identical between sections. */
|
| 25 |
button.action, .pill {
|
| 26 |
font-family: "JetBrains Mono", monospace;
|
| 27 |
+
font-size: 11px; font-weight: 400;
|
| 28 |
padding: 5px 11px; border: 1px solid #ccc; border-radius: 3px;
|
| 29 |
background: #fff; color: #555; cursor: pointer;
|
| 30 |
text-transform: uppercase; letter-spacing: 1.5px;
|
|
|
|
| 36 |
button.action:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 37 |
button.action.primary:disabled { background: #888; border-color: #888; }
|
| 38 |
.pill.active { background: #1f1f1d; color: #fff; border-color: #1f1f1d; }
|
| 39 |
+
.pills { display: inline-flex; gap: 6px; }
|
|
|
|
| 40 |
|
| 41 |
/* --- Stub placeholder for unbuilt demos --- */
|
| 42 |
.stub {
|
|
@@ -120,9 +120,11 @@ section:last-of-type { border-bottom: none; }
|
|
| 120 |
|
| 121 |
Layout math: container.wide is 1200px max with 32px padding =>
|
| 122 |
1136px usable. 280px rail + 28px gap + 828px right column. Below
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
| 126 |
.section--two-col {
|
| 127 |
display: grid;
|
| 128 |
grid-template-columns: 280px 1fr;
|
|
@@ -167,7 +169,7 @@ section:last-of-type { border-bottom: none; }
|
|
| 167 |
margin: 0; /* gap on section-body handles vertical rhythm */
|
| 168 |
max-width: none; /* fill the right column rather than capping at 640 */
|
| 169 |
}
|
| 170 |
-
@media (max-width:
|
| 171 |
.section--two-col {
|
| 172 |
grid-template-columns: 1fr;
|
| 173 |
row-gap: 16px;
|
|
|
|
| 120 |
|
| 121 |
Layout math: container.wide is 1200px max with 32px padding =>
|
| 122 |
1136px usable. 280px rail + 28px gap + 828px right column. Below
|
| 123 |
+
960px the right column gets squeezed under ~620px (280 + 28 + ~620
|
| 124 |
+
≈ 928px usable inside a ~960px viewport), which is the point where
|
| 125 |
+
the demos (gene tracks, SVG bars, 3D viewers) start clipping or
|
| 126 |
+
becoming unreadable. We collapse to single-column and unstick the
|
| 127 |
+
rail there so the narrative stacks above the demo + takeaway. */
|
| 128 |
.section--two-col {
|
| 129 |
display: grid;
|
| 130 |
grid-template-columns: 280px 1fr;
|
|
|
|
| 169 |
margin: 0; /* gap on section-body handles vertical rhythm */
|
| 170 |
max-width: none; /* fill the right column rather than capping at 640 */
|
| 171 |
}
|
| 172 |
+
@media (max-width: 960px) {
|
| 173 |
.section--two-col {
|
| 174 |
grid-template-columns: 1fr;
|
| 175 |
row-gap: 16px;
|
|
@@ -2,12 +2,72 @@
|
|
| 2 |
with #panel-sandbox to avoid leaking onto the Demo / Recipe panels).
|
| 3 |
Originally ported from the legacy index.html sandbox. */
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
font-family: "JetBrains Mono", monospace;
|
| 7 |
-
font-size:
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
| 12 |
#panel-sandbox .sb-examples {
|
| 13 |
display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px;
|
|
@@ -41,11 +101,28 @@
|
|
| 41 |
#panel-sandbox .sb-prompt-area {
|
| 42 |
width: 100%; resize: none; overflow: hidden;
|
| 43 |
letter-spacing: 1px; line-height: 1.7;
|
| 44 |
-
min-height:
|
|
|
|
|
|
|
| 45 |
}
|
|
|
|
|
|
|
|
|
|
| 46 |
#panel-sandbox .sb-controls {
|
| 47 |
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
| 48 |
-
margin-top:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
#panel-sandbox .sb-control {
|
| 51 |
display: flex; align-items: center; gap: 6px;
|
|
@@ -74,18 +151,20 @@
|
|
| 74 |
#panel-sandbox .sb-mode-btn:hover { color: #1f1f1d; }
|
| 75 |
#panel-sandbox .sb-mode-btn.active { background: #1f1f1d; color: #fff; border-color: #1f1f1d; }
|
| 76 |
|
| 77 |
-
#panel-sandbox .sb-button-row { margin-left: auto; display: flex; gap: 6px; }
|
| 78 |
-
|
| 79 |
#panel-sandbox .sb-status {
|
| 80 |
font-family: "JetBrains Mono", monospace;
|
| 81 |
font-size: 10px; color: #666;
|
| 82 |
text-transform: uppercase; letter-spacing: 1.5px;
|
| 83 |
-
margin-top:
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
|
|
|
| 85 |
#panel-sandbox .sb-status.error { color: #b00020; text-transform: none; letter-spacing: 0.3px; }
|
| 86 |
#panel-sandbox .sb-status .dot {
|
| 87 |
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
| 88 |
-
background: #888;
|
| 89 |
}
|
| 90 |
/* `pulse` keyframe lives in base.css. */
|
| 91 |
#panel-sandbox .sb-status.streaming .dot { background: #317f3f; animation: pulse 1.2s ease-in-out infinite; }
|
|
@@ -94,13 +173,16 @@
|
|
| 94 |
display: grid;
|
| 95 |
grid-template-columns: minmax(0, 1fr) 200px;
|
| 96 |
gap: 16px;
|
| 97 |
-
align-items: start;
|
| 98 |
}
|
| 99 |
@media (max-width: 720px) {
|
| 100 |
#panel-sandbox .sb-output-row { grid-template-columns: 1fr; }
|
| 101 |
}
|
| 102 |
|
| 103 |
-
#panel-sandbox .sb-seq-wrap {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
#panel-sandbox .sb-copy-btn {
|
| 105 |
position: absolute; top: 8px; right: 8px; z-index: 2;
|
| 106 |
font-family: "JetBrains Mono", monospace;
|
|
@@ -117,10 +199,11 @@
|
|
| 117 |
#panel-sandbox .sb-seq-block {
|
| 118 |
font-family: "JetBrains Mono", monospace;
|
| 119 |
background: #f4f4f4; border: 1px solid #ddd;
|
| 120 |
-
padding: 16px 20px;
|
| 121 |
white-space: pre; font-size: 12px; font-weight: 400;
|
| 122 |
line-height: 1.85; letter-spacing: 1.5px;
|
| 123 |
min-height: 80px;
|
|
|
|
| 124 |
}
|
| 125 |
#panel-sandbox .sb-seq-block.empty { color: #aaa; font-weight: 300; letter-spacing: normal; }
|
| 126 |
#panel-sandbox .sb-seq-line { white-space: pre; }
|
|
|
|
| 2 |
with #panel-sandbox to avoid leaking onto the Demo / Recipe panels).
|
| 3 |
Originally ported from the legacy index.html sandbox. */
|
| 4 |
|
| 5 |
+
/* Extra breathing room between the intro lede and the first card stack
|
| 6 |
+
so the playground header doesn't feel glued to the paragraph above. */
|
| 7 |
+
#panel-sandbox .tab-lede { margin-bottom: 24px; }
|
| 8 |
+
|
| 9 |
+
/* --- Connection header strip (model + endpoint at the top of the panel).
|
| 10 |
+
Same paper/border treatment as the .sb-card panels below so the whole
|
| 11 |
+
page reads as a stack of layered cards. --- */
|
| 12 |
+
#panel-sandbox .sb-header {
|
| 13 |
+
display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
|
| 14 |
+
padding: 12px 18px;
|
| 15 |
+
background: #fbfaf3;
|
| 16 |
+
border: 1px solid var(--hairline);
|
| 17 |
+
border-radius: 4px;
|
| 18 |
+
margin-bottom: 18px;
|
| 19 |
+
}
|
| 20 |
+
#panel-sandbox .sb-header__meta {
|
| 21 |
+
font-family: "JetBrains Mono", monospace;
|
| 22 |
+
font-size: 11px; color: #555; font-weight: 400;
|
| 23 |
+
letter-spacing: 0.4px; word-break: break-all;
|
| 24 |
+
flex: 1 1 0; min-width: 0;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* --- Card panel (Prompt / Sequence). Replaces the old .sb-section-title
|
| 28 |
+
single-line headers — each section now sits in its own bordered card
|
| 29 |
+
with an eyebrow/title/hint header on top and a padded body below. */
|
| 30 |
+
#panel-sandbox .sb-card {
|
| 31 |
+
background: #fbfaf3;
|
| 32 |
+
border: 1px solid var(--hairline);
|
| 33 |
+
border-radius: 4px;
|
| 34 |
+
margin-bottom: 18px;
|
| 35 |
+
padding: 12px 12px;
|
| 36 |
+
}
|
| 37 |
+
#panel-sandbox .sb-card__header {
|
| 38 |
+
padding: 0 12px 12px;
|
| 39 |
+
border-bottom: 1px solid #ece9da;
|
| 40 |
+
}
|
| 41 |
+
#panel-sandbox .sb-card__eyebrow {
|
| 42 |
+
display: inline-block;
|
| 43 |
+
font-family: "JetBrains Mono", monospace;
|
| 44 |
+
font-size: 9px; font-weight: 500;
|
| 45 |
+
color: var(--green);
|
| 46 |
+
text-transform: uppercase; letter-spacing: 1.5px;
|
| 47 |
+
margin-right: 6px;
|
| 48 |
+
}
|
| 49 |
+
#panel-sandbox .sb-card__title {
|
| 50 |
+
margin: 4px 0 0;
|
| 51 |
font-family: "JetBrains Mono", monospace;
|
| 52 |
+
font-size: 15px; font-weight: 500;
|
| 53 |
+
letter-spacing: 0.04em; text-transform: uppercase;
|
| 54 |
+
color: var(--ink);
|
| 55 |
+
}
|
| 56 |
+
#panel-sandbox .sb-card__hint {
|
| 57 |
+
margin: 4px 0 0;
|
| 58 |
+
font-family: "Inter", sans-serif;
|
| 59 |
+
font-size: 11.5px; color: #777; font-weight: 300;
|
| 60 |
+
line-height: 1.5;
|
| 61 |
+
}
|
| 62 |
+
#panel-sandbox .sb-card__hint code {
|
| 63 |
+
font-family: "JetBrains Mono", monospace;
|
| 64 |
+
font-size: 10.5px;
|
| 65 |
+
background: #ece9da;
|
| 66 |
+
padding: 1px 5px; border-radius: 2px;
|
| 67 |
+
color: #1f1f1d;
|
| 68 |
+
}
|
| 69 |
+
#panel-sandbox .sb-card__body {
|
| 70 |
+
padding: 16px 20px 0;
|
| 71 |
}
|
| 72 |
#panel-sandbox .sb-examples {
|
| 73 |
display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px;
|
|
|
|
| 101 |
#panel-sandbox .sb-prompt-area {
|
| 102 |
width: 100%; resize: none; overflow: hidden;
|
| 103 |
letter-spacing: 1px; line-height: 1.7;
|
| 104 |
+
min-height: 72px;
|
| 105 |
+
padding: 12px 14px;
|
| 106 |
+
margin-top: 12px;
|
| 107 |
}
|
| 108 |
+
/* Controls row: params on the left (max tokens, temperature, top-p,
|
| 109 |
+
color mode), action buttons pinned to the right with a divider in
|
| 110 |
+
between. Wraps cleanly on narrow viewports. */
|
| 111 |
#panel-sandbox .sb-controls {
|
| 112 |
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
| 113 |
+
margin-top: 14px;
|
| 114 |
+
padding-top: 14px;
|
| 115 |
+
border-top: 1px solid #ece9da;
|
| 116 |
+
}
|
| 117 |
+
#panel-sandbox .sb-controls__params {
|
| 118 |
+
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
|
| 119 |
+
flex: 1 1 auto;
|
| 120 |
+
}
|
| 121 |
+
#panel-sandbox .sb-controls__actions {
|
| 122 |
+
display: flex; gap: 6px;
|
| 123 |
+
margin-left: auto;
|
| 124 |
+
padding-left: 16px;
|
| 125 |
+
border-left: 1px solid #ece9da;
|
| 126 |
}
|
| 127 |
#panel-sandbox .sb-control {
|
| 128 |
display: flex; align-items: center; gap: 6px;
|
|
|
|
| 151 |
#panel-sandbox .sb-mode-btn:hover { color: #1f1f1d; }
|
| 152 |
#panel-sandbox .sb-mode-btn.active { background: #1f1f1d; color: #fff; border-color: #1f1f1d; }
|
| 153 |
|
|
|
|
|
|
|
| 154 |
#panel-sandbox .sb-status {
|
| 155 |
font-family: "JetBrains Mono", monospace;
|
| 156 |
font-size: 10px; color: #666;
|
| 157 |
text-transform: uppercase; letter-spacing: 1.5px;
|
| 158 |
+
margin-top: 12px;
|
| 159 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 160 |
+
padding: 5px 10px;
|
| 161 |
+
background: #fff; border: 1px solid #e6e3d4; border-radius: 12px;
|
| 162 |
}
|
| 163 |
+
#panel-sandbox .sb-status.is-hidden { display: none; }
|
| 164 |
#panel-sandbox .sb-status.error { color: #b00020; text-transform: none; letter-spacing: 0.3px; }
|
| 165 |
#panel-sandbox .sb-status .dot {
|
| 166 |
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
| 167 |
+
background: #888;
|
| 168 |
}
|
| 169 |
/* `pulse` keyframe lives in base.css. */
|
| 170 |
#panel-sandbox .sb-status.streaming .dot { background: #317f3f; animation: pulse 1.2s ease-in-out infinite; }
|
|
|
|
| 173 |
display: grid;
|
| 174 |
grid-template-columns: minmax(0, 1fr) 200px;
|
| 175 |
gap: 16px;
|
|
|
|
| 176 |
}
|
| 177 |
@media (max-width: 720px) {
|
| 178 |
#panel-sandbox .sb-output-row { grid-template-columns: 1fr; }
|
| 179 |
}
|
| 180 |
|
| 181 |
+
#panel-sandbox .sb-seq-wrap {
|
| 182 |
+
position: relative;
|
| 183 |
+
display: flex;
|
| 184 |
+
flex-direction: column;
|
| 185 |
+
}
|
| 186 |
#panel-sandbox .sb-copy-btn {
|
| 187 |
position: absolute; top: 8px; right: 8px; z-index: 2;
|
| 188 |
font-family: "JetBrains Mono", monospace;
|
|
|
|
| 199 |
#panel-sandbox .sb-seq-block {
|
| 200 |
font-family: "JetBrains Mono", monospace;
|
| 201 |
background: #f4f4f4; border: 1px solid #ddd;
|
| 202 |
+
padding: 16px 20px;
|
| 203 |
white-space: pre; font-size: 12px; font-weight: 400;
|
| 204 |
line-height: 1.85; letter-spacing: 1.5px;
|
| 205 |
min-height: 80px;
|
| 206 |
+
flex: 1 0 auto;
|
| 207 |
}
|
| 208 |
#panel-sandbox .sb-seq-block.empty { color: #aaa; font-weight: 300; letter-spacing: normal; }
|
| 209 |
#panel-sandbox .sb-seq-line { white-space: pre; }
|
|
@@ -13,19 +13,13 @@
|
|
| 13 |
margin-bottom: 14px;
|
| 14 |
}
|
| 15 |
.tree-toolbar .spacer { flex: 1; }
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
.tree-toolbar .pills .pill {
|
| 21 |
-
background: #fff; border: 0; padding: 5px 11px;
|
| 22 |
-
font: inherit; color: #666; cursor: pointer;
|
| 23 |
-
border-right: 1px solid #d8d5c8;
|
| 24 |
text-transform: lowercase;
|
| 25 |
-
|
| 26 |
-
.tree-toolbar .pills .pill:last-child { border-right: 0; }
|
| 27 |
-
.tree-toolbar .pills .pill.active {
|
| 28 |
-
background: #1f1f1d; color: #f7f5ee;
|
| 29 |
}
|
| 30 |
/* Big agreement score chip up top — the headline metric for §7. */
|
| 31 |
.tree-score {
|
|
|
|
| 13 |
margin-bottom: 14px;
|
| 14 |
}
|
| 15 |
.tree-toolbar .spacer { flex: 1; }
|
| 16 |
+
/* Pill sizing + spacing live in controls.css now (canonical across demos);
|
| 17 |
+
we only override the casing here because §7's vocabulary — `ward`,
|
| 18 |
+
`upgma`, `kingdom-level`, `sister-level` — reads better lowercase than
|
| 19 |
+
in the global uppercase + tracked treatment. */
|
| 20 |
.tree-toolbar .pills .pill {
|
|
|
|
|
|
|
|
|
|
| 21 |
text-transform: lowercase;
|
| 22 |
+
letter-spacing: 0;
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
/* Big agreement score chip up top — the headline metric for §7. */
|
| 25 |
.tree-score {
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -91,9 +91,19 @@
|
|
| 91 |
IntersectionObserver watching the original #tab-nav. -->
|
| 92 |
<nav id="tab-nav-sticky" class="sticky-nav" aria-label="Section navigation (sticky)">
|
| 93 |
<div class="sticky-nav__inner">
|
| 94 |
-
<
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
</nav>
|
| 99 |
|
|
@@ -228,11 +238,6 @@
|
|
| 228 |
<div class="demo-toolbar">
|
| 229 |
<span>variant</span>
|
| 230 |
<span id="d2-pills" class="pills"></span>
|
| 231 |
-
<span class="spacer"></span>
|
| 232 |
-
<!-- See §1 toolbar for why status sits before the buttons. -->
|
| 233 |
-
<span class="status is-hidden" id="d2-status"><span class="dot"></span><span></span></span>
|
| 234 |
-
<button id="d2-go" class="action primary">▶ score</button>
|
| 235 |
-
<button id="d2-all" class="action">score all</button>
|
| 236 |
</div>
|
| 237 |
|
| 238 |
<div class="gene-info" id="d2-info">loading variants…</div>
|
|
@@ -283,13 +288,13 @@
|
|
| 283 |
|
| 284 |
<div class="section-body">
|
| 285 |
<div class="demo" id="demo3">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
<div class="demo-toolbar">
|
| 287 |
<span>gene</span>
|
| 288 |
<span id="d3-pills" class="pills"></span>
|
| 289 |
-
<span class="spacer"></span>
|
| 290 |
-
<!-- See §1 toolbar for why status sits before the buttons. -->
|
| 291 |
-
<span class="status is-hidden" id="d3-status"><span class="dot"></span><span></span></span>
|
| 292 |
-
<button id="d3-go" class="action primary">▶ score</button>
|
| 293 |
</div>
|
| 294 |
|
| 295 |
<div class="gene-info" id="d3-info">loading genes…</div>
|
|
@@ -854,79 +859,109 @@
|
|
| 854 |
|
| 855 |
<div class="container" style="max-width:1200px">
|
| 856 |
|
| 857 |
-
<
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
<div class="sb-
|
| 862 |
-
|
| 863 |
-
<div class="sb-
|
| 864 |
-
<span class="sb-examples-label">examples</span>
|
| 865 |
-
<button class="sb-ex-btn" data-ex="">empty<span class="sb-ex-label">unconditional</span></button>
|
| 866 |
-
<button class="sb-ex-btn" data-ex="ATG">ATG<span class="sb-ex-label">start codon</span></button>
|
| 867 |
-
<button class="sb-ex-btn" data-ex="TATAAA">TATAAA<span class="sb-ex-label">TATA box</span></button>
|
| 868 |
-
<button class="sb-ex-btn" data-ex="CGCGCGCGCG">CGCG…<span class="sb-ex-label">CpG island</span></button>
|
| 869 |
-
<button class="sb-ex-btn" data-ex="ATGGCCAAGCTGACCAGCGAGCTGCTG">ATGGCC…<span class="sb-ex-label">ORF start</span></button>
|
| 870 |
-
<button class="sb-ex-btn" data-ex="AAAAAAAAAAAAAAAA">A·16<span class="sb-ex-label">poly-A</span></button>
|
| 871 |
</div>
|
| 872 |
|
| 873 |
-
<
|
| 874 |
-
|
| 875 |
-
<
|
| 876 |
-
|
| 877 |
-
<
|
| 878 |
-
|
| 879 |
-
<
|
| 880 |
-
|
| 881 |
-
<
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 885 |
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
<
|
| 891 |
</div>
|
| 892 |
-
</div>
|
| 893 |
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
<
|
| 897 |
-
<button id="sb-generate-btn" class="action primary">generate</button>
|
| 898 |
</div>
|
| 899 |
-
</
|
| 900 |
-
|
| 901 |
-
<div class="sb-status" id="sb-status"><span class="dot"></span><span id="sb-status-text">idle</span></div>
|
| 902 |
-
|
| 903 |
-
<div class="sb-section-title">Sequence</div>
|
| 904 |
|
| 905 |
-
<
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
<
|
| 909 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
|
|
|
|
|
|
| 927 |
</div>
|
| 928 |
</div>
|
| 929 |
-
</
|
| 930 |
|
| 931 |
</div>
|
| 932 |
</div> <!-- /panel-sandbox -->
|
|
|
|
| 91 |
IntersectionObserver watching the original #tab-nav. -->
|
| 92 |
<nav id="tab-nav-sticky" class="sticky-nav" aria-label="Section navigation (sticky)">
|
| 93 |
<div class="sticky-nav__inner">
|
| 94 |
+
<!-- Mini breadcrumb on the left: same identity as the in-banner
|
| 95 |
+
.banner-breadcrumb (title + model path stacked) so the sticky
|
| 96 |
+
strip carries the "you're on the Carbon model card" cue even
|
| 97 |
+
after the hero has scrolled out of view. -->
|
| 98 |
+
<a class="sticky-nav__brand" href="#" aria-label="Carbon — go to top">
|
| 99 |
+
<span class="banner-title">CARBON</span>
|
| 100 |
+
<span class="banner-path">huggingfacebio/carbon-3b</span>
|
| 101 |
+
</a>
|
| 102 |
+
<div class="sticky-nav__tabs">
|
| 103 |
+
<button class="tab active" data-tab="demo">Demo</button>
|
| 104 |
+
<button class="tab" data-tab="model">Model</button>
|
| 105 |
+
<button class="tab" data-tab="sandbox">Sandbox</button>
|
| 106 |
+
</div>
|
| 107 |
</div>
|
| 108 |
</nav>
|
| 109 |
|
|
|
|
| 238 |
<div class="demo-toolbar">
|
| 239 |
<span>variant</span>
|
| 240 |
<span id="d2-pills" class="pills"></span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
</div>
|
| 242 |
|
| 243 |
<div class="gene-info" id="d2-info">loading variants…</div>
|
|
|
|
| 288 |
|
| 289 |
<div class="section-body">
|
| 290 |
<div class="demo" id="demo3">
|
| 291 |
+
<!-- Likelihood tracks are precomputed (each gene ships with its
|
| 292 |
+
token logprobs in data/genes.json), so this toolbar is just
|
| 293 |
+
the gene selector — selecting a pill renders the track from
|
| 294 |
+
cache instantly, no live /score call needed. -->
|
| 295 |
<div class="demo-toolbar">
|
| 296 |
<span>gene</span>
|
| 297 |
<span id="d3-pills" class="pills"></span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
</div>
|
| 299 |
|
| 300 |
<div class="gene-info" id="d3-info">loading genes…</div>
|
|
|
|
| 859 |
|
| 860 |
<div class="container" style="max-width:1200px">
|
| 861 |
|
| 862 |
+
<!-- Connection strip: tells you which model the playground is talking to.
|
| 863 |
+
Same eyebrow + value pattern reused by the two card headers below so
|
| 864 |
+
the whole panel reads as a single layered stack rather than a flat
|
| 865 |
+
wall of controls. -->
|
| 866 |
+
<div class="sb-header">
|
| 867 |
+
<span class="sb-card__eyebrow">Connected to</span>
|
| 868 |
+
<div id="sb-meta" class="sb-header__meta">loading…</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
</div>
|
| 870 |
|
| 871 |
+
<!-- INPUT card: examples → prompt → controls → status. -->
|
| 872 |
+
<section class="sb-card">
|
| 873 |
+
<header class="sb-card__header">
|
| 874 |
+
<span class="sb-card__eyebrow">§ Input</span>
|
| 875 |
+
<h2 class="sb-card__title">Prompt</h2>
|
| 876 |
+
<p class="sb-card__hint">DNA prefix in <code>{A, C, G, T}</code> — pick an example or type your own.</p>
|
| 877 |
+
</header>
|
| 878 |
+
|
| 879 |
+
<div class="sb-card__body">
|
| 880 |
+
<div class="sb-examples">
|
| 881 |
+
<span class="sb-examples-label">examples</span>
|
| 882 |
+
<button class="sb-ex-btn" data-ex="">empty<span class="sb-ex-label">unconditional</span></button>
|
| 883 |
+
<button class="sb-ex-btn" data-ex="ATG">ATG<span class="sb-ex-label">start codon</span></button>
|
| 884 |
+
<button class="sb-ex-btn" data-ex="TATAAA">TATAAA<span class="sb-ex-label">TATA box</span></button>
|
| 885 |
+
<button class="sb-ex-btn" data-ex="CGCGCGCGCG">CGCG…<span class="sb-ex-label">CpG island</span></button>
|
| 886 |
+
<button class="sb-ex-btn" data-ex="ATGGCCAAGCTGACCAGCGAGCTGCTG">ATGGCC…<span class="sb-ex-label">ORF start</span></button>
|
| 887 |
+
<button class="sb-ex-btn" data-ex="AAAAAAAAAAAAAAAA">A·16<span class="sb-ex-label">poly-A</span></button>
|
| 888 |
+
</div>
|
| 889 |
+
|
| 890 |
+
<textarea id="sb-prompt" class="sb-prompt-area" rows="3" spellcheck="false" autocapitalize="characters">AGT</textarea>
|
| 891 |
+
|
| 892 |
+
<!-- Controls split into two visual halves: sampling/display params on
|
| 893 |
+
the left, action buttons pinned to the right. The vertical rule
|
| 894 |
+
between them makes the parameter cluster read as one group. -->
|
| 895 |
+
<div class="sb-controls">
|
| 896 |
+
<div class="sb-controls__params">
|
| 897 |
+
<label class="sb-control">max tokens
|
| 898 |
+
<input type="number" id="sb-max-tokens" value="128" min="1" max="2048" step="1">
|
| 899 |
+
</label>
|
| 900 |
+
<label class="sb-control">temperature
|
| 901 |
+
<input type="number" id="sb-temperature" value="1.0" min="0" max="2" step="0.1">
|
| 902 |
+
</label>
|
| 903 |
+
<label class="sb-control">top-p
|
| 904 |
+
<input type="number" id="sb-top-p" value="1.0" min="0" max="1" step="0.05">
|
| 905 |
+
</label>
|
| 906 |
+
|
| 907 |
+
<div class="sb-mode-group">color
|
| 908 |
+
<div class="sb-mode-btns" id="sb-mode-btns">
|
| 909 |
+
<button class="sb-mode-btn active" data-mode="none">none</button>
|
| 910 |
+
<button class="sb-mode-btn" data-mode="bases">bases</button>
|
| 911 |
+
<button class="sb-mode-btn" data-mode="logprob">logprob</button>
|
| 912 |
+
</div>
|
| 913 |
+
</div>
|
| 914 |
+
</div>
|
| 915 |
|
| 916 |
+
<div class="sb-controls__actions">
|
| 917 |
+
<button id="sb-clear-btn" class="action">clear</button>
|
| 918 |
+
<button id="sb-stop-btn" class="action" disabled>stop</button>
|
| 919 |
+
<button id="sb-generate-btn" class="action primary">▶ generate</button>
|
| 920 |
+
</div>
|
| 921 |
</div>
|
|
|
|
| 922 |
|
| 923 |
+
<!-- Hidden by setStatus("idle") so the toolbar stays clean until
|
| 924 |
+
something actually happens (connecting / streaming / done). -->
|
| 925 |
+
<div class="sb-status is-hidden" id="sb-status"><span class="dot"></span><span id="sb-status-text">idle</span></div>
|
|
|
|
| 926 |
</div>
|
| 927 |
+
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 928 |
|
| 929 |
+
<!-- OUTPUT card: streamed sequence + sticky stats sidebar. -->
|
| 930 |
+
<section class="sb-card">
|
| 931 |
+
<header class="sb-card__header">
|
| 932 |
+
<span class="sb-card__eyebrow">§ Output</span>
|
| 933 |
+
<h2 class="sb-card__title">Sequence</h2>
|
| 934 |
+
<p class="sb-card__hint">Streams as the model generates · live stats on the right.</p>
|
| 935 |
+
</header>
|
| 936 |
+
|
| 937 |
+
<div class="sb-card__body">
|
| 938 |
+
<div class="sb-output-row">
|
| 939 |
+
<div class="sb-seq-wrap">
|
| 940 |
+
<button id="sb-copy-btn" class="sb-copy-btn" disabled>copy</button>
|
| 941 |
+
<div class="sb-seq-block empty" id="sb-seq">— prompt + generated bases will stream here —</div>
|
| 942 |
+
</div>
|
| 943 |
|
| 944 |
+
<div>
|
| 945 |
+
<div class="sb-stats" id="sb-stats">
|
| 946 |
+
<div class="sb-stat"><span class="sb-stat-label">prompt</span><span class="sb-stat-value" id="sb-stat-prompt">0<span class="sb-unit">bp</span></span></div>
|
| 947 |
+
<div class="sb-stat"><span class="sb-stat-label">generated</span><span class="sb-stat-value" id="sb-stat-gen">0<span class="sb-unit">bp</span></span></div>
|
| 948 |
+
<div class="sb-stat"><span class="sb-stat-label">tokens</span><span class="sb-stat-value" id="sb-stat-tok">0</span></div>
|
| 949 |
+
<div class="sb-stat"><span class="sb-stat-label">elapsed</span><span class="sb-stat-value" id="sb-stat-time">0.0<span class="sb-unit">s</span></span></div>
|
| 950 |
+
<div class="sb-stat"><span class="sb-stat-label">throughput</span><span class="sb-stat-value" id="sb-stat-rate">0<span class="sb-unit">bp/s</span></span></div>
|
| 951 |
+
<div class="sb-stat"><span class="sb-stat-label">GC content</span><span class="sb-stat-value" id="sb-stat-gc">—</span></div>
|
| 952 |
+
<div class="sb-stat"><span class="sb-stat-label">mean logprob</span><span class="sb-stat-value" id="sb-stat-lp">—</span></div>
|
| 953 |
+
<div class="sb-stat"><span class="sb-stat-label">perplexity</span><span class="sb-stat-value" id="sb-stat-ppl">—</span></div>
|
| 954 |
+
</div>
|
| 955 |
+
<div class="sb-legend" id="sb-legend">
|
| 956 |
+
<div>token logprob</div>
|
| 957 |
+
<div class="sb-legend-bar" id="sb-legend-bar"></div>
|
| 958 |
+
<div class="sb-legend-row"><span id="sb-lp-min">—</span><span id="sb-lp-mid">—</span><span id="sb-lp-max">—</span></div>
|
| 959 |
+
<svg id="sb-lp-chart" class="sb-lp-chart" preserveAspectRatio="none"></svg>
|
| 960 |
+
</div>
|
| 961 |
+
</div>
|
| 962 |
</div>
|
| 963 |
</div>
|
| 964 |
+
</section>
|
| 965 |
|
| 966 |
</div>
|
| 967 |
</div> <!-- /panel-sandbox -->
|
|
@@ -7,9 +7,10 @@ Writes results back into data/genes.json and data/variants.json:
|
|
| 7 |
- per-gene `fold_example` (Carbon /generate + NVIDIA NIM ESMFold)
|
| 8 |
|
| 9 |
Usage:
|
| 10 |
-
python scripts/precompute.py
|
| 11 |
-
python scripts/precompute.py --folds
|
| 12 |
-
python scripts/precompute.py --no-folds
|
|
|
|
| 13 |
"""
|
| 14 |
import json
|
| 15 |
import os
|
|
@@ -297,7 +298,7 @@ def nim_fold(api_key, sequence):
|
|
| 297 |
}
|
| 298 |
|
| 299 |
|
| 300 |
-
def precompute_folds(client):
|
| 301 |
api_key = os.environ.get("NVIDIA_API_KEY")
|
| 302 |
if not api_key:
|
| 303 |
raise RuntimeError("NVIDIA_API_KEY missing (set in .env or env)")
|
|
@@ -306,6 +307,9 @@ def precompute_folds(client):
|
|
| 306 |
for g in genes:
|
| 307 |
if not g.get("exons"):
|
| 308 |
continue
|
|
|
|
|
|
|
|
|
|
| 309 |
last_exon_end = g["exons"][-1]["end"]
|
| 310 |
n_tries = FOLD_BEST_OF_LONG if last_exon_end > FOLD_LONG_THRESHOLD else FOLD_BEST_OF_SHORT
|
| 311 |
print(f" folding {g['symbol']} (last exon end {last_exon_end} bp, best-of-{n_tries})…", flush=True)
|
|
@@ -376,6 +380,7 @@ def main():
|
|
| 376 |
argv = set(sys.argv[1:])
|
| 377 |
only_folds = "--folds" in argv
|
| 378 |
skip_folds = "--no-folds" in argv
|
|
|
|
| 379 |
|
| 380 |
client = make_client()
|
| 381 |
if not only_folds:
|
|
@@ -387,7 +392,7 @@ def main():
|
|
| 387 |
if not skip_folds:
|
| 388 |
print()
|
| 389 |
print("=== precomputing fold fixtures ===")
|
| 390 |
-
precompute_folds(client)
|
| 391 |
|
| 392 |
|
| 393 |
if __name__ == "__main__":
|
|
|
|
| 7 |
- per-gene `fold_example` (Carbon /generate + NVIDIA NIM ESMFold)
|
| 8 |
|
| 9 |
Usage:
|
| 10 |
+
python scripts/precompute.py # everything
|
| 11 |
+
python scripts/precompute.py --folds # only the folding fixtures
|
| 12 |
+
python scripts/precompute.py --no-folds # skip folding fixtures
|
| 13 |
+
python scripts/precompute.py --folds --only-missing # only genes lacking fold_example
|
| 14 |
"""
|
| 15 |
import json
|
| 16 |
import os
|
|
|
|
| 298 |
}
|
| 299 |
|
| 300 |
|
| 301 |
+
def precompute_folds(client, only_missing=False):
|
| 302 |
api_key = os.environ.get("NVIDIA_API_KEY")
|
| 303 |
if not api_key:
|
| 304 |
raise RuntimeError("NVIDIA_API_KEY missing (set in .env or env)")
|
|
|
|
| 307 |
for g in genes:
|
| 308 |
if not g.get("exons"):
|
| 309 |
continue
|
| 310 |
+
if only_missing and g.get("fold_example"):
|
| 311 |
+
print(f" skipping {g['symbol']} (fold_example already cached)")
|
| 312 |
+
continue
|
| 313 |
last_exon_end = g["exons"][-1]["end"]
|
| 314 |
n_tries = FOLD_BEST_OF_LONG if last_exon_end > FOLD_LONG_THRESHOLD else FOLD_BEST_OF_SHORT
|
| 315 |
print(f" folding {g['symbol']} (last exon end {last_exon_end} bp, best-of-{n_tries})…", flush=True)
|
|
|
|
| 380 |
argv = set(sys.argv[1:])
|
| 381 |
only_folds = "--folds" in argv
|
| 382 |
skip_folds = "--no-folds" in argv
|
| 383 |
+
only_missing = "--only-missing" in argv
|
| 384 |
|
| 385 |
client = make_client()
|
| 386 |
if not only_folds:
|
|
|
|
| 392 |
if not skip_folds:
|
| 393 |
print()
|
| 394 |
print("=== precomputing fold fixtures ===")
|
| 395 |
+
precompute_folds(client, only_missing=only_missing)
|
| 396 |
|
| 397 |
|
| 398 |
if __name__ == "__main__":
|
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.0">
|
| 6 |
+
<title>Carbon · social banner</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap">
|
| 10 |
+
|
| 11 |
+
<!-- Reuses the same hero stylesheet as demo.html so the banner reads
|
| 12 |
+
identical to the editorial hero on the main page. -->
|
| 13 |
+
<link rel="stylesheet" href="/assets/styles/base.css">
|
| 14 |
+
<link rel="stylesheet" href="/assets/styles/banner.css">
|
| 15 |
+
|
| 16 |
+
<style>
|
| 17 |
+
/* ------------------------------------------------------------------ */
|
| 18 |
+
/* Standalone page chrome. */
|
| 19 |
+
/* The banner sits in the middle of the viewport at one of a handful */
|
| 20 |
+
/* of fixed social-media canvas sizes (Twitter / OG / LinkedIn / HF). */
|
| 21 |
+
/* The page paper around it is a touch darker than the banner so the */
|
| 22 |
+
/* edges of the social-media canvas read clearly — handy when you're */
|
| 23 |
+
/* about to crop a screenshot. */
|
| 24 |
+
/* ------------------------------------------------------------------ */
|
| 25 |
+
html, body { height: 100%; }
|
| 26 |
+
body {
|
| 27 |
+
background: #e8e5d6;
|
| 28 |
+
display: flex;
|
| 29 |
+
flex-direction: column;
|
| 30 |
+
align-items: center;
|
| 31 |
+
justify-content: center;
|
| 32 |
+
gap: 28px;
|
| 33 |
+
padding: 40px 24px;
|
| 34 |
+
overflow: hidden;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* The social-banner-stage IS the social-media canvas: a fixed width
|
| 38 |
+
and height drawn at 1:1 device pixels so what you see is exactly
|
| 39 |
+
what gets exported. Sizes are driven by --sb-w / --sb-h CSS vars
|
| 40 |
+
set on <html> by the format switcher below. */
|
| 41 |
+
.social-banner-stage {
|
| 42 |
+
--sb-w: 1500px;
|
| 43 |
+
--sb-h: 500px;
|
| 44 |
+
width: var(--sb-w);
|
| 45 |
+
height: var(--sb-h);
|
| 46 |
+
flex-shrink: 0;
|
| 47 |
+
position: relative;
|
| 48 |
+
box-shadow:
|
| 49 |
+
0 1px 0 #cfcdbf inset,
|
| 50 |
+
0 30px 60px -28px rgba(31, 31, 29, 0.28),
|
| 51 |
+
0 8px 18px -10px rgba(31, 31, 29, 0.18);
|
| 52 |
+
/* Auto-scale to fit the viewport when the window is smaller than
|
| 53 |
+
the social canvas. The factor is set by JS (see fitStage()) so
|
| 54 |
+
the banner always renders centred and intact, even on a laptop
|
| 55 |
+
that can't physically fit a 1584-wide canvas. */
|
| 56 |
+
transform: scale(var(--sb-scale, 1));
|
| 57 |
+
transform-origin: center center;
|
| 58 |
+
transition: width 0.22s ease, height 0.22s ease;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Inside the stage the banner fills it entirely (no max-width cap
|
| 62 |
+
like on the main page — here the banner IS the canvas). */
|
| 63 |
+
.social-banner-stage .carbon-banner {
|
| 64 |
+
width: 100%;
|
| 65 |
+
height: 100%;
|
| 66 |
+
}
|
| 67 |
+
.social-banner-stage .banner-inner {
|
| 68 |
+
max-width: none;
|
| 69 |
+
height: 100%;
|
| 70 |
+
/* Tabs are gone — drop the asymmetric bottom-zero padding from the
|
| 71 |
+
main hero so the headline sits visually centred top↔bottom. */
|
| 72 |
+
padding: 36px 56px;
|
| 73 |
+
/* 2-col grid: headline on the left, helix on the right. The right
|
| 74 |
+
track is sized as a share of the total stage width so the helix
|
| 75 |
+
gets more room on a wide LinkedIn cover and less on a tall OG
|
| 76 |
+
card. */
|
| 77 |
+
grid-template-columns: minmax(0, 1fr) clamp(280px, 36%, 520px);
|
| 78 |
+
gap: 32px;
|
| 79 |
+
min-height: 0;
|
| 80 |
+
}
|
| 81 |
+
.social-banner-stage .banner-left {
|
| 82 |
+
/* No more bottom tab row → 2-row layout: identity strip on top,
|
| 83 |
+
headline filling the rest. */
|
| 84 |
+
grid-template-rows: auto 1fr;
|
| 85 |
+
gap: 14px;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* Headline sizing: tied to the stage height instead of the viewport
|
| 89 |
+
so the wordmark scales with the social canvas, not the browser
|
| 90 |
+
window. cqh = container-query-height, falls back to a fixed size
|
| 91 |
+
on browsers without container queries. */
|
| 92 |
+
.social-banner-stage { container-type: size; }
|
| 93 |
+
.social-banner-stage .banner-wordmark {
|
| 94 |
+
font-size: clamp(64px, 22cqh, 140px);
|
| 95 |
+
}
|
| 96 |
+
.social-banner-stage .banner-subtitle {
|
| 97 |
+
font-size: clamp(11px, 2.6cqh, 16px);
|
| 98 |
+
margin-top: 10px;
|
| 99 |
+
}
|
| 100 |
+
.social-banner-stage .banner-specs {
|
| 101 |
+
font-size: clamp(10px, 2.2cqh, 13px);
|
| 102 |
+
margin-top: 10px;
|
| 103 |
+
}
|
| 104 |
+
.social-banner-stage .banner-title { font-size: clamp(12px, 2.6cqh, 16px); }
|
| 105 |
+
.social-banner-stage .banner-path { font-size: clamp(10px, 2.2cqh, 13px); }
|
| 106 |
+
|
| 107 |
+
/* Slightly stronger inset hairline replaces the bottom one we lose
|
| 108 |
+
with the tabs gone — keeps the banner from looking unfinished. */
|
| 109 |
+
.social-banner-stage .carbon-banner {
|
| 110 |
+
box-shadow: inset 0 0 0 1px #cfcdbf;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* ------------------------------------------------------------------ */
|
| 114 |
+
/* Format switcher — discreet row of pills below the banner. Sets a */
|
| 115 |
+
/* CSS var on <html> + a data attribute the JS reads to recompute */
|
| 116 |
+
/* the scale-to-fit factor. Excluded from screenshots: the row sits */
|
| 117 |
+
/* far enough below the stage that any crop framed on the banner */
|
| 118 |
+
/* itself won't catch it. */
|
| 119 |
+
/* ------------------------------------------------------------------ */
|
| 120 |
+
.sb-switcher {
|
| 121 |
+
display: flex;
|
| 122 |
+
align-items: center;
|
| 123 |
+
gap: 6px;
|
| 124 |
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
| 125 |
+
font-size: 11px;
|
| 126 |
+
letter-spacing: 0.12em;
|
| 127 |
+
text-transform: uppercase;
|
| 128 |
+
color: #6f6d65;
|
| 129 |
+
}
|
| 130 |
+
.sb-switcher__label {
|
| 131 |
+
margin-right: 8px;
|
| 132 |
+
color: #8a8a85;
|
| 133 |
+
}
|
| 134 |
+
.sb-switcher__btn {
|
| 135 |
+
appearance: none;
|
| 136 |
+
border: 1px solid #cfcdbf;
|
| 137 |
+
background: #f7f5ee;
|
| 138 |
+
color: inherit;
|
| 139 |
+
font-family: inherit;
|
| 140 |
+
font-size: inherit;
|
| 141 |
+
letter-spacing: inherit;
|
| 142 |
+
text-transform: inherit;
|
| 143 |
+
padding: 7px 12px;
|
| 144 |
+
cursor: pointer;
|
| 145 |
+
transition: background 0.16s, border-color 0.16s, color 0.16s;
|
| 146 |
+
}
|
| 147 |
+
.sb-switcher__btn:hover {
|
| 148 |
+
background: #fff;
|
| 149 |
+
border-color: #b8b5a6;
|
| 150 |
+
color: #1f1f1d;
|
| 151 |
+
}
|
| 152 |
+
.sb-switcher__btn.is-active {
|
| 153 |
+
background: #1f1f1d;
|
| 154 |
+
border-color: #1f1f1d;
|
| 155 |
+
color: #f7f5ee;
|
| 156 |
+
}
|
| 157 |
+
.sb-switcher__dim {
|
| 158 |
+
margin-left: 8px;
|
| 159 |
+
color: #b0ada0;
|
| 160 |
+
font-feature-settings: "tnum";
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/* Print: hide chrome, show the banner at its true size. */
|
| 164 |
+
@media print {
|
| 165 |
+
body { background: white; padding: 0; }
|
| 166 |
+
.sb-switcher { display: none; }
|
| 167 |
+
.social-banner-stage { transform: none !important; box-shadow: none; }
|
| 168 |
+
}
|
| 169 |
+
</style>
|
| 170 |
+
</head>
|
| 171 |
+
<body>
|
| 172 |
+
|
| 173 |
+
<!-- The social-media canvas. The wrapping .social-banner-stage carries
|
| 174 |
+
the exact pixel dimensions (1500×500 for Twitter by default); the
|
| 175 |
+
.carbon-banner inside is the same hero block as on demo.html, minus
|
| 176 |
+
the logo card and minus the tab strip. -->
|
| 177 |
+
<div class="social-banner-stage" id="sb-stage">
|
| 178 |
+
<header class="carbon-banner" aria-label="Carbon DNA model banner">
|
| 179 |
+
<div class="banner-inner">
|
| 180 |
+
<div class="banner-left">
|
| 181 |
+
|
| 182 |
+
<!-- Identity strip — no logo card here, just the model-path
|
| 183 |
+
breadcrumb so the banner reads as a Hugging Face model
|
| 184 |
+
card identifier without the square thumbnail. -->
|
| 185 |
+
<div class="banner-identity">
|
| 186 |
+
<div class="banner-breadcrumb">
|
| 187 |
+
<div class="banner-title">CARBON</div>
|
| 188 |
+
<div class="banner-path">huggingfacebio/carbon-3b</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<!-- Headline: oversized wordmark with blinking caret, then
|
| 193 |
+
tagline + spec sheet underneath. Same structure as
|
| 194 |
+
demo.html so the styling carries over verbatim. -->
|
| 195 |
+
<div class="banner-headline">
|
| 196 |
+
<h1 class="banner-wordmark"><span>CARBON</span><span class="banner-cursor" aria-hidden="true"></span></h1>
|
| 197 |
+
<p class="banner-subtitle">Autoregressive Genomic Foundation Model</p>
|
| 198 |
+
<ul class="banner-specs" aria-label="Model specs">
|
| 199 |
+
<li class="banner-spec"><strong>49,152</strong> bp context</li>
|
| 200 |
+
<li class="banner-spec"><strong>6-mer</strong> tokenizer</li>
|
| 201 |
+
<li class="banner-spec"><strong>1T</strong> train tokens</li>
|
| 202 |
+
</ul>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<!-- Animated DNA helix, painted by banner.js. -->
|
| 208 |
+
<div class="banner-helix" aria-hidden="true">
|
| 209 |
+
<canvas class="cb-helix-canvas"></canvas>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</header>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<!-- Format switcher. Stays outside the stage so it doesn't show up
|
| 216 |
+
when you crop the banner. -->
|
| 217 |
+
<div class="sb-switcher" role="group" aria-label="Banner format">
|
| 218 |
+
<span class="sb-switcher__label">format</span>
|
| 219 |
+
<button class="sb-switcher__btn is-active" data-format="twitter" data-w="1500" data-h="500">twitter / x · 1500×500</button>
|
| 220 |
+
<button class="sb-switcher__btn" data-format="og" data-w="1200" data-h="630">og / share · 1200×630</button>
|
| 221 |
+
<button class="sb-switcher__btn" data-format="linkedin" data-w="1584" data-h="396">linkedin · 1584×396</button>
|
| 222 |
+
<button class="sb-switcher__btn" data-format="hf" data-w="1280" data-h="640">hugging face · 1280×640</button>
|
| 223 |
+
<span class="sb-switcher__dim" id="sb-scale-readout">100%</span>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<!-- Helix animation. Same script as the main demo — it queries
|
| 227 |
+
.carbon-banner + .cb-helix-canvas, both of which exist here. -->
|
| 228 |
+
<script src="/assets/js/banner.js"></script>
|
| 229 |
+
|
| 230 |
+
<script>
|
| 231 |
+
// ----------------------------------------------------------------
|
| 232 |
+
// Format switcher + auto-fit-to-viewport.
|
| 233 |
+
//
|
| 234 |
+
// The .social-banner-stage carries the EXACT social-media canvas
|
| 235 |
+
// dimensions (1500×500 for a Twitter header, etc). On a screen that
|
| 236 |
+
// can't physically fit that many pixels we don't want a scrollbar —
|
| 237 |
+
// we want the banner to scale down uniformly so it stays centred and
|
| 238 |
+
// fully visible. We compute the largest scale that fits both axes
|
| 239 |
+
// inside the viewport (minus some breathing room for the switcher
|
| 240 |
+
// row) and apply it via CSS transform.
|
| 241 |
+
// ----------------------------------------------------------------
|
| 242 |
+
(function initSocialBanner() {
|
| 243 |
+
const stage = document.getElementById("sb-stage");
|
| 244 |
+
const readout = document.getElementById("sb-scale-readout");
|
| 245 |
+
const buttons = document.querySelectorAll(".sb-switcher__btn");
|
| 246 |
+
|
| 247 |
+
// Vertical breathing room reserved for the format switcher
|
| 248 |
+
// (height of the row + body padding + gap). Kept conservative so
|
| 249 |
+
// the banner always has clear margin around it.
|
| 250 |
+
const CHROME_V = 160;
|
| 251 |
+
const CHROME_H = 80;
|
| 252 |
+
|
| 253 |
+
function fitStage() {
|
| 254 |
+
const w = parseFloat(getComputedStyle(stage).getPropertyValue("--sb-w"));
|
| 255 |
+
const h = parseFloat(getComputedStyle(stage).getPropertyValue("--sb-h"));
|
| 256 |
+
const availW = Math.max(200, window.innerWidth - CHROME_H);
|
| 257 |
+
const availH = Math.max(200, window.innerHeight - CHROME_V);
|
| 258 |
+
const scale = Math.min(1, availW / w, availH / h);
|
| 259 |
+
stage.style.setProperty("--sb-scale", scale.toFixed(4));
|
| 260 |
+
readout.textContent = Math.round(scale * 100) + "%";
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
function setFormat(btn) {
|
| 264 |
+
buttons.forEach(b => b.classList.toggle("is-active", b === btn));
|
| 265 |
+
const w = btn.dataset.w + "px";
|
| 266 |
+
const h = btn.dataset.h + "px";
|
| 267 |
+
stage.style.setProperty("--sb-w", w);
|
| 268 |
+
stage.style.setProperty("--sb-h", h);
|
| 269 |
+
// Reflect in the URL so a chosen format is shareable / bookmarkable.
|
| 270 |
+
const url = new URL(window.location);
|
| 271 |
+
url.searchParams.set("format", btn.dataset.format);
|
| 272 |
+
window.history.replaceState(null, "", url);
|
| 273 |
+
// Wait one frame so the new --sb-w/--sb-h are applied before we
|
| 274 |
+
// measure for the fit-to-viewport calculation.
|
| 275 |
+
requestAnimationFrame(() => {
|
| 276 |
+
fitStage();
|
| 277 |
+
// Nudge the canvas to resize against its new CSS box. The
|
| 278 |
+
// banner.js ResizeObserver should catch this on its own, but
|
| 279 |
+
// firing a synthetic resize is a cheap belt-and-braces.
|
| 280 |
+
window.dispatchEvent(new Event("resize"));
|
| 281 |
+
});
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
buttons.forEach(btn => btn.addEventListener("click", () => setFormat(btn)));
|
| 285 |
+
|
| 286 |
+
// Restore from URL query (?format=twitter / og / linkedin / hf).
|
| 287 |
+
const initial = new URL(window.location).searchParams.get("format");
|
| 288 |
+
if (initial) {
|
| 289 |
+
const match = Array.from(buttons).find(b => b.dataset.format === initial);
|
| 290 |
+
if (match) setFormat(match);
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
fitStage();
|
| 294 |
+
window.addEventListener("resize", fitStage);
|
| 295 |
+
})();
|
| 296 |
+
</script>
|
| 297 |
+
|
| 298 |
+
</body>
|
| 299 |
+
</html>
|