tfrere HF Staff Cursor commited on
Commit
286aae5
·
1 Parent(s): 543de2f

§5 Folding: HTML + CSS scaffolding with 3Dmol side-by-side viewers

Browse files

Replaces the "coming soon" stub with a real section that wires up the
/fold backend to a pair of 3Dmol cartoon viewers. Layout mirrors §1's
two-track convention (carbon completion on the left, reference on the
right) so the comparison feels native to the rest of the demo.

Frontend:
- Pulls 3Dmol 2.5.1 from jsDelivr (defer) — small enough to inline-cache,
no bundler needed. Pinned version for reproducibility.
- New CSS classes (.fold-grid, .fold-viewer, .fold-viewer-label,
.fold-legend, .fold-legend-bar). Two-column at >=720px, stacked below.
- Cartoon coloured by B-factor via 3Dmol's rwb gradient — ESMFold writes
pLDDT into the B-factor column so per-residue confidence shading is
free. Legend shows the same red→white→blue map.
- Hand-rolled pill UI for gene selection (HBB / INS / LYZ), reusing the
existing .pill and .pills classes.
- Stat row (residues, pLDDT mean carbon, pLDDT mean ref, 1D identity)
reuses .stat-row / .stat-pair, so the typography matches §1-§4.
- Viewers spin (auto-rotate) once loaded, like the ↻ glyph the stub used
to promise.

Scaffolding caveat:
For this commit both viewers are fed the same PDB so we can verify the
render path, the colouring, and the stat row end-to-end with a single
NIM call. The full DNA→ORF→AA→fold pipeline that produces a real
divergence between the two viewers lands in the next commit.

Touches only the §5 section + 3 small zones (<head> script, CSS block,
JS IIFE between §4 and §7). Everything else is left alone.

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (1) hide show
  1. demo.html +243 -7
demo.html CHANGED
@@ -7,6 +7,9 @@
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&family=Inter:wght@300;400;500;600&display=swap">
 
 
 
10
  <style>
11
  * { margin: 0; padding: 0; box-sizing: border-box; }
12
  html { scroll-behavior: smooth; }
@@ -329,6 +332,51 @@
329
  .stat-pair-val { color: #1f1f1d; font-variant-numeric: tabular-nums; }
330
  .stat-pair-val.muted { color: #aaa; }
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  /* Mismatch highlighting in reference row */
333
  .ref-mismatch { background: rgba(188, 46, 37, 0.18); color: #b00020; }
334
  .ref-match { color: #999; }
@@ -933,22 +981,67 @@
933
  </section>
934
 
935
  <!-- ============================================================ -->
936
- <!-- §5 — FOLDING (stub, coming soon) -->
937
  <!-- ============================================================ -->
938
  <section id="folding">
939
- <div class="section-num">§5 · Coming soon</div>
940
  <div class="section-title">From sequence to structure</div>
941
  <p class="lede">
942
  When Carbon completes an open reading frame, the resulting bases translate to a protein —
943
- a protein that folds. We'll plug the generated ORF into a folding model and render the
944
  3D structure inline, alongside the same protein folded from the reference sequence so you
945
  can see whether Carbon's continuation produced something biologically plausible.
946
  </p>
947
 
948
- <div class="stub" style="padding:64px 24px">
949
- <div class="stub-tag">coming soon</div>
950
- <div style="margin-top:6px">DNA → mRNA → protein → 3D structure</div>
951
- <div style="margin-top:18px;font-size:9px;color:#bbb;letter-spacing:1px">A T G G C C → M A → 🧬 ↻</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
952
  </div>
953
  </section>
954
 
@@ -2426,6 +2519,149 @@ function loadGenes() {
2426
  window.addEventListener("resize", () => renderAll());
2427
  })();
2428
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2429
  // =========================================================================
2430
  // §7 — Tokenizer (1-mer vs 6-mer)
2431
  // =========================================================================
 
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&family=Inter:wght@300;400;500;600&display=swap">
10
+ <!-- 3Dmol.js: lightweight WebGL molecular viewer, used by §5 (folding) to
11
+ render ESMFold-predicted protein cartoons. Pinned for reproducibility. -->
12
+ <script defer src="https://cdn.jsdelivr.net/npm/3dmol@2.5.1/build/3Dmol-min.js"></script>
13
  <style>
14
  * { margin: 0; padding: 0; box-sizing: border-box; }
15
  html { scroll-behavior: smooth; }
 
332
  .stat-pair-val { color: #1f1f1d; font-variant-numeric: tabular-nums; }
333
  .stat-pair-val.muted { color: #aaa; }
334
 
335
+ /* --- §5 Folding viewers --- */
336
+ /* Two square 3Dmol canvases side by side. On narrow screens (<720px) we
337
+ stack them vertically so each viewer keeps a comfortable size. */
338
+ .fold-grid {
339
+ display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
340
+ margin-top: 12px;
341
+ }
342
+ .fold-viewer-col { display: flex; flex-direction: column; }
343
+ .fold-viewer-label {
344
+ font-family: "JetBrains Mono", monospace;
345
+ font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1.5px;
346
+ margin-bottom: 4px;
347
+ }
348
+ .fold-viewer {
349
+ position: relative;
350
+ width: 100%; aspect-ratio: 1 / 1;
351
+ background: #fafaf7;
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;
359
+ font-family: "JetBrains Mono", monospace; font-size: 10px;
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;
366
+ display: flex; align-items: center; gap: 8px;
367
+ margin-top: 10px;
368
+ }
369
+ /* pLDDT colour key: red = low confidence, white = mid, blue = high.
370
+ Matches the 3Dmol "rwb" gradient applied to cartoon B-factors. */
371
+ .fold-legend-bar {
372
+ width: 120px; height: 6px;
373
+ background: linear-gradient(to right, #b00020 0%, #f0e8e0 50%, #2c5aa0 100%);
374
+ border-radius: 1px;
375
+ }
376
+ @media (max-width: 720px) {
377
+ .fold-grid { grid-template-columns: 1fr; }
378
+ }
379
+
380
  /* Mismatch highlighting in reference row */
381
  .ref-mismatch { background: rgba(188, 46, 37, 0.18); color: #b00020; }
382
  .ref-match { color: #999; }
 
981
  </section>
982
 
983
  <!-- ============================================================ -->
984
+ <!-- §5 — FOLDING (DNA protein → 3D structure via ESMFold) -->
985
  <!-- ============================================================ -->
986
  <section id="folding">
987
+ <div class="section-num">§5 · Folding</div>
988
  <div class="section-title">From sequence to structure</div>
989
  <p class="lede">
990
  When Carbon completes an open reading frame, the resulting bases translate to a protein —
991
+ a protein that folds. We feed the resulting ORF into ESMFold and render the
992
  3D structure inline, alongside the same protein folded from the reference sequence so you
993
  can see whether Carbon's continuation produced something biologically plausible.
994
  </p>
995
 
996
+ <div class="demo" id="demoFold">
997
+ <div class="demo-toolbar">
998
+ <span>gene</span>
999
+ <span id="dfold-pills" class="pills"></span>
1000
+ <span class="spacer"></span>
1001
+ <button id="dfold-go" class="action primary">▶ fold</button>
1002
+ <span class="status" id="dfold-status"><span class="dot"></span><span>idle</span></span>
1003
+ </div>
1004
+
1005
+ <div class="gene-info" id="dfold-info">pick a gene and hit fold</div>
1006
+
1007
+ <div class="seq-label">protein sequence (translated from reference ORF)</div>
1008
+ <div class="seq-block" id="dfold-aa">—</div>
1009
+
1010
+ <div class="fold-grid">
1011
+ <div class="fold-viewer-col">
1012
+ <div class="fold-viewer-label">carbon completion</div>
1013
+ <div class="fold-viewer" id="dfold-viewer-carbon">
1014
+ <div class="fold-empty">— no structure yet —</div>
1015
+ </div>
1016
+ </div>
1017
+ <div class="fold-viewer-col">
1018
+ <div class="fold-viewer-label">reference</div>
1019
+ <div class="fold-viewer" id="dfold-viewer-ref">
1020
+ <div class="fold-empty">— no structure yet —</div>
1021
+ </div>
1022
+ </div>
1023
+ </div>
1024
+
1025
+ <div class="fold-legend">
1026
+ pLDDT
1027
+ <span class="fold-legend-bar" aria-hidden="true"></span>
1028
+ low → high · drag to rotate · scroll to zoom
1029
+ </div>
1030
+
1031
+ <div class="stat-row" id="dfold-stats">
1032
+ <div class="stat-pair"><span class="stat-pair-label">residues</span><span class="stat-pair-val muted" id="dfold-n">—</span></div>
1033
+ <div class="stat-pair"><span class="stat-pair-label">pLDDT mean (carbon)</span><span class="stat-pair-val muted" id="dfold-plddt-c">—</span></div>
1034
+ <div class="stat-pair"><span class="stat-pair-label">pLDDT mean (ref)</span><span class="stat-pair-val muted" id="dfold-plddt-r">—</span></div>
1035
+ <div class="stat-pair"><span class="stat-pair-label">identity (1D)</span><span class="stat-pair-val muted" id="dfold-id">—</span></div>
1036
+ </div>
1037
+ </div>
1038
+
1039
+ <div class="takeaway">
1040
+ <strong>What to look for</strong>
1041
+ A high <em>pLDDT</em> means ESMFold is confident the predicted structure
1042
+ is correct for that residue. When Carbon's completion diverges at the
1043
+ base level but still produces a sequence whose 3D fold matches the
1044
+ reference, the model has captured something deeper than memorization.
1045
  </div>
1046
  </section>
1047
 
 
2519
  window.addEventListener("resize", () => renderAll());
2520
  })();
2521
 
2522
+ // =========================================================================
2523
+ // §5 — Folding (ESMFold via /fold + 3Dmol cartoon)
2524
+ //
2525
+ // SCAFFOLDING NOTE
2526
+ // ----------------
2527
+ // For this commit we hard-code three short reference proteins (HBB, INS,
2528
+ // LYZ) and fold each one to populate both viewers with the same structure.
2529
+ // That's enough to validate the layout, the 3Dmol render path, the pLDDT
2530
+ // colouring, and the stat row. The full DNA→ORF→AA→fold pipeline (which
2531
+ // will make the two viewers actually disagree) lands in the next commit.
2532
+ // =========================================================================
2533
+ (function initDemoFold() {
2534
+ // Famous short proteins, hand-picked because they fit ESMFold's 1024 aa
2535
+ // cap with room to spare and have textbook folds that are instantly
2536
+ // recognisable (HBB = the 8-helix globin sandwich, INS = the disulfide
2537
+ // hairpin, LYZ = the α+β lysozyme architecture).
2538
+ const SEQS = {
2539
+ HBB: "MVHLTPEEKSAVTALWGKVNVDEVGGEALGRLLVVYPWTQRFFESFGDLSTPDAVMGNPKVKAHGKKVLGAFSDGLAHLDNLKGTFATLSELHCDKLHVDPENFRLLGNVLVCVLAHHFGKEFTPPVQAAYQKVVAGVANALAHKYH",
2540
+ INS: "MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAEDLQVGQVELGGGPGAGSLQPLALEGSLQKRGIVEQCCTSICSLYQLENYCN",
2541
+ LYZ: "MKALIVLGLVLLSVTVQGKVFERCELARTLKRLGMDGYRGISLANWMCLAKWESGYNTRATNYNAGDRSTDYGIFQINSRYWCNDGKTPGAVNACHLSCSALLQDNIADAVACAKRVVRDPQGIRAWVAWRNRCQNRDVRQYVQGCGV",
2542
+ };
2543
+
2544
+ let currentGene = "HBB";
2545
+ let viewerCarbon = null;
2546
+ let viewerRef = null;
2547
+
2548
+ const els = {
2549
+ pills: document.getElementById("dfold-pills"),
2550
+ info: document.getElementById("dfold-info"),
2551
+ aa: document.getElementById("dfold-aa"),
2552
+ go: document.getElementById("dfold-go"),
2553
+ status: document.getElementById("dfold-status"),
2554
+ statusText: document.querySelector("#dfold-status span:last-child"),
2555
+ vCarbon: document.getElementById("dfold-viewer-carbon"),
2556
+ vRef: document.getElementById("dfold-viewer-ref"),
2557
+ nRes: document.getElementById("dfold-n"),
2558
+ plddtC: document.getElementById("dfold-plddt-c"),
2559
+ plddtR: document.getElementById("dfold-plddt-r"),
2560
+ identity: document.getElementById("dfold-id"),
2561
+ };
2562
+
2563
+ function renderPills() {
2564
+ els.pills.innerHTML = "";
2565
+ for (const g of Object.keys(SEQS)) {
2566
+ const b = document.createElement("button");
2567
+ b.className = "pill" + (g === currentGene ? " active" : "");
2568
+ b.dataset.gene = g;
2569
+ b.textContent = g;
2570
+ b.addEventListener("click", () => {
2571
+ currentGene = g;
2572
+ renderPills();
2573
+ renderInfo();
2574
+ });
2575
+ els.pills.appendChild(b);
2576
+ }
2577
+ }
2578
+
2579
+ function renderInfo() {
2580
+ const seq = SEQS[currentGene];
2581
+ els.info.innerHTML = `<strong>${currentGene}</strong> · ${seq.length} aa · click <em>fold</em> to predict structure`;
2582
+ // Wrap at 60 chars for readability — matches the seq-block widths used in §1.
2583
+ els.aa.textContent = seq.replace(/(.{60})/g, "$1\n");
2584
+ }
2585
+
2586
+ function setStatus(text, cls) {
2587
+ els.status.className = "status" + (cls ? " " + cls : "");
2588
+ els.statusText.textContent = text;
2589
+ }
2590
+
2591
+ async function fold(sequence) {
2592
+ const resp = await fetch("/fold", {
2593
+ method: "POST",
2594
+ headers: { "Content-Type": "application/json" },
2595
+ body: JSON.stringify({ sequence }),
2596
+ });
2597
+ return resp.json();
2598
+ }
2599
+
2600
+ function makeViewer(host) {
2601
+ if (!window.$3Dmol) return null;
2602
+ // Clear placeholder.
2603
+ host.innerHTML = "";
2604
+ return $3Dmol.createViewer(host, {
2605
+ backgroundColor: "#fafaf7",
2606
+ antialias: true,
2607
+ });
2608
+ }
2609
+
2610
+ function renderStructure(viewer, pdb) {
2611
+ if (!viewer) return;
2612
+ viewer.removeAllModels();
2613
+ viewer.addModel(pdb, "pdb");
2614
+ // Cartoon coloured by B-factor (pLDDT) using the rwb gradient.
2615
+ // ESMFold writes pLDDT into the B-factor column on every atom, so
2616
+ // this gives us per-residue confidence shading for free.
2617
+ viewer.setStyle({}, {
2618
+ cartoon: {
2619
+ colorscheme: { prop: "b", gradient: "rwb", min: 50, max: 100 },
2620
+ },
2621
+ });
2622
+ viewer.zoomTo();
2623
+ viewer.render();
2624
+ viewer.spin(true);
2625
+ }
2626
+
2627
+ async function runFold() {
2628
+ if (!window.$3Dmol) {
2629
+ setStatus("3Dmol not loaded — retry in a sec", "error");
2630
+ return;
2631
+ }
2632
+ const seq = SEQS[currentGene];
2633
+ setStatus(`folding ${currentGene}…`, "streaming");
2634
+ els.go.disabled = true;
2635
+ try {
2636
+ const result = await fold(seq);
2637
+ if (result.error) throw new Error(result.error);
2638
+ // Scaffolding: feed the same PDB to both viewers. Commit 4 will fold
2639
+ // a Carbon-generated sequence here and produce a real divergence.
2640
+ if (!viewerCarbon) viewerCarbon = makeViewer(els.vCarbon);
2641
+ if (!viewerRef) viewerRef = makeViewer(els.vRef);
2642
+ renderStructure(viewerCarbon, result.pdb);
2643
+ renderStructure(viewerRef, result.pdb);
2644
+
2645
+ els.nRes.textContent = result.n_residues ?? "—";
2646
+ els.plddtC.textContent = (result.plddt_mean ?? 0).toFixed(1);
2647
+ els.plddtR.textContent = (result.plddt_mean ?? 0).toFixed(1);
2648
+ els.identity.textContent = "100%";
2649
+ for (const el of [els.nRes, els.plddtC, els.plddtR, els.identity]) {
2650
+ el.classList.remove("muted");
2651
+ }
2652
+ setStatus(result.cached ? "folded (cached)" : "folded", "");
2653
+ } catch (e) {
2654
+ setStatus("error: " + (e.message || e), "error");
2655
+ } finally {
2656
+ els.go.disabled = false;
2657
+ }
2658
+ }
2659
+
2660
+ renderPills();
2661
+ renderInfo();
2662
+ els.go.addEventListener("click", runFold);
2663
+ })();
2664
+
2665
  // =========================================================================
2666
  // §7 — Tokenizer (1-mer vs 6-mer)
2667
  // =========================================================================