Spaces:
Running
§5 Folding: in-flight indicator on viewers + stats while folding runs
Browse filesClicking ▶ fold used to leave the cached cartoons and stats visible
unchanged while a fresh run was in flight — the only feedback was a
small status line ("carbon generating…") at the bottom of the toolbar,
which is easy to miss. Visitors couldn't tell whether the button did
anything for the 10-20s the pipeline takes.
Now during runFold:
- both viewers dim their canvas (opacity .28) and overlay a JetBrains
Mono "computing" label with a pulsing green dot; the cached cartoon
stays dimly visible underneath for visual continuity rather than
blanking out
- the overlay label updates per phase: "generating · 1608 bp" while
streaming from Carbon, "folding · esmfold" while the two ESMFold
calls are in flight
- the four stat slots (residues, both pLDDTs, identity) get muted to
signal their values are stale
- the action button reads "running…" instead of "▶ fold"
Everything reverts to the live result on success or to the cached
example on error, via the existing try/finally.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@@ -352,7 +352,7 @@
|
|
| 352 |
border: 1px solid #eee;
|
| 353 |
overflow: hidden;
|
| 354 |
}
|
| 355 |
-
.fold-viewer canvas { display: block; }
|
| 356 |
.fold-viewer .fold-empty {
|
| 357 |
position: absolute; inset: 0;
|
| 358 |
display: flex; align-items: center; justify-content: center;
|
|
@@ -360,6 +360,23 @@
|
|
| 360 |
color: #bbb; letter-spacing: 1.5px; text-transform: uppercase;
|
| 361 |
pointer-events: none;
|
| 362 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
.fold-legend {
|
| 364 |
font-family: "JetBrains Mono", monospace;
|
| 365 |
font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1.2px;
|
|
@@ -2813,8 +2830,8 @@ function loadGenes() {
|
|
| 2813 |
let viewersLinked = false;
|
| 2814 |
function ensureViewers() {
|
| 2815 |
if (!window.$3Dmol) return false;
|
| 2816 |
-
if (!viewerCarbon) viewerCarbon = makeViewer(els.vCarbon);
|
| 2817 |
-
if (!viewerRef) viewerRef = makeViewer(els.vRef);
|
| 2818 |
if (!viewersLinked && viewerCarbon && viewerRef &&
|
| 2819 |
typeof viewerCarbon.linkViewer === "function") {
|
| 2820 |
viewerCarbon.linkViewer(viewerRef);
|
|
@@ -2824,6 +2841,31 @@ function loadGenes() {
|
|
| 2824 |
return !!(viewerCarbon && viewerRef);
|
| 2825 |
}
|
| 2826 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2827 |
// Editorial pLDDT palette. The three anchor colours match the legend
|
| 2828 |
// bar under the viewers (#b00020 demo-red / #f0e8e0 paper-beige /
|
| 2829 |
// #2c5aa0 demo-blue) — same tones used throughout §1 mismatches and
|
|
@@ -2893,6 +2935,7 @@ function loadGenes() {
|
|
| 2893 |
abortCtrl?.abort();
|
| 2894 |
abortCtrl = new AbortController();
|
| 2895 |
els.go.disabled = true;
|
|
|
|
| 2896 |
|
| 2897 |
try {
|
| 2898 |
// --- Reference: spliced mRNA → longest ORF → AA -------------------
|
|
@@ -2910,6 +2953,7 @@ function loadGenes() {
|
|
| 2910 |
const genBP = Math.max(0, targetBP - prefixLen) + 60; // 60-bp safety margin
|
| 2911 |
const maxTokens = Math.ceil(genBP / 6) + 8;
|
| 2912 |
|
|
|
|
| 2913 |
setStatus(`carbon generating (${prefixLen}→${targetBP} bp)…`, "streaming");
|
| 2914 |
const continuation = await streamGenerate(promptDNA, maxTokens, 0.7, abortCtrl.signal);
|
| 2915 |
const carbonDNA = (promptDNA + continuation).slice(0, prefixLen + genBP);
|
|
@@ -2922,6 +2966,7 @@ function loadGenes() {
|
|
| 2922 |
}
|
| 2923 |
|
| 2924 |
// --- Fold both in parallel ----------------------------------------
|
|
|
|
| 2925 |
setStatus("folding both…", "streaming");
|
| 2926 |
const [carbonR, refR] = await Promise.all([
|
| 2927 |
postFold(carbonORF.aa),
|
|
@@ -2931,7 +2976,6 @@ function loadGenes() {
|
|
| 2931 |
if (refR.error) throw new Error("ref fold: " + refR.error);
|
| 2932 |
|
| 2933 |
// --- Render -------------------------------------------------------
|
| 2934 |
-
ensureViewers();
|
| 2935 |
renderStructure(viewerCarbon, carbonR.pdb);
|
| 2936 |
renderStructure(viewerRef, refR.pdb);
|
| 2937 |
|
|
@@ -2952,6 +2996,7 @@ function loadGenes() {
|
|
| 2952 |
if (e.name === "AbortError") setStatus("aborted", "");
|
| 2953 |
else setStatus("error: " + (e.message || e), "error");
|
| 2954 |
} finally {
|
|
|
|
| 2955 |
abortCtrl = null;
|
| 2956 |
els.go.disabled = false;
|
| 2957 |
}
|
|
|
|
| 352 |
border: 1px solid #eee;
|
| 353 |
overflow: hidden;
|
| 354 |
}
|
| 355 |
+
.fold-viewer canvas { display: block; transition: opacity .15s ease-out; }
|
| 356 |
.fold-viewer .fold-empty {
|
| 357 |
position: absolute; inset: 0;
|
| 358 |
display: flex; align-items: center; justify-content: center;
|
|
|
|
| 360 |
color: #bbb; letter-spacing: 1.5px; text-transform: uppercase;
|
| 361 |
pointer-events: none;
|
| 362 |
}
|
| 363 |
+
/* Loading overlay shown over the cartoon while runFold() is in flight.
|
| 364 |
+
The cached cartoon stays dimly visible underneath so the visitor can
|
| 365 |
+
still compare to it once the fresh result lands. */
|
| 366 |
+
.fold-viewer .fold-overlay {
|
| 367 |
+
position: absolute; inset: 0;
|
| 368 |
+
display: none; align-items: center; justify-content: center; gap: 8px;
|
| 369 |
+
background: rgba(250, 250, 247, 0.72);
|
| 370 |
+
font-family: "JetBrains Mono", monospace; font-size: 10px;
|
| 371 |
+
color: #555; letter-spacing: 1.5px; text-transform: uppercase;
|
| 372 |
+
pointer-events: none;
|
| 373 |
+
}
|
| 374 |
+
.fold-viewer .fold-overlay .dot {
|
| 375 |
+
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
| 376 |
+
background: #317f3f; animation: pulse 1.2s ease-in-out infinite;
|
| 377 |
+
}
|
| 378 |
+
.fold-viewer.running .fold-overlay { display: flex; }
|
| 379 |
+
.fold-viewer.running canvas { opacity: 0.28; }
|
| 380 |
.fold-legend {
|
| 381 |
font-family: "JetBrains Mono", monospace;
|
| 382 |
font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1.2px;
|
|
|
|
| 2830 |
let viewersLinked = false;
|
| 2831 |
function ensureViewers() {
|
| 2832 |
if (!window.$3Dmol) return false;
|
| 2833 |
+
if (!viewerCarbon) { viewerCarbon = makeViewer(els.vCarbon); attachOverlay(els.vCarbon); }
|
| 2834 |
+
if (!viewerRef) { viewerRef = makeViewer(els.vRef); attachOverlay(els.vRef); }
|
| 2835 |
if (!viewersLinked && viewerCarbon && viewerRef &&
|
| 2836 |
typeof viewerCarbon.linkViewer === "function") {
|
| 2837 |
viewerCarbon.linkViewer(viewerRef);
|
|
|
|
| 2841 |
return !!(viewerCarbon && viewerRef);
|
| 2842 |
}
|
| 2843 |
|
| 2844 |
+
// Inject the "running" overlay once per viewer host. CSS keeps it
|
| 2845 |
+
// hidden until the host gets the .running class via setRunning().
|
| 2846 |
+
function attachOverlay(host) {
|
| 2847 |
+
if (host.querySelector(".fold-overlay")) return;
|
| 2848 |
+
const o = document.createElement("div");
|
| 2849 |
+
o.className = "fold-overlay";
|
| 2850 |
+
o.innerHTML = '<span class="dot"></span><span class="fold-overlay-label">computing</span>';
|
| 2851 |
+
host.appendChild(o);
|
| 2852 |
+
}
|
| 2853 |
+
|
| 2854 |
+
// Toggle the running state on both viewers + the stat row. The cached
|
| 2855 |
+
// cartoon stays underneath at low opacity so the visitor still has
|
| 2856 |
+
// visual context while waiting (vs blanking everything to a spinner).
|
| 2857 |
+
function setRunning(running, label = "computing") {
|
| 2858 |
+
for (const host of [els.vCarbon, els.vRef]) {
|
| 2859 |
+
host.classList.toggle("running", running);
|
| 2860 |
+
const t = host.querySelector(".fold-overlay-label");
|
| 2861 |
+
if (t) t.textContent = label;
|
| 2862 |
+
}
|
| 2863 |
+
for (const el of [els.nRes, els.plddtC, els.plddtR, els.identity]) {
|
| 2864 |
+
el.classList.toggle("muted", running);
|
| 2865 |
+
}
|
| 2866 |
+
els.go.textContent = running ? "running…" : "▶ fold";
|
| 2867 |
+
}
|
| 2868 |
+
|
| 2869 |
// Editorial pLDDT palette. The three anchor colours match the legend
|
| 2870 |
// bar under the viewers (#b00020 demo-red / #f0e8e0 paper-beige /
|
| 2871 |
// #2c5aa0 demo-blue) — same tones used throughout §1 mismatches and
|
|
|
|
| 2935 |
abortCtrl?.abort();
|
| 2936 |
abortCtrl = new AbortController();
|
| 2937 |
els.go.disabled = true;
|
| 2938 |
+
ensureViewers(); // overlay must exist before we toggle .running on it
|
| 2939 |
|
| 2940 |
try {
|
| 2941 |
// --- Reference: spliced mRNA → longest ORF → AA -------------------
|
|
|
|
| 2953 |
const genBP = Math.max(0, targetBP - prefixLen) + 60; // 60-bp safety margin
|
| 2954 |
const maxTokens = Math.ceil(genBP / 6) + 8;
|
| 2955 |
|
| 2956 |
+
setRunning(true, `generating · ${targetBP} bp`);
|
| 2957 |
setStatus(`carbon generating (${prefixLen}→${targetBP} bp)…`, "streaming");
|
| 2958 |
const continuation = await streamGenerate(promptDNA, maxTokens, 0.7, abortCtrl.signal);
|
| 2959 |
const carbonDNA = (promptDNA + continuation).slice(0, prefixLen + genBP);
|
|
|
|
| 2966 |
}
|
| 2967 |
|
| 2968 |
// --- Fold both in parallel ----------------------------------------
|
| 2969 |
+
setRunning(true, "folding · esmfold");
|
| 2970 |
setStatus("folding both…", "streaming");
|
| 2971 |
const [carbonR, refR] = await Promise.all([
|
| 2972 |
postFold(carbonORF.aa),
|
|
|
|
| 2976 |
if (refR.error) throw new Error("ref fold: " + refR.error);
|
| 2977 |
|
| 2978 |
// --- Render -------------------------------------------------------
|
|
|
|
| 2979 |
renderStructure(viewerCarbon, carbonR.pdb);
|
| 2980 |
renderStructure(viewerRef, refR.pdb);
|
| 2981 |
|
|
|
|
| 2996 |
if (e.name === "AbortError") setStatus("aborted", "");
|
| 2997 |
else setStatus("error: " + (e.message || e), "error");
|
| 2998 |
} finally {
|
| 2999 |
+
setRunning(false);
|
| 3000 |
abortCtrl = null;
|
| 3001 |
els.go.disabled = false;
|
| 3002 |
}
|