SeaWolf-AI commited on
Commit
c6f4734
ยท
verified ยท
1 Parent(s): 52fd4ca

Per-board participation guide with a ready-to-paste prompt and a runnable skeleton

Browse files

A generic guide makes each entrant adapt it to their board, and most drop out at that step. The guide tab now fills in this board target, split year, experimental error and baselines, so the prompt and the starter script can be copied as they are.

Files changed (2) hide show
  1. i18n.js +50 -2
  2. index.html +119 -1
i18n.js CHANGED
@@ -171,7 +171,31 @@ ko: {
171
 
172
  secOpen: "๊ฐœ์„ค ์˜ˆ์ •",
173
  openLead: "๊ฐ ๋ถ€๋ฌธ์€ ํ™€๋“œ์•„์›ƒ๊ณผ ์‹คํ—˜ ์˜ค์ฐจ๊ฐ€ ํ™•๋ณด๋œ ๋’ค์— ์—ฝ๋‹ˆ๋‹ค. ํ™•๋ณด๋˜์ง€ ์•Š์€ ๋ถ€๋ฌธ์€ ์—ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
174
- learnMore: "์ž์„ธํžˆ"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  },
176
 
177
  en: {
@@ -355,6 +379,30 @@ en: {
355
  secOpen: "Opening next",
356
  openLead: "A board opens only once its holdout and experimental error are established. " +
357
  "Boards without them are not opened.",
358
- learnMore: "Detail"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
  };
 
171
 
172
  secOpen: "๊ฐœ์„ค ์˜ˆ์ •",
173
  openLead: "๊ฐ ๋ถ€๋ฌธ์€ ํ™€๋“œ์•„์›ƒ๊ณผ ์‹คํ—˜ ์˜ค์ฐจ๊ฐ€ ํ™•๋ณด๋œ ๋’ค์— ์—ฝ๋‹ˆ๋‹ค. ํ™•๋ณด๋˜์ง€ ์•Š์€ ๋ถ€๋ฌธ์€ ์—ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
174
+ learnMore: "์ž์„ธํžˆ",
175
+
176
+ tabGuide: "์ฐธ์—ฌ ๊ฐ€์ด๋“œ",
177
+ gdIntro: "์˜ˆ์ธก ๋„๊ตฌ๋ฅผ ์ฒ˜์Œ ๋งŒ๋“œ์‹ ๋‹ค๋ฉด ์•„๋ž˜ ์ˆœ์„œ๋Œ€๋กœ ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. " +
178
+ "์ง์ ‘ ์ฝ”๋”ฉํ•˜์…”๋„ ๋˜๊ณ , ์•„๋ž˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์–ธ์–ด๋ชจ๋ธ์— ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ ๋„ฃ์œผ์…”๋„ ๋ฉ๋‹ˆ๋‹ค.",
179
+ gdWhat: "๋ฌด์—‡์„ ๋งŒ๋“œ๋Š”๊ฐ€",
180
+ gdWhatD: "ํ™”ํ•ฉ๋ฌผ ๊ตฌ์กฐ(SMILES)๋ฅผ ๋ฐ›์•„ <b>๊ฐ’ ํ•˜๋‚˜</b>๋ฅผ ๋‚ด๋†“๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. " +
181
+ "๊ทธ ๊ฐ’์ด ์‹ค์ธก๊ณผ ์–ผ๋งˆ๋‚˜ ๊ฐ€๊นŒ์šด์ง€๋กœ ์ฑ„์ ํ•ฉ๋‹ˆ๋‹ค.",
182
+ gdPrompt: "์–ธ์–ด๋ชจ๋ธ์šฉ ํ”„๋กฌํ”„ํŠธ",
183
+ gdPromptD: "์•„๋ž˜๋ฅผ ํ†ต์งธ๋กœ ๋ณต์‚ฌํ•ด Claude ยท GPT ยท Gemini ๋“ฑ์— ๋ถ™์—ฌ ๋„ฃ์œผ์‹ญ์‹œ์˜ค. " +
184
+ "์ด ๋ถ€๋ฌธ์˜ ๊ฐ’์ด ์ด๋ฏธ ์ฑ„์›Œ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.",
185
+ gdCopy: "๋ณต์‚ฌ", gdCopied: "๋ณต์‚ฌ๋จ",
186
+ gdStarter: "๋ฐ”๋กœ ๋Œ๋ ค๋ณผ ์ˆ˜ ์žˆ๋Š” ๋ผˆ๋Œ€",
187
+ gdStarterD: "์ง์ ‘ ์งœ์‹œ๋ ค๋ฉด ์ด ๋ผˆ๋Œ€์—์„œ ์‹œ์ž‘ํ•˜์‹ญ์‹œ์˜ค. ํ›ˆ๋ จ ์ž๋ฃŒ๋ฅผ ๋ฐ›์•„ ์ง€๋ฌธ์„ ๋งŒ๋“ค๊ณ  " +
188
+ "๋ชจ๋ธ์„ ํ•™์Šตํ•ด ์ œ์ถœ ํŒŒ์ผ๊นŒ์ง€ ์”๋‹ˆ๋‹ค.",
189
+ gdRules: "์ง€์ผœ์•ผ ํ•  ๊ฒƒ",
190
+ gdR1: "๊ธฐ์ค€์ผ ์ดํ›„ ์ตœ์ดˆ ๋ณด๊ณ ๋œ ํ™”ํ•ฉ๋ฌผ์€ ํ•™์Šต์— ์“ฐ์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ๊ทธ๊ฒƒ์ด ํ…Œ์ŠคํŠธ์…‹์ž…๋‹ˆ๋‹ค.",
191
+ gdR2: "๊ฐ™์€ ํ™”ํ•ฉ๋ฌผ์ด ์—ฌ๋Ÿฌ ๋…ผ๋ฌธ์— ์žˆ์œผ๋ฉด InChIKey ๋กœ ์ ‘์–ด ์ค‘์•™๊ฐ’์„ ์“ฐ์‹ญ์‹œ์˜ค. " +
192
+ "๋ฌธ์ž์—ด๋กœ ์ ‘์œผ๋ฉด ๊ฐ™์€ ๋ถ„์ž๊ฐ€ ์—ฌ๋Ÿฌ ๋ฒˆ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.",
193
+ gdR3: "์ •๋‹ต์„ ์กฐํšŒํ•ด ์ฑ„์šฐ์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ์˜ˆ์ธก๊ฐ’์ด ์‹ค์ธก๊ณผ ์†Œ์ˆ˜ ์ž๋ฆฌ๊นŒ์ง€ ์ผ์น˜ํ•˜๋Š” ๋ฐ€๋„๋ฅผ " +
194
+ "๊ฒ€์‚ฌํ•ด ์ˆœ์œ„ํ‘œ์— ํ•จ๊ป˜ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.",
195
+ gdR4: "๋ฐฉ๋ฒ•์—๋Š” ์ œํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ง€๋ฌธ+ํŠธ๋ฆฌ ยท ๊ทธ๋ž˜ํ”„ ์‹ ๊ฒฝ๋ง ยท ์‚ฌ์ „ํ•™์Šต ๋ชจ๋ธ ยท ๋ฌผ๋ฆฌ ๊ณ„์‚ฐ " +
196
+ "๋ฌด์—‡์ด๋“  ์ข‹์Šต๋‹ˆ๋‹ค.",
197
+ gdTarget: "๋„˜์–ด์•ผ ํ•  ์„ ",
198
+ gdTargetD: "๊ธฐ์ค€์„ ์„ ๋„˜์ง€ ๋ชปํ•˜๋ฉด ์ˆœ์œ„ํ‘œ์—์„œ ๊ทธ ์•„๋ž˜์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.",
199
  },
200
 
201
  en: {
 
379
  secOpen: "Opening next",
380
  openLead: "A board opens only once its holdout and experimental error are established. " +
381
  "Boards without them are not opened.",
382
+ learnMore: "Detail",
383
+
384
+ tabGuide: "Guide",
385
+ gdIntro: "If this is your first predictor, follow the steps below. Write it yourself, " +
386
+ "or paste the prompt into a language model as it is.",
387
+ gdWhat: "What you are building",
388
+ gdWhatD: "A function that takes a structure (SMILES) and returns <b>one number</b>. " +
389
+ "We score how close that number is to the measured value.",
390
+ gdPrompt: "Prompt for a language model",
391
+ gdPromptD: "Copy the whole block into Claude, GPT, Gemini or similar. " +
392
+ "The values for this board are already filled in.",
393
+ gdCopy: "Copy", gdCopied: "Copied",
394
+ gdStarter: "A skeleton you can run",
395
+ gdStarterD: "If you would rather write it yourself, start here. It pulls the training data, " +
396
+ "builds fingerprints, fits a model and writes the submission file.",
397
+ gdRules: "Rules to observe",
398
+ gdR1: "Do not train on compounds first reported on or after the split year. That is the test set.",
399
+ gdR2: "Fold duplicate measurements by InChIKey and take the median. Folding by SMILES string " +
400
+ "leaves the same molecule in several times.",
401
+ gdR3: "Do not fill predictions by looking up the answer. We measure how many predictions match " +
402
+ "the measured value to the decimal and show it on the leaderboard.",
403
+ gdR4: "There is no restriction on method. Fingerprints and trees, graph networks, pretrained " +
404
+ "models, physics-based calculation โ€” all are fine.",
405
+ gdTarget: "Lines to cross",
406
+ gdTargetD: "A submission that does not clear a baseline is shown below it on the leaderboard.",
407
  }
408
  };
index.html CHANGED
@@ -469,7 +469,7 @@ async function board(name,tab){
469
  const b=BOARDS[name]||await j("/api/board/"+encodeURIComponent(name));
470
  const cat=CATS.categories.find(c=>(c.boards||[]).includes(name));
471
  const cn=cat?(L==="ko"?cat.name:cat.en):"";
472
- const TT=[["rank",t.tabRank],["submit",t.tabSubmit],["info",t.tabInfo]];
473
  app.innerHTML=W(`
474
  <div class="crumb"><a href="#/">${t.all}</a> โ€บ <a href="#/c/${cat?cat.id:""}">${esc(cn)}</a> โ€บ ${esc(b.board)}</div>
475
  <h1 class="ttl">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></h1>
@@ -480,6 +480,7 @@ async function board(name,tab){
480
  <div id="pane"><div class="skel" style="margin-top:20px"></div></div>`);
481
  if(tab==="rank")return paneRank(b);
482
  if(tab==="submit")return paneSubmit(b);
 
483
  paneInfo(b);
484
  }
485
 
@@ -511,6 +512,123 @@ async function paneRank(b){
511
  : t.noEntries}</p>`;
512
  }
513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  function paneInfo(b){
515
  const nf=b.noise_floor, bl=b.baselines;
516
  $("#pane").innerHTML=`
 
469
  const b=BOARDS[name]||await j("/api/board/"+encodeURIComponent(name));
470
  const cat=CATS.categories.find(c=>(c.boards||[]).includes(name));
471
  const cn=cat?(L==="ko"?cat.name:cat.en):"";
472
+ const TT=[["rank",t.tabRank],["submit",t.tabSubmit],["guide",t.tabGuide],["info",t.tabInfo]];
473
  app.innerHTML=W(`
474
  <div class="crumb"><a href="#/">${t.all}</a> โ€บ <a href="#/c/${cat?cat.id:""}">${esc(cn)}</a> โ€บ ${esc(b.board)}</div>
475
  <h1 class="ttl">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></h1>
 
480
  <div id="pane"><div class="skel" style="margin-top:20px"></div></div>`);
481
  if(tab==="rank")return paneRank(b);
482
  if(tab==="submit")return paneSubmit(b);
483
+ if(tab==="guide")return paneGuide(b);
484
  paneInfo(b);
485
  }
486
 
 
512
  : t.noEntries}</p>`;
513
  }
514
 
515
+ /* ๋ถ€๋ฌธ ๊ฐ’์ด ์ฑ„์›Œ์ง„ ์ฐธ์—ฌ ๊ฐ€์ด๋“œ. ์ผ๋ฐ˜๋ฌธ์œผ๋กœ ๋‘๋ฉด ์ฐธ๊ฐ€์ž๊ฐ€ ์ž๊ธฐ ๋ถ€๋ฌธ์— ๋งž๊ฒŒ ๊ณ ์ณ์•ผ ํ•˜๊ณ ,
516
+ ๋Œ€๋ถ€๋ถ„ ๊ทธ ๋‹จ๊ณ„์—์„œ ๋–จ์–ด์ง„๋‹ค. */
517
+ function promptFor(b){
518
+ const nf=(b.noise_floor||{}).sd_single;
519
+ const bl=Object.entries(b.baselines||{})
520
+ .map(([k,v])=>` ${k}: ${v.mae!=null?("MAE "+f(v.mae)):("AUROC "+f(v.auroc))}`).join("\n");
521
+ const unit = b.type && b.type.indexOf("logS")>=0
522
+ ? "logS = log10(์šฉํ•ด๋„ in mol/L)"
523
+ : b.threshold==null ? "๋ถ€๋ฌธ ์ •์˜๊ฐ’" : "p ์Šค์ผ€์ผ = -log10(๋†๋„ in M)";
524
+ return [
525
+ `๋‚˜๋Š” ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ ์‹œํ—˜๋Œ€ LEADBOARD ์˜ "${b.board}" ๋ถ€๋ฌธ์— ์ฐธ๊ฐ€ํ•œ๋‹ค.`,
526
+ `์•„๋ž˜ ์กฐ๊ฑด๋Œ€๋กœ ์˜ˆ์ธก ํŒŒ์ผ์„ ๋งŒ๋“œ๋Š” Python ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•ด ๋‹ฌ๋ผ.`,
527
+ ``,
528
+ `[๊ณผ์ œ]`,
529
+ ` ์ž…๋ ฅ ${b.board.toLowerCase()}_test.json โ€” compound_id ์™€ SMILES ๋งŒ ๋“ค์–ด ์žˆ๋‹ค (${b.n_test}๊ฐœ)`,
530
+ ` ์ถœ๋ ฅ predictions.csv โ€” ๋‘ ์—ด: compound_id,prediction`,
531
+ ` ์˜ˆ์ธก๊ฐ’ ${unit}`,
532
+ b.target && b.target!=="โ€”" ? ` ๋Œ€์ƒ ${b.target} ยท ${b.type}` : ` ๋Œ€์ƒ ${b.type}`,
533
+ ``,
534
+ `[ํ•™์Šต ์ž๋ฃŒ]`,
535
+ b.cut ? ` ChEMBL ์—์„œ ${b.target} ์˜ ${b.type} ํ™œ์„ฑ์„ ๋ฐ›์•„ ์“ด๋‹ค.` : ` ๊ณต๊ฐœ ์ž๋ฃŒ๋ฅผ ์ง์ ‘ ํ๋ ˆ์ด์…˜ํ•ด ์“ด๋‹ค.`,
536
+ b.cut ? ` โ˜… ${b.cut}๋…„ ์ดํ›„ ์ฒ˜์Œ ๋ณด๊ณ ๋œ ํ™”ํ•ฉ๋ฌผ์€ ํ•™์Šต์— ๋„ฃ์ง€ ๋ง ๊ฒƒ โ€” ๊ทธ๊ฒƒ์ด ํ…Œ์ŠคํŠธ์…‹์ด๋‹ค.` : ``,
537
+ ` ๊ฐ™์€ ํ™”ํ•ฉ๋ฌผ์ด ์—ฌ๋Ÿฌ ๋…ผ๋ฌธ์— ์žˆ์œผ๋ฉด InChIKey ๋กœ ์ ‘์–ด ์ค‘์•™๊ฐ’์„ ์“ธ ๊ฒƒ.`,
538
+ ``,
539
+ `[์ œ์•ฝ]`,
540
+ ` ๋ฐฉ๋ฒ•์— ์ œํ•œ์€ ์—†๋‹ค. ์ง€๋ฌธ+ํŠธ๋ฆฌ, ๊ทธ๋ž˜ํ”„ ์‹ ๊ฒฝ๋ง, ์‚ฌ์ „ํ•™์Šต ๋ชจ๋ธ, ๋ฌผ๋ฆฌ ๊ณ„์‚ฐ ๋ชจ๋‘ ๊ฐ€๋Šฅ.`,
541
+ ` ์ •๋‹ต์„ ์กฐํšŒํ•ด ์ฑ„์šฐ์ง€ ๋ง ๊ฒƒ. ์˜ˆ์ธก๊ฐ’์ด ์‹ค์ธก๊ณผ ์†Œ์ˆ˜ ์ž๋ฆฌ๊นŒ์ง€ ์ผ์น˜ํ•˜๋Š” ๋ฐ€๋„๋ฅผ ๊ฒ€์‚ฌํ•œ๋‹ค.`,
542
+ ``,
543
+ `[ํ‰๊ฐ€]`,
544
+ ` ์ฃผ์ง€ํ‘œ ${b.metric==="auroc"?"AUROC (๋†’์„์ˆ˜๋ก ์ข‹์Œ)":"MAE (๋‚ฎ์„์ˆ˜๋ก ์ข‹์Œ)"}`,
545
+ nf ? ` ์ด ๋ถ€๋ฌธ์˜ ์‹คํ—˜ ์žฌํ˜„ ์˜ค์ฐจ๋Š” ${f(nf)} ์ด๋‹ค. ๊ทธ๋ณด๋‹ค ์ž‘์€ ์ฐจ์ด๋Š” ์ˆœ์œ„๋กœ ์ธ์ •๋˜์ง€ ์•Š๋Š”๋‹ค.` : ``,
546
+ ` ๋„˜์–ด์•ผ ํ•  ๊ธฐ์ค€์„ :`,
547
+ bl,
548
+ ``,
549
+ `์Šคํฌ๋ฆฝํŠธ๋Š” ์ž๋ฃŒ ์ˆ˜์ง‘ โ†’ ์ •์ œ โ†’ ํŠน์ง• โ†’ ํ•™์Šต โ†’ ์˜ˆ์ธก โ†’ CSV ์ €์žฅ๊นŒ์ง€ ํ•œ ํŒŒ์ผ๋กœ ์ž‘์„ฑํ•ด ๋‹ฌ๋ผ.`
550
+ ].filter(x=>x!==``||true).join("\n");
551
+ }
552
+
553
+ function starterFor(b){
554
+ return [
555
+ `# ${b.board} ์ œ์ถœ ๋ผˆ๋Œ€`,
556
+ `import json, math, requests, numpy as np`,
557
+ `from rdkit import Chem`,
558
+ `from rdkit.Chem import rdFingerprintGenerator`,
559
+ `import lightgbm as lgb`,
560
+ ``,
561
+ `# 1) ํ›ˆ๋ จ ์ž๋ฃŒ (ChEMBL ์—์„œ ๋ฐ›๋Š”๋‹ค)`,
562
+ `API = "https://www.ebi.ac.uk/chembl/api/data/activity.json"`,
563
+ `rows, off = [], 0`,
564
+ `while True:`,
565
+ ` q = {"target_chembl_id": "${b.target}", "standard_type": "${b.type}",`,
566
+ ` "standard_relation": "=", "limit": 1000, "offset": off}`,
567
+ ` d = requests.get(API, params=q, timeout=120).json()`,
568
+ ` rows += d["activities"]; off += 1000`,
569
+ ` if off >= d["page_meta"]["total_count"]: break`,
570
+ ``,
571
+ `# 2) ์ •์ œ โ€” InChIKey ๋กœ ์ ‘๊ณ , ๊ธฐ์ค€์ผ ์ดํ›„๋Š” ๋ฒ„๋ฆฐ๋‹ค`,
572
+ `rec = {}`,
573
+ `for r in rows:`,
574
+ ` s, v, u, y = r["canonical_smiles"], r["standard_value"], r["standard_units"], r["document_year"]`,
575
+ ` if not (s and v and u == "nM" and y): continue`,
576
+ ` m = Chem.MolFromSmiles(s)`,
577
+ ` if m is None or float(v) <= 0: continue`,
578
+ ` k = Chem.MolToInchiKey(m)`,
579
+ ` rec.setdefault(k, {"y": [], "p": [], "m": m})`,
580
+ ` rec[k]["y"].append(int(y)); rec[k]["p"].append(9.0 - math.log10(float(v)))`,
581
+ `train = {k: v for k, v in rec.items() if min(v["y"]) < ${b.cut||2022}} # โ˜… ํ…Œ์ŠคํŠธ์…‹ ์ œ์™ธ`,
582
+ ``,
583
+ `# 3) ํŠน์ง• ยท ํ•™์Šต`,
584
+ `gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)`,
585
+ `X = np.array([gen.GetFingerprintAsNumPy(v["m"]) for v in train.values()])`,
586
+ `Y = np.array([float(np.median(v["p"])) for v in train.values()])`,
587
+ `model = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05).fit(X, Y)`,
588
+ ``,
589
+ `# 4) ์˜ˆ์ธก ยท ์ €์žฅ`,
590
+ `test = json.load(open("${b.board.toLowerCase()}_test.json", encoding="utf-8"))`,
591
+ `Xt = np.array([gen.GetFingerprintAsNumPy(Chem.MolFromSmiles(c["smiles"]))`,
592
+ ` for c in test["compounds"]])`,
593
+ `p = model.predict(Xt)`,
594
+ `with open("predictions.csv", "w") as f:`,
595
+ ` f.write("compound_id,prediction\n")`,
596
+ ` for c, v in zip(test["compounds"], p):`,
597
+ ` f.write("%s,%.4f\n" % (c["compound_id"], v))`
598
+ ].join("\n");
599
+ }
600
+
601
+ function paneGuide(b){
602
+ const pr=promptFor(b), sk=starterFor(b);
603
+ const bl=Object.entries(b.baselines||{}).sort((x,y)=>
604
+ (x[1].mae!=null?x[1].mae:-x[1].auroc)-(y[1].mae!=null?y[1].mae:-y[1].auroc));
605
+ $("#pane").innerHTML=`
606
+ <p class="lede" style="margin:6px 0 18px">${t.gdIntro}</p>
607
+ <h3>${t.gdWhat}</h3><p class="dim" style="font-size:14.5px">${t.gdWhatD}</p>
608
+ <h3>${t.gdPrompt}</h3><p class="dim" style="font-size:14px">${t.gdPromptD}</p>
609
+ <div style="position:relative">
610
+ <button class="btn" id="cp1" style="position:absolute;right:12px;top:12px;padding:6px 13px;font-size:12px">${t.gdCopy}</button>
611
+ <pre class="fig" style="white-space:pre-wrap;margin:0">${esc(pr)}</pre></div>
612
+ <h3>${t.gdStarter}</h3><p class="dim" style="font-size:14px">${t.gdStarterD}</p>
613
+ <div style="position:relative">
614
+ <button class="btn" id="cp2" style="position:absolute;right:12px;top:12px;padding:6px 13px;font-size:12px">${t.gdCopy}</button>
615
+ <pre class="fig" style="white-space:pre;overflow-x:auto;margin:0;font-size:12px">${esc(sk)}</pre></div>
616
+ <h3>${t.gdTarget}</h3><p class="dim" style="font-size:14px">${t.gdTargetD}</p>
617
+ <div class="tw"><table><thead><tr><th style="text-align:left;width:auto">${t.baseH}</th>
618
+ <th>MAE</th><th>AUROC</th></tr></thead><tbody>
619
+ ${bl.map(([k,v])=>`<tr><td style="text-align:left">${esc(k)}</td>
620
+ <td>${v.mae!=null?f(v.mae):"โ€”"}</td><td>${v.auroc!=null?f(v.auroc):"โ€”"}</td></tr>`).join("")}
621
+ </tbody></table></div>
622
+ <h3>${t.gdRules}</h3>
623
+ <ol class="cl" style="grid-template-columns:1fr">
624
+ ${[t.gdR1,t.gdR2,t.gdR3,t.gdR4].map(x=>`<li><span>${x}</span></li>`).join("")}
625
+ </ol>`;
626
+ const wireCopy=(id,txt)=>{const el=$("#"+id); if(!el)return;
627
+ el.onclick=async()=>{try{await navigator.clipboard.writeText(txt);
628
+ el.textContent=t.gdCopied; setTimeout(()=>el.textContent=t.gdCopy,1600);}catch(e){}};};
629
+ wireCopy("cp1",pr); wireCopy("cp2",sk);
630
+ }
631
+
632
  function paneInfo(b){
633
  const nf=b.noise_floor, bl=b.baselines;
634
  $("#pane").innerHTML=`