lvwerra HF Staff Claude Opus 4.7 (1M context) commited on
Commit
e9eb9ae
·
1 Parent(s): 1f90847

§1: replace prefix/gen pills with three draggable handles

Browse files

Two dark-grey handles bracket the prompt window (drag ▼ on top, ▲ on
bottom); a third green ▼ handle on top of the gen-region sets where
generation stops. Removes the prefix-length and generate-size pill rows
in favor of direct manipulation on the gene track.

Also drops the reference sequence block — generated bases are now
underlined green (match) or red (mismatch) inline. Legend redrawn as
mini-glyphs (exon shape, intron shape, full widget) with instant
:hover tooltips. Takeaway prompts the user to try the green window on
exons vs introns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. demo.html +206 -69
demo.html CHANGED
@@ -427,11 +427,63 @@
427
  width: 100%; height: 28px; display: block;
428
  margin: 4px 0 8px;
429
  }
 
430
  .gene-track .exon { fill: #317f3f; }
431
  .gene-track .intron { stroke: #aaa; stroke-width: 1; }
432
  .gene-track .playhead { stroke: #bc2e25; stroke-width: 2; }
433
- .gene-track .gen-region { fill: #bc2e25; opacity: 0.10; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  .gene-track text { font-family: "JetBrains Mono", monospace; font-size: 9px; fill: #888; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  .track-axis-label {
436
  font-family: "JetBrains Mono", monospace; font-size: 9px;
437
  color: #888; text-transform: uppercase; letter-spacing: 1px;
@@ -1219,39 +1271,59 @@
1219
  <span>gene</span>
1220
  <span id="d1-pills" class="pills"></span>
1221
  <span class="spacer"></span>
1222
- <span>prefix</span>
1223
- <span id="d1-prefix-pills" class="pills">
1224
- <button class="pill" data-prefix="50">50</button>
1225
- <button class="pill active" data-prefix="200">200</button>
1226
- <button class="pill" data-prefix="400">400</button>
1227
- <button class="pill" data-prefix="600">600</button>
1228
- </span>
1229
- <span>generate</span>
1230
- <span id="d1-gen-pills" class="pills">
1231
- <button class="pill active" data-gen="60">60</button>
1232
- <button class="pill" data-gen="300">300</button>
1233
- <button class="pill" data-gen="600">600</button>
1234
- </span>
1235
  <button id="d1-go" class="action primary">▶ generate</button>
1236
  <button id="d1-stop" class="action" disabled>stop</button>
1237
  <span class="status" id="d1-status"><span class="dot"></span><span>idle</span></span>
1238
  </div>
1239
 
1240
  <div class="gene-info" id="d1-info">loading genes…</div>
1241
- <svg class="gene-track" id="d1-track" viewBox="0 0 1000 28" preserveAspectRatio="none"></svg>
1242
- <div class="track-axis-label">
1243
- <span><span class="legend-swatch" style="background:#317f3f"></span>exon</span>
1244
- <span><span class="legend-swatch" style="background:#aaa;height:2px;border-radius:0"></span>intron</span>
1245
- <span><span class="legend-swatch" style="background:rgba(188,46,37,0.20)"></span>generated region</span>
1246
- <span style="color:#bc2e25">│ prompt end</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1247
  </div>
1248
 
1249
- <div class="seq-label">model output · <span style="color:#aaa">prompt in gray</span> · <span>generated colored by logprob (red=uncertain)</span></div>
1250
  <div class="seq-block" id="d1-seq">— pick a gene and hit generate —</div>
1251
 
1252
- <div class="seq-label">reference · <span style="color:#b00020">mismatches highlighted</span></div>
1253
- <div class="seq-block" id="d1-ref">—</div>
1254
-
1255
  <div class="stat-row" id="d1-stats">
1256
  <div class="stat-pair"><span class="stat-pair-label">identity</span><span class="stat-pair-val muted" id="d1-id">—</span></div>
1257
  <div class="stat-pair"><span class="stat-pair-label">in-exon</span><span class="stat-pair-val muted" id="d1-id-exon">—</span></div>
@@ -1264,9 +1336,12 @@
1264
 
1265
  <div class="takeaway">
1266
  <strong>What to look for</strong>
1267
- The boundaries between high- and low-confidence stretches in Carbon's output tend to fall
1268
- near real exon/intron edges even though the model has never seen a single annotation.
1269
- For shorter genes (HBB, INS), Carbon often nails the exon/intron structure outright.
 
 
 
1270
  </div>
1271
  </section>
1272
 
@@ -2064,12 +2139,9 @@ function loadGenes() {
2064
  (function initDemo1() {
2065
  const els = {
2066
  pills: document.getElementById("d1-pills"),
2067
- prefixPills: document.getElementById("d1-prefix-pills"),
2068
- genPills: document.getElementById("d1-gen-pills"),
2069
  info: document.getElementById("d1-info"),
2070
  track: document.getElementById("d1-track"),
2071
  seq: document.getElementById("d1-seq"),
2072
- ref: document.getElementById("d1-ref"),
2073
  go: document.getElementById("d1-go"),
2074
  stop: document.getElementById("d1-stop"),
2075
  status: document.getElementById("d1-status"),
@@ -2083,9 +2155,14 @@ function loadGenes() {
2083
  };
2084
 
2085
  let gene = null;
2086
- let prefixLen = 200;
2087
- let genLen = 60;
 
 
 
 
2088
  let abortCtrl = null;
 
2089
 
2090
  let promptBases = "";
2091
  let genText = "";
@@ -2098,30 +2175,62 @@ function loadGenes() {
2098
  }
2099
 
2100
  function renderTrack() {
2101
- const W = 1000, H = 28;
2102
  if (!gene) { els.track.innerHTML = ""; return; }
2103
  const scaleX = (bp) => (bp / gene.length) * W;
 
 
2104
  let svg = "";
2105
  // Background line through introns
2106
- svg += `<line class="intron" x1="0" y1="${H/2}" x2="${W}" y2="${H/2}"/>`;
2107
  // Exon rectangles
2108
  for (const e of gene.exons) {
2109
  const x = scaleX(e.start);
2110
  const w = Math.max(1, scaleX(e.end - e.start));
2111
- svg += `<rect class="exon" x="${x.toFixed(1)}" y="6" width="${w.toFixed(1)}" height="16"/>`;
2112
  }
2113
- // Generated region (faint red box)
2114
- const genStart = scaleX(prefixLen);
2115
- const genEnd = scaleX(Math.min(gene.length, prefixLen + genLen));
2116
- svg += `<rect class="gen-region" x="${genStart.toFixed(1)}" y="0" width="${(genEnd - genStart).toFixed(1)}" height="${H}"/>`;
2117
- // Prompt-end playhead
2118
- svg += `<line class="playhead" x1="${genStart.toFixed(1)}" y1="0" x2="${genStart.toFixed(1)}" y2="${H}"/>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2119
  els.track.innerHTML = svg;
2120
  }
2121
 
 
 
 
 
 
 
 
2122
  function renderInfo() {
2123
  if (!gene) { els.info.textContent = "loading genes…"; return; }
2124
- els.info.innerHTML = `<strong>${gene.symbol}</strong> · ${gene.blurb} · <span style="color:#888">${gene.length.toLocaleString("en-US")} bp</span>`;
 
 
 
 
2125
  }
2126
 
2127
  function basesPerLine() {
@@ -2151,33 +2260,27 @@ function loadGenes() {
2151
  const total = prompt + genText;
2152
  const lpRange = lpRangeOf(genTokens);
2153
 
2154
- // Output: prompt in gray, generated colored by logprob
2155
- const colorOutput = (absIdx) => {
2156
  if (absIdx < prompt.length) {
2157
  return { style: `color:rgb(${PROMPT_RGB.join(",")})` };
2158
  }
2159
- const tok = genTokens[genTokenAtBase[absIdx - prompt.length]];
 
2160
  const [r, g, b] = logprobRgb(tok ? tok.logprob : null, lpRange);
2161
- return { style: `color:rgb(${r},${g},${b})` };
 
 
 
 
 
 
 
 
 
 
2162
  };
2163
  renderSeq(els.seq, total, bpl, colorOutput);
2164
-
2165
- // Reference: same window, but colored by match status (against generated bases)
2166
- if (!gene) { els.ref.classList.add("empty"); els.ref.textContent = "—"; return; }
2167
- const refEnd = Math.min(gene.length, prompt.length + genLen);
2168
- const refSeq = gene.seq.slice(0, refEnd);
2169
- const colorRef = (absIdx, base) => {
2170
- if (absIdx < prompt.length) {
2171
- return { style: `color:rgb(${PROMPT_RGB.join(",")})` };
2172
- }
2173
- const genIdx = absIdx - prompt.length;
2174
- if (genIdx >= genText.length) return { style: "color:#ccc" }; // not yet generated
2175
- const matches = genText[genIdx] === base;
2176
- return matches
2177
- ? { style: "color:#bbb" }
2178
- : { style: "color:#b00020;background:rgba(188,46,37,0.18)" };
2179
- };
2180
- renderSeq(els.ref, refSeq, bpl, colorRef);
2181
  }
2182
 
2183
  function updateStats() {
@@ -2187,7 +2290,7 @@ function loadGenes() {
2187
  });
2188
  return;
2189
  }
2190
- const refSlice = gene.seq.slice(promptBases.length, promptBases.length + genText.length);
2191
  let match = 0, total = 0;
2192
  let exonMatch = 0, exonTotal = 0;
2193
  let intronMatch = 0, intronTotal = 0;
@@ -2196,7 +2299,7 @@ function loadGenes() {
2196
  total++;
2197
  const ok = genText[i] === refSlice[i];
2198
  if (ok) match++;
2199
- const ann = annotationAt(promptBases.length + i);
2200
  if (ann === "exon") { exonTotal++; if (ok) exonMatch++; }
2201
  else if (ann === "intron") { intronTotal++; if (ok) intronMatch++; }
2202
  }
@@ -2212,10 +2315,11 @@ function loadGenes() {
2212
  }
2213
 
2214
  function reset() {
2215
- promptBases = gene ? gene.seq.slice(0, prefixLen) : "";
2216
  genText = "";
2217
  genTokens = [];
2218
  genTokenAtBase = [];
 
2219
  renderTrack();
2220
  renderSequenceAndRef();
2221
  updateStats();
@@ -2229,6 +2333,8 @@ function loadGenes() {
2229
  els.stop.disabled = false;
2230
  setStatus("connecting…", "streaming");
2231
 
 
 
2232
  try {
2233
  const resp = await fetch("/generate", {
2234
  method: "POST",
@@ -2297,8 +2403,11 @@ function loadGenes() {
2297
  const g = GENES.find(x => x.symbol === symbol);
2298
  if (!g) return;
2299
  gene = g;
 
 
 
 
2300
  els.pills.querySelectorAll(".pill").forEach(p => p.classList.toggle("active", p.dataset.gene === symbol));
2301
- renderInfo();
2302
  reset();
2303
  }
2304
 
@@ -2323,11 +2432,39 @@ function loadGenes() {
2323
  els.info.textContent = "failed to load genes: " + e.message;
2324
  });
2325
 
2326
- bindPills(els.prefixPills, "prefix", (v) => { prefixLen = +v; reset(); });
2327
- bindPills(els.genPills, "gen", (v) => { genLen = +v; reset(); });
2328
  els.go.addEventListener("click", generate);
2329
  els.stop.addEventListener("click", stop);
2330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2331
  window.addEventListener("resize", () => {
2332
  if (gene) renderSequenceAndRef();
2333
  });
 
427
  width: 100%; height: 28px; display: block;
428
  margin: 4px 0 8px;
429
  }
430
+ .gene-track.draggable { height: 40px; touch-action: none; }
431
  .gene-track .exon { fill: #317f3f; }
432
  .gene-track .intron { stroke: #aaa; stroke-width: 1; }
433
  .gene-track .playhead { stroke: #bc2e25; stroke-width: 2; }
434
+ .gene-track .gen-region { fill: #317f3f; opacity: 0.15; }
435
+ .gene-track .prompt-region { fill: #1f1f1d; opacity: 0.04; }
436
+ .gene-track .handle { cursor: ew-resize; }
437
+ .gene-track .handle line { stroke: #1f1f1d; stroke-width: 1.5; }
438
+ .gene-track .handle polygon { fill: #1f1f1d; }
439
+ .gene-track .handle:hover line,
440
+ .gene-track .handle.dragging line { stroke: #000; stroke-width: 2; }
441
+ .gene-track .handle:hover polygon,
442
+ .gene-track .handle.dragging polygon { fill: #000; }
443
+ .gene-track .handle.gen line { stroke: #317f3f; }
444
+ .gene-track .handle.gen polygon { fill: #317f3f; }
445
+ .gene-track .handle.gen:hover line,
446
+ .gene-track .handle.gen.dragging line { stroke: #1f5024; stroke-width: 2; }
447
+ .gene-track .handle.gen:hover polygon,
448
+ .gene-track .handle.gen.dragging polygon { fill: #1f5024; }
449
  .gene-track text { font-family: "JetBrains Mono", monospace; font-size: 9px; fill: #888; }
450
+
451
+ /* Instant tooltips (no native-title delay) for legend items. */
452
+ .legend-tip { position: relative; }
453
+ .legend-tip:hover::after {
454
+ content: attr(data-tip);
455
+ position: absolute;
456
+ bottom: calc(100% + 8px);
457
+ left: 50%;
458
+ transform: translateX(-50%);
459
+ background: #1f1f1d;
460
+ color: #f7f5ee;
461
+ padding: 6px 10px;
462
+ border-radius: 3px;
463
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
464
+ font-family: 'Inter', sans-serif;
465
+ font-size: 11px;
466
+ font-weight: 400;
467
+ letter-spacing: normal;
468
+ text-transform: none;
469
+ white-space: normal;
470
+ width: max-content;
471
+ max-width: 260px;
472
+ line-height: 1.4;
473
+ z-index: 10;
474
+ pointer-events: none;
475
+ }
476
+ .legend-tip:hover::before {
477
+ content: "";
478
+ position: absolute;
479
+ bottom: calc(100% + 2px);
480
+ left: 50%;
481
+ transform: translateX(-50%);
482
+ border: 4px solid transparent;
483
+ border-top-color: #1f1f1d;
484
+ z-index: 10;
485
+ pointer-events: none;
486
+ }
487
  .track-axis-label {
488
  font-family: "JetBrains Mono", monospace; font-size: 9px;
489
  color: #888; text-transform: uppercase; letter-spacing: 1px;
 
1271
  <span>gene</span>
1272
  <span id="d1-pills" class="pills"></span>
1273
  <span class="spacer"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
1274
  <button id="d1-go" class="action primary">▶ generate</button>
1275
  <button id="d1-stop" class="action" disabled>stop</button>
1276
  <span class="status" id="d1-status"><span class="dot"></span><span>idle</span></span>
1277
  </div>
1278
 
1279
  <div class="gene-info" id="d1-info">loading genes…</div>
1280
+ <svg class="gene-track draggable" id="d1-track" viewBox="0 0 1000 40" preserveAspectRatio="none"></svg>
1281
+ <div class="track-axis-label" style="justify-content:flex-end;gap:20px;align-items:center">
1282
+ <span class="legend-tip"
1283
+ data-tip="Exon — coding segment of the gene. Stays in the mature mRNA and gets translated into protein."
1284
+ style="display:inline-flex;align-items:center;gap:6px">
1285
+ <svg width="44" height="12" viewBox="0 0 44 12" style="overflow:visible">
1286
+ <line x1="0" y1="6" x2="14" y2="6" stroke="#aaa" stroke-width="1"/>
1287
+ <rect x="14" y="0" width="16" height="12" fill="#317f3f"/>
1288
+ <line x1="30" y1="6" x2="44" y2="6" stroke="#aaa" stroke-width="1"/>
1289
+ </svg>
1290
+ exon
1291
+ </span>
1292
+ <span class="legend-tip"
1293
+ data-tip="Intron — non-coding stretch between exons. Spliced out of the pre-mRNA before translation."
1294
+ style="display:inline-flex;align-items:center;gap:6px">
1295
+ <svg width="44" height="12" viewBox="0 0 44 12" style="overflow:visible">
1296
+ <rect x="0" y="0" width="6" height="12" fill="#317f3f"/>
1297
+ <line x1="6" y1="6" x2="38" y2="6" stroke="#aaa" stroke-width="1"/>
1298
+ <rect x="38" y="0" width="6" height="12" fill="#317f3f"/>
1299
+ </svg>
1300
+ intron
1301
+ </span>
1302
+ <span class="legend-tip"
1303
+ data-tip="Drag the dark ▼ and ▲ markers to set the DNA window fed to the model (the prompt). Drag the green ▼ marker to set where generation stops. The model fills in the green region."
1304
+ style="display:inline-flex;align-items:center;gap:6px">
1305
+ <svg width="100" height="20" viewBox="0 0 100 20" style="overflow:visible">
1306
+ <!-- prompt-region (faint dark) between start and end -->
1307
+ <rect x="10" y="4" width="30" height="12" fill="#1f1f1d" opacity="0.06"/>
1308
+ <!-- gen-region (muted green) between end and gen-end -->
1309
+ <rect x="40" y="4" width="50" height="12" fill="#317f3f" opacity="0.15"/>
1310
+ <!-- start handle: ▼ on top, line through body -->
1311
+ <line x1="10" y1="4" x2="10" y2="16" stroke="#1f1f1d" stroke-width="1.5"/>
1312
+ <polygon points="7,0 13,0 10,4" fill="#1f1f1d"/>
1313
+ <!-- end handle: ▲ on bottom, line through body -->
1314
+ <line x1="40" y1="4" x2="40" y2="16" stroke="#1f1f1d" stroke-width="1.5"/>
1315
+ <polygon points="40,16 37,20 43,20" fill="#1f1f1d"/>
1316
+ <!-- gen-end handle: ▼ on top, GREEN, line through body -->
1317
+ <line x1="90" y1="4" x2="90" y2="16" stroke="#317f3f" stroke-width="1.5"/>
1318
+ <polygon points="87,0 93,0 90,4" fill="#317f3f"/>
1319
+ </svg>
1320
+ prompt → generated
1321
+ </span>
1322
  </div>
1323
 
1324
+ <div class="seq-label">model output · <span style="color:#aaa">prompt in gray</span> · <span>generated colored by logprob (red = uncertain)</span> · <span><span style="color:#317f3f;font-weight:600">_</span> match</span> · <span><span style="color:#b00020;font-weight:600">_</span> mismatch</span></div>
1325
  <div class="seq-block" id="d1-seq">— pick a gene and hit generate —</div>
1326
 
 
 
 
1327
  <div class="stat-row" id="d1-stats">
1328
  <div class="stat-pair"><span class="stat-pair-label">identity</span><span class="stat-pair-val muted" id="d1-id">—</span></div>
1329
  <div class="stat-pair"><span class="stat-pair-label">in-exon</span><span class="stat-pair-val muted" id="d1-id-exon">—</span></div>
 
1336
 
1337
  <div class="takeaway">
1338
  <strong>What to look for</strong>
1339
+ Try dragging the prompt window so the green generated region lands on an exon (the dark
1340
+ green blocks) and see how many green underlines you get exons are under selection
1341
+ pressure, so getting them right takes real biological understanding, not just DNA
1342
+ statistics. Then try the same length over an intron and compare. Boundaries between
1343
+ high- and low-confidence stretches in Carbon's output also tend to fall near real
1344
+ exon/intron edges, even though the model has never seen a single annotation.
1345
  </div>
1346
  </section>
1347
 
 
2139
  (function initDemo1() {
2140
  const els = {
2141
  pills: document.getElementById("d1-pills"),
 
 
2142
  info: document.getElementById("d1-info"),
2143
  track: document.getElementById("d1-track"),
2144
  seq: document.getElementById("d1-seq"),
 
2145
  go: document.getElementById("d1-go"),
2146
  stop: document.getElementById("d1-stop"),
2147
  status: document.getElementById("d1-status"),
 
2155
  };
2156
 
2157
  let gene = null;
2158
+ let prefixStart = 0;
2159
+ let prefixEnd = 200;
2160
+ let genEnd = 260; // end of generated region (genLen = genEnd - prefixEnd)
2161
+ const MIN_PROMPT_BP = 6; // at least one BPE token's worth
2162
+ const MIN_GEN_BP = 6;
2163
+ const DEFAULT_GEN_BP = 60;
2164
  let abortCtrl = null;
2165
+ let dragging = null; // "start" | "end" | "genend" | null
2166
 
2167
  let promptBases = "";
2168
  let genText = "";
 
2175
  }
2176
 
2177
  function renderTrack() {
2178
+ const W = 1000, H = 40;
2179
  if (!gene) { els.track.innerHTML = ""; return; }
2180
  const scaleX = (bp) => (bp / gene.length) * W;
2181
+ // Track body sits y=8..32; arrows live at y=0..8 (start, top) and y=32..40 (end, bottom).
2182
+ const TRACK_TOP = 8, TRACK_BOT = 32, INTRON_Y = 20, EXON_Y = 14, EXON_H = 12;
2183
  let svg = "";
2184
  // Background line through introns
2185
+ svg += `<line class="intron" x1="0" y1="${INTRON_Y}" x2="${W}" y2="${INTRON_Y}"/>`;
2186
  // Exon rectangles
2187
  for (const e of gene.exons) {
2188
  const x = scaleX(e.start);
2189
  const w = Math.max(1, scaleX(e.end - e.start));
2190
+ svg += `<rect class="exon" x="${x.toFixed(1)}" y="${EXON_Y}" width="${w.toFixed(1)}" height="${EXON_H}"/>`;
2191
  }
2192
+ // Selected prompt region (very faint, between handles)
2193
+ const xStart = scaleX(prefixStart);
2194
+ const xEnd = scaleX(prefixEnd);
2195
+ svg += `<rect class="prompt-region" x="${xStart.toFixed(1)}" y="${TRACK_TOP}" width="${(xEnd - xStart).toFixed(1)}" height="${TRACK_BOT - TRACK_TOP}"/>`;
2196
+ // Generated region (muted green box, between prompt-end and gen-end handles)
2197
+ const xGenEnd = scaleX(genEnd);
2198
+ svg += `<rect class="gen-region" x="${xEnd.toFixed(1)}" y="${TRACK_TOP}" width="${(xGenEnd - xEnd).toFixed(1)}" height="${TRACK_BOT - TRACK_TOP}"/>`;
2199
+ // START handle: vertical line through the track body + downward triangle on top.
2200
+ svg += `<g class="handle${dragging === "start" ? " dragging" : ""}" data-role="start" transform="translate(${xStart.toFixed(1)},0)">`
2201
+ + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
2202
+ + `<polygon points="-4,0 4,0 0,${TRACK_TOP}"/>`
2203
+ + `<rect x="-8" y="0" width="16" height="${H}" fill="transparent"/>`
2204
+ + `</g>`;
2205
+ // END handle (prompt end / gen start): vertical line + upward triangle on bottom.
2206
+ svg += `<g class="handle${dragging === "end" ? " dragging" : ""}" data-role="end" transform="translate(${xEnd.toFixed(1)},0)">`
2207
+ + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
2208
+ + `<polygon points="0,${TRACK_BOT} -4,${H} 4,${H}"/>`
2209
+ + `<rect x="-8" y="0" width="16" height="${H}" fill="transparent"/>`
2210
+ + `</g>`;
2211
+ // GEN-END handle: vertical line + downward triangle on top, green.
2212
+ svg += `<g class="handle gen${dragging === "genend" ? " dragging" : ""}" data-role="genend" transform="translate(${xGenEnd.toFixed(1)},0)">`
2213
+ + `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
2214
+ + `<polygon points="-4,0 4,0 0,${TRACK_TOP}"/>`
2215
+ + `<rect x="-8" y="0" width="16" height="${H}" fill="transparent"/>`
2216
+ + `</g>`;
2217
  els.track.innerHTML = svg;
2218
  }
2219
 
2220
+ function bpFromClientX(clientX) {
2221
+ if (!gene) return 0;
2222
+ const rect = els.track.getBoundingClientRect();
2223
+ const frac = (clientX - rect.left) / rect.width;
2224
+ return Math.max(0, Math.min(gene.length, Math.round(frac * gene.length)));
2225
+ }
2226
+
2227
  function renderInfo() {
2228
  if (!gene) { els.info.textContent = "loading genes…"; return; }
2229
+ const promptLen = prefixEnd - prefixStart;
2230
+ const genLen = genEnd - prefixEnd;
2231
+ els.info.innerHTML = `<strong>${gene.symbol}</strong> · ${gene.blurb} · <span style="color:#888">${gene.length.toLocaleString("en-US")} bp</span>`
2232
+ + ` · <span style="color:#888">prompt: ${prefixStart}–${prefixEnd} (${promptLen} bp)</span>`
2233
+ + ` · <span style="color:#317f3f">generate: ${prefixEnd}–${genEnd} (${genLen} bp)</span>`;
2234
  }
2235
 
2236
  function basesPerLine() {
 
2260
  const total = prompt + genText;
2261
  const lpRange = lpRangeOf(genTokens);
2262
 
2263
+ // Output: prompt in gray; generated colored by logprob, underlined green/red by ref match.
2264
+ const colorOutput = (absIdx, base) => {
2265
  if (absIdx < prompt.length) {
2266
  return { style: `color:rgb(${PROMPT_RGB.join(",")})` };
2267
  }
2268
+ const genIdx = absIdx - prompt.length;
2269
+ const tok = genTokens[genTokenAtBase[genIdx]];
2270
  const [r, g, b] = logprobRgb(tok ? tok.logprob : null, lpRange);
2271
+ const refBase = gene ? gene.seq[prefixEnd + genIdx] : undefined;
2272
+ const ulColor = refBase == null
2273
+ ? "transparent"
2274
+ : (base === refBase ? "#317f3f" : "#b00020");
2275
+ return {
2276
+ style: `color:rgb(${r},${g},${b});`
2277
+ + `text-decoration:underline;`
2278
+ + `text-decoration-color:${ulColor};`
2279
+ + `text-decoration-thickness:1.5px;`
2280
+ + `text-underline-offset:2px`
2281
+ };
2282
  };
2283
  renderSeq(els.seq, total, bpl, colorOutput);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2284
  }
2285
 
2286
  function updateStats() {
 
2290
  });
2291
  return;
2292
  }
2293
+ const refSlice = gene.seq.slice(prefixEnd, prefixEnd + genText.length);
2294
  let match = 0, total = 0;
2295
  let exonMatch = 0, exonTotal = 0;
2296
  let intronMatch = 0, intronTotal = 0;
 
2299
  total++;
2300
  const ok = genText[i] === refSlice[i];
2301
  if (ok) match++;
2302
+ const ann = annotationAt(prefixEnd + i);
2303
  if (ann === "exon") { exonTotal++; if (ok) exonMatch++; }
2304
  else if (ann === "intron") { intronTotal++; if (ok) intronMatch++; }
2305
  }
 
2315
  }
2316
 
2317
  function reset() {
2318
+ promptBases = gene ? gene.seq.slice(prefixStart, prefixEnd) : "";
2319
  genText = "";
2320
  genTokens = [];
2321
  genTokenAtBase = [];
2322
+ renderInfo();
2323
  renderTrack();
2324
  renderSequenceAndRef();
2325
  updateStats();
 
2333
  els.stop.disabled = false;
2334
  setStatus("connecting…", "streaming");
2335
 
2336
+ const genLen = genEnd - prefixEnd;
2337
+
2338
  try {
2339
  const resp = await fetch("/generate", {
2340
  method: "POST",
 
2403
  const g = GENES.find(x => x.symbol === symbol);
2404
  if (!g) return;
2405
  gene = g;
2406
+ // Reset prompt + generate windows to defaults, clamped to this gene's length.
2407
+ prefixStart = 0;
2408
+ prefixEnd = Math.min(200, Math.max(MIN_PROMPT_BP, gene.length - DEFAULT_GEN_BP));
2409
+ genEnd = Math.min(gene.length, prefixEnd + DEFAULT_GEN_BP);
2410
  els.pills.querySelectorAll(".pill").forEach(p => p.classList.toggle("active", p.dataset.gene === symbol));
 
2411
  reset();
2412
  }
2413
 
 
2432
  els.info.textContent = "failed to load genes: " + e.message;
2433
  });
2434
 
 
 
2435
  els.go.addEventListener("click", generate);
2436
  els.stop.addEventListener("click", stop);
2437
 
2438
+ // Drag handles on the track to set the prompt range.
2439
+ els.track.addEventListener("pointerdown", (e) => {
2440
+ const target = e.target.closest(".handle");
2441
+ if (!target || !gene) return;
2442
+ dragging = target.dataset.role;
2443
+ els.track.setPointerCapture(e.pointerId);
2444
+ renderTrack(); // re-render so the picked handle shows its `.dragging` style
2445
+ e.preventDefault();
2446
+ });
2447
+ els.track.addEventListener("pointermove", (e) => {
2448
+ if (!dragging || !gene) return;
2449
+ const bp = bpFromClientX(e.clientX);
2450
+ if (dragging === "start") {
2451
+ prefixStart = Math.max(0, Math.min(bp, prefixEnd - MIN_PROMPT_BP));
2452
+ } else if (dragging === "end") {
2453
+ prefixEnd = Math.max(prefixStart + MIN_PROMPT_BP, Math.min(bp, genEnd - MIN_GEN_BP));
2454
+ } else if (dragging === "genend") {
2455
+ genEnd = Math.max(prefixEnd + MIN_GEN_BP, Math.min(bp, gene.length));
2456
+ }
2457
+ reset();
2458
+ });
2459
+ const endDrag = (e) => {
2460
+ if (!dragging) return;
2461
+ dragging = null;
2462
+ try { els.track.releasePointerCapture(e.pointerId); } catch (_) {}
2463
+ renderTrack();
2464
+ };
2465
+ els.track.addEventListener("pointerup", endDrag);
2466
+ els.track.addEventListener("pointercancel", endDrag);
2467
+
2468
  window.addEventListener("resize", () => {
2469
  if (gene) renderSequenceAndRef();
2470
  });