| <!doctype html> |
| <html lang="ko"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width,initial-scale=1"> |
| <title>Open Materials Challenge #1 전고체 전해질</title> |
| |
| |
| <style> |
| :root{ |
| --bg:#f7f8fa; --card:#fff; --ink:#16181d; --muted:#6b7280; --line:#e5e7eb; |
| --accent:#1f6f4a; --accent-soft:#e8f3ed; --warn:#9a6700; --warn-soft:#fff8e1; |
| --anchor:#fffbe6; --anchor-line:#f2e3a8; |
| } |
| :root:not([data-theme="light"]){} |
| @media (prefers-color-scheme: dark){ |
| :root:not([data-theme="light"]){ |
| --bg:#101215; --card:#171a1f; --ink:#e8eaed; --muted:#9aa1ab; --line:#2a2f37; |
| --accent:#4bbb87; --accent-soft:#16281f; --warn:#e0b23c; --warn-soft:#2a2312; |
| --anchor:#241f10; --anchor-line:#4a4022; |
| } |
| } |
| :root[data-theme="dark"]{ |
| --bg:#101215; --card:#171a1f; --ink:#e8eaed; --muted:#9aa1ab; --line:#2a2f37; |
| --accent:#4bbb87; --accent-soft:#16281f; --warn:#e0b23c; --warn-soft:#2a2312; |
| --anchor:#241f10; --anchor-line:#4a4022; |
| } |
| *{box-sizing:border-box} |
| body{margin:0;background:var(--bg);color:var(--ink); |
| font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans KR",sans-serif} |
| .wrap{max-width:1120px;margin:0 auto;padding:20px 16px 80px} |
| header{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:8px 0 4px} |
| h1{font-size:22px;margin:0;letter-spacing:-.3px} |
| h2{font-size:17px;margin:0 0 10px;display:flex;align-items:baseline;gap:10px} |
| .sub{color:var(--muted);font-size:14px} |
| .card{background:var(--card);border:1px solid var(--line);border-radius:14px; |
| padding:18px 20px;margin:14px 0} |
| .grid{display:grid;grid-template-columns:1fr 360px;gap:14px;align-items:start} |
| @media (max-width:900px){.grid{grid-template-columns:1fr}} |
| .cnt{color:var(--muted);font-size:13px;font-weight:400;margin-left:auto} |
| #langbar,#seasonbar{display:flex;gap:6px;flex-wrap:wrap} |
| #langbar{margin-left:auto} |
| #langbar button,#seasonbar button{border:1px solid var(--line);background:var(--card); |
| color:var(--muted);border-radius:8px;padding:5px 12px;cursor:pointer;font:inherit;font-size:13px} |
| #langbar button.on,#seasonbar button.on{background:var(--accent);color:#fff;border-color:var(--accent)} |
| .scroll{overflow-x:auto} |
| table{width:100%;border-collapse:collapse;font-size:13.5px;min-width:780px} |
| th,td{padding:8px 10px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap} |
| th{color:var(--muted);font-weight:600;font-size:12.5px;cursor:pointer;user-select:none} |
| th .ar{opacity:.4;margin-left:3px;font-size:10px} |
| th.sorted{color:var(--accent)} th.sorted .ar{opacity:1} |
| tr.anchor{background:var(--anchor)} |
| tr.anchor td{border-bottom-color:var(--anchor-line)} |
| td.num{text-align:right;font-variant-numeric:tabular-nums} |
| .pill{display:inline-block;padding:1px 7px;border-radius:999px;font-size:11.5px; |
| background:var(--accent-soft);color:var(--accent);border:1px solid transparent} |
| .note{color:var(--muted);font-size:12.5px;margin-top:12px;line-height:1.7} |
| .pager{display:flex;align-items:center;justify-content:center;gap:6px;flex-wrap:wrap; |
| padding:14px 4px 2px;font-size:13px} |
| .pager button{border:1px solid var(--line);background:var(--card);color:var(--muted); |
| border-radius:8px;padding:5px 11px;cursor:pointer;font:inherit;min-width:36px} |
| .pager button.on{background:var(--accent);color:#fff;border-color:var(--accent)} |
| .pager button:disabled{opacity:.4;cursor:default} |
| .pager .pinfo{color:var(--muted);margin:0 8px} |
| .pager .gap{color:var(--muted);padding:0 2px} |
| label{display:block;font-size:13px;color:var(--muted);margin:12px 0 5px} |
| input,textarea,select{width:100%;padding:9px 11px;border:1px solid var(--line); |
| border-radius:9px;background:var(--bg);color:var(--ink);font:inherit;font-size:14px} |
| textarea{resize:vertical;min-height:64px} |
| .radio{display:flex;gap:8px;margin-top:6px} |
| .radio label{flex:1;display:flex;align-items:center;gap:7px;margin:0;padding:10px; |
| border:1px solid var(--line);border-radius:9px;cursor:pointer;font-size:13px;color:var(--ink)} |
| .radio label.on{border-color:var(--accent);background:var(--accent-soft)} |
| .go{width:100%;margin-top:16px;padding:12px;border:0;border-radius:10px; |
| background:var(--accent);color:#fff;font:inherit;font-size:15px;font-weight:600;cursor:pointer} |
| .go:disabled{opacity:.5;cursor:default} |
| .msg{margin-top:10px;padding:10px 12px;border-radius:9px;font-size:13px;display:none} |
| .msg.ok{display:block;background:var(--accent-soft);color:var(--accent)} |
| .msg.bad{display:block;background:var(--warn-soft);color:var(--warn)} |
| .callout{background:var(--warn-soft);border:1px solid var(--anchor-line); |
| border-radius:10px;padding:12px 14px;font-size:13px;color:var(--ink);margin:12px 0} |
| .axes{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0 2px} |
| .axes span{font-size:12.5px;color:var(--muted);background:var(--bg); |
| border:1px solid var(--line);border-radius:999px;padding:3px 10px} |
| .axes b{color:var(--ink)} |
| a{color:var(--accent)} |
| .badges{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px} |
| .badges img{height:22px} |
| #divbar{display:flex;gap:6px;flex-wrap:wrap;margin:0 0 12px} |
| #divbar button{border:1px solid var(--line);background:var(--card);color:var(--muted); |
| border-radius:999px;padding:4px 12px;cursor:pointer;font:inherit;font-size:12.5px} |
| #divbar button.on{background:var(--accent);color:#fff;border-color:var(--accent)} |
| #divbar button .dn{opacity:.65;margin-left:5px;font-size:11.5px} |
| .hf-user{color:var(--accent);text-decoration:none;white-space:nowrap} |
| .hf-user:hover{text-decoration:underline} |
| .signin{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--accent); |
| background:var(--accent);color:#fff;border-radius:9px;padding:6px 13px;font-size:13px; |
| text-decoration:none;font-weight:600} |
| .signin.out{background:var(--card);color:var(--muted);border-color:var(--line);font-weight:400} |
| .me{display:flex;align-items:center;gap:9px;margin:10px 0 0;font-size:13px;color:var(--muted)} |
| .me img{width:22px;height:22px;border-radius:50%} |
| .xlink{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line); |
| background:var(--card);border-radius:9px;padding:6px 12px;font-size:12.5px; |
| color:var(--muted);text-decoration:none;margin-top:8px} |
| .xlink:hover{border-color:var(--accent);color:var(--accent)} |
| .xlink b{color:var(--ink);font-weight:600} |
| .xlink em{font-style:normal;opacity:.75} |
| .lead{font-size:15px;margin:0 0 12px} |
| .sinfo{display:flex;gap:16px;flex-wrap:wrap;color:var(--muted);font-size:13px;margin:10px 0 0} |
| .sinfo b{color:var(--ink);font-weight:600} |
| #tabbar{display:flex;gap:2px;margin:16px 0 0;border-bottom:1px solid var(--line);flex-wrap:wrap} |
| #tabbar button{border:0;background:none;color:var(--muted);font:inherit;font-size:14px; |
| padding:9px 15px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px} |
| #tabbar button:hover{color:var(--ink)} |
| #tabbar button.on{color:var(--accent);border-bottom-color:var(--accent);font-weight:600} |
| #tabbar .tc{margin-left:5px;font-size:11.5px;opacity:.7} |
| .axhelp{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:8px;margin:12px 0 2px} |
| .axhelp div{font-size:12.5px;color:var(--muted);line-height:1.55} |
| .axhelp b{color:var(--ink)} |
| tr.det td{background:var(--bg);white-space:normal;font-size:12.5px;color:var(--muted); |
| border-bottom:1px solid var(--line)} |
| tr.det ul{margin:2px 0;padding-left:16px} tr.det li{margin:2px 0} |
| tr.held td{opacity:.72} |
| .pill.warn{background:var(--warn-soft);color:var(--warn)} |
| tr.pick{cursor:pointer} |
| tr.pick:hover td{background:var(--accent-soft)} |
| .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:10px;margin:12px 0} |
| .fact{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:11px 13px;font-size:13px} |
| .fact b{display:block;color:var(--accent);font-size:12px;margin-bottom:3px} |
| .pcard{border:1px solid var(--line);border-radius:11px;margin:10px 0;overflow:hidden} |
| .phead{display:flex;align-items:center;gap:9px;padding:9px 13px;background:var(--bg); |
| border-bottom:1px solid var(--line);flex-wrap:wrap} |
| .ptag{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px; |
| border-radius:6px;background:var(--accent);color:#fff;font-size:12px;font-weight:700} |
| .phead b{font-size:14px} .phead span{color:var(--muted);font-size:12.5px} |
| .pcopy{margin-left:auto;border:1px solid var(--line);background:var(--card);color:var(--muted); |
| border-radius:7px;padding:4px 11px;cursor:pointer;font:inherit;font-size:12px} |
| .pcopy:hover{border-color:var(--accent);color:var(--accent)} |
| .pbody{padding:12px 14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace; |
| font-size:12.5px;line-height:1.6;white-space:pre-wrap;color:var(--ink);max-height:280px;overflow:auto} |
| .pbody em{color:var(--warn);font-style:normal;font-weight:600} |
| .mrow{display:grid;grid-template-columns:minmax(120px,1.4fr) 1fr auto;gap:10px; |
| align-items:center;padding:7px 0;border-bottom:1px solid var(--line);font-size:13px} |
| .mrow:last-child{border-bottom:0} |
| .mname{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| .mbar{position:relative;height:16px;background:var(--bg);border-radius:5px;overflow:hidden} |
| .mbar i{position:absolute;inset:0 auto 0 0;background:var(--accent);border-radius:5px} |
| .mbar u{position:absolute;top:0;bottom:0;width:2px;background:var(--ink);opacity:.55} |
| .mnum{font-variant-numeric:tabular-nums;color:var(--muted);white-space:nowrap;font-size:12.5px} |
| .mlegend{display:flex;gap:14px;color:var(--muted);font-size:12px;margin-top:10px;flex-wrap:wrap} |
| .mlegend i{display:inline-block;width:10px;height:10px;background:var(--accent);border-radius:2px;margin-right:4px} |
| .mlegend u{display:inline-block;width:2px;height:11px;background:var(--ink);opacity:.55;margin-right:4px; |
| vertical-align:-1px;text-decoration:none} |
| </style> |
| </head> |
| <body> |
| <div class="wrap"> |
|
|
| <header> |
| <h1 id="t_title">Open Materials Challenge</h1> |
| <span class="sub" id="t_sub"></span> |
| <div id="langbar"> |
| <button data-lang="ko" class="on">한국어</button> |
| <button data-lang="en">English</button> |
| </div> |
| </header> |
| <div id="seasonbar" style="margin:14px 0 0"></div> |
| <div class="sinfo" id="sinfo"></div> |
| <a class="xlink" id="xlink" href="https://huggingface.co/spaces/FINAL-Bench/open-discovery-challenge" target="_blank" rel="noopener"></a> |
| <a class="xlink" id="xlink2" href="https://huggingface.co/spaces/FINAL-Bench/leadboard" target="_blank" rel="noopener"></a> |
| <div id="tabbar"></div> |
|
|
| <section data-tab="board"> |
| <div class="card"> |
| <h2 id="t_about_h"></h2> |
| <div id="t_about"></div> |
| <div class="axes" id="wts"></div> |
| <div class="axhelp" id="axhelp"></div> |
| <div class="callout" id="t_honest"></div> |
| </div> |
|
|
| </section> |
|
|
| <section data-tab="guide" hidden> |
| <div class="card"> |
| <h2 id="t_guide_h"></h2> |
| <p class="lead" id="t_guide_lead"></p> |
| <div class="facts" id="facts"></div> |
| <div id="t_guide_how"></div> |
| <div id="prompts"></div> |
| </div> |
|
|
| </section> |
|
|
| <section data-tab="models" hidden> |
| <div class="card"> |
| <h2><span id="t_models_h"></span><span class="cnt" id="mcounts"></span></h2> |
| <div id="t_models_lead" class="note" style="margin-top:0"></div> |
| <div id="models"></div> |
| <div class="mlegend" id="t_mlegend"></div> |
| </div> |
| </section> |
|
|
| <section data-tab="board" hidden> |
| <div class="grid"> |
| <div class="card"> |
| <h2><span id="t_board_h"></span><span class="cnt" id="counts"></span></h2> |
| <div id="divbar"></div> |
| <div id="trackbar"></div> |
| <div class="scroll"> |
| <table> |
| <thead><tr id="head"></tr></thead> |
| <tbody id="rows"></tbody> |
| </table> |
| </div> |
| <div class="pager" id="pager"></div> |
| <div class="note" id="t_boardnote"></div> |
| </div> |
|
|
| <div class="card"> |
| <h2 id="t_submit_h"></h2> |
| <div id="gate" class="callout" style="display:none"></div> |
| <div class="me" id="me"></div> |
| <label id="l_formula"></label> |
| <input id="f_formula" placeholder="Li3YCl6"> |
| <label id="l_name"></label> |
| <input id="f_name" placeholder=""> |
| <label id="l_model"></label> |
| <input id="f_model" placeholder="anthropic/claude-…, Qwen/…"> |
| <label id="l_cif"></label> |
| <textarea id="f_cif" placeholder="# generated using pymatgen data_…"></textarea> |
| <div class="note" id="t_cifnote" style="margin-top:4px"></div> |
| <label id="l_why"></label> |
| <textarea id="f_why"></textarea> |
| <label id="l_vis"></label> |
| <div class="radio" id="visbox"> |
| <label class="on"><input type="radio" name="vis" value="private" checked><span id="t_priv"></span></label> |
| <label><input type="radio" name="vis" value="public"><span id="t_pub"></span></label> |
| </div> |
| <button class="go" id="go"></button> |
| <div class="msg" id="msg"></div> |
| <div class="note" id="t_limits"></div> |
| </div> |
| </div> |
|
|
| </section> |
|
|
| <section data-tab="board" hidden> |
| <div class="card"> |
| <h2 id="t_broker_h"></h2> |
| <div id="t_broker"></div> |
| </div> |
| </section> |
|
|
| </div> |
| <script> |
| const T = { |
| ko:{ |
| sub:"인류의 난제를 AI 와 함께 — 개인이 참여하는 신소재 탐색", |
| about_h:"이 대회는 무엇인가", |
| about:`<p>전고체 배터리는 리튬이온 전지의 액체 전해질을 고체로 바꾼 것입니다. 더 안전하고 더 오래 갈 수 있지만, |
| <b>고체이면서 리튬만 잘 통과시키는 물질</b>을 찾는 일이 아직 풀리지 않았습니다.</p> |
| <p>이 대회는 그 후보 물질을 찾습니다. 참가자는 <b>조성 하나</b>를 냅니다 — 예: <code>Li3YCl6</code>. |
| 저희가 계산으로 채점해 순위표에 올립니다. 상금은 없습니다.</p>`, |
| honest:`<b>이 시즌이 채점하는 것.</b> <b>전기화학적 강건성</b>입니다 — 높은 전압을 견디는지, |
| 리튬 금속에 닿아도 버티는지, 리튬이 지나갈 길이 뚫려 있는지. |
| <b>이온전도도는 이번 시즌의 채점 항목이 아닙니다.</b> 따라서 전도도가 높은 계열이라도 |
| 산화창이 좁거나 리튬 금속과 반응하면 낮게 나옵니다 — 그 두 가지가 이 시즌이 묻는 문제입니다. |
| 모든 값은 <b>계산 추정치</b>이며 실제 성능이나 안전성을 뜻하지 않습니다.`, |
| guide_h:"참가 가이드", |
| guide_lead:`전고체 전지는 리튬 이온이 <b>고체 안을</b> 지나가야 합니다. 그런데 그 물질은 동시에 |
| <b>전자는 통과시키지 않아야</b> 하고, 충전 전압을 견뎌야 하고, 음극의 <b>리튬 금속</b>에 닿아도 |
| 분해되지 않아야 합니다. 이 네 가지를 한 물질에서 동시에 만족시키는 것이 아직 풀리지 않은 문제입니다.`, |
| facts:[ |
| ["표적", "리튬이 지나갈 통로가 있는 고체. 리튬을 반드시 포함해야 합니다."], |
| ["넘어야 할 관문", "전자가 통하면 전지가 그대로 단락됩니다. 전자 절연성이 전제 조건입니다."], |
| ["가장 어려운 곳", "산화 안정성과 리튬금속 안정성은 서로 잘 안 맞습니다. 둘 다 잡는 것이 이 시즌의 과제입니다."], |
| ["제출", "조성 하나. 구조(CIF)는 선택이며, 내면 채점이 더 빠르고 정확합니다."]], |
| guide_how:`<p style="margin:14px 0 2px">아래 프롬프트를 쓰시는 AI 모델에 그대로 넣으시면 됩니다. |
| <b>복사</b> 버튼으로 가져가신 뒤, 기울임체로 표시된 부분은 본인 관심사로 바꿔 주십시오 — |
| 그래야 다른 참가자와 같은 후보가 겹치지 않습니다. 나온 조성을 오른쪽 칸에 하나씩 제출하시면 됩니다.</p>`, |
| models_h:"AI 모델별 성과", |
| models_lead:`어느 모델이 <b>가장 좋은 후보</b>를 냈는지(막대)와 <b>얼마나 쓰이는지</b>(오른쪽)를 |
| 함께 봅니다. 많이 쓰인다고 잘하는 것은 아니므로 따로 표시합니다.`, |
| mempty:"채점된 제출이 쌓이면 모델별 성과가 여기에 표시됩니다.", |
| mcounts:(m,n)=>`모델 ${m} · 제출 ${n}`, |
| mlegend:`<span><i></i>최고 점수</span><span><u></u>평균</span><span>제출 수</span>`, |
| tabs:{board:"대회 · 순위표", guide:"참가 가이드", models:"모델별 성과"}, |
| tracks:{all:"전체", mp:"MP 수록 (MP 값으로 채점)", novel:"신규 조성 (직접 계산)"}, |
| tracknote:"MP 에 값이 있으면 그 값을, 없으면 저희가 직접 계산합니다. 두 자의 눈금이 달라 (같은 조성 40건 실측, 직접 계산 쪽이 88%에서 평균 1.0점 낮음) 순위를 따로 매깁니다.", |
| divs:{all:"전체", fluoride:"불화물", halide:"염화·브롬화·요오드화", sulfide:"황화물", |
| oxide:"산화물", nitride:"질화물", hydride:"수소화물", other:"기타"}, |
| divnote:"음이온 골격에 따라 부문을 나눴습니다. <b>점수는 부문과 무관하게 동일한 기준</b>으로 매겨지며, 부문은 같은 화학 계열 안에서 비교하시라고 둔 것입니다.", |
| xlink:`🧪 <b>Open Discovery Challenge</b> <em>— 신약 후보 물질을 찾는 자매 대회</em> ↗`, |
| xlink2:`🎯 <b>LEADBOARD</b> <em>— 신약 예측 도구를 분야별로 재는 시험대</em> ↗`, |
| sinfo:(o,c)=>[`시즌 기간 <b>${o} ~ ${c}</b>`,`상금 <b>없음</b>`,`제출 <b>조성 하나</b>`], |
| axhelp:[ |
| ["산화 안정성","충전 전압이 높아졌을 때 전해질 자신이 분해되지 않고 버티는 정도."], |
| ["Li금속 안정성","리튬 금속 음극에 닿았을 때 반응해 망가지지 않는 정도."], |
| ["용도 신규성","이 조성이 고체전해질 용도로 보고된 적이 없을수록 높습니다."], |
| ["리튬 이동 경로","점수가 아니라 <b>참가 조건</b>입니다. 리튬이 지나갈 관통 경로가 없으면 접수되지 않습니다."]], |
| limits:(L)=>`제출 제한 — 리튬 필수 · 기약 조성 원자 수 ${L.max_atoms}개 이하 · 원소 6종 이하. |
| 같은 물질을 정수배로 바꿔 다시 내면 같은 제출로 처리됩니다.`, |
| detail_h:"채점 내역", |
| board_h:"순위표", |
| boardnote:`<b>기준물질(노란 행)</b>은 점수를 받되 등수를 갖지 않습니다. 실제로 쓰이는 전해질이 몇 점인지 |
| 눈으로 비교하시라고 넣었습니다.<br> |
| 비공개 제출은 조성을 가리고 <b>원소 종류만</b> 표시합니다. 원본은 비공개로 보관됩니다.<br> |
| <b>구조 필요 · 비교자료 부족</b>는 채점 요건이 갖춰지지 않은 상태입니다. 0점으로 기록하지 |
| 않고 등수도 매기지 않습니다 — 재지 않은 것을 성적으로 적지 않기 때문입니다. |
| <b>대부분은 구조 파일(CIF)을 첨부해 다시 제출하시면 채점됩니다.</b> 행을 누르면 필요한 것이 나옵니다.<br> |
| 열 제목을 눌러 정렬할 수 있습니다.`, |
| submit_h:"제출", |
| l_formula:"조성 (화학식)", l_name:"표시 ID (이름·소속·닉네임)", l_model:"사용 모델명 (선택)", |
| l_cif:"구조 파일 CIF (선택)", l_why:"설계 근거 (선택)", l_vis:"공개 여부", |
| cifnote:`구조를 함께 내면 더 빠르고 정확하게 채점됩니다. 없으면 저희가 구조를 추정하며, |
| 추정이 되지 않는 조성은 <b>구조 필요</b>로 표시되고 구조를 첨부해 다시 제출하시면 채점됩니다.`, |
| priv:"🔒 비공개 (기본)", pub:"🔓 조성 공개", |
| go:"제출하기", |
| gate:"제출하려면 <b>Hugging Face 로그인</b>이 필요합니다. 제출자 계정이 순위표에 함께 표시되며, 이는 다른 사람 이름으로 올리는 것을 막고 모델별 집계를 신뢰할 수 있게 합니다.", |
| signin:"Hugging Face 로 로그인", signout:"로그아웃", signed_as:"로그인:", |
| broker_h:"제출 기록과 제3자 연결", |
| broker:`<p>제출은 시각과 함께 저희 원장에 기록됩니다. 조성을 <b>비공개</b>로 두면 저희만 보관하며, |
| 공개하지 않는 한 외부에 드러나지 않습니다. 이 기록이 향후 권리 주장에 참고가 될 수 있으나, |
| <b>특허 출원을 대신하지 않으며 법적 효력을 보장하지 않습니다.</b> 출원은 전문가와 상의하십시오.</p> |
| <p>제3자가 특정 제출에 대해 협의를 요청하는 경우, 운영자가 <b>중개</b>합니다. |
| 제출자의 신원이나 연락처는 <b>본인 동의 없이 전달하지 않습니다.</b></p>`, |
| cols:{rank:"#",cand:"후보",user:"제출자",who:"모델",date:"제출일", |
| oxidation:"산화",li_stability:"Li안정",migration:"이동",novelty:"신규",total:"총점"}, |
| counts:(a,b,h)=>`채점 ${a} · 기준물질 ${b}` + (h ? ` · 미산출 ${h}` : ""), |
| prange:(a,b,n)=>`${a.toLocaleString()}–${b.toLocaleString()} / 전체 ${n.toLocaleString()}`, |
| empty:"아직 채점된 제출이 없습니다.", |
| fail:"순위표를 불러오지 못했습니다: ", |
| ok:(id,q)=>`접수되었습니다. 후보 ID <b>${id}</b> · 대기 ${q}건`, |
| need:"조성과 표시 ID를 입력해 주세요.", |
| hold:"미산출", |
| holds:{need_structure:"구조 필요", no_reference:"비교자료 부족", no_value:"산출 불가"}, |
| holdnote:"아래 요건을 갖춰 다시 제출하시면 채점됩니다." |
| }, |
| en:{ |
| sub:"Open problems, worked on together — materials discovery anyone can join", |
| about_h:"What this is", |
| about:`<p>A solid-state battery replaces the liquid electrolyte of a lithium-ion cell with a solid. |
| It can be safer and last longer, but finding a material that is <b>solid and still lets lithium through</b> |
| remains unsolved.</p> |
| <p>This challenge looks for candidates. You submit <b>one composition</b> — for example <code>Li3YCl6</code>. |
| We score it computationally and place it on the board. There is no prize.</p>`, |
| honest:`<b>What this season scores.</b> <b>Electrochemical robustness</b>: whether the material holds up at |
| high voltage, survives contact with lithium metal, and has a path for lithium to move through. |
| <b>Ionic conductivity is not a scored axis this season.</b> A highly conductive family therefore still |
| scores low if its oxidation window is narrow or it reacts with lithium metal — those two are the problem |
| this season puts to you. |
| Every value is a <b>computational estimate</b> and implies nothing about real performance or safety.`, |
| guide_h:"How to take part", |
| guide_lead:`In a solid-state cell the lithium ion has to travel <b>through a solid</b>. That same solid |
| must <b>block electrons</b>, hold up at the charging voltage, and survive contact with the |
| <b>lithium metal</b> anode without decomposing. Meeting all four in one material is the open problem.`, |
| facts:[ |
| ["Target", "A solid with a path for lithium to move through. It must contain lithium."], |
| ["Get past this", "If electrons pass, the cell simply shorts. Electronic insulation is a precondition."], |
| ["Hardest part", "Oxidation stability and lithium-metal stability pull against each other. Holding both is the task."], |
| ["Submission", "One composition. A structure (CIF) is optional and makes scoring faster and more accurate."]], |
| guide_how:`<p style="margin:14px 0 2px">Paste a prompt below into the model you use. Hit <b>Copy</b>, |
| then change the italicised part to something of your own — otherwise your candidates will collide with |
| other entrants'. Submit the compositions it returns, one at a time, on the right.</p>`, |
| models_h:"By model", |
| models_lead:`Which model produced the <b>best candidate</b> (bar) and <b>how often it is used</b> (right). |
| Being used a lot is not the same as doing well, so the two are shown separately.`, |
| mempty:"Model standings appear here once scored entries accumulate.", |
| mcounts:(m,n)=>`${m} models · ${n} entries`, |
| mlegend:`<span><i></i>Best score</span><span><u></u>Mean</span><span>Entries</span>`, |
| tabs:{board:"Challenge · Board", guide:"How to take part", models:"By model"}, |
| tracks:{all:"All", mp:"In MP (scored on MP values)", novel:"Novel (computed here)"}, |
| tracknote:"Where Materials Project has a value we use it; where it does not, we compute one. The two rulers disagree (40 identical compositions measured; the computed path scores 1.0 point lower on 88% of them), so the two are ranked separately.", |
| divs:{all:"All", fluoride:"Fluoride", halide:"Chloride / bromide / iodide", sulfide:"Sulfide", |
| oxide:"Oxide", nitride:"Nitride", hydride:"Hydride", other:"Other"}, |
| divnote:"Divisions follow the anion framework. <b>Scores are identical across divisions</b> - the split is there so you can compare within a chemical family.", |
| xlink:`🧪 <b>Open Discovery Challenge</b> <em>— the sister challenge, for drug candidates</em> ↗`, |
| xlink2:`🎯 <b>LEADBOARD</b> <em>— the benchmark for drug prediction tools</em> ↗`, |
| sinfo:(o,c)=>[`Season <b>${o} – ${c}</b>`,`Prize <b>none</b>`,`Submit <b>one composition</b>`], |
| axhelp:[ |
| ["Oxidation","How well the electrolyte itself resists decomposing as the cell voltage rises."], |
| ["Li-metal stability","How well it survives contact with a lithium-metal anode."], |
| ["Use novelty","Higher when the composition has not been reported as a solid electrolyte."], |
| ["Lithium pathway","Not a score but an <b>entry condition</b>: without a percolating path for lithium, an entry is not accepted."]], |
| limits:(L)=>`Limits — lithium required · at most ${L.max_atoms} atoms in the reduced composition · |
| at most 6 elements. Re-submitting the same material as an integer multiple counts as the same entry.`, |
| detail_h:"How this was scored", |
| board_h:"Leaderboard", |
| boardnote:`<b>Reference materials (amber rows)</b> are scored but hold no rank. They are there so you can see |
| where materials in actual use land.<br> |
| Private entries show <b>elements only</b>; the composition is held back.<br> |
| <b>Structure needed / no reference data</b> means the requirements for scoring are not yet met. |
| Nothing is recorded as zero and no rank is assigned - an unmeasured entry is not given a standing. |
| <b>Most cases are scored once a structure file (CIF) is attached and the entry resubmitted.</b> |
| Click the row to see what is required.<br> |
| Click a column header to sort.`, |
| submit_h:"Submit", |
| l_formula:"Composition (formula)", l_name:"Display ID (name, affiliation or handle)", |
| l_model:"Model used (optional)", l_cif:"Structure file, CIF (optional)", |
| l_why:"Design rationale (optional)", l_vis:"Visibility", |
| cifnote:`Including a structure makes scoring faster and more accurate. Without one we estimate the |
| structure; where that is not possible the entry is marked <b>structure needed</b> and is scored |
| once a structure is attached and it is resubmitted.`, |
| priv:"🔒 Private (default)", pub:"🔓 Show composition", |
| go:"Submit", |
| gate:"Submitting requires a <b>Hugging Face sign-in</b>. Your account appears alongside the entry, which stops anyone posting under another person's name and keeps the per-model standings trustworthy.", |
| signin:"Sign in with Hugging Face", signout:"Sign out", signed_as:"Signed in as", |
| broker_h:"Record of submission, and third-party contact", |
| broker:`<p>Entries are recorded with a timestamp in our ledger. A <b>private</b> composition stays with us and |
| is not disclosed unless you choose to publish it. This record may serve as a reference later, but it |
| <b>is not a patent filing and carries no guaranteed legal effect.</b> Consult a professional before filing.</p> |
| <p>If a third party asks to discuss a specific entry, we pass the request along. We never share a |
| submitter's identity or contact details <b>without their consent.</b></p>`, |
| cols:{rank:"#",cand:"Entry",user:"Submitter",who:"Model",date:"Date", |
| oxidation:"Oxid.",li_stability:"Li-stab.",migration:"Migration",novelty:"Novelty",total:"Total"}, |
| counts:(a,b,h)=>`${a} scored · ${b} references` + (h ? ` · ${h} not scored` : ""), |
| prange:(a,b,n)=>`${a.toLocaleString()}–${b.toLocaleString()} of ${n.toLocaleString()}`, |
| empty:"No entries have been scored yet.", |
| fail:"Could not load the leaderboard: ", |
| ok:(id,q)=>`Accepted. Entry <b>${id}</b> · ${q} in queue`, |
| need:"Please enter a composition and a display ID.", |
| hold:"not scored", |
| holds:{need_structure:"structure needed", no_reference:"no reference data", no_value:"no value"}, |
| holdnote:"Meet the requirement below and resubmit; the entry will then be scored." |
| } |
| }; |
| |
| |
| |
| const PROMPTS_BY_SEASON = { |
| 1: [ |
| {id:"A", ko:["기본형","처음 시작한다면 이것부터"], en:["Starter","Begin here"], |
| body:`You are proposing candidate solid electrolytes for an all-solid-state lithium battery, |
| for an open materials challenge. |
| |
| Requirements for every candidate: |
| - Contains lithium, with a plausible connected pathway for Li+ to migrate through the |
| lattice. |
| - An electronic insulator. If electrons conduct, the cell short-circuits; this is a |
| precondition, not a trade-off. |
| - Wide electrochemical window: stable against oxidation at high cell voltage. |
| - Stable in contact with lithium metal at the anode, or decomposing only into a passivating |
| layer rather than an electronically conductive one. |
| - Plausibly synthesisable: a composition close to a stable phase, not an exotic guess. |
| |
| Propose 8 candidate compositions as a JSON array: |
| [{"formula":"...","name":"...","rationale":"why lithium should move through it, why it |
| should not conduct electrons, and how it behaves against lithium metal"}] |
| |
| Use plain chemical formulae, e.g. Li3YCl6 or LiZr2(PO4)3. |
| |
| <em>VARY THIS: name a chemical family you want to explore (halides, oxides, phosphates, |
| oxyhalides, anti-perovskites, …) or a constraint of your own, so your set does not collide |
| with other entrants'. Identical submissions are rejected.</em>`}, |
| |
| {id:"B", ko:["산화 안정성","높은 전압을 견디는 설계 (40점)"], en:["Oxidation","Holding up at high voltage (40 pts)"], |
| body:`Propose solid lithium-ion electrolytes designed specifically for a WIDE oxidation |
| window, so they survive against a high-voltage cathode. |
| |
| Reason from this: |
| - The anion is usually what oxidises first. Sulfide frameworks conduct well but oxidise at |
| low potential; more electronegative anions push the oxidation limit higher. |
| - The counter-cation matters: a cation already in a high oxidation state has less room to |
| be oxidised further. |
| - A material that resists oxidation but has no lithium pathway is worthless. A connected |
| migration path is a precondition, not a trade-off. |
| |
| For each candidate state explicitly: which anion framework you chose and why you expect it |
| to resist oxidation, and how lithium still moves through it. |
| |
| Propose 8 candidates as JSON: [{"formula","name","rationale"}] |
| |
| <em>VARY THIS: commit to ONE anion chemistry and build the whole set around it, rather than |
| eight unrelated guesses.</em>`}, |
| |
| {id:"C", ko:["리튬금속 안정성","음극에서 버티는 설계 (35점)"], en:["Lithium-metal stability","Surviving the anode (35 pts)"], |
| body:`Propose solid lithium-ion electrolytes designed to survive direct contact with |
| LITHIUM METAL. |
| |
| Reason from this: |
| - Lithium metal is strongly reducing. Any cation in the electrolyte that can be reduced |
| will be. Transition metals in high oxidation states are the classic failure: they reduce, |
| the interface becomes electronically conductive, and the reaction never stops. |
| - The useful case is a decomposition product that is itself an insulator, so the interface |
| passivates and the reaction halts. |
| - Wide oxidation window alone does not help if the anode side fails. Both ends of the |
| window matter. |
| |
| For each candidate state explicitly: which element you expect lithium metal to attack, and |
| why the interface should stop reacting rather than keep going. |
| |
| Propose 8 candidates as JSON: [{"formula","name","rationale"}] |
| |
| <em>VARY THIS: choose one strategy — reduction-proof cations, or a self-passivating |
| decomposition product — and build the set around it.</em>`}, |
| |
| {id:"D", ko:["용도 신규성","가장 큰 점수 여지 (25점)"], en:["Use novelty","Where the headroom is (25 pts)"], |
| body:`Propose lithium-containing compositions that are NOT already reported as solid |
| electrolytes. |
| |
| Important distinction: the composition does not have to be brand new. What matters is |
| whether anyone has proposed it FOR THIS USE. A known compound that nobody has evaluated as |
| a lithium electrolyte is exactly the target; a well-studied electrolyte family is not. |
| |
| So avoid the ones already worked over — the common sulfide, garnet, NASICON and |
| argyrodite electrolyte families — and look at lithium compounds studied for other reasons |
| entirely, or not studied much at all. |
| |
| Every candidate must still meet the basics: a lithium migration path, electronic |
| insulation, and reasonable stability at both electrodes. Novelty with no function scores |
| nothing. |
| |
| For each candidate state: what the composition is normally known for (if anything), and |
| why it might work as an electrolyte. |
| |
| Propose 8 candidates as JSON: [{"formula","name","rationale"}] |
| |
| <em>VARY THIS: pick one source of overlooked compounds — a mineral class, a family studied |
| for optics or ionics elsewhere, a ternary system that is sparsely mapped — and work it.</em>`} |
| ] |
| }; |
| |
| const $ = s => document.querySelector(s); |
| const COLS = ["rank","cand","user","who","date","oxidation","li_stability","novelty","total"]; |
| |
| const PAGE_SIZE = 500; |
| let LANG = "ko", DATA = null, SEASONS = [], SEASON = 1; |
| let sortKey = "total", sortDir = -1, page = 0, DIV = "all", TRK = "all"; |
| const t = () => T[LANG]; |
| |
| let ME = null; |
| |
| function renderAuth(){ |
| const d = t(), g = $("#gate"), m = $("#me"); |
| if(!ME || !ME.oauth_configured){ g.style.display="none"; m.innerHTML=""; return; } |
| if(ME.signed_in && ME.user){ |
| g.style.display = "none"; |
| const u = ME.user; |
| const pic = u.picture ? `<img src="${esc(u.picture)}" alt="">` : "🤗"; |
| m.innerHTML = `${pic}<span>${d.signed_as} ${hfProfile(u.name)}</span>` |
| + `<a class="signin out" href="/logout" style="margin-left:auto">${esc(d.signout)}</a>`; |
| } else { |
| g.style.display = "block"; |
| g.innerHTML = d.gate; |
| m.innerHTML = `<a class="signin" href="/login">🤗 ${esc(d.signin)}</a>`; |
| } |
| } |
| |
| function hfProfile(user){ |
| if(!user) return ""; |
| const url = `https://huggingface.co/${encodeURIComponent(user)}`; |
| return `<a class="hf-user" href="${url}" target="_blank" rel="noopener noreferrer"` |
| + ` title="Hugging Face: ${esc(user)}">🤗 ${esc(user)}</a>`; |
| } |
| |
| function esc(s){ return String(s ?? "").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } |
| |
| function setLang(l){ |
| LANG = l; |
| document.documentElement.lang = l; |
| document.querySelectorAll("#langbar button").forEach(b=>b.classList.toggle("on", b.dataset.lang===l)); |
| const d = t(); |
| $("#t_sub").textContent = d.sub; |
| $("#t_about_h").textContent = d.about_h; |
| $("#t_about").innerHTML = d.about; |
| $("#t_honest").innerHTML = d.honest; |
| $("#t_board_h").textContent = d.board_h; |
| $("#t_boardnote").innerHTML = d.boardnote; |
| $("#t_submit_h").textContent = d.submit_h; |
| $("#t_broker_h").textContent = d.broker_h; |
| $("#t_broker").innerHTML = d.broker; |
| $("#t_cifnote").innerHTML = d.cifnote; |
| $("#xlink").innerHTML = d.xlink; |
| { const x2 = document.querySelector("#xlink2"); if(x2 && d.xlink2) x2.innerHTML = d.xlink2; } |
| renderAuth(); |
| $("#t_guide_h").textContent = d.guide_h; |
| $("#t_guide_lead").innerHTML = d.guide_lead; |
| $("#t_guide_how").innerHTML = d.guide_how; |
| $("#t_models_h").textContent = d.models_h; |
| $("#t_models_lead").innerHTML = d.models_lead; |
| renderFacts(); |
| renderPrompts(); |
| renderTabs(); |
| renderSeasonInfo(); |
| renderAxHelp(); |
| $("#t_priv").textContent = d.priv; |
| $("#t_pub").textContent = d.pub; |
| $("#go").textContent = d.go; |
| for(const k of ["formula","name","model","cif","why","vis"]) $("#l_"+k).textContent = d["l_"+k]; |
| const g = $("#gate"); if(g.style.display !== "none") g.innerHTML = d.gate; |
| renderSeasonBar(); |
| if(DATA) render(); |
| } |
| document.querySelectorAll("#langbar button").forEach(b=>b.onclick=()=>setLang(b.dataset.lang)); |
| |
| function renderSeasonBar(){ |
| $("#seasonbar").innerHTML = SEASONS.slice().sort((a,b)=>b.number-a.number).map(s=>{ |
| const nm = LANG==="ko" ? (s.topic_ko||s.topic) : s.topic; |
| return `<button data-season="${s.number}" class="${s.number===SEASON?"on":""}"> |
| #${s.number} ${esc(nm)}</button>`; |
| }).join(""); |
| document.querySelectorAll("#seasonbar button").forEach(b=> |
| b.onclick=()=>{ SEASON=parseInt(b.dataset.season,10); page=0; |
| renderSeasonBar(); renderPrompts(); renderSeasonInfo(); renderAxHelp(); |
| MODELS_LOADED = false; load(); if(TAB==="models"){ MODELS_LOADED=true; loadModels(); } }); |
| } |
| |
| function sortVal(e,k){ |
| if(k==="rank") return e.rank ?? 1e9; |
| if(k==="cand") return String(e.candidate_id||"").toLowerCase(); |
| if(k==="user") return String(e.hf_user||e.display_name||"").toLowerCase(); |
| if(k==="who") return String(e.model_name||"").toLowerCase(); |
| if(k==="date") return e.submitted_at ?? 0; |
| if(k==="total") return e.total ?? -1; |
| return (e.axes||{})[k] ?? -1; |
| } |
| |
| let TAB = "board"; |
| |
| function renderTabs(){ |
| const d = t(); |
| $("#tabbar").innerHTML = ["board","guide","models"].map(k=> |
| `<button data-tab="${k}" class="${k===TAB?"on":""}">${esc(d.tabs[k])}</button>`).join(""); |
| $("#tabbar").querySelectorAll("button").forEach(b=>b.onclick=()=>{ |
| TAB = b.dataset.tab; renderTabs(); showTab(); |
| window.scrollTo({top:0,behavior:"smooth"}); |
| }); |
| } |
| |
| function showTab(){ |
| document.querySelectorAll("section[data-tab]").forEach(sec=>{ |
| sec.hidden = sec.dataset.tab !== TAB; |
| }); |
| |
| if(TAB==="models" && !MODELS_LOADED){ MODELS_LOADED = true; loadModels(); } |
| } |
| let MODELS_LOADED = false; |
| |
| function seasonInfo(){ return SEASONS.find(x=>x.number===SEASON) || SEASONS[0] || {}; } |
| |
| function renderSeasonInfo(){ |
| const si = seasonInfo(), d = t(); |
| if(!si.opens){ $("#sinfo").innerHTML=""; return; } |
| $("#sinfo").innerHTML = d.sinfo(si.opens, si.closes).map(x=>`<span>${x}</span>`).join(""); |
| } |
| |
| function renderAxHelp(){ |
| const d = t(); |
| $("#axhelp").innerHTML = (d.axhelp||[]).map(([k,v])=> |
| `<div><b>${esc(k)}</b> — ${esc(v)}</div>`).join(""); |
| const L = (seasonInfo().limits)||{}; |
| $("#t_limits").innerHTML = L.max_atoms ? d.limits(L) : ""; |
| } |
| |
| function promptsFor(){ return PROMPTS_BY_SEASON[SEASON] || PROMPTS_BY_SEASON[1]; } |
| |
| function renderPrompts(){ |
| const d = t(), host = $("#prompts"); |
| if(!host) return; |
| host.innerHTML = promptsFor().map((p,i)=>{ |
| const [title,sub] = (LANG==="ko" ? p.ko : p.en); |
| return `<div class="pcard"> |
| <div class="phead"><span class="ptag">${p.id}</span><b>${esc(title)}</b><span>${esc(sub)}</span> |
| <button class="pcopy" data-i="${i}">${LANG==="ko"?"복사":"Copy"}</button></div> |
| <div class="pbody">${p.body.trim()}</div></div>`; |
| }).join(""); |
| host.querySelectorAll(".pcopy").forEach(b=>b.onclick=async()=>{ |
| |
| const raw = promptsFor()[+b.dataset.i].body.replace(/<\/?em>/g,"").trim(); |
| try{ await navigator.clipboard.writeText(raw); } |
| catch(_){ const ta=document.createElement("textarea"); ta.value=raw; |
| document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove(); } |
| const was=b.textContent; |
| b.textContent = LANG==="ko" ? "복사됨 ✓" : "Copied ✓"; |
| setTimeout(()=>{ b.textContent=was; },1500); |
| }); |
| } |
| |
| function renderFacts(){ |
| const d = t(); |
| $("#facts").innerHTML = (d.facts||[]).map(([k,v])=> |
| `<div class="fact"><b>${esc(k)}</b>${esc(v)}</div>`).join(""); |
| } |
| |
| function renderModels(m){ |
| const d = t(), host = $("#models"); |
| const rows = (m && m.models) || []; |
| $("#mcounts").textContent = d.mcounts((m&&m.counts&&m.counts.models)||0, |
| (m&&m.counts&&m.counts.entries)||0); |
| $("#t_mlegend").innerHTML = d.mlegend; |
| if(!rows.length){ host.innerHTML = `<div class="note">${d.mempty}</div>`; return; } |
| const max = Math.max(...rows.map(r=>r.best), 1); |
| host.innerHTML = rows.slice(0,14).map(r=>` |
| <div class="mrow"> |
| <div class="mname" title="${esc(r.model)}">${esc(r.model)}</div> |
| <div class="mbar"><i style="width:${(r.best/max*100).toFixed(1)}%"></i> |
| <u style="left:${(r.mean/max*100).toFixed(1)}%"></u></div> |
| <div class="mnum">${r.best.toFixed(1)} · ${r.n}</div> |
| </div>`).join(""); |
| } |
| |
| async function loadModels(){ |
| try{ renderModels(await (await fetch(`/api/models?season=${SEASON}`)).json()); } |
| catch(e){ renderModels(null); } |
| } |
| |
| function renderDivBar(){ |
| const d = t(), host = $("#divbar"); |
| const tally = (DATA && DATA.divisions) || {}; |
| const keys = Object.keys(tally).sort((a,b)=>(tally[b]||0)-(tally[a]||0)); |
| if(!keys.length){ host.innerHTML=""; return; } |
| const total = Object.values(tally).reduce((a,b)=>a+b,0); |
| const btn = (k,n)=>`<button data-d="${k}" class="${k===DIV?"on":""}">${esc(d.divs[k]||k)}<span class="dn">${n}</span></button>`; |
| host.innerHTML = btn("all",total) + keys.map(k=>btn(k,tally[k])).join(""); |
| host.querySelectorAll("button[data-d]").forEach(b=>b.onclick=()=>{ |
| DIV=b.dataset.d; page=0; render(); |
| }); |
| } |
| |
| function renderTrackBar(){ |
| const d = t(), host = $("#trackbar"); |
| if(!host) return; |
| const tally = {all:0, mp:0, novel:0}; |
| (DATA.entries||[]).forEach(e=>{ if(e.is_anchor) return; tally.all++; |
| if(e.track==="mp") tally.mp++; else if(e.track==="novel") tally.novel++; }); |
| if(!tally.mp && !tally.novel){ host.innerHTML=""; return; } |
| const btn=(k,n)=>`<button data-k="${k}" class="${k===TRK?"on":""}">${esc(d.tracks[k]||k)}<span class="dn">${n}</span></button>`; |
| host.innerHTML = btn("all",tally.all)+btn("mp",tally.mp)+btn("novel",tally.novel) |
| + `<div class="sub" style="margin-top:6px">${esc(d.tracknote||"")}</div>`; |
| host.querySelectorAll("button[data-k]").forEach(b=>b.onclick=()=>{ |
| TRK=b.dataset.k; page=0; render(); |
| }); |
| } |
| |
| function renderPager(total,pages){ |
| const host = $("#pager"), d = t(); |
| if(pages<=1){ host.innerHTML=""; return; } |
| const from = page*PAGE_SIZE+1, to = Math.min(total,(page+1)*PAGE_SIZE); |
| let nums=[]; |
| if(pages<=9){ for(let i=0;i<pages;i++) nums.push(i); } |
| else{ |
| const near=[page-1,page,page+1].filter(i=>i>0&&i<pages-1); |
| nums=[...new Set([0,...near,pages-1])].sort((a,b)=>a-b); |
| } |
| let out=`<button ${page===0?"disabled":""} data-p="${page-1}">‹</button>`, prev=-1; |
| for(const i of nums){ |
| if(i-prev>1) out+=`<span class="gap">…</span>`; |
| out+=`<button class="${i===page?"on":""}" data-p="${i}">${i+1}</button>`; prev=i; |
| } |
| out+=`<button ${page>=pages-1?"disabled":""} data-p="${page+1}">›</button>`; |
| out+=`<span class="pinfo">${d.prange(from,to,total)}</span>`; |
| host.innerHTML=out; |
| host.querySelectorAll("button[data-p]").forEach(b=>b.onclick=()=>{ |
| if(b.disabled) return; |
| page=parseInt(b.dataset.p,10); render(); |
| const c=document.querySelector(".card .scroll"); |
| if(c) window.scrollTo({top:c.getBoundingClientRect().top+window.scrollY-80,behavior:"smooth"}); |
| }); |
| } |
| |
| function render(){ |
| const d = t(), W = (DATA.season && DATA.season.weights) || {}; |
| $("#wts").innerHTML = Object.entries(W) |
| .map(([k,v])=>`<span>${esc(d.cols[k]||k)} <b>${v}</b></span>`).join(""); |
| $("#counts").textContent = d.counts(DATA.counts.scored, DATA.counts.anchors, DATA.counts.held || 0); |
| |
| $("#head").innerHTML = COLS.map(k=>{ |
| const on = sortKey===k, ar = on ? (sortDir<0?"▼":"▲") : "▾"; |
| return `<th data-k="${k}" class="${on?"sorted":""}">${esc(d.cols[k]||k)}<span class="ar">${ar}</span></th>`; |
| }).join(""); |
| document.querySelectorAll("#head th").forEach(th=>th.onclick=()=>{ |
| const k=th.dataset.k; |
| if(sortKey===k) sortDir=-sortDir; |
| else { sortKey=k; sortDir=(k==="rank"||k==="cand"||k==="who")?1:-1; } |
| page=0; render(); |
| }); |
| |
| renderDivBar(); |
| renderTrackBar(); |
| |
| let pool = DIV==="all" ? DATA.entries |
| : DATA.entries.filter(e=>e.is_anchor || e.division===DIV); |
| |
| if(TRK!=="all") pool = pool.filter(e=>e.is_anchor || e.track===TRK); |
| const all = pool.slice().sort((a,b)=>{ |
| const x=sortVal(a,sortKey), y=sortVal(b,sortKey); |
| if(x<y) return -sortDir; if(x>y) return sortDir; return 0; |
| }); |
| const pages = Math.max(1, Math.ceil(all.length/PAGE_SIZE)); |
| if(page>=pages) page=pages-1; |
| if(page<0) page=0; |
| const rows = all.slice(page*PAGE_SIZE, page*PAGE_SIZE+PAGE_SIZE); |
| renderPager(all.length, pages); |
| |
| { const bn = $("#t_boardnote"); if(bn) bn.innerHTML = d.boardnote + "<br>" + d.divnote; } |
| $("#rows").innerHTML = rows.length ? rows.map(e=>{ |
| const a = e.axes||{}; |
| const unscored = !e.is_anchor && (e.total == null || (e.held_axes||[]).length > 0); |
| const cls = [e.is_anchor?"anchor":"", unscored?"held":""].join(" ").trim(); |
| const who = e.is_anchor ? esc(e.display_name) |
| : (e.hf_user ? hfProfile(e.hf_user) |
| : `<span title="${esc(e.display_name||"")}">${esc(e.display_name||"–")}</span>`); |
| const num = k => { |
| const v = a[k]; |
| return `<td class="num">${v==null?`<span class="pill">${d.hold}</span>`:v.toFixed(1)}</td>`; |
| }; |
| const dt = e.submitted_at ? new Date(e.submitted_at*1000).toISOString().slice(0,10) : ""; |
| const hk = e.hold_kind && d.holds[e.hold_kind]; |
| const tot = e.total==null ? `<span class="pill warn">${esc(hk || d.hold)}</span>` |
| : `${e.total.toFixed(1)}<span class="sub" style="font-size:11px"> /${e.total_max||100}</span>` |
| + (unscored ? ` <span class="pill warn">${esc(hk || d.hold)}</span>` : ""); |
| const has = (e.detail && Object.keys(e.detail).length) || (unscored && e.gate); |
| return `<tr class="${cls}${has?" pick":""}"${has?` data-i="${e.candidate_id}"`:""}> |
| <td class="num">${(TRK!=="all" ? e.track_rank : (DIV==="all" ? e.rank : e.div_rank)) ?? "—"}</td> |
| <td>${esc(e.formula||e.candidate_id||"")}${e.masked?' <span class="pill">🔒</span>':""}</td> |
| <td>${who}</td><td>${esc(e.model_name||"")}</td><td>${dt}</td> |
| ${num("oxidation")}${num("li_stability")}${num("novelty")} |
| <td class="num"><b>${tot}</b></td></tr>`; |
| }).join("") : `<tr><td colspan="${COLS.length}" class="sub">${d.empty}</td></tr>`; |
| |
| |
| const byId = {}; rows.forEach(e=>{ if(e.candidate_id) byId[e.candidate_id]=e; }); |
| $("#rows").querySelectorAll("tr.pick").forEach(tr=>tr.onclick=()=>{ |
| const nxt = tr.nextElementSibling; |
| if(nxt && nxt.classList.contains("det")){ nxt.remove(); return; } |
| $("#rows").querySelectorAll("tr.det").forEach(x=>x.remove()); |
| const e = byId[tr.dataset.i]; if(!e) return; |
| const items = Object.entries(e.detail||{}) |
| .map(([k,v])=>`<li><b>${esc(d.cols[k]||k)}</b> — ${esc(v)}</li>`).join("") |
| || (e.gate ? `<li>${esc(e.gate)}</li>` : ""); |
| const extra = (e.notes||[]).map(n=>`<li>${esc(n)}</li>`).join(""); |
| const note = e.note ? `<div style="margin-top:6px">${esc(e.note)}</div>` |
| : (extra ? `<ul>${extra}</ul>` : ""); |
| const row = document.createElement("tr"); |
| row.className = "det"; |
| row.innerHTML = `<td colspan="${COLS.length}"><b>${esc(d.detail_h)}</b><ul>${items}</ul>${note}</td>`; |
| tr.after(row); |
| }); |
| } |
| |
| async function load(){ |
| try{ |
| const r = await fetch(`/api/leaderboard?season=${SEASON}`); |
| DATA = await r.json(); |
| render(); |
| }catch(e){ |
| $("#rows").innerHTML = `<tr><td colspan="${COLS.length}" class="sub">${t().fail}${esc(e)}</td></tr>`; |
| } |
| } |
| |
| document.querySelectorAll('input[name="vis"]').forEach(r=>r.onchange=()=>{ |
| document.querySelectorAll("#visbox label").forEach(l=>l.classList.toggle("on", l.querySelector("input").checked)); |
| }); |
| |
| $("#go").onclick = async () => { |
| const d = t(), m = $("#msg"); |
| const formula = $("#f_formula").value.trim(), name = $("#f_name").value.trim(); |
| if(!formula || !name){ m.className="msg bad"; m.textContent=d.need; return; } |
| $("#go").disabled = true; |
| try{ |
| const r = await fetch("/api/submit",{method:"POST",headers:{"Content-Type":"application/json"}, |
| body:JSON.stringify({formula, display_name:name, model_name:$("#f_model").value.trim(), |
| rationale:$("#f_why").value.trim(), structure:$("#f_cif").value.trim(), |
| visibility:document.querySelector('input[name="vis"]:checked').value, season:SEASON})}); |
| const j = await r.json(); |
| if(!r.ok){ m.className="msg bad"; m.textContent = j.detail || ("HTTP "+r.status); } |
| else{ m.className="msg ok"; m.innerHTML = d.ok(j.candidate_id, j.queue_position); |
| $("#f_formula").value=""; $("#f_cif").value=""; load(); MODELS_LOADED=false; } |
| }catch(e){ m.className="msg bad"; m.textContent=String(e); } |
| $("#go").disabled = false; |
| }; |
| |
| (async function boot(){ |
| try{ |
| const s = await (await fetch("/api/seasons")).json(); |
| SEASONS = s.seasons || []; |
| const open = SEASONS.filter(x=>x.open); |
| SEASON = (open.length?open:SEASONS).slice(-1)[0]?.number || 1; |
| }catch(e){} |
| try{ ME = await (await fetch("/api/me")).json(); }catch(e){ ME = null; } |
| renderAuth(); |
| setLang("ko"); |
| showTab(); |
| await load(); |
| setInterval(load, 20000); |
| setInterval(()=>{ if(TAB==="models") loadModels(); }, 600000); |
| })(); |
| </script> |
| </body> |
| </html> |
|
|