SeaWolf-AI's picture
ui: show MP and novel tracks separately
8e1a6ec verified
Raw
History Blame Contribute Delete
50.2 kB
<!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>
<!-- <head> 는 선택이 아니다: 플랫폼이 자기 태그를 문서에 주입하는데, 받을 head 가 없으면
그것들이 doctype 앞에 놓이고 페이지 전체가 쿼크 모드로 떨어진다. -->
<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&#10;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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[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>