Spaces:
Running
Running
§1: replace prefix/gen pills with three draggable handles
Browse filesTwo 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>
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: #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1242 |
-
<div class="track-axis-label">
|
| 1243 |
-
<span
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1268 |
-
|
| 1269 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 2087 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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="${
|
| 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="
|
| 2112 |
}
|
| 2113 |
-
//
|
| 2114 |
-
const
|
| 2115 |
-
const
|
| 2116 |
-
svg += `<rect class="
|
| 2117 |
-
//
|
| 2118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2119 |
els.track.innerHTML = svg;
|
| 2120 |
}
|
| 2121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2122 |
function renderInfo() {
|
| 2123 |
if (!gene) { els.info.textContent = "loading genes…"; return; }
|
| 2124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 2155 |
-
const colorOutput = (absIdx) => {
|
| 2156 |
if (absIdx < prompt.length) {
|
| 2157 |
return { style: `color:rgb(${PROMPT_RGB.join(",")})` };
|
| 2158 |
}
|
| 2159 |
-
const
|
|
|
|
| 2160 |
const [r, g, b] = logprobRgb(tok ? tok.logprob : null, lpRange);
|
| 2161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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(
|
| 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(
|
| 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 |
});
|