tfrere HF Staff Cursor commited on
Commit
28428b8
·
1 Parent(s): 39bf380

§5 Folding: in-flight indicator on viewers + stats while folding runs

Browse files

Clicking ▶ 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>

Files changed (1) hide show
  1. demo.html +49 -4
demo.html CHANGED
@@ -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
  }