1<!doctype html> 2<html lang="en" data-theme="dark"> 3<head> 4 <meta charset="utf-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 <title>Nexa_Mat Gen Stack</title> 7 <link rel="preconnect" href="https://fonts.googleapis.com"> 8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet"> 10 <style> 11 :root { 12 --bg: #04040c; 13 --panel: #080b17; 14 --panel-2: #0e1426; 15 --line: rgba(88, 119, 255, 0.22); 16 --text: #e6e8ff; 17 --muted: #8a93c5; 18 --soft: #2b335f; 19 --blue: #5577ff; 20 --cyan: #00ccff; 21 --violet: #c050ff; 22 --gold: #ffbb00; 23 --green: #30e58a; 24 --red: #ff667a; 25 --shadow: rgba(0,0,0,.34); 26 } 27 [data-theme="light"] { 28 --bg: #f6f8ff; 29 --panel: #ffffff; 30 --panel-2: #eef3ff; 31 --line: rgba(45, 69, 146, 0.18); 32 --text: #111827; 33 --muted: #55627f; 34 --soft: #d7dff6; 35 --shadow: rgba(38,51,94,.12); 36 } 37 * { box-sizing: border-box; } 38 body { 39 margin: 0; 40 width: 100%; 41 height: 100%; 42 overflow: hidden;
43 overscroll-behavior: none; 44 touch-action: none; 45 color: var(--text); 46 background: radial-gradient(circle at 22% 18%, rgba(85,119,255,.20), transparent 28%), var(--bg); 47 font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 48 } 49 button, input, textarea, select { font: inherit; } 50 button { 51 border: 1px solid var(--line); 52 background: linear-gradient(180deg, rgba(85,119,255,.18), rgba(85,119,255,.07)); 53 color: var(--text); 54 border-radius: 7px; 55 padding: 9px 11px; 56 cursor: pointer; 57 } 58 button:hover { border-color: rgba(85,119,255,.55); } 59 button:disabled { 60 cursor: wait; 61 opacity: .62; 62 border-color: color-mix(in srgb, var(--cyan) 45%, var(--line)); 63 } 64 textarea, input, select { 65 width: 100%; 66 border: 1px solid var(--line); 67 background: rgba(0,0,0,.12); 68 color: var(--text); 69 border-radius: 7px; 70 padding: 9px 10px; 71 outline: none; 72 } 73 [data-theme="light"] textarea, [data-theme="light"] input, [data-theme="light"] select { background: #f9fbff; } 74 label { display: grid; gap: 6px; font-size: 12px; color: var(--muted); } 75 input[type="file"] { 76 border-style: dashed; 77 padding: 10px; 78 background: color-mix(in srgb, var(--panel-2) 72%, transparent); 79 } 80 input[type="range"] { padding: 0; accent-color: var(--blue); } 81 html { width: 100%; height: 100%; overflow: hidden;
81 overscroll-behavior: none; } 82 .app { width: 100%; height: 100%; display: grid; grid-template-rows: 56px minmax(0, 1fr) 28px; overflow: hidden; } 83 header { 84 display: flex; 85 align-items: center; 86 gap: 16px; 87 padding: 0 18px; 88 border-bottom: 1px solid var(--line); 89 background: color-mix(in srgb, var(--panel) 88%, transparent); 90 box-shadow: 0 10px 32px var(--shadow); 91 z-index: 10; 92 } 93 .brand { display: flex; align-items: center; gap: 10px; min-width: 280px; } 94 .brand-mark { width: 4px; height: 28px; background: linear-gradient(var(--violet), var(--blue)); box-shadow: 0 0 16px var(--violet); } 95 .brand h1 { margin: 0; font-size: 15px; letter-spacing: .04em; } 96 .brand span { color: var(--blue); font: 11px JetBrains Mono, monospace; } 97 .top-status { display: flex; gap: 9px; align-items: center; color: var(--muted); font: 11px JetBrains Mono, monospace; } 98 .dot { width: 7px; height: 7px; border-radius: 999px; background: var(--green); box-shadow: 0 0 10px var(--green); } 99 .main { min-height: 0; min-width: 0; display: grid; grid-template-columns: minmax(250px, 310px) minmax(360px, 1fr) minmax(320px, 420px); overflow: hidden; } 100 .rail, .side { 101 min-height: 0; 102 overflow: auto; 103 background: color-mix(in srgb, var(--panel) 92%, transparent); 104 border-right: 1px solid var(--line); 105 padding: 14px; 106 } 107 .side { border-right: 0; border-left: 1px solid var(--line); } 108 .viewer-wrap { min-width: 0; min-height: 0; position: relative; overflow: hidden; background: var(--bg);
108 overscroll-behavior: none; touch-action: none; } 109 #viewer { position: absolute; inset: 0; } 110 #viewer canvas { display: block; width: 100% !important; height: 100% !important; overscroll-behavior: none; touch-action: none; } 111 .hud { 112 position: absolute; 113 left: 14px; 114 top: 14px; 115 display: none; 116 gap: 8px; 117 flex-wrap: wrap; 118 pointer-events: none; 119 } 120 .pill { 121 border: 1px solid var(--line); 122 border-radius: 999px; 123 padding: 6px 9px; 124 color: var(--muted); 125 background: color-mix(in srgb, var(--panel) 82%, transparent); 126 font: 11px JetBrains Mono, monospace; 127 } 128 .viewer-footer { 129 position: absolute; 130 left: 0; 131 right: 0; 132 bottom: 0; 133 display: flex; 134 align-items: center; 135 justify-content: space-between; 136 gap: 12px; 137 height: 34px; 138 padding: 0 14px; 139 background: color-mix(in srgb, var(--panel) 82%, transparent); 140 border-top: 1px solid var(--line); 141 color: var(--muted); 142 font: 11px JetBrains Mono, monospace; 143 } 144 .atom-tip { 145 position: absolute; 146 display: none; 147 pointer-events: none; 148 z-index: 8; 149 max-width: 230px; 150 border: 1px solid var(--line); 151 border-radius: 8px; 152 padding: 9px 10px; 153 background: color-mix(in srgb, var(--panel) 92%, transparent); 154 box-shadow: 0 14px 38px var(--shadow); 155 color: var(--text); 156 font-size: 12px; 157 line-height: 1.45; 158 } 159 .atom-tip b { display: block; font-size: 13px; margin-bottom: 3px; } 160 .atom-tip span { color: var(--muted); font: 11px JetBrains Mono, monospace; } 161 .run-progress { 162 position: absolute; 163 left: 50%; 164 top: 18px; 165 width: min(420px, calc(100% - 42px)); 166 transform: translateX(-50%) translateY(-8px); 167 z-index: 7; 168 opacity: 0; 169 pointer-events: none; 170 border: 1px solid color-mix(in srgb, var(--cyan) 35%, var(--line)); 171 border-radius: 8px; 172 padding: 11px 12px 10px; 173 background: color-mix(in srgb, var(--panel) 88%, transparent); 174 box-shadow: 0 18px 48px var(--shadow); 175 transition: opacity .18s ease, transform .18s ease; 176 } 177 .run-progress.active { 178 opacity: 1; 179 transform: translateX(-50%) translateY(0); 180 } 181 .progress-copy { 182 display: flex; 183 justify-content: space-between; 184 gap: 12px; 185 color: var(--text); 186 font-size: 12px; 187 font-weight: 700; 188 letter-spacing: .02em; 189 } 190 .progress-copy span:last-child { 191 color: var(--cyan); 192 font: 700 11px JetBrains Mono, monospace; 193 } 194 .progress-track { 195 height: 7px; 196 margin-top: 8px; 197 border-radius: 999px; 198 overflow: hidden; 199 background: color-mix(in srgb, var(--soft) 70%, transparent); 200 } 201 .progress-fill { 202 width: 0%; 203 height: 100%; 204 border-radius: inherit; 205 background: linear-gradient(90deg, var(--blue), var(--cyan), var(--green)); 206 box-shadow: 0 0 18px color-mix(in srgb, var(--cyan) 45%, transparent); 207 transition: width .22s ease; 208 } 209 .progress-stage { 210 margin-top: 7px; 211 color: var(--muted); 212 font: 11px JetBrains Mono, monospace; 213 } 214 .card { 215 background: linear-gradient(180deg, color-mix(in srgb, var(--panel-2) 86%, transparent), color-mix(in srgb, var(--panel) 92%, transparent)); 216 border: 1px solid var(--line); 217 border-radius: 8px; 218 padding: 12px; 219 margin-bottom: 12px; 220 } 221 .card h2, .card h3 { margin: 0 0 8px; font-size: 13px; letter-spacing: .03em; } 222 .subtle { color: var(--muted); font-size: 12px; line-height: 1.55; } 223 .metric { display: grid; grid-template-columns: 1fr auto; gap: 8px; margin: 8px 0; font-size: 12px; } 224 .bar { grid-column: 1 / -1; height: 6px; border-radius: 999px; background: color-mix(in srgb, var(--soft) 65%, transparent); overflow: hidden; } 225 .fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--blue), var(--cyan)); } 226 .stack-step { border-left: 2px solid var(--soft); padding: 8px 0 8px 12px; margin-left: 6px; } 227 .stack-step.active { border-color: var(--cyan); } 228 .stack-step b { display: block; font-size: 12px; } 229 .stack-step span { color: var(--muted); font-size: 11px; line-height: 1.45; } 230 .tabs { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; margin-bottom: 12px; } 231 .tabs button.active { border-color: var(--cyan); box-shadow: inset 0 -2px 0 var(--cyan); } 232 .tab { display: none; } 233 .tab.active { display: block; } 234 .row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } 235 .candidate { 236 display: grid; 237 grid-template-columns: auto 1fr auto; 238 gap: 8px; 239 align-items: center; 240 padding: 8px 0; 241 border-top: 1px solid var(--line); 242 border-radius: 7px; 243 } 244 .candidate.pickable { 245 cursor: pointer; 246 padding: 8px 7px; 247 margin: 0 -7px; 248 } 249 .candidate.pickable:hover { background: color-mix(in srgb, var(--soft) 28%, transparent); } 250 .candidate.selected { 251 background: color-mix(in srgb, var(--blue) 18%, transparent); 252 border-top-color: color-mix(in srgb, var(--cyan) 50%, var(--line)); 253 box-shadow: inset 2px 0 0 var(--cyan); 254 }
255 .candidate-controls { 256 display: grid; 257 grid-template-columns: auto 1fr auto; 258 gap: 8px; 259 align-items: center; 260 margin: 8px 0 6px; 261 } 262 .candidate-controls button { padding: 7px 9px; } 263 .candidate-count { text-align: center; color: var(--muted); font: 11px JetBrains Mono, monospace; } 264 .rank { color: var(--gold); font: 700 12px JetBrains Mono, monospace; } 265 .score { color: var(--green); font: 700 12px JetBrains Mono, monospace; } 266 footer { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; color: var(--muted); border-top: 1px solid var(--line); font: 10px JetBrains Mono, monospace; } 267 @media (max-width: 1120px) { 268 .main { grid-template-columns: minmax(220px, 280px) minmax(300px, 1fr) minmax(280px, 360px); } 269 .rail, .side { padding: 10px; } 270 .brand { min-width: 220px; } 271 } 272 </style> 273</head> 274<body> 275 <div class="app"> 276 <header> 277 <div class="brand"> 278 <div class="brand-mark"></div> 279 <div><h1>Nexa_Mat Gen Stack</h1><span>materials science · generative on demand</span></div> 280 </div> 281 <div class="top-status"><span class="dot"></span><span id="run-state">ready</span></div> 282 <div style="flex:1"></div> 283 <button id="theme-btn">Light mode</button> 284 </header> 285 <main class="main"> 286 <aside class="rail"> 287 <div class="card"> 288 <h2>Generated Structure</h2> 289 <div id="metrics"></div> 290 </div> 291 <div class="card"> 292 <h2>Explanation</h2> 293 <div id="structure-explanation" class="subtle">Generate a candidate to inspect structure-level reasoning.</div> 294 </div> 295 <div class="card"> 296 <h2>Applications</h2> 297 <div id="applications" class="subtle">Application hypotheses appear here after generation or reverse ranking.</div> 298 </div> 299 <div class="card"> 300 <h2>Candidate Distribution</h2> 301 <div class="candidate-controls"> 302 <button id="prev-candidate">Prev</button> 303 <span id="candidate-count" class="candidate-count">No samples yet</span> 304 <button id="next-candidate">Next</button> 305 </div> 306 <div id="candidates"></div> 307 </div> 308 <div class="card"> 309 <h2>Reverse Results</h2> 310 <div id="reverse-results" class="subtle">Run reverse pool search to rank a candidate list.</div> 311 </div> 312 </aside> 313 <section class="viewer-wrap"> 314 <div id="viewer"></div> 315 <div class="run-progress" id="run-progress" aria-live="polite"> 316 <div class="progress-copy"><span id="progress-title">Generating candidate</span><span id="progress-percent">0%</span></div> 317 <div class="progress-track"><div class="progress-fill" id="progress-fill"></div></div> 318 <div class="progress-stage" id="progress-stage">Preparing model stack</div> 319 </div> 320 <div class="hud"> 321 <div class="pill" id="formula-pill">La7Ge7Te7</div> 322 <div class="pill" id="score-pill">stack 0.782</div> 323 <div class="pill" id="mind-pill">min d 1.104 à </div> 324 <div class="pill">drag rotate · fixed zoom</div> 325 </div> 326 <div id="atom-tooltip" class="atom-tip"></div> 327 <div class="viewer-footer"> 328 <span>Encoder constrains physical plausibility · diffusion expands candidates · multimodal layer supplies evidence</span> 329 <span id="hover-label">hover atoms for species</span> 330 </div> 331 </section> 332 <aside class="side"> 333 <div class="tabs"> 334 <button class="active" data-tab="tools">Tools</button> 335 <button data-tab="stack">Models</button> 336 <button data-tab="about">Guide</button> 337 </div> 338 <section id="tab-tools" class="tab active"> 339 <div class="card"> 340 <h2>Forward Generation</h2> 341 <p class="subtle">Generate a plausible crystal candidate from constraints, then trace encoder, diffusion, and multimodal agreement.</p> 342 <label>Forward use case<textarea id="prompt" rows="3">Generate a thermoelectric telluride with stable local geometry</textarea></label> 343 <div class="row" style="margin-top:10px"> 344 <label>Allowed elements<input id="elements" value="La Ge Te"></label> 345 <label>Samples<input id="samples" type="number" min="4" max="64" value="12"></label> 346 </div> 347 <label style="margin-top:10px">Forward constraints<input id="forward-constraints" value="baseline geometry pass, conservative lattice, DFT triage candidate"></label> 348 <label style="margin-top:10px">Novelty temperature <span id="novelty-value">0.42</span><input id="novelty" type="range" min="0" max="1" step="0.01" value="
3480.42"></label> 349 <label style="margin-top:10px">Lattice strictness <span id="lattice-value">0.72</span><input id="lattice" type="range" min="0" max="1" step="0.01" value="0.72"></label> 350 <div class="row" style="margin-top:10px"> 351 <button id="generate">Build Stack</button> 352 <button id="reset-view">Reset View</button> 353 </div> 354 </div> 355 <div class="card"> 356 <h2>Reverse Pool Search</h2> 357 <p class="subtle">Upload or paste a CSV, JSON, or JSONL candidate pool. The stack goes in reverse: parse candidates, score them against the use case, then rank what should be inspected first.</p> 358 <label>Reverse use case<input id="target" value="stable ion conductor with conservative geometry"></label> 359 <label style="margin-top:10px">Pool file (.csv, .json, .jsonl)<input id="pool-file" type="file" accept=".csv,.json,.jsonl,application/json,text/csv,application/jsonl"></label> 360 <label style="margin-top:10px">Candidate pool<textarea id="pool" rows="6">Li P S Cl | sulfide electrolyte 361La Ge Te | thermoelectric 362Na Zr O | oxide conductor 363Li Zr O Cl | stable halide conductor</textarea></label> 364 <label style="margin-top:10px">Reverse constraints<input id="reverse-constraints" value="physical plausibility, evidence support, application fit"></label> 365 <button id="rank" style="margin-top:10px;width:100%">Rank Pool</button> 366 </div> 367 <div class="card"> 368 <h2>Viewer Knobs</h2> 369 <div class="row"> 370 <button id="rotate">Rotate: on</button> 371 <button id="bonds">Bonds: on</button> 372 <button id="cell">Cell: on</button> 373 <button id="sample-dist">Sample 512</button> 374 </div> 375 </div> 376 </section> 377 <section id="tab-stack" class="tab"> 378 <div class="card"> 379 <h2>Realtime Inference Pass</h2> 380 <div class="stack-step active"><b>1. Physics Encoder</b><span id="encoder-text">Validates atoms, local distances, lattice volume, and manifold score.</span></div> 381 <div class="stack-step active"><b>2. Diffusion Generator</b><span id="diffusion-text">Repairs noisy coordinates and proposes candidate variants under constraints.</span></div> 382 <div class="stack-step active"><b>3. Multimodal Controller</b><span id="mm-text">Adds application context and evidence while flagging unsupported claims.</span></div> 383 <div class="stack-step"><b>4. Decision Layer</b><span id="decision-text">Selects candidates for DFT follow-up or reverse ranking.</span></div> 384 </div> 385 <div class="card"> 386 <h2>Adapter Contract</h2> 387 <p class="subtle">This app is wired so local demo functions can be replaced with real endpoints: `encode(structure)`, `diffuse(prompt, constraints)`, `ground(candidate, context)`, and `rank(pool, target)`.</p> 388 </div> 389 </section> 390 <section id="tab-about" class="tab"> 391 <div class="card"> 392 <h2>How To Use This App</h2> 393 <p class="subtle">Use <b>Forward Generation</b> when you want the stack to propose a new material from a design intent and element constraints. Use <b>Reverse Pool Search</b> when you already have candidates and want the stack to rank them against a target application.</p> 394 <p class="subtle">The 3D view is inspection-first: drag to rotate, scroll to zoom, hover atoms for species details, and toggle bonds or the unit cell when the structure gets dense.</p> 395 </div> 396 <div class="card"> 397 <h2>What It Can Do</h2> 398 <p class="subtle">It can sketch candidate crystals, show basic geometry health, expose candidate distributions, rank a pool, and explain which model lane is responsible for each part of the decision.</p> 399 </div> 400 <div class="card"> 401 <h2>Limits</h2> 402 <p class="subtle">This is a UI/controller render. It is not a DFT result and should not be treated as proof of stability. Production use should plug in the frozen encoder, diffusion checkpoint, multimodal evidence model, and downstream DFT triage.</p> 403 </div> 404 </section> 405 </aside> 406 </main> 407 <footer><span>Nexa_Mat Gen Stack · local app.py server</span><span>La / Ge / Te seed candidate · Three.js visual retained</span></footer> 408 </div> 409
409<script type="importmap"> 410 { 411 "imports": { 412 "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js" 413 } 414 } 415 </script>
415 416
416<script type="module"> 417 import * as THREE from "three"; 418 import { OrbitControls } from "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js"; 419 420 const elements = { 421 La: { z: 57, name: "Lanthanum", color: "#ffbb00", radius: 1.87 }, 422 Ge: { z: 32, name: "Germanium", color: "#c050ff", radius: 1.22 }, 423 Te: { z: 52, name: "Tellurium", color: "#00ccff", radius: 1.40 }, 424 Li: { z: 3, name: "Lithium", color: "#7c6cff", radius: 1.28 }, 425 O: { z: 8, name: "Oxygen", color: "#ef4444", radius: 0.66 }, 426 S: { z: 16, name: "Sulfur", color: "#eab308", radius: 1.05 }, 427 P: { z: 15, name: "Phosphorus", color: "#f97316", radius: 1.07 }, 428 Na: { z: 11, name: "Sodium", color: "#38bdf8", radius: 1.66 }, 429 Zr: { z: 40, name: "Zirconium", color: "#94a3b8", radius: 1.60 }, 430 Cl: { z: 17, name: "Chlorine", color: "#22c55e", radius: 1.02 } 431 }; 432 const baseAtoms = [ 433 [2.5,1,0.5],[5.5,1,0.5],[2.5,4.5,0.5],[5.5,4.5,3.5],[5.5,4.5,6.5],[5.5,7,6.5],[5.5,7,7.5], 434 [5.5,7,8.5],[8,6.5,7.5],[8,6.5,6.5],[8,8.5,8.5],[5.5,9.5,9.5],[5.5,1,2],[8,7,8], 435 [5.5,7,5],[5.5,1.5,0.5],[10.5,1.5,2],[5.5,9.5,10.5],[7,1.5,0.5],[7,9.5,9.5],[7,9.5,10.5] 436 ]; 437 const latticeBase = { a: [10.5,0,0], b: [2.5,10.5,0], c: [0,2.5,10.5] }; 438 let state = { autoRotate: true, showBonds: true, showCell: true, structure: null }; 439 let scene, camera, renderer, controls, root, raycaster, pointer; 440 const atomMeshes = []; 441 442 function seedHash(text) { 443 let h = 2166136261; 444 for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); } 445 return h >>> 0; 446 } 447 function rng(seed) { 448 let t = seed + 0x6D2B79F5; 449 return () => { 450 t = Math.imul(t ^ t >>> 15, t | 1); 451 t ^= t + Math.imul(t ^ t >>> 7, t | 61); 452 return ((t ^ t >>> 14) >>> 0) / 4294967296; 453 }; 454 } 455 function parseElements(raw) { 456 const found = raw.replaceAll(",", " ").replaceAll("+", " ").split(/\s+/).map(x => x.trim()).filter(Boolean) 457 .map(x => x[0]?.toUpperCase() + x.slice(1).toLowerCase()).filter(x => elements[x]); 458 return [...new Set(found)].length ? [...new Set(found)] : ["La", "Ge", "Te"]; 459 } 460 function buildStructure(prompt, rawElements, novelty, strictness, index = 0) { 461 const selected = parseElements(rawElements); 462 const rand = rng(seedHash(`${prompt}|${rawElements}|${novelty}|${strictness}|${index}`)); 463 const jitter = 0.04 + novelty * 0.22; 464 const scale = 0.94 + strictness * 0.12; 465 const atoms = baseAtoms.map((p, i) => { 466 const symbol = selected[i % selected.length]; 467 const meta = elements[symbol]; 468 return { 469 id: i, 470 symbol, 471 name: meta.name, 472 atomicNumber: meta.z, 473 color: meta.color, 474 radius: meta.radius, 475 x: +(p[0] * scale + (rand() - .5) * jitter).toFixed(3), 476 y: +(p[1] * scale + (rand() - .5) * jitter).toFixed(3), 477 z: +(p[2] * scale + (rand() - .5) * jitter).toFixed(3) 478 }; 479 }); 480 const lattice = Object.fromEntries(Object.entries(latticeBase).map(([k, vec]) => [k, vec.map(v => +(v * scale + (rand() - .5) * 0.04 * (1 - strictness)).toFixed(3))])); 481 const metrics = scoreStructure(atoms, novelty, strictness); 482 const formula = formulaFromAtoms(atoms); 483 const family = selected.some(x => ["Li","Na","S","P","Cl"].includes(x)) ? "ion-conductor candidate" : "inorganic discovery candidate"; 484 return { atoms, lattice, metrics, formula, family, selected }; 485 } 486 function formulaFromAtoms(atoms) { 487 const counts = {}; 488 atoms.forEach(a => counts[a.symbol] = (counts[a.symbol] || 0) + 1); 489 return Object.entries(counts).map(([k,v]) => `${k}${v > 1 ? v : ""}`).join(""); 490 } 491 function dist(a,b) { return Math.hypot(a.x-b.x, a.y-b.y, a.z-b.z); } 492 function scoreStructure(atoms, novelty, strictness) { 493 const ds = []; 494 for (let i=0; i<atoms.length; i++) for (let j=i+1; j<atoms.length; j++) ds.push(dist(atoms[i], atoms[j])); 495 const minD = Math.min(...ds); 496 const violations = ds.filter(d => d < .85).length / ds.length; 497 const valid = minD >= .85 ? 1 : 0; 498 const encoder = clamp(.58 + .22 * valid + .12 * strictness - .05 * novelty); 499 const diffusion = clamp(.54 + .16 * Math.min(1, minD / 1.3) + .12 * (1 - Math.abs(novelty - .5))); 500 const evidence = clamp(.50 + .20 * encoder + .10 * strictness); 501 const stack = clamp(encoder * .42 + diffusion * .36 + evidence * .22); 502 return { minD, violations, valid, encoder, diffusion, evidence, stack }; 503 } 504 function clamp(x) { return Math.max(0, Math.min(1, x)); } 505 function fmt(x) { return Number(x).toFixed(3); } 506 507 function initScene() { 508 root = document.getElementById("viewer"); 509 renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 510 renderer.setSize(root.clientWidth, root.clientHeight); 511 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 512 root.appendChild(renderer.domElement); 513 scene = new THREE.Scene(); 514 camera = new THREE.PerspectiveCamera(45, root.clientWidth / root.clientHeight, .1, 500); 515 camera.position.set(24, 20, 24); 516 camera.lookAt(0, 0, 0); 517 controls = new OrbitControls(camera, renderer.domElement); 518 controls.target.set(0, 0, 0); 519 controls.enableDamping = true; 520 controls.enableZoom = false;
521 controls.enablePan = false; 522 controls.autoRotate = true; 523 controls.autoRotateSpeed = .8; 524 raycaster = new THREE.Raycaster(); 525 pointer = new THREE.Vector2(); 526 renderer.domElement.addEventListener("mousemove", onPointerMove); 527 renderer.domElement.addEventListener("mouseleave", hideAtomTip); 528 renderer.domElement.addEventListener("wheel", event => { 529 event.preventDefault(); 530 event.stopPropagation(); 531 }, { passive: false }); 532 window.addEventListener("wheel", event => { 533 if (event.target && event.target.closest && event.target.closest(".viewer-wrap")) { 534 event.preventDefault(); 535 event.stopPropagation(); 536 } 537 }, { passive: false }); 538 window.addEventListener("touchmove", event => { 539 if (event.target && event.target.closest && event.target.closest(".viewer-wrap")) { 540 event.preventDefault(); 541 event.stopPropagation(); 542 } 543 }, { passive: false }); 544 scene.add(new THREE.AmbientLight(0xffffff, 1.15)); 545 const key = new THREE.DirectionalLight(0xffffff, 1.9); key.position.set(20, 30, 20); scene.add(key); 546 const violet = new THREE.PointLight(0xc050ff, 2.0, 50); violet.position.set(-8, 10, -5); scene.add(violet); 547 const cyan = new THREE.PointLight(0x00ccff, 1.8, 50); cyan.position.set(15, -5, 15); scene.add(cyan); 548 window.addEventListener("resize", onResize); 549 animate(); 550 } 551 function clearScene() { 552 while (scene.children.length > 0) scene.remove(scene.children[0]); 553 atomMeshes.length = 0; 554 scene.add(new THREE.AmbientLight(0xffffff, 1.15)); 555 const key = new THREE.DirectionalLight(0xffffff, 1.9); key.position.set(20, 30, 20); scene.add(key); 556 const violet = new THREE.PointLight(0xc050ff, 2.0, 50); violet.position.set(-8, 10, -5); scene.add(violet); 557 const cyan = new THREE.PointLight(0x00ccff, 1.8, 50); cyan.position.set(15, -5, 15); scene.add(cyan); 558 }
559 function renderStructure(structure) { 560 state.structure = structure; 561 clearScene(); 562 camera.lookAt(0, 0, 0); 563 controls.target.set(0, 0, 0); 564 const themeLight = document.documentElement.dataset.theme === "light"; 565 scene.fog = new THREE.FogExp2(themeLight ? 0xf6f8ff : 0x04040c, 0.018); 566 const grid = new THREE.GridHelper(42, 42, themeLight ? 0xcbd5e1 : 0x1a1a4a, themeLight ? 0xe2e8f0 : 0x0d0d2a); 567 grid.position.set(0, -5.5, 0); scene.add(grid); 568 const center = new THREE.Vector3(6.5, 5.5, 5.5); 569 const point = atom => new THREE.Vector3(atom.x, atom.y, atom.z).sub(center); 570 structure.atoms.forEach(atom => { 571 const col = new THREE.Color(atom.color); 572 const mesh = new THREE.Mesh(new THREE.SphereGeometry(atom.radius * .38, 32, 32), new THREE.MeshStandardMaterial({ color: col, emissive: col, emissiveIntensity: .45, roughness: .18, metalness: .45 })); 573 mesh.position.copy(point(atom)); mesh.userData = atom; scene.add(mesh); atomMeshes.push(mesh); 574 const glow = new THREE.Mesh(new THREE.SphereGeometry(atom.radius * .55, 16, 16), new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: .08, side: THREE.BackSide })); 575 mesh.add(glow); 576 }); 577 if (state.showBonds) { 578 for (let i=0; i<structure.atoms.length; i++) for (let j=i+1; j<structure.atoms.length; j++) { 579 const a = point(structure.atoms[i]), b = point(structure.atoms[j]), d = a.distanceTo(b); 580 if (d < 4.2) { 581 const dir = b.clone().sub(a); 582 const mesh = new THREE.Mesh(new THREE.CylinderGeometry(.055, .055, d, 8, 1), new THREE.MeshStandardMaterial({ color: 0x334155, transparent: true, opacity: .48 })); 583 mesh.position.copy(a.clone().lerp(b, .5)); 584 mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize()); 585 scene.add(mesh); 586 } 587 } 588 } 589 if (state.showCell) drawCell(structure.lattice, center); 590 const axes = new THREE.AxesHelper(3.2); 591 axes.position.set(-6.5, -5.5, -5.5); 592 scene.add(axes); 593 updatePanels(structure); 594 } 595 function onPointerMove(event) { 596 if (!raycaster || !camera || atomMeshes.length === 0) return; 597 const rect = renderer.domElement.getBoundingClientRect(); 598 pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; 599 pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; 600 raycaster.setFromCamera(pointer, camera); 601 const hits = raycaster.intersectObjects(atomMeshes, false); 602 if (!hits.length) { hideAtomTip(); return; } 603 const atom = hits[0].object.userData; 604 const tip = document.getElementById("atom-tooltip"); 605 tip.style.display = "block"; 606 tip.style.left = `${event.clientX - rect.left + 16}px`; 607 tip.style.top = `${event.clientY - rect.top + 14}px`; 608 tip.innerHTML = `<b style="color:${atom.color}">${atom.symbol} â ${atom.name}</b><span>Z=${atom.atomicNumber} · atom #${atom.id + 1}</span><br><span>coord (${fmt(atom.x)}, ${fmt(atom.y)}, ${fmt(atom.z)}) à </span><br><span>radius ${fmt(atom.radius)} à </span>`; 609 document.getElementById("hover-label").textContent = `${atom.symbol} Z=${atom.atomicNumber} · (${fmt(atom.x)}, ${fmt(atom.y)}, ${fmt(atom.z)}) à `; 610 } 611 function hideAtomTip() { 612 const tip = document.getElementById("atom-tooltip"); 613 if (tip) tip.style.display = "none"; 614 const label = document.getElementById("hover-label"); 615 if (label) label.textContent = "hover atoms for species"; 616 } 617 function drawCell(lattice, center) { 618 const av = new THREE.Vector3(...lattice.a), bv = new THREE.Vector3(...lattice.b), cv = new THREE.Vector3(...lattice.c); 619 const o = center.clone().multiplyScalar(-1); 620 const c = [o.clone(), o.clone().add(av), o.clone().add(bv), o.clone().add(cv), o.clone().add(av).add(bv), o.clone().add(av).add(cv), o.clone().add(bv).add(cv), o.clone().add(av).add(bv).add(cv)]; 621 [[0,1],[0,2],[0,3],[1,4],[1,5],[2,4],[2,6],[3,5],[3,6],[4,7],[5,7],[6,7]].forEach(([i,j]) => { 622 scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([c[i], c[j]]), new THREE.LineBasicMaterial({ color: 0x4466ff, transparent: true, op
622acity: .65 }))); 623 }); 624 } 625 function metricRow(name, value, goodHigh = true) { 626 const pct = Math.max(0, Math.min(100, value * 100)); 627 const color = goodHigh ? "linear-gradient(90deg, var(--blue), var(--cyan))" : "linear-gradient(90deg, var(--gold), var(--red))"; 628 return `<div class="metric"><span>${name}</span><b>${fmt(value)}</b><div class="bar"><div class="fill" style="width:${pct}%;background:${color}"></div></div></div>`; 629 } 630 function updatePanels(s) { 631 document.getElementById("formula-pill").textContent = s.formula; 632 document.getElementById("score-pill").textContent = `stack ${fmt(s.metrics.stack)}`; 633 document.getElementById("mind-pill").textContent = `min d ${fmt(s.metrics.minD)} à `; 634 document.getElementById("metrics").innerHTML = [ 635 metricRow("Stack agreement", s.metrics.stack), 636 metricRow("Physics encoder", s.metrics.encoder), 637 metricRow("Diffusion repair", s.metrics.diffusion), 638 metricRow("Evidence grounding", s.metrics.evidence), 639 metricRow("Distance violations", s.metrics.violations, false), 640 `<p class="subtle">Formula <b>${s.formula}</b><br>Family ${s.family}<br>Geometry ${s.metrics.valid ? "passes baseline" : "needs review"}</p>` 641 ].join(""); 642 document.getElementById("structure-explanation").innerHTML = `The current candidate is <b>${s.formula}</b>. The encoder lane reads local distances and lattice geometry, the diffusion lane proposes a repaired crystal arrangement, and the multimodal lane turns that into an evidence packet for screening. This candidate is a <b>${s.family}</b> with baseline geometry marked <b>${s.metrics.valid ? "pass" : "review"}</b>.`; 643 document.getElementById("applications").innerHTML = applicationText(s); 644 document.getElementById("encoder-text").textContent = `Physical manifold score ${fmt(s.metrics.encoder)} with min distance ${fmt(s.metrics.minD)} à .`; 645 document.getElementById("diffusion-text").textContent = `Candidate repair score ${fmt(s.metrics.diffusion)} under ${s.selected.join(", ")} constraints.`; 646 document.getElementById("mm-text").textContent = `Evidence score ${fmt(s.metrics.evidence)}. Unsupported claims remain marked as screening hypotheses.`; 647 document.getElementById("decision-text").textContent = `Stack score ${fmt(s.metrics.stack)}: ${s.metrics.stack > .74 ? "promote to DFT triage" : "keep as candidate, needs more evidence"}.`; 648 } 649 function applicationText(s) { 650 const set = new Set(s.selected); 651 if (set.has("Li") || set.has("Na")) { 652 return "Best-fit use case: solid electrolyte or ion-transport screening. Treat high-scoring outputs as candidates for DFT relaxation, migration-barrier checks, and interface stability review."; 653 } 654 if (set.has("Te") || set.has("Ge")) { 655 return "Best-fit use case: thermoelectric, phase-change, or narrow-gap inorganic screening. The app can surface candidates, but band structure and stability still require downstream physics validation."; 656 } 657 return "Best-fit use case: broad inorganic discovery. Use the stack score to triage candidates, then push promising structures into DFT or domain-specific adapters."; 658 } 659 let uploadedPoolRows = []; 660 let gradioClientPromise = null; 661 let progressTimer = null; 662 let progressStartedAt = 0; 663 let forwardCandidates = []; 664 let selectedCandidateIndex = 0; 665 let forwardCandidateSource = "backend"; 666 const progressStages = { 667 forward: ["Encoding constraints", "Sampling diffusion candidates", "Repairing geometry", "Grounding evidence", "Rendering structure"], 668 reverse: ["Parsing candidate pool", "Encoding target use case", "Scoring physical fit", "Grounding evidence", "Ranking candidates"] 669 }; 670 function setControlsBusy(isBusy) { 671 ["generate", "rank", "sample-dist", "prev-candidate", "next-candidate"].forEach(id => { 672 const el = document.getElementById(id); 673 if (el) el.disabled = isBusy; 674 }); 675 } 676 function setForwardCandidates(candidates, sourceLabel = "backend") { 677 forwardCandidates = candidates || []; 678 forwardCandidateSource = sourceLabel; 679 selectedCandidateIndex = 0; 680 selectForwardCandidate(0); 681 } 682 function selectForwardCandidate(index) { 683 if (!forwardCandidates.length) { 684 document.getElementById("candidate-count").textContent = "No samples yet"; 685 document.getElementById("candidates").innerHTML = ""; 686 return; 687 } 688 selectedCandidateIndex = (index + forwardCandidates.length) % forwardCandidates.length; 689 const candidate = forwardCandidates[selectedCandidateIndex]; 690 renderStructure(candidate); 691 renderCandidateList(); 692 } 693 function renderCandidateList() { 694 const total = forwardCandidates.length; 695 document.getElementById("candidate-count").textContent = total ? `Sample ${selectedCandidateIndex + 1} of ${total}` : "No samples yet"; 696 document.getElementById("candidates").innerHTML = forwardCandidates.map((c, i) => { 697 const active = i === selectedCandidateIndex ? " selected" : ""; 698 return `<div class="candidate pickable${active}" role="button" tabindex="0" data-candidate-index="${i}"><span class="rank">#${i+1}</span><span>${escapeHtml(c.formula)}<br><small class="subtle">${escapeHtml(c.family)} · min d ${fmt(c.metrics.minD)} à · ${forwardCandidateSource}</small></span><span class="score">${fmt(c.metrics.stack)}</span></div>`; 699 }).join(""); 700 document.querySelectorAll("[data-candidate-index]").forEach(row => { 701 row.addEventListener("click", () => selectForwardCandidate(Number(row.dataset.candidateIndex))); 702 row.addEventListener("keydown", event => { 703 if (event.key === "Enter" || event.key === " ") { 704 event.preventDefault(); 705 selectForwardCandidate(Number(row.dataset.candidateIndex)); 706 } 707 }); 708 }); 709 } 710 function setProgress(value, stage) { 711 const pct = Math.max(0, Math.min(100, Math.round(value))); 712 document.getElementById("progress-fill").style.width = `${pct}%`; 713 document.getElementById("progress-percent").textContent = `${pct}%`; 714 if (stage) document.getElementById("progress-stage").textContent = stage; 715 } 716 function startProgress(mode) { 717 clearInterval(progressTimer); 718 progressStartedAt = performance.now(); 719 const stages = progressStages[mode] || progressStages.forward; 720 const panel = document.getElementById("run-progress"); 721 document.getElementById("progress-title").textContent = mode === "reverse" ? "Ranking candidate pool" : "Generating candidate"; 722 panel.classList.add("active"); 723 setControlsBusy(true); 724 setProgress(7, stages[0]); 725 let pct = 7; 726 progressTimer = setInterval(() => { 727 pct = Math.min(92, pct + 4 + Math.random() * 7); 728 const stage = stages[Math.min(stages.length - 1, Math.floor((pct / 100) * stages.length))]; 729 setProgress(pct, stage); 730 }, 180); 731 } 732 async function finishProgress(message = "Ready") { 733 clearInterval(progressTimer); 734 setProgress(100, message);
735 const elapsed = performance.now() - progressStartedAt; 736 if (elapsed < 650) await new Promise(resolve => setTimeout(resolve, 650 - elapsed)); 737 await new Promise(resolve => setTimeout(resolve, 220)); 738 document.getElementById("run-progress").classList.remove("active"); 739 setControlsBusy(false); 740 } 741 async function callQueuedModelStack(path, payload) { 742 const mode = path.includes("reverse") ? "reverse" : "forward"; 743 const fullPayload = { ...payload, mode }; 744 if (!gradioClientPromise) { 745 gradioClientPromise = import("https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js") 746 .then(({ Client }) => Client.connect(window.location.origin)); 747 } 748 const client = await gradioClientPromise; 749 const result = await client.predict("/model_stack", { payload_json: JSON.stringify(fullPayload) }); 750 const raw = Array.isArray(result?.data) ? result.data[0] : result?.data; 751 return typeof raw === "string" ? JSON.parse(raw) : raw; 752 } 753 async function postJson(path, payload) { 754 try { 755 return await callQueuedModelStack(path, payload); 756 } catch (queuedErr) { 757 const response = await fetch(path, { 758 method: "POST", 759 headers: { "Content-Type": "application/json" }, 760 body: JSON.stringify(payload) 761 }); 762 if (!response.ok) throw queuedErr || new Error(`${path} failed with ${response.status}`); 763 return await response.json(); 764 } 765 } 766 async function runForward() { 767 const prompt = document.getElementById("prompt").value; 768 const constraints = document.getElementById("forward-constraints").value; 769 const raw = document.getElementById("elements").value; 770 const n = Number(document.getElementById("samples").value); 771 const novelty = Number(document.getElementById("novelty").value); 772 const strictness = Number(document.getElementById("lattice").value); 773 document.getElementById("run-state").textContent = "ZeroGPU backend: encoder â diffusion â multimodal"; 774 startProgress("forward");
775 try { 776 const result = await postJson("/api/forward", { prompt, constraints, elements: raw, samples: n, novelty, strictness }); 777 const candidates = result.candidates || []; 778 setForwardCandidates(candidates.length ? candidates : [result.structure], "backend ranked"); 779 document.getElementById("decision-text").textContent += ` Cross-attention: ${result.cross_attention?.status || "adapter ready"}.`; 780 await finishProgress("Candidate rendered"); 781 } catch (err) { 782 document.getElementById("run-state").textContent = "backend failed, local fallback rendered"; 783 const candidates = [...Array(n)].map((_, i) => buildStructure(`${prompt} | ${constraints}`, raw, novelty, strictness, i)).sort((a,b) => b.metrics.stack - a.metrics.stack); 784 setForwardCandidates(candidates, "local fallback"); 785 await finishProgress("Fallback rendered"); 786 } 787 setTimeout(() => document.getElementById("run-state").textContent = "ready", 700); 788 } 789 async function runReverse() { 790 const target = document.getElementById("target").value; 791 const reverseConstraints = document.getElementById("reverse-constraints").value; 792 const strictness = Number(document.getElementById("lattice").value); 793 const pastedRows = document.getElementById("pool").value.split("\n").map(x => x.trim()).filter(Boolean); 794 const rows = uploadedPoolRows.length ? uploadedPoolRows : pastedRows; 795 document.getElementById("run-state").textContent = "ZeroGPU backend: reverse ranking pool"; 796 startProgress("reverse"); 797 try { 798 const result = await postJson("/api/reverse", { target, constraints: reverseConstraints, strictness, rows }); 799 const ranked = result.ranked || []; 800 renderStructure(result.structure); 801 document.getElementById("reverse-results").innerHTML = ranked.map((r, i) => `<div class="candidate"><span class="rank">#${i+1}</span><span>${escapeHtml(r.row)}<br><small class="subtle">${r.structure.formula} · min d ${fmt(r.structure.metrics.minD)} à · ${escapeHtml(r.notes || "pool candidate")} · backend ranked</small></span><span class="score">${fmt(r.score)}</span></div>`).join(""); 802 document.getElementById("decision-text").textContent += ` Reverse cross-attention: ${result.cross_attention?.status || "adapter ready"}.`; 803 await finishProgress("Pool ranked"); 804 } catch (err) { 805 const ranked = rows.map((row, i) => { 806 const normalized = normalizePoolRow(row); 807 const structure = buildStructure(`${target} | ${reverseConstraints}`, normalized.elements, .38, strictness, i + 100); 808 const haystack = `${normalized.label} ${normalized.notes}`.toLowerCase(); 809 const bonus = haystack.split(/\W+/).some(t => t && `${target} ${reverseConstraints}`.toLowerCase().includes(t)) ? .08 : 0; 810 return { row: normalized.label, structure, score: clamp(structure.metrics.stack + bonus), notes: normalized.notes }; 811 }).sort((a,b) => b.score - a.score); 812 renderStructure(ranked[0].structure); 813 document.getElementById("reverse-results").innerHTML = ranked.map((r, i) => `<div class="candidate"><span class="rank">#${i+1}</span><span>${escapeHtml(r.row)}<br><small class="subtle">${r.structure.formula} · min d ${fmt(r.structure.metrics.minD)} à · ${escapeHtml(r.notes || "pool candidate")} · local fallback</small></span><span class="score">${fmt(r.score)}</span></div>`).join(""); 814 await finishProgress("Fallback ranked"); 815 } 816 setTimeout(() => document.getElementById("run-state").textContent = "ready", 700); 817 } 818 function normalizePoolRow(row) { 819 if (typeof row === "string") { 820 const symbols = row.split(/[\s|,;]+/).filter(x => elements[toSymbol(x)]).map(toSymbol).join(" "); 821 return { label: row, elements: symbols || row, notes: row }; 822 } 823 const label = row.formula || row.composition || row.material || row.name || row.id || JSON.stringify(row); 824 const notes = row.notes || row.description || row.application || row.use_case || row.target || ""; 825 const rawElements = row.elements || row.allowed_elements || row.formula || row.composition || label; 826 const symbols = String(rawElements).split(/[\s|,;+\-_\d]+/).filter(x => elements[toSymbol(x)]).map(toSymbol).join(" "); 827 return { label: String(label), elements: symbols || String(rawElements), notes: String(notes) }; 828 } 829 function toSymbol(x) { 830 if (!x) return ""; 831 return String(x).trim().replace(/[^a-zA-Z]/g, "").replace(/^([a-z])/, m => m.toUpperCase()).replace(/^([A-Z])([A-Z]+)/, (_, a, b) => a + b.toLowerCase()); 832 } 833 function parseCsv(text) {
834 const lines = text.split(/\r?\n/).filter(Boolean); 835 if (!lines.length) return []; 836 const headers = splitCsvLine(lines[0]).map(h => h.trim()); 837 const hasHeader = headers.some(h => /formula|composition|material|name|application|notes|elements/i.test(h)); 838 if (!hasHeader) return lines.map(line => splitCsvLine(line).join(" | ")); 839 return lines.slice(1).map(line => { 840 const cells = splitCsvLine(line); 841 return Object.fromEntries(headers.map((h, i) => [h, cells[i] || ""])); 842 }); 843 } 844 function splitCsvLine(line) { 845 const cells = []; 846 let cur = "", quote = false; 847 for (const ch of line) { 848 if (ch === '"') quote = !quote; 849 else if (ch === "," && !quote) { cells.push(cur); cur = ""; } 850 else cur += ch; 851 } 852 cells.push(cur); 853 return cells.map(c => c.trim()); 854 } 855 function parsePoolFile(name, text) { 856 const lower = name.toLowerCase(); 857 if (lower.endsWith(".jsonl")) return text.split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line)); 858 if (lower.endsWith(".json")) { 859 const parsed = JSON.parse(text); 860 return Array.isArray(parsed) ? parsed : (parsed.candidates || parsed.materials || parsed.rows || [parsed]); 861 } 862 return parseCsv(text); 863 } 864 function escapeHtml(text) { 865 return String(text).replace(/[&<>"']/g, ch => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch])); 866 } 867 function sampleDistribution() { 868 const prompt = document.getElementById("prompt").value, raw = document.getElementById("elements").value; 869 const novelty = Number(document.getElementById("novelty").value), strictness = Number(document.getElementById("lattice").value); 870 const values = [...Array(512)].map((_, i) => buildStructure(prompt, raw, novelty, strictness, i).metrics.stack); 871 const mean = values.reduce((a,b) => a+b, 0) / values.length; 872 const sd = Math.sqrt(values.reduce((a,b) => a + (b-mean)**2, 0) / values.length); 873 document.getElementById("candidates").innerHTML = `<p class="subtle">512-sample distribution<br>mean stack=${fmt(mean)} · std=${fmt(sd)} · min=${fmt(Math.min(...values))} · max=${fmt(Math.max(...values))}</p>`; 874 } 875 function onResize() { camera.aspect = root.clientWidth / root.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(root.clientWidth, root.clientHeight); } 876 function animate() { requestAnimationFrame(animate); controls.autoRotate = state.autoRotate; controls.update(); renderer.render(scene, camera); } 877 document.querySelectorAll(".tabs button").forEach(btn => btn.addEventListener("click", () => { 878 document.querySelectorAll(".tabs button").forEach(b => b.classList.remove("active")); 879 document.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); 880 btn.classList.add("active"); document.getElementById(`tab-${btn.dataset.tab}`).classList.add("active"); 881 })); 882 document.getElementById("theme-btn").onclick = () => { 883 const next = document.documentElement.dataset.theme === "dark" ? "light" : "dark"; 884 document.documentElement.dataset.theme = next; 885 document.getElementById("theme-btn").textContent = next === "dark" ? "Light mode" : "Dark mode"; 886 renderStructure(state.structure); 887 }; 888 document.getElementById("generate").onclick = runForward; 889 document.getElementById("rank").onclick = runReverse; 890 document.getElementById("pool-file").onchange = async event => { 891 const file = event.target.files?.[0]; 892 if (!file) return; 893 document.getElementById("run-state").textContent = "parsing reverse pool file"; 894 try { 895 const text = await file.text(); 896 uploadedPoolRows = parsePoolFile(file.name, text); 897 document.getElementById("pool").value = uploadedPoolRows.slice(0, 24).map(row => normalizePoolRow(row).label).join("\n"); 898 document.getElementById("reverse-results").innerHTML = `<p class="subtle">Loaded ${uploadedPoolRows.length} pool rows from ${escapeHtml(file.name)}. Press Rank Pool to run reverse search.</p>`; 899 } catch (err) { 900 uploadedPoolRows = []; 901 document.getElementById("reverse-results").innerHTML = `<p class="subtle" style="color:var(--red)">Could not parse file: ${escapeHtml(err.message || err)}</p>`; 902 } finally { 903 document.getElementById("run-state").textContent = "ready"; 904 } 905 }; 906 document.getElementById("sample-dist").onclick = sampleDistribution; 907 document.getElementById("prev-candidate").onclick = () => selectForwardCandidate(selectedCandidateIndex - 1); 908 document.getElementById("next-candidate").onclick = () => selectForwardCandidate(selectedCandidateIndex + 1); 909 document.getElementById("rotate").onclick = e => { state.autoRotate = !state.autoRotate; e.target.textContent = `Rotate: ${state.autoRotate ? "on" : "off"}`; }; 910 document.getElementById("bonds").onclick = e => { state.showBonds = !state.showBonds; e.target.textContent = `Bonds: ${state.showBonds ? "on" : "off"}`; renderStructure(state.structure); }; 911 document.getElementById("cell").onclick = e => { state.showCell = !state.showCell; e.target.textContent = `Cell: ${state.showCell ? "on" : "off"}`; renderStructure(state.structure); }; 912 document.getElementById("reset-view").onclick = () => { camera.position.set(24,20,24); camera.lookAt(0,0,0); controls.target.set(0,0,0); controls.update(); }; 913 document.getElementById("novelty").oninput = e => document.getElementById("novelty-value").textContent = e.target.value; 914 document.getElementById("lattice").oninput = e => document.getElementById("lattice-value").textContent = e.target.value; 915 initScene(); 916 runForward(); 917 </script>
917 918</body> 919</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.