SeaWolf-AI commited on
Commit
45fe5ce
ยท
verified ยท
1 Parent(s): 66e787f

UI: noise-floor axis visualization, drag-drop submit, polished shell

Browse files

Signature visual puts the experimental noise band on the MAE axis so a method sitting inside the measurement limit is visible at a glance. Adds file drop for predictions, CSV parsing, rank medals, leakage chips.

Files changed (1) hide show
  1. index.html +424 -236
index.html CHANGED
@@ -4,118 +4,223 @@
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
  <title>LEADBOARD โ€” ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ ์‹œํ—˜๋Œ€</title>
 
7
  <!-- <head> ๋Š” ์„ ํƒ์ด ์•„๋‹ˆ๋‹ค: ํ”Œ๋žซํผ์ด ์ž๊ธฐ ํƒœ๊ทธ๋ฅผ ๋ฌธ์„œ์— ์ฃผ์ž…ํ•˜๋Š”๋ฐ, ๋ฐ›์„ head ๊ฐ€ ์—†์œผ๋ฉด
8
  ๊ทธ๊ฒƒ๋“ค์ด doctype ์•ž์— ๋†“์ด๊ณ  ํŽ˜์ด์ง€ ์ „์ฒด๊ฐ€ ์ฟผํฌ ๋ชจ๋“œ๋กœ ๋–จ์–ด์ง„๋‹ค. -->
9
  <style>
10
  :root{
11
- --bg:#f6f7f9;--card:#fff;--ink:#15171c;--muted:#68707d;--line:#e3e6ea;
12
- --accent:#2a4d8f;--soft:#e9eff9;--edge:#c8d4e8;--warn:#8a5a00;--warnbg:#fff6e0;
13
- --good:#1d7a4d;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
 
 
 
 
 
14
  }
15
  @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
16
- --bg:#0f1115;--card:#171a20;--ink:#e7eaee;--muted:#98a0ac;--line:#282d36;
17
- --accent:#7aa2e8;--soft:#161e2c;--edge:#2f3a4d;--warn:#e0b23c;--warnbg:#282112;--good:#4cc98a;}}
 
 
 
 
18
  :root[data-theme="dark"]{
19
- --bg:#0f1115;--card:#171a20;--ink:#e7eaee;--muted:#98a0ac;--line:#282d36;
20
- --accent:#7aa2e8;--soft:#161e2c;--edge:#2f3a4d;--warn:#e0b23c;--warnbg:#282112;--good:#4cc98a;}
 
 
21
  *{box-sizing:border-box}
22
- body{margin:0;background:var(--bg);color:var(--ink);line-height:1.7;
 
23
  font-family:-apple-system,BlinkMacSystemFont,"Pretendard","Apple SD Gothic Neo",
24
- "Malgun Gothic",Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased}
25
- .wrap{max-width:960px;margin:0 auto;padding:0 20px 90px}
26
- a{color:var(--accent);text-decoration:none}
27
- .mono,code{font-family:var(--mono)}
 
 
 
28
 
29
- /* nav */
30
- nav{position:sticky;top:0;z-index:9;background:color-mix(in srgb,var(--bg) 88%,transparent);
31
- backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
32
- nav .in{max-width:960px;margin:0 auto;padding:12px 20px;display:flex;align-items:center;gap:14px}
33
- nav .lg{font-weight:800;letter-spacing:-.03em;font-size:18px}
34
- nav .lg span{color:var(--accent)}
35
- nav .sp{flex:1}
36
- nav .u{font-size:13px;color:var(--muted)}
37
- .btn{display:inline-block;padding:7px 14px;border-radius:8px;background:var(--accent);
38
- color:#fff;font-size:13px;font-weight:600;border:0;cursor:pointer}
39
- .btn.g{background:transparent;color:var(--accent);border:1px solid var(--edge)}
40
- .crumb{font-size:13px;color:var(--muted);margin:22px 0 4px}
 
 
 
 
 
 
 
 
 
41
 
42
- header.hero{padding:52px 0 30px;border-bottom:1px solid var(--line);margin-bottom:26px}
43
- .brand{font-size:clamp(34px,7vw,54px);font-weight:800;letter-spacing:-.035em;margin:0 0 4px;line-height:1.05}
44
- .brand span{color:var(--accent)}
45
- .tag{font-size:clamp(17px,3vw,22px);font-weight:700;margin:16px 0 6px;letter-spacing:-.015em}
46
- .sub{font-size:14px;color:var(--muted);margin:0}
 
 
 
 
 
 
 
 
47
 
48
- .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;margin:24px 0 8px}
49
- .stat{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
50
- .stat b{display:block;font-size:24px;font-weight:800;font-family:var(--mono);letter-spacing:-.02em}
51
- .stat span{font-size:12px;color:var(--muted)}
 
 
 
52
 
53
- h2{font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
54
- color:var(--accent);margin:44px 0 14px}
55
- h3{font-size:19px;font-weight:700;margin:26px 0 8px;letter-spacing:-.01em}
56
- p{margin:0 0 14px}
 
57
 
58
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}
59
- .cc{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px;
60
- cursor:pointer;transition:border-color .15s,transform .1s;display:block}
61
- .cc:hover{border-color:var(--edge);transform:translateY(-1px)}
62
- .cc.off{opacity:.62;cursor:default}
63
- .cc .ico{font-size:22px}
64
- .cc .nm{font-weight:700;font-size:16px;margin:6px 0 2px}
65
- .cc .ds{font-size:13px;color:var(--muted);line-height:1.55;min-height:42px}
66
- .cc .mt{margin-top:12px;padding-top:10px;border-top:1px solid var(--line);
67
- font-family:var(--mono);font-size:12px;color:var(--muted);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  display:flex;justify-content:space-between;gap:8px}
69
- .pill{display:inline-block;font-size:11px;font-family:var(--mono);padding:2px 7px;
70
- border-radius:20px;background:var(--soft);color:var(--accent);font-weight:700}
71
- .pill.gr{background:transparent;border:1px solid var(--line);color:var(--muted)}
72
 
73
- .tabs{display:flex;gap:4px;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:18px 0 0}
74
- .tab{padding:9px 15px;font-size:14px;font-weight:600;color:var(--muted);cursor:pointer;
 
 
 
 
75
  border:0;background:none;border-bottom:2px solid transparent;margin-bottom:-1px}
76
- .tab.on{color:var(--accent);border-bottom-color:var(--accent)}
77
- .tabs.sub{border-bottom:0;margin:14px 0 6px}
78
- .tabs.sub .tab{font-size:13px;padding:6px 12px;border-radius:7px;border-bottom:0}
79
- .tabs.sub .tab.on{background:var(--soft)}
 
 
 
80
 
81
- table{width:100%;border-collapse:collapse;font-size:13.5px;margin:14px 0}
82
- th{text-align:right;font-weight:600;color:var(--muted);font-size:11px;letter-spacing:.05em;
83
- text-transform:uppercase;padding:8px 9px;border-bottom:1px solid var(--line)}
84
- th:first-child,td:first-child{text-align:left}
85
- th:nth-child(2),td:nth-child(2){text-align:left}
86
- td{padding:10px 9px;border-bottom:1px solid var(--line);text-align:right;font-family:var(--mono)}
87
- td:nth-child(2){font-family:inherit;font-weight:600}
88
- .tw{overflow-x:auto}
 
 
 
 
 
89
  tr.base td{color:var(--muted)}
90
- tr.nb td{background:color-mix(in srgb,var(--warn) 7%,transparent)}
 
 
 
 
 
 
91
  .fig{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px 20px;
92
- margin:16px 0;font-family:var(--mono);font-size:13px;line-height:1.95;overflow-x:auto}
93
- .fig .n{color:var(--accent);font-weight:700}.fig .dim{color:var(--muted)}
94
- .note{background:var(--warnbg);border:1px solid color-mix(in srgb,var(--warn) 30%,transparent);
95
- border-radius:10px;padding:13px 16px;font-size:14px;margin:16px 0}
96
- .kv{display:grid;grid-template-columns:150px 1fr;gap:6px 14px;font-size:14px;margin:14px 0}
 
 
97
  .kv dt{color:var(--muted)}
98
- .kv dd{margin:0;font-family:var(--mono);font-size:13px}
99
- textarea,input[type=text]{width:100%;padding:10px 12px;border:1px solid var(--line);
100
- border-radius:8px;background:var(--card);color:var(--ink);font-family:var(--mono);font-size:13px}
101
- label{display:block;font-size:13px;font-weight:600;margin:14px 0 5px}
102
  .hint{font-size:12px;color:var(--muted);font-weight:400}
103
- footer{margin-top:60px;padding-top:22px;border-top:1px solid var(--line);
104
- font-size:13px;color:var(--muted);display:flex;gap:16px;flex-wrap:wrap}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </style>
106
  </head>
107
  <body>
108
 
109
  <nav><div class="in">
110
- <a class="lg" href="#/">LEAD<span>BOARD</span></a>
111
- <span class="pill gr" id="spec">v1.1</span>
112
  <span class="sp"></span>
113
- <span class="u" id="who"></span>
114
  </div></nav>
115
 
116
- <div class="wrap" id="app"></div>
117
 
118
- <footer class="wrap" style="padding-bottom:40px">
119
  <span>LEADBOARD ยท FINAL-Bench / VIDRAFT</span>
120
  <a href="/api/health">API</a>
121
  <a href="https://huggingface.co/spaces/FINAL-Bench/open-discovery-challenge">ODC</a>
@@ -123,229 +228,312 @@
123
  </footer>
124
 
125
  <script>
126
- const $ = s => document.querySelector(s);
127
- const app = $("#app");
128
- const f = (x,d=3) => (x==null||isNaN(x)) ? "โ€”" : Number(x).toFixed(d);
129
- const esc = s => String(s??"").replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
130
- let CATS=null, ME=null;
 
 
131
 
132
- async function j(u,o){ const r=await fetch(u,o); if(!r.ok) throw new Error(await r.text()); return r.json(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  async function boot(){
135
- try{ ME = await j("/api/me"); }catch(e){ ME={user:null}; }
136
  $("#who").innerHTML = ME.user
137
  ? `${esc(ME.user.user)} ยท <a href="/logout">๋กœ๊ทธ์•„์›ƒ</a>`
138
- : (ME.login_enabled ? `<a href="/login?next=${encodeURIComponent(location.hash||"#/")}">HF ๋กœ๊ทธ์ธ</a>` : "");
139
- CATS = await j("/api/categories");
140
- $("#spec").textContent = CATS.spec;
 
141
  route();
142
  }
143
- window.addEventListener("hashchange", route);
144
-
145
  function route(){
146
- const p = (location.hash||"#/").slice(2).split("/").filter(Boolean);
147
- window.scrollTo(0,0);
148
- if(!p.length) return hub();
149
- if(p[0]==="c") return category(p[1]);
150
- if(p[0]==="b") return board(p[1], p[2]||"rank");
151
  hub();
152
  }
153
 
154
- /* ---------------- ํ—ˆ๋ธŒ ---------------- */
155
  function hub(){
156
- const t = CATS.totals;
157
- app.innerHTML = `
158
- <header class="hero">
159
- <h1 class="brand">LEAD<span>BOARD</span></h1>
 
160
  <p class="sub">๋ฆฌ๋“œ๋ณด๋“œ ยท AI ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ ์‹œํ—˜๋Œ€</p>
161
- <p class="tag">์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ๋ฅผ, ๋ถ„์•ผ๋ณ„๋กœ, ๊ฐ™์€ ์žฃ๋Œ€๋กœ ์žฐ๋‹ค.</p>
162
- <p class="sub">์ •๋‹ต์€ ๋‚˜๋ˆ ์ฃผ์ง€ ์•Š๋Š”๋‹ค. ๋Œ€์‹  ๊ธฐ์ค€์„ ๊ณผ ์‹คํ—˜ ์˜ค์ฐจ๋ฅผ ๋จผ์ € ๊ณต๊ฐœํ•œ๋‹ค.</p>
163
- </header>
164
- <div class="stats">
165
- <div class="stat"><b>${t.open}</b><span>๊ฐœ์„ค ๋ถ€๋ฌธ</span></div>
166
- <div class="stat"><b>${t.planned}</b><span>๊ณ„ํš ๋ถ€๋ฌธ</span></div>
167
- <div class="stat"><b>${CATS.categories.length}</b><span>์นดํ…Œ๊ณ ๋ฆฌ</span></div>
168
- <div class="stat"><b>8</b><span>๊ทœ๊ฒฉ ์กฐํ•ญ</span></div>
 
169
  </div>
 
 
 
 
 
 
 
 
 
 
 
170
  <h2>์นดํ…Œ๊ณ ๋ฆฌ</h2>
 
171
  <div class="grid">${CATS.categories.map(cardHTML).join("")}</div>
172
- <h2>์ด ํ‘œ๋ฅผ ๋ฏฟ์„ ์ด์œ </h2>
173
- <p>์ฑ„์ ์ž์ธ ์šฐ๋ฆฌ๊ฐ€ <b>๋จผ์ €</b> ๊ณต๊ฐœํ•ฉ๋‹ˆ๋‹ค โ€” ์•„๋ฌด๊ฒƒ๋„ ํ•™์Šตํ•˜์ง€ ์•Š์€ <b>๊ธฐ์ค€์„ </b>,
174
- ๊ทธ ๋ถ„์•ผ์˜ <b>์‹คํ—˜ ์˜ค์ฐจ ๋ฒ”์œ„</b>, ์ ์ˆ˜๋งˆ๋‹ค ๊ฐ์ธ๋œ <b>๋ฐ์ดํ„ฐ ์ง€๋ฌธ</b>.
175
- <b>์šฐ๋ฆฌ ์ ์ˆ˜๋ฅผ ๋ฏฟ์–ด๋‹ฌ๋ผ๊ณ  ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ™•์ธํ•ด๋ณด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.</b></p>
176
- <div class="fig">
177
- 1 ์ •๋‹ต ๋น„๋ฐฐํฌ <span class="dim">โ€” ๋ถ€๋ฌธ๋งˆ๋‹ค ๋ถ„ํ•  ๋“ฑ๊ธ‰ยท์ •๋‹ต ๋“ฑ๊ธ‰์„ ๊ณต์‹œ</span><br>
178
- 2 ์ ์ˆ˜๋งˆ๋‹ค ๋ฐ์ดํ„ฐยท์ฑ„์ ๊ธฐ ์ง€๋ฌธ<br>
179
- 3 ๊ธฐ์ค€์„ ์€ ํ‘œ์—์„œ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋Š”๋‹ค<br>
180
- 4 ์žก์Œ ๋ฐ”๋‹ฅ์„ ๋จผ์ € ๊ณต์‹œํ•œ๋‹ค <span class="dim">โ€” ๊ทธ ์•ˆ์— ๋“  ์ฐจ์ด๋Š” ์ˆœ์œ„๋กœ ์•ˆ ์นœ๋‹ค</span><br>
181
- 5 ๋ˆ„์ถœ์€ ๋งค ์ œ์ถœ๋งˆ๋‹ค ๊ฒ€์‚ฌํ•œ๋‹ค<br>
182
- 6 ๊ฒฐ๊ณผ ์ œ์ถœ๊ณผ ๋ชจ๋ธ ์ œ์ถœ์„ ๊ตฌ๋ถ„ํ•œ๋‹ค<br>
183
- 7 ๋ฏธ์‚ฐ์ถœ์€ 0์ ์ด ์•„๋‹ˆ๋‹ค<br>
184
- 8 ๋˜ํ’€์ด ์ œ์ถœ๋กœ๋Š” ์ˆœ์œ„๊ฐ€ ์˜ค๋ฅด์ง€ ์•Š๋Š”๋‹ค <span class="dim">โ€” Ladder, ICML 2015</span>
185
- </div>`;
186
  }
187
 
188
  function cardHTML(c){
189
- const on = c.open>0;
190
- const best = (c.board_cards||[]).sort((a,b)=>b.n_test-a.n_test)[0];
191
- const inner = `
192
- <div class="ico">${c.icon}</div>
193
- <div class="nm">${esc(c.name)} <span class="pill ${on?"":"gr"}">${on?c.open+" ๊ฐœ์„ค":"์ค€๋น„ ์ค‘"}</span></div>
194
  <div class="ds">${esc(c.desc)}</div>
195
  <div class="mt"><span>๋ถ€๋ฌธ ${c.planned}</span>
196
- <span>${best?`${esc(best.board)} ์žก์Œ ${f(best.noise_floor.sd_single)}`:"โ€”"}</span></div>`;
197
- return on ? `<a class="cc" href="#/c/${c.id}">${inner}</a>`
198
- : `<div class="cc off">${inner}</div>`;
199
  }
200
 
201
- /* ---------------- ์นดํ…Œ๊ณ ๋ฆฌ ---------------- */
202
  function category(id){
203
- const c = CATS.categories.find(x=>x.id===id);
204
- if(!c) return hub();
205
- const bs = c.board_cards||[];
206
- app.innerHTML = `
207
  <div class="crumb"><a href="#/">์ „์ฒด</a> โ€บ ${esc(c.name)}</div>
208
- <h1 style="font-size:30px;margin:0 0 4px;letter-spacing:-.02em">${c.icon} ${esc(c.name)}</h1>
209
- <p class="sub">${esc(c.desc)} ยท ๊ณ„ํš ${c.planned}๋ถ€๋ฌธ ยท ๊ฐœ์„ค ${c.open}</p>
210
- ${bs.length ? `<div class="tabs">${bs.map(b=>
211
- `<a class="tab" href="#/b/${encodeURIComponent(b.board)}">${esc(b.board)}</a>`).join("")}</div>
212
- <div class="grid" style="margin-top:18px">${bs.map(boardMini).join("")}</div>`
213
- : `<div class="note">์ด ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ์•„์ง ๊ฐœ์„ค๋œ ๋ถ€๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค.
214
- ํ™€๋“œ์•„์›ƒ๊ณผ ์žก์Œ ๋ฐ”๋‹ฅ์ด ์ค€๋น„๋˜๋Š” ๋Œ€๋กœ ์—ฝ๋‹ˆ๋‹ค.</div>`}`;
 
 
 
 
 
 
 
215
  }
216
-
217
  function boardMini(b){
 
218
  return `<a class="cc" href="#/b/${encodeURIComponent(b.board)}">
219
- <div class="nm">${esc(b.board)}
220
- <span class="pill">${b.split_grade}/${b.answer_grade}</span></div>
221
  <div class="ds">ํ…Œ์ŠคํŠธ ${b.n_test.toLocaleString()}๊ฐœ ยท ์žก์Œ ๋ฐ”๋‹ฅ ${f(b.noise_floor.sd_single)}<br>
222
- ์ตœ์„  ๊ธฐ์ค€์„  ${esc(b.best_baseline)} ${f((b.baselines[b.best_baseline]||{}).mae)}</div>
223
  <div class="mt"><span>${esc(b.source)}</span><span>์˜ค์ฐจรท์žก์Œ ${f(b.error_over_noise,2)}ร—</span></div></a>`;
224
  }
225
 
226
- /* ---------------- ๋ถ€๋ฌธ ---------------- */
227
- async function board(name, tab){
228
- const b = await j("/api/board/"+encodeURIComponent(name));
229
- const cat = CATS.categories.find(c=>(c.boards||[]).includes(name));
230
- const T = [["rank","์ˆœ์œ„"],["submit","์ œ์ถœ"],["info","๋ถ€๋ฌธ ์ •๋ณด"]];
231
- app.innerHTML = `
232
  <div class="crumb"><a href="#/">์ „์ฒด</a> โ€บ <a href="#/c/${cat?cat.id:""}">${esc(cat?cat.name:"")}</a> โ€บ ${esc(b.board)}</div>
233
- <h1 style="font-size:30px;margin:0 0 4px;letter-spacing:-.02em">${esc(b.board)}
234
- <span class="pill">${b.split_grade}/${b.answer_grade}</span></h1>
235
- <p class="sub">${esc(b.source)} ยท ${esc(b.target)} ยท ${esc(b.type)} ยท
236
- ํ…Œ์ŠคํŠธ ${b.n_test.toLocaleString()}๊ฐœ ยท ์žก์Œ ๋ฐ”๋‹ฅ ${f(b.noise_floor.sd_single)} log</p>
237
- <div class="tabs sub">${T.map(([k,l])=>
238
- `<a class="tab ${k===tab?"on":""}" href="#/b/${encodeURIComponent(name)}/${k}">${l}</a>`).join("")}</div>
239
- <div id="pane">๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ</div>`;
240
- if(tab==="rank") return paneRank(b);
241
- if(tab==="submit") return paneSubmit(b);
242
  paneInfo(b);
243
  }
244
 
245
  async function paneRank(b){
246
- const d = await j(`/api/board/${encodeURIComponent(b.board)}/leaderboard`);
247
- const nf = d.noise_floor;
248
- const rows = d.rows.map(r=>`<tr class="${r.kind==="baseline"?"base":""} ${r.within_noise?"nb":""}">
249
- <td>${r.rank??"โ€”"}</td><td>${esc(r.method)}</td><td>${esc(r.user)}</td>
 
 
 
 
 
250
  <td>${f(r.mae)}</td><td>${f(r.auroc)}</td><td>${f(r.prauc)}</td>
251
- <td>${r.kind==="baseline"?'<span class="pill gr">๊ธฐ์ค€์„ </span>':
252
- (r.verified?'<span class="pill">๊ฒ€์ฆ๋จ</span>':'<span class="pill gr">์ œ์ถœ๋จ</span>')}</td></tr>`).join("");
253
- $("#pane").innerHTML = `
 
 
 
 
254
  <div class="tw"><table>
255
- <thead><tr><th>#</th><th>๋ฐฉ๋ฒ•</th><th>์ œ์ถœ์ž</th><th>MAE</th><th>AUROC</th><th>PR-AUC</th><th>๋“ฑ๊ธ‰</th></tr></thead>
 
256
  <tbody>${rows}</tbody></table></div>
257
- <div class="note"><b>์žก์Œ ๋ฐ”๋‹ฅ ${f(nf)} log.</b> ์ตœ๊ณ  ์„ฑ์ ์—์„œ ์ด ํญ ์•ˆ์— ๋“  ํ•ญ๋ชฉ์€
258
- ๋…ธ๋ž€ ๋ฐ”ํƒ•์œผ๋กœ ํ‘œ์‹œํ–ˆ๊ณ , <b>์ˆœ์œ„ ์ฐจ์ด๋กœ ์ฃผ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค</b> (4์กฐ).
259
  ๊ธฐ์ค€์„  3์ข…์€ ํ‘œ์—์„œ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค (3์กฐ).</div>
260
- ${d.updated?`<p class="sub">์ˆœ์œ„ ๊ฐฑ์‹  ${new Date(d.updated*1000).toLocaleString("ko-KR")}</p>`:
261
- `<p class="sub">์•„์ง ์ฐธ๊ฐ€ ์ œ์ถœ์ด ์—†์Šต๋‹ˆ๋‹ค. ํ‘œ์— ์žˆ๋Š” ๊ฒƒ์€ ๊ธฐ์ค€์„ ๋ฟ์ž…๋‹ˆ๋‹ค.</p>`}`;
262
  }
263
 
264
  function paneInfo(b){
265
- const nf=b.noise_floor, bl=b.baselines;
266
- $("#pane").innerHTML = `
267
  <h3>๋“ฑ๊ธ‰</h3>
268
  <dl class="kv">
269
- <dt>๋ถ„ํ•  ๋“ฑ๊ธ‰</dt><dd>${b.split_grade} โ€” ${esc(b.split_grade_note||"")}</dd>
270
- <dt>์ •๋‹ต ๋“ฑ๊ธ‰</dt><dd>${b.answer_grade} โ€” ${esc(b.answer_grade_note||"")}</dd>
271
  </dl>
272
  <h3>ํ™€๋“œ์•„์›ƒ</h3>
273
  <dl class="kv">
274
  <dt>์ถœ์ฒ˜</dt><dd>${esc(b.source)} ยท ${esc(b.target)} ยท ${esc(b.type)}</dd>
275
- <dt>๋ถ„ํ•  ๊ธฐ์ค€์ผ</dt><dd>${b.cut} (์ตœ์ดˆ ๋ณด๊ณ  ์—ฐ๋„)</dd>
276
  <dt>ํ›ˆ๋ จ / ํ…Œ์ŠคํŠธ</dt><dd>${b.n_train.toLocaleString()} / ${b.n_test.toLocaleString()}</dd>
277
  <dt>๊ตฌ์กฐ ๊ฒน์นจ</dt><dd>0 (์‹œ๊ฐ„๋ถ„ํ•  ์ •์˜์ƒ)</dd>
278
  <dt>๋ถ„๋ฅ˜ ์ž„๊ณ„</dt><dd>p &gt; ${b.threshold} ยท ์–‘์„ฑ ${(b.positive_rate*100).toFixed(1)}%</dd>
279
  <dt>๋ฐ์ดํ„ฐ ์ง€๋ฌธ</dt><dd>${esc(b.dataset_sha)}</dd>
280
  </dl>
281
- <h3>์žก์Œ ๋ฐ”๋‹ฅ (4์กฐ)</h3>
282
  <div class="fig">
283
- ๋น„๊ต์Œ <span class="n">${nf.pairs.toLocaleString()}</span> ยท ํ™”ํ•ฉ๋ฌผ <span class="n">${nf.compounds.toLocaleString()}</span>
284
- <span class="dim">โ€” ๊ฐ™์€ ํ™”ํ•ฉ๋ฌผ์„ ์„œ๋กœ ๋‹ค๋ฅธ ๋…ผ๋ฌธ์—์„œ ์žฐ ์ฐจ์ด</span><br>
285
  ์ค‘์•™๊ฐ’ ${f(nf.median)} ยท ํ‰๊ท  ${f(nf.mean)} ยท 90๋ถ„์œ„ ${f(nf.p90)}<br>
286
  <b>๋‹จ์ผ ์ธก์ • SD ์ถ”์ • <span class="n">${f(nf.sd_single)}</span> log</b>
287
  </div>
288
- <div class="note">๋ถ„๋ฅ˜ ์ž„๊ณ„์„ ์—์„œ ์‹คํ—˜ ์˜ค์ฐจ ํ•œ ์นธ ์•ˆ์— ์žˆ๋Š” ํ…Œ์ŠคํŠธ ํ™”ํ•ฉ๋ฌผ์ด
289
- <b>${b.near_pct}%</b> ์ž…๋‹ˆ๋‹ค. ${b.near_pct>45?"์ ˆ๋ฐ˜ ๊ฐ€๊นŒ์ด๊ฐ€ ์žฌ์ธก์ •ํ•˜๋ฉด ๋ผ๋ฒจ์ด ๋’ค์ง‘ํž ์ˆ˜ ์žˆ์–ด <b>ํšŒ๊ท€๋ฅผ ์ฃผ์ง€ํ‘œ</b>๋กœ ๋‘ก๋‹ˆ๋‹ค.":"๋ถ„๋ฅ˜ ์ง€ํ‘œ๋ฅผ ํ•จ๊ป˜ ์”๋‹ˆ๋‹ค."}</div>
290
- <h3>๊ธฐ์ค€์„  (3์กฐ)</h3>
291
- <div class="tw"><table><thead><tr><th>๊ธฐ์ค€์„ </th><th>MAE</th><th>AUROC</th><th>PR-AUC</th></tr></thead>
292
- <tbody>${Object.entries(bl).map(([k,v])=>
293
- `<tr><td>${esc(k)}</td><td>${f(v.mae)}</td><td>${f(v.auroc)}</td><td>${f(v.prauc)}</td></tr>`).join("")}
294
- <tr class="base"><td>(์‹คํ—˜ ์žก์Œ ๋ฐ”๋‹ฅ)</td><td>${f(nf.sd_single)}</td><td>โ€”</td><td>โ€”</td></tr>
 
 
 
 
295
  </tbody></table></div>
296
- <p class="sub">${b.beats_constant
297
  ? "ํ•™์Šต ๊ธฐ์ค€์„ ๏ฟฝ๏ฟฝ ์ƒ์ˆ˜ ์˜ˆ์ธก์„ ๋„˜์—ˆ์Šต๋‹ˆ๋‹ค."
298
- : "<b>ํ•™์Šต ๊ธฐ์ค€์„ ์ด ์ƒ์ˆ˜ ์˜ˆ์ธก์„ ๋„˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.</b> 3์กฐ๊ฐ€ ์—†์—ˆ๋‹ค๋ฉด ์ด ์‚ฌ์‹ค์€ ํ‘œ์— ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."}</p>`;
299
  }
300
 
301
  function paneSubmit(b){
302
- const url = `/api/board/${encodeURIComponent(b.board)}/testset`;
303
- $("#pane").innerHTML = `
304
- <h3>โ‘  ํ…Œ์ŠคํŠธ์…‹ ๋ฐ›๊ธฐ</h3>
305
- <p>ํ™”ํ•ฉ๋ฌผ <b>${b.n_test.toLocaleString()}๊ฐœ</b>์˜ ๊ตฌ์กฐ(SMILES)๋งŒ ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ผ๋ฒจ์€ ์—†์Šต๋‹ˆ๋‹ค.</p>
306
- <p><a class="btn" href="${url}" download>ํ…Œ์ŠคํŠธ์…‹ ๋‚ด๋ ค๋ฐ›๊ธฐ</a>
307
- <span class="hint">&nbsp;${esc(b.board)}_test.json</span></p>
308
- <h3>โ‘ก ์˜ˆ์ธก ์˜ฌ๋ฆฌ๊ธฐ</h3>
309
- ${ME.user ? `
310
- <label>๋ฐฉ๋ฒ• ์ด๋ฆ„ <span class="hint">์ˆœ์œ„ํ‘œ์— ์ด๋ ‡๊ฒŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค</span></label>
311
- <input type="text" id="m" placeholder="์˜ˆ: my-gnn-ensemble-v2" maxlength="60">
312
- <label>ํ•™์Šต์— ์“ด ๋ฐ์ดํ„ฐ <span class="hint">5์กฐ ๋ˆ„์ถœ ๊ฒ€์‚ฌ์— ์”๋‹ˆ๋‹ค</span></label>
313
- <input type="text" id="td" placeholder="์˜ˆ: ChEMBL34 + ์‚ฌ๋‚ด ๋ฐ์ดํ„ฐ">
314
- <label>์˜ˆ์ธก๊ฐ’ JSON <span class="hint">{"LB-XXXX": 6.42, ...} โ€” p ์Šค์ผ€์ผ (โˆ’log10 M)</span></label>
315
- <textarea id="p" rows="7" placeholder='{"LB-A1B2C3D4E5F6": 6.42, "...": 5.10}'></textarea>
316
- <p style="margin-top:14px"><button class="btn" id="go">์ œ์ถœ</button>
317
- <span class="hint" id="msg"></span></p>
318
- <div class="note"><b>8์กฐ.</b> ๋˜ํ’€์ด ์ œ์ถœ๋กœ๋Š” ์ˆœ์œ„๊ฐ€ ์˜ค๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
319
- ์ง์ „ ์ตœ๊ณ ์ ์„ ์žก์Œ ๋ฐ”๋‹ฅ <b>${f(b.noise_floor.sd_single)}</b> ์ด์ƒ์œผ๋กœ ๋„˜์—ˆ์„ ๋•Œ๋งŒ ์ƒˆ ์ ์ˆ˜๊ฐ€ ๊ณต๊ฐœ๋ฉ๋‹ˆ๋‹ค.
320
- ํ•˜๋ฃจ ์ œ์ถœ ํ•œ๋„ ${ME.daily_cap}ํšŒ.</div>`
321
- : `<div class="note">์ œ์ถœํ•˜๋ ค๋ฉด HF ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
322
- <a href="/login?next=${encodeURIComponent(location.hash)}">๋กœ๊ทธ์ธ</a></div>`}
323
- <h3>โ‘ข ์ปจํ…Œ์ด๋„ˆ๊นŒ์ง€ ์˜ฌ๋ฆฌ๋ฉด (์„ ํƒ)</h3>
324
- <p>๋ชจ๋ธ๊ณผ ์ถ”๋ก  ํ™˜๊ฒฝ์„ ํ•จ๊ป˜ ์ œ์ถœํ•˜๋ฉด <b>์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๋Œ๋ ค</b> ๊ฐ™์€ ์ˆซ์ž๊ฐ€ ๋‚˜์˜ค๋Š”์ง€ ํ™•์ธํ•˜๊ณ 
325
- <span class="pill">๊ฒ€์ฆ๋จ</span> ํ‘œ์‹œ๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค (6์กฐ).</p>`;
326
- const go = $("#go");
327
- if(go) go.onclick = async () => {
328
- const msg = $("#msg");
329
- let preds;
330
- try{ preds = JSON.parse($("#p").value); }
331
- catch(e){ msg.textContent = "์˜ˆ์ธก๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅธ JSON ์ด ์•„๋‹™๋‹ˆ๋‹ค."; return; }
332
- msg.textContent = "๋ณด๋‚ด๋Š” ์ค‘โ€ฆ"; go.disabled = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  try{
334
- const r = await j("/api/submit",{method:"POST",headers:{"Content-Type":"application/json"},
335
- body:JSON.stringify({board:b.board, method:$("#m").value.trim(),
336
- predictions:preds, training_data:$("#td").value.trim()})});
337
- msg.innerHTML = `์ ‘์ˆ˜๋์Šต๋‹ˆ๋‹ค. ์ œ์ถœ ๋ฒˆํ˜ธ <code>${esc(r.submission_id)}</code> ยท ์ฑ„์ ์€ ์›Œ์ปค๊ฐ€ ํ•ฉ๋‹ˆ๋‹ค.`;
338
- }catch(e){
339
- let t = String(e.message||e);
340
- try{ t = JSON.parse(t).detail || t; }catch(_){}
341
- msg.textContent = t;
342
- }
343
- go.disabled = false;
344
  };
345
  }
346
 
347
- boot().catch(e => { app.innerHTML =
348
- `<div class="note">ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ${esc(e.message||e)}</div>`; });
349
  </script>
350
  </body>
351
  </html>
 
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
  <title>LEADBOARD โ€” ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ ์‹œํ—˜๋Œ€</title>
7
+ <meta name="description" content="์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ๋ฅผ ๋ถ„์•ผ๋ณ„๋กœ ๊ฐ™์€ ์žฃ๋Œ€๋กœ ์žฐ๋‹ค. ์ •๋‹ต์€ ๋‚˜๋ˆ ์ฃผ์ง€ ์•Š๊ณ , ๊ธฐ์ค€์„ ๊ณผ ์‹คํ—˜ ์˜ค์ฐจ๋ฅผ ๋จผ์ € ๊ณต๊ฐœํ•œ๋‹ค.">
8
  <!-- <head> ๋Š” ์„ ํƒ์ด ์•„๋‹ˆ๋‹ค: ํ”Œ๋žซํผ์ด ์ž๊ธฐ ํƒœ๊ทธ๋ฅผ ๋ฌธ์„œ์— ์ฃผ์ž…ํ•˜๋Š”๋ฐ, ๋ฐ›์„ head ๊ฐ€ ์—†์œผ๋ฉด
9
  ๊ทธ๊ฒƒ๋“ค์ด doctype ์•ž์— ๋†“์ด๊ณ  ํŽ˜์ด์ง€ ์ „์ฒด๊ฐ€ ์ฟผํฌ ๋ชจ๋“œ๋กœ ๋–จ์–ด์ง„๋‹ค. -->
10
  <style>
11
  :root{
12
+ --bg:#f7f8fa; --bg2:#eef1f5; --card:#fff; --ink:#12141a; --ink2:#3c4351;
13
+ --muted:#6b7280; --line:#e4e7ec; --line2:#d3d8e0;
14
+ --acc:#2a4d8f; --acc2:#4d7fd6; --soft:#e8eff9;
15
+ --warn:#9a6a00; --warnbg:#fff5dc; --good:#15794c; --goodbg:#e4f5ec;
16
+ --bad:#a8352a; --badbg:#fdeceb;
17
+ --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
18
+ --r:14px; --sh:0 1px 2px rgba(16,24,40,.04),0 1px 3px rgba(16,24,40,.06);
19
+ --sh2:0 4px 6px -1px rgba(16,24,40,.06),0 12px 20px -4px rgba(16,24,40,.08);
20
  }
21
  @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
22
+ --bg:#0d0f13; --bg2:#12151b; --card:#161a21; --ink:#e9ecf1; --ink2:#c2c8d2;
23
+ --muted:#8d95a3; --line:#242932; --line2:#2f3540;
24
+ --acc:#7fa6ea; --acc2:#5b86d4; --soft:#141d2d;
25
+ --warn:#e2b451; --warnbg:#251f10; --good:#4ecb92; --goodbg:#0f2419;
26
+ --bad:#f08a7e; --badbg:#2a1512;
27
+ --sh:0 1px 2px rgba(0,0,0,.3); --sh2:0 8px 24px rgba(0,0,0,.4);}}
28
  :root[data-theme="dark"]{
29
+ --bg:#0d0f13;--bg2:#12151b;--card:#161a21;--ink:#e9ecf1;--ink2:#c2c8d2;
30
+ --muted:#8d95a3;--line:#242932;--line2:#2f3540;--acc:#7fa6ea;--acc2:#5b86d4;
31
+ --soft:#141d2d;--warn:#e2b451;--warnbg:#251f10;--good:#4ecb92;--goodbg:#0f2419;
32
+ --bad:#f08a7e;--badbg:#2a1512;--sh:0 1px 2px rgba(0,0,0,.3);--sh2:0 8px 24px rgba(0,0,0,.4);}
33
  *{box-sizing:border-box}
34
+ html{scroll-behavior:smooth}
35
+ body{margin:0;background:var(--bg);color:var(--ink);line-height:1.65;font-size:15px;
36
  font-family:-apple-system,BlinkMacSystemFont,"Pretendard","Apple SD Gothic Neo",
37
+ "Malgun Gothic",Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased;
38
+ text-rendering:optimizeLegibility}
39
+ .wrap{max-width:1000px;margin:0 auto;padding:0 22px}
40
+ a{color:var(--acc);text-decoration:none}
41
+ a:hover{text-decoration:underline}
42
+ .mono,code{font-family:var(--mono);font-variant-numeric:tabular-nums}
43
+ .dim{color:var(--muted)}
44
 
45
+ /* โ”€โ”€ nav โ”€โ”€ */
46
+ nav{position:sticky;top:0;z-index:20;background:color-mix(in srgb,var(--bg) 82%,transparent);
47
+ backdrop-filter:saturate(180%) blur(14px);border-bottom:1px solid var(--line)}
48
+ nav .in{max-width:1000px;margin:0 auto;padding:11px 22px;display:flex;align-items:center;gap:12px}
49
+ .lg{font-weight:800;letter-spacing:-.035em;font-size:17px;color:var(--ink)}
50
+ .lg:hover{text-decoration:none}
51
+ .lg i{color:var(--acc);font-style:normal}
52
+ .sp{flex:1}
53
+ .chip{font-size:11px;font-family:var(--mono);padding:3px 9px;border-radius:99px;
54
+ background:var(--soft);color:var(--acc);font-weight:700;letter-spacing:.02em}
55
+ .chip.g{background:transparent;border:1px solid var(--line2);color:var(--muted)}
56
+ .chip.ok{background:var(--goodbg);color:var(--good)}
57
+ .chip.wr{background:var(--warnbg);color:var(--warn)}
58
+ .chip.bd{background:var(--badbg);color:var(--bad)}
59
+ .btn{display:inline-flex;align-items:center;gap:7px;padding:9px 17px;border-radius:10px;
60
+ background:var(--acc);color:#fff;font-size:13.5px;font-weight:600;border:0;cursor:pointer;
61
+ transition:filter .15s,transform .1s}
62
+ .btn:hover{filter:brightness(1.08);text-decoration:none}
63
+ .btn:active{transform:translateY(1px)}
64
+ .btn:disabled{opacity:.5;cursor:default}
65
+ .btn.g{background:transparent;color:var(--acc);border:1px solid var(--line2)}
66
 
67
+ /* โ”€โ”€ hero โ”€โ”€ */
68
+ .hero{padding:66px 0 40px;position:relative;overflow:hidden}
69
+ .hero::before{content:"";position:absolute;inset:-40% -20% auto -20%;height:340px;
70
+ background:radial-gradient(ellipse at 30% 40%,color-mix(in srgb,var(--acc) 13%,transparent),transparent 62%);
71
+ pointer-events:none}
72
+ .hero > *{position:relative}
73
+ .brand{font-size:clamp(40px,8.5vw,68px);font-weight:850;letter-spacing:-.042em;
74
+ margin:0 0 2px;line-height:1}
75
+ .brand i{color:var(--acc);font-style:normal}
76
+ .sub{font-size:13.5px;color:var(--muted);margin:0;letter-spacing:.01em}
77
+ .tag{font-size:clamp(20px,3.6vw,28px);font-weight:700;margin:26px 0 8px;
78
+ letter-spacing:-.022em;line-height:1.38;max-width:18em}
79
+ .lede{font-size:16px;color:var(--ink2);margin:0;max-width:34em}
80
 
81
+ /* โ”€โ”€ stats โ”€โ”€ */
82
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:12px;margin:36px 0 0}
83
+ .stat{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:15px 17px;
84
+ box-shadow:var(--sh)}
85
+ .stat b{display:block;font-size:27px;font-weight:800;font-family:var(--mono);
86
+ letter-spacing:-.03em;line-height:1.15}
87
+ .stat span{font-size:11.5px;color:var(--muted);letter-spacing:.01em}
88
 
89
+ h2{font-size:11.5px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;
90
+ color:var(--acc);margin:56px 0 6px}
91
+ h2 + .h2s{font-size:14px;color:var(--muted);margin:0 0 20px}
92
+ h3{font-size:18px;font-weight:700;margin:30px 0 8px;letter-spacing:-.015em}
93
+ p{margin:0 0 15px}
94
 
95
+ /* โ”€โ”€ noise band (์„œ๋ช… ์‹œ๊ฐํ™”) โ”€โ”€ */
96
+ .band{background:var(--card);border:1px solid var(--line);border-radius:12px;
97
+ padding:16px 18px 12px;margin:12px 0;box-shadow:var(--sh)}
98
+ .band .hd{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}
99
+ .band .hd b{font-size:15px}
100
+ .band .hd .r{margin-left:auto;font-family:var(--mono);font-size:12px;color:var(--muted)}
101
+ .axis{position:relative;height:46px;margin:0 4px}
102
+ .axis .line{position:absolute;left:0;right:0;top:22px;height:2px;background:var(--line2);border-radius:2px}
103
+ .axis .floor{position:absolute;left:0;top:12px;height:22px;border-radius:4px;
104
+ background:repeating-linear-gradient(135deg,color-mix(in srgb,var(--warn) 22%,transparent) 0 5px,transparent 5px 10px);
105
+ border-right:2px solid color-mix(in srgb,var(--warn) 65%,transparent)}
106
+ .axis .mk{position:absolute;top:9px;width:2px;height:28px;background:var(--acc);border-radius:2px;
107
+ transform:translateX(-1px)}
108
+ .axis .mk.b{background:var(--muted);opacity:.75}
109
+ .axis .mk::after{content:attr(data-l);position:absolute;top:31px;left:50%;transform:translateX(-50%);
110
+ font-family:var(--mono);font-size:10px;color:var(--muted);white-space:nowrap}
111
+ .axis .mk.top::after{top:-16px}
112
+ .bandnote{font-size:12px;color:var(--muted);margin-top:16px;display:flex;gap:14px;flex-wrap:wrap}
113
+ .bandnote i{font-style:normal;display:inline-flex;align-items:center;gap:5px}
114
+ .swatch{width:22px;height:9px;border-radius:2px;display:inline-block;
115
+ background:repeating-linear-gradient(135deg,color-mix(in srgb,var(--warn) 30%,transparent) 0 4px,transparent 4px 8px);
116
+ border:1px solid color-mix(in srgb,var(--warn) 45%,transparent)}
117
+
118
+ /* โ”€โ”€ cards โ”€โ”€ */
119
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(258px,1fr));gap:14px}
120
+ .cc{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:19px;
121
+ display:block;box-shadow:var(--sh);transition:box-shadow .18s,transform .12s,border-color .18s}
122
+ a.cc:hover{box-shadow:var(--sh2);transform:translateY(-2px);border-color:var(--line2);text-decoration:none}
123
+ .cc.off{opacity:.55}
124
+ .cc .ico{font-size:23px;line-height:1}
125
+ .cc .nm{font-weight:700;font-size:16.5px;margin:9px 0 3px;color:var(--ink);
126
+ display:flex;align-items:center;gap:8px;flex-wrap:wrap}
127
+ .cc .ds{font-size:13px;color:var(--muted);line-height:1.55;min-height:40px}
128
+ .cc .mt{margin-top:13px;padding-top:11px;border-top:1px solid var(--line);
129
+ font-family:var(--mono);font-size:11.5px;color:var(--muted);
130
  display:flex;justify-content:space-between;gap:8px}
 
 
 
131
 
132
+ /* โ”€โ”€ tabs โ”€โ”€ */
133
+ .crumb{font-size:13px;color:var(--muted);margin:26px 0 10px}
134
+ .ttl{font-size:clamp(26px,4.4vw,34px);font-weight:800;letter-spacing:-.028em;margin:0 0 5px;
135
+ display:flex;align-items:center;gap:11px;flex-wrap:wrap}
136
+ .tabs{display:flex;gap:5px;flex-wrap:wrap;margin:22px 0 4px;border-bottom:1px solid var(--line)}
137
+ .tab{padding:10px 16px;font-size:14px;font-weight:600;color:var(--muted);cursor:pointer;
138
  border:0;background:none;border-bottom:2px solid transparent;margin-bottom:-1px}
139
+ .tab:hover{color:var(--ink2);text-decoration:none}
140
+ .tab.on{color:var(--acc);border-bottom-color:var(--acc)}
141
+ .pills{display:flex;gap:6px;flex-wrap:wrap;margin:18px 0 2px}
142
+ .pilltab{padding:7px 14px;border-radius:9px;font-size:13px;font-weight:600;
143
+ color:var(--muted);border:1px solid var(--line);background:var(--card)}
144
+ .pilltab:hover{text-decoration:none;color:var(--ink2)}
145
+ .pilltab.on{background:var(--acc);color:#fff;border-color:var(--acc)}
146
 
147
+ /* โ”€โ”€ table โ”€โ”€ */
148
+ .tw{overflow-x:auto;border:1px solid var(--line);border-radius:12px;background:var(--card);
149
+ box-shadow:var(--sh);margin:16px 0}
150
+ table{width:100%;border-collapse:collapse;font-size:13.5px}
151
+ th{text-align:right;font-weight:600;color:var(--muted);font-size:10.5px;letter-spacing:.07em;
152
+ text-transform:uppercase;padding:11px 12px;border-bottom:1px solid var(--line);white-space:nowrap}
153
+ th:first-child,td:first-child{text-align:center;width:44px}
154
+ th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3){text-align:left}
155
+ td{padding:12px;border-bottom:1px solid var(--line);text-align:right;
156
+ font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap}
157
+ td:nth-child(2){font-family:inherit;font-weight:650}
158
+ tbody tr:last-child td{border-bottom:0}
159
+ tbody tr:hover td{background:var(--bg2)}
160
  tr.base td{color:var(--muted)}
161
+ tr.base td:nth-child(2){font-weight:600}
162
+ .rk{display:inline-flex;width:23px;height:23px;align-items:center;justify-content:center;
163
+ border-radius:7px;background:var(--bg2);font-size:11.5px;font-weight:700}
164
+ .rk.g1{background:#f5c84c22;color:#a8820f}
165
+ .rk.g2{background:#b9c2cf22;color:#7a8698}
166
+ .rk.g3{background:#cd8a5a22;color:#9b6234}
167
+
168
  .fig{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px 20px;
169
+ margin:16px 0;font-family:var(--mono);font-size:13px;line-height:1.95;overflow-x:auto;
170
+ box-shadow:var(--sh)}
171
+ .fig .n{color:var(--acc);font-weight:700}
172
+ .note{background:var(--warnbg);border:1px solid color-mix(in srgb,var(--warn) 32%,transparent);
173
+ border-radius:11px;padding:14px 17px;font-size:14px;margin:16px 0;color:var(--ink2)}
174
+ .note.ok{background:var(--goodbg);border-color:color-mix(in srgb,var(--good) 30%,transparent)}
175
+ .kv{display:grid;grid-template-columns:160px 1fr;gap:9px 16px;font-size:14px;margin:14px 0}
176
  .kv dt{color:var(--muted)}
177
+ .kv dd{margin:0;font-family:var(--mono);font-size:13px;word-break:break-word}
178
+ label{display:block;font-size:13.5px;font-weight:650;margin:16px 0 6px}
 
 
179
  .hint{font-size:12px;color:var(--muted);font-weight:400}
180
+ input[type=text],textarea{width:100%;padding:11px 13px;border:1px solid var(--line2);
181
+ border-radius:10px;background:var(--card);color:var(--ink);font-family:var(--mono);font-size:13px}
182
+ input[type=text]:focus,textarea:focus{outline:2px solid color-mix(in srgb,var(--acc) 45%,transparent);
183
+ outline-offset:-1px;border-color:var(--acc)}
184
+ .drop{border:2px dashed var(--line2);border-radius:12px;padding:26px;text-align:center;
185
+ color:var(--muted);font-size:14px;cursor:pointer;transition:border-color .15s,background .15s}
186
+ .drop:hover,.drop.hot{border-color:var(--acc);background:var(--soft);color:var(--acc)}
187
+ .steps{counter-reset:s;list-style:none;padding:0;margin:16px 0}
188
+ .steps li{counter-increment:s;position:relative;padding:0 0 22px 42px;border-left:2px solid var(--line);
189
+ margin-left:14px}
190
+ .steps li:last-child{border-left-color:transparent;padding-bottom:0}
191
+ .steps li::before{content:counter(s);position:absolute;left:-15px;top:-2px;width:28px;height:28px;
192
+ border-radius:50%;background:var(--soft);color:var(--acc);font-weight:700;font-size:13px;
193
+ display:flex;align-items:center;justify-content:center;font-family:var(--mono);
194
+ border:2px solid var(--bg)}
195
+ .steps b{display:block;font-size:15.5px;margin-bottom:3px}
196
+ ol.cl{list-style:none;counter-reset:c;padding:0;margin:18px 0;
197
+ display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:0}
198
+ ol.cl li{counter-increment:c;position:relative;padding:15px 15px 15px 50px;border-top:1px solid var(--line)}
199
+ ol.cl li::before{content:counter(c);position:absolute;left:8px;top:15px;width:28px;height:28px;
200
+ border-radius:8px;background:var(--soft);color:var(--acc);font-weight:700;font-size:12.5px;
201
+ display:flex;align-items:center;justify-content:center;font-family:var(--mono)}
202
+ ol.cl b{display:block;font-size:15px;margin-bottom:2px}
203
+ ol.cl span{font-size:13px;color:var(--muted);line-height:1.5}
204
+ .skel{height:120px;border-radius:12px;background:linear-gradient(90deg,var(--bg2),var(--card),var(--bg2));
205
+ background-size:200% 100%;animation:sh 1.3s infinite}
206
+ @keyframes sh{0%{background-position:200% 0}100%{background-position:-200% 0}}
207
+ footer{margin-top:76px;padding:26px 0 60px;border-top:1px solid var(--line);
208
+ font-size:13px;color:var(--muted);display:flex;gap:18px;flex-wrap:wrap}
209
+ @media(max-width:640px){.kv{grid-template-columns:1fr}.kv dt{margin-top:8px}}
210
  </style>
211
  </head>
212
  <body>
213
 
214
  <nav><div class="in">
215
+ <a class="lg" href="#/">LEAD<i>BOARD</i></a>
216
+ <span class="chip g" id="spec">v1.1</span>
217
  <span class="sp"></span>
218
+ <span id="who" style="font-size:13px;color:var(--muted)"></span>
219
  </div></nav>
220
 
221
+ <div id="app"><div class="wrap"><div class="skel" style="margin-top:60px"></div></div></div>
222
 
223
+ <footer class="wrap">
224
  <span>LEADBOARD ยท FINAL-Bench / VIDRAFT</span>
225
  <a href="/api/health">API</a>
226
  <a href="https://huggingface.co/spaces/FINAL-Bench/open-discovery-challenge">ODC</a>
 
228
  </footer>
229
 
230
  <script>
231
+ const $=s=>document.querySelector(s), app=$("#app");
232
+ const f=(x,d=3)=>(x==null||isNaN(x))?"โ€”":Number(x).toFixed(d);
233
+ const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
234
+ let CATS=null, ME={user:null}, BOARDS={};
235
+
236
+ async function j(u,o){const r=await fetch(u,o);if(!r.ok)throw new Error(await r.text());return r.json();}
237
+ const W=h=>`<div class="wrap">${h}</div>`;
238
 
239
+ /* โ•โ•โ•โ•โ•โ• ์„œ๋ช… ์‹œ๊ฐํ™” ยท ์žก์Œ ๋ฐ”๋‹ฅ ์ถ• โ•โ•โ•โ•โ•โ•
240
+ MAE ์ถ• ์œ„์— ์‹คํ—˜ ์˜ค์ฐจ ๊ตฌ๊ฐ„์„ ๋น—๊ธˆ์œผ๋กœ ๊น”๊ณ  ๋ฐฉ๋ฒ•๋“ค์„ ์ ์œผ๋กœ ์ฐ๋Š”๋‹ค.
241
+ ๋น—๊ธˆ ์•ˆ์ชฝ์€ "์‹คํ—˜๋ณด๋‹ค ์ •๋ฐ€" = ์ธก์ •์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์—†๋Š” ์˜์—ญ์ด๋‹ค. */
242
+ function bandHTML(title, floor, marks, right){
243
+ const hi = Math.max(floor*1.35, ...marks.map(m=>m.v)) * 1.12;
244
+ const pc = v => Math.min(100, 100*v/hi);
245
+ return `<div class="band">
246
+ <div class="hd"><b>${esc(title)}</b>
247
+ <span class="chip wr">์žก์Œ ${f(floor)}</span>
248
+ <span class="r">${esc(right||"")}</span></div>
249
+ <div class="axis">
250
+ <div class="line"></div>
251
+ <div class="floor" style="width:${pc(floor)}%"></div>
252
+ ${marks.map((m,i)=>`<div class="mk ${m.base?"b":""} ${i%2?"top":""}"
253
+ style="left:${pc(m.v)}%" data-l="${esc(m.l)} ${f(m.v)}"></div>`).join("")}
254
+ </div>
255
+ <div class="bandnote">
256
+ <i><span class="swatch"></span> ์‹คํ—˜ ์˜ค์ฐจ ๊ตฌ๊ฐ„ โ€” ์ด ์•ˆ์ชฝ์€ ์ธก์ •์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์—†๋‹ค</i>
257
+ <i style="margin-left:auto">MAE (log) โ†’</i>
258
+ </div></div>`;
259
+ }
260
 
261
  async function boot(){
262
+ try{ME=await j("/api/me");}catch(e){}
263
  $("#who").innerHTML = ME.user
264
  ? `${esc(ME.user.user)} ยท <a href="/logout">๋กœ๊ทธ์•„์›ƒ</a>`
265
+ : (ME.login_enabled?`<a href="/login?next=${encodeURIComponent(location.hash||"#/")}">HF ๋กœ๊ทธ์ธ</a>`:"");
266
+ CATS=await j("/api/categories");
267
+ $("#spec").textContent=CATS.spec;
268
+ CATS.categories.forEach(c=>(c.board_cards||[]).forEach(b=>BOARDS[b.board]=b));
269
  route();
270
  }
271
+ addEventListener("hashchange",route);
 
272
  function route(){
273
+ const p=(location.hash||"#/").slice(2).split("/").filter(Boolean);
274
+ scrollTo(0,0);
275
+ if(!p.length)return hub();
276
+ if(p[0]==="c")return category(p[1]);
277
+ if(p[0]==="b")return board(decodeURIComponent(p[1]),p[2]||"rank");
278
  hub();
279
  }
280
 
281
+ /* โ•โ•โ•โ•โ•โ• ํ—ˆ๋ธŒ โ•โ•โ•โ•โ•โ• */
282
  function hub(){
283
+ const t=CATS.totals, bs=Object.values(BOARDS).sort((a,b)=>b.n_test-a.n_test);
284
+ const lost=bs.filter(b=>b.beats_constant===false).length;
285
+ app.innerHTML=W(`
286
+ <div class="hero">
287
+ <h1 class="brand">LEAD<i>BOARD</i></h1>
288
  <p class="sub">๋ฆฌ๋“œ๋ณด๋“œ ยท AI ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ ์‹œํ—˜๋Œ€</p>
289
+ <p class="tag">์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ๋ฅผ,<br>๋ถ„์•ผ๋ณ„๋กœ, ๊ฐ™์€ ์žฃ๋Œ€๋กœ ์žฐ๋‹ค.</p>
290
+ <p class="lede">์ •๋‹ต์€ ๋‚˜๋ˆ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  <b>์•„๋ฌด๊ฒƒ๋„ ํ•™์Šตํ•˜์ง€ ์•Š์€ ๊ธฐ์ค€์„ </b>๊ณผ
291
+ <b>๊ทธ ๋ถ„์•ผ์˜ ์‹คํ—˜ ์˜ค์ฐจ</b>๋ฅผ ๋จผ์ € ๊ณต๊ฐœํ•ฉ๋‹ˆ๋‹ค. ๋ฏฟ์–ด๋‹ฌ๋ผ๊ณ  ํ•˜์ง€ ์•Š๊ณ , ํ™•์ธํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.</p>
292
+ <div class="stats">
293
+ <div class="stat"><b>${t.open}</b><span>๊ฐœ์„ค ๋ถ€๋ฌธ</span></div>
294
+ <div class="stat"><b>${t.planned}</b><span>๊ณ„ํš ๋ถ€๋ฌธ</span></div>
295
+ <div class="stat"><b>${CATS.categories.length}</b><span>์นดํ…Œ๊ณ ๋ฆฌ</span></div>
296
+ <div class="stat"><b>8</b><span>๊ทœ๊ฒฉ ์กฐํ•ญ</span></div>
297
+ </div>
298
  </div>
299
+
300
+ <h2>์ธก์ • ํ•œ๊ณ„</h2>
301
+ <p class="h2s">๊ฐ ๋ถ€๋ฌธ์—์„œ <b>์‹คํ—˜์„ ๋‹ค์‹œ ํ•ด๋„ ์ด๋งŒํผ์€ ํ”๋“ค๋ฆฐ๋‹ค</b>๋Š” ํญ(๋น—๊ธˆ)๊ณผ,
302
+ ํ˜„์žฌ ๊ธฐ์ค€์„ ์ด ์–ด๋””์— ์„œ ์žˆ๋Š”์ง€์ž…๋‹ˆ๋‹ค.</p>
303
+ ${bs.map(b=>bandHTML(b.board,b.noise_floor.sd_single,
304
+ Object.entries(b.baselines).map(([k,v])=>({l:k,v:v.mae,base:true})),
305
+ `${b.split_grade}/${b.answer_grade} ยท ํ…Œ์ŠคํŠธ ${b.n_test.toLocaleString()}`)).join("")}
306
+ ${lost?`<div class="note"><b>${lost}๊ฐœ ๋ถ€๋ฌธ์—์„œ ํ•™์Šต ๊ธฐ์ค€์„ ์ด ์ƒ์ˆ˜ ์˜ˆ์ธก์„ ๋„˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.</b>
307
+ ์•„๋ฌด๊ฒƒ๋„ ๋ฐฐ์šฐ์ง€ ์•Š์€ ์ชฝ์ด ๋” ์ •ํ™•ํ–ˆ๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค.
308
+ ๊ธฐ์ค€์„ ์„ ํ‘œ์— ์ƒ์ฃผ์‹œํ‚ค์ง€ ์•Š์œผ๋ฉด(3์กฐ) ์ด ์‚ฌ์‹ค์€ ์–ด๋””์—๋„ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.</div>`:""}
309
+
310
  <h2>์นดํ…Œ๊ณ ๋ฆฌ</h2>
311
+ <p class="h2s">๋ถ„์•ผ๋ฅผ ๊ณ ๋ฅด๋ฉด ๊ทธ ์•ˆ์˜ ๋ถ€๋ฌธ๊ณผ ์ˆœ์œ„ยท์ œ์ถœ๋กœ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.</p>
312
  <div class="grid">${CATS.categories.map(cardHTML).join("")}</div>
313
+
314
+ <h2>์ฑ„์ ์ž๊ฐ€ ์Šค์Šค๋กœ์—๊ฒŒ ๋ถ€๊ณผํ•˜๋Š” 8๊ฐœ ์กฐํ•ญ</h2>
315
+ <p class="h2s">์ฐธ๊ฐ€์ž์—๊ฒŒ ์š”๊ตฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์šฐ๋ฆฌ๊ฐ€ ์ง€ํ‚ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.</p>
316
+ <ol class="cl">
317
+ ${[["์ •๋‹ต์€ ๋ฐฐํฌํ•˜์ง€ ์•Š๋Š”๋‹ค","๋ถ€๋ฌธ๋งˆ๋‹ค ๋ถ„ํ•  ๋“ฑ๊ธ‰๊ณผ ์ •๋‹ต ๋“ฑ๊ธ‰์„ ๊ณต์‹œํ•œ๋‹ค"],
318
+ ["์ ์ˆ˜๋งˆ๋‹ค ๋‘ ๊ฐœ์˜ ์ง€๋ฌธ","๋ฐ์ดํ„ฐ ํŒ๋ณธ๊ณผ ์ฑ„์  ์ฝ”๋“œ์˜ SHA-256"],
319
+ ["๊ธฐ์ค€์„ ์€ ํ‘œ์—์„œ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋Š”๋‹ค","์ƒ์ˆ˜ ์˜ˆ์ธก ยท ์ตœ๊ทผ์ ‘ ์ด์›ƒ ยท Morgan+LightGBM ์˜๊ตฌ ๋“ฑ์žฌ"],
320
+ ["์žก์Œ ๋ฐ”๋‹ฅ์„ ๋จผ์ € ๊ณต์‹œํ•œ๋‹ค","๊ฐ™์€ ํ™”ํ•ฉ๋ฌผ์„ ๋‹ค๋ฅธ ๋…ผ๋ฌธ์—์„œ ์žฐ ์ฐจ์ด๋กœ ์‚ฐ์ถœํ•œ๋‹ค"],
321
+ ["๋ˆ„์ถœ์€ ๋งค ์ œ์ถœ๋งˆ๋‹ค ๊ฒ€์‚ฌํ•œ๋‹ค","์ •๋‹ต๊ณผ ์†Œ์ˆ˜์ ๊นŒ์ง€ ๊ฐ™์€ ํ•ญ๋ชฉ์ด ๋ฌด๋”๊ธฐ๋ฉด ํ‘œ์— ์ ๋Š”๋‹ค"],
322
+ ["๊ฒฐ๊ณผ ์ œ์ถœ๊ณผ ๋ชจ๋ธ ์ œ์ถœ์„ ๊ตฌ๋ถ„ํ•œ๋‹ค","์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๋Œ๋ ค๋ณธ ๊ฒƒ๋งŒ [๊ฒ€์ฆ๋จ]"],
323
+ ["๋ฏธ์‚ฐ์ถœ์€ 0์ ์ด ์•„๋‹ˆ๋‹ค","์ธก์ •ํ•˜์ง€ ๋ชปํ•œ ๊ฒƒ๊ณผ ์ธก์ •ํ•ด์„œ ๋‚˜์œ ๊ฒƒ์€ ๋‹ค๋ฅด๋‹ค"],
324
+ ["๋˜ํ’€์ด ์ œ์ถœ๋กœ๋Š” ์ˆœ์œ„๊ฐ€ ์˜ค๋ฅด์ง€ ์•Š๋Š”๋‹ค","Ladder ยท Blum &amp; Hardt, ICML 2015"]]
325
+ .map(([a,b])=>`<li><b>${a}</b><span>${b}</span></li>`).join("")}
326
+ </ol>`);
327
  }
328
 
329
  function cardHTML(c){
330
+ const on=c.open>0, best=(c.board_cards||[]).sort((a,b)=>b.n_test-a.n_test)[0];
331
+ const inner=`<div class="ico">${c.icon}</div>
332
+ <div class="nm">${esc(c.name)}
333
+ <span class="chip ${on?"ok":"g"}">${on?c.open+" ๊ฐœ์„ค":"์ค€๋น„ ์ค‘"}</span></div>
 
334
  <div class="ds">${esc(c.desc)}</div>
335
  <div class="mt"><span>๋ถ€๋ฌธ ${c.planned}</span>
336
+ <span>${best?`${esc(best.board)} ยท ์žก์Œ ${f(best.noise_floor.sd_single)}`:"โ€”"}</span></div>`;
337
+ return on?`<a class="cc" href="#/c/${c.id}">${inner}</a>`:`<div class="cc off">${inner}</div>`;
 
338
  }
339
 
340
+ /* โ•โ•โ•โ•โ•โ• ์นดํ…Œ๊ณ ๋ฆฌ โ•โ•โ•โ•โ•โ• */
341
  function category(id){
342
+ const c=CATS.categories.find(x=>x.id===id); if(!c)return hub();
343
+ const bs=c.board_cards||[];
344
+ app.innerHTML=W(`
 
345
  <div class="crumb"><a href="#/">์ „์ฒด</a> โ€บ ${esc(c.name)}</div>
346
+ <h1 class="ttl">${c.icon} ${esc(c.name)}</h1>
347
+ <p class="lede">${esc(c.desc)}</p>
348
+ <div class="stats" style="margin-top:22px">
349
+ <div class="stat"><b>${c.open}</b><span>๊ฐœ์„ค ๋ถ€๋ฌธ</span></div>
350
+ <div class="stat"><b>${c.planned}</b><span>๊ณ„ํš ๋ถ€๋ฌธ</span></div>
351
+ <div class="stat"><b>${bs.reduce((s,b)=>s+b.n_test,0).toLocaleString()}</b><span>ํ…Œ์ŠคํŠธ ํ™”ํ•ฉ๋ฌผ</span></div>
352
+ </div>
353
+ ${bs.length?`<h2>๋ถ€๋ฌธ</h2><div class="grid">${bs.map(boardMini).join("")}</div>
354
+ <h2>์ธก์ • ํ•œ๊ณ„</h2>
355
+ ${bs.map(b=>bandHTML(b.board,b.noise_floor.sd_single,
356
+ Object.entries(b.baselines).map(([k,v])=>({l:k,v:v.mae,base:true})),
357
+ `ํ…Œ์ŠคํŠธ ${b.n_test.toLocaleString()}`)).join("")}`
358
+ :`<div class="note">์ด ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ์•„์ง ๊ฐœ์„ค๋œ ๋ถ€๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค.
359
+ ํ™€๋“œ์•„์›ƒ๊ณผ ์žก์Œ ๋ฐ”๋‹ฅ์ด ์ค€๋น„๋˜๋Š” ๋Œ€๋กœ ์—ฝ๋‹ˆ๋‹ค.</div>`}`);
360
  }
 
361
  function boardMini(b){
362
+ const bl=b.baselines[b.best_baseline]||{};
363
  return `<a class="cc" href="#/b/${encodeURIComponent(b.board)}">
364
+ <div class="nm">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></div>
 
365
  <div class="ds">ํ…Œ์ŠคํŠธ ${b.n_test.toLocaleString()}๊ฐœ ยท ์žก์Œ ๋ฐ”๋‹ฅ ${f(b.noise_floor.sd_single)}<br>
366
+ ์ตœ์„  ๊ธฐ์ค€์„  <b>${esc(b.best_baseline)}</b> ${f(bl.mae)}</div>
367
  <div class="mt"><span>${esc(b.source)}</span><span>์˜ค์ฐจรท์žก์Œ ${f(b.error_over_noise,2)}ร—</span></div></a>`;
368
  }
369
 
370
+ /* โ•โ•โ•โ•โ•โ• ๋ถ€๋ฌธ โ•โ•โ•โ•โ•โ• */
371
+ async function board(name,tab){
372
+ const b=BOARDS[name]||await j("/api/board/"+encodeURIComponent(name));
373
+ const cat=CATS.categories.find(c=>(c.boards||[]).includes(name));
374
+ const T=[["rank","์ˆœ์œ„"],["submit","์ œ์ถœ"],["info","๋ถ€๋ฌธ ์ •๋ณด"]];
375
+ app.innerHTML=W(`
376
  <div class="crumb"><a href="#/">์ „์ฒด</a> โ€บ <a href="#/c/${cat?cat.id:""}">${esc(cat?cat.name:"")}</a> โ€บ ${esc(b.board)}</div>
377
+ <h1 class="ttl">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></h1>
378
+ <p class="lede">${esc(b.source)} ยท ${esc(b.target)} ยท ${esc(b.type)} ยท
379
+ ํ…Œ์ŠคํŠธ <b>${b.n_test.toLocaleString()}</b>๊ฐœ ยท ์žก์Œ ๋ฐ”๋‹ฅ <b>${f(b.noise_floor.sd_single)}</b> log</p>
380
+ <div class="pills">${T.map(([k,l])=>
381
+ `<a class="pilltab ${k===tab?"on":""}" href="#/b/${encodeURIComponent(name)}/${k}">${l}</a>`).join("")}</div>
382
+ <div id="pane"><div class="skel" style="margin-top:18px"></div></div>`);
383
+ if(tab==="rank")return paneRank(b);
384
+ if(tab==="submit")return paneSubmit(b);
 
385
  paneInfo(b);
386
  }
387
 
388
  async function paneRank(b){
389
+ const d=await j(`/api/board/${encodeURIComponent(b.board)}/leaderboard`);
390
+ const nf=d.noise_floor, ent=d.rows.filter(r=>r.kind!=="baseline");
391
+ const leakChip=l=>({high:'<span class="chip bd">์กฐํšŒ ํ”์ </span>',
392
+ elevated:'<span class="chip wr">๊ทผ์ ‘ ๊ณผ๋ฐ€</span>'}[l]||'<span class="chip g">โ€”</span>');
393
+ const rows=d.rows.map(r=>{
394
+ const g=r.rank&&r.rank<=3?` g${r.rank}`:"";
395
+ return `<tr class="${r.kind==="baseline"?"base":""}">
396
+ <td>${r.rank?`<span class="rk${g}">${r.rank}</span>`:"ยท"}</td>
397
+ <td>${esc(r.method)}</td><td>${esc(r.user)}</td>
398
  <td>${f(r.mae)}</td><td>${f(r.auroc)}</td><td>${f(r.prauc)}</td>
399
+ <td>${r.kind==="baseline"?'<span class="chip g">๊ธฐ์ค€์„ </span>':leakChip(r.leak)}</td>
400
+ <td>${r.kind==="baseline"?"โ€”":(r.verified?'<span class="chip ok">๊ฒ€์ฆ๋จ</span>'
401
+ :'<span class="chip g">์ œ์ถœ๋จ</span>')}</td></tr>`;}).join("");
402
+ $("#pane").innerHTML=`
403
+ ${bandHTML(b.board+" โ€” ํ˜„์žฌ ์ˆœ์œ„ ์ง€ํ˜•", nf,
404
+ d.rows.map(r=>({l:r.kind==="baseline"?r.method:r.user, v:r.mae, base:r.kind==="baseline"})),
405
+ `${ent.length}๊ฑด ์ฐธ๊ฐ€`)}
406
  <div class="tw"><table>
407
+ <thead><tr><th>#</th><th>๋ฐฉ๋ฒ•</th><th>์ œ์ถœ์ž</th><th>MAE</th><th>AUROC</th>
408
+ <th>PR-AUC</th><th>๋ˆ„์ถœ</th><th>๋“ฑ๊ธ‰</th></tr></thead>
409
  <tbody>${rows}</tbody></table></div>
410
+ <div class="note"><b>์žก์Œ ๋ฐ”๋‹ฅ ${f(nf)} log.</b> ๋น—๊ธˆ ์•ˆ์ชฝ์€ ์‹คํ—˜์„ ๋‹ค์‹œ ํ•ด๋„ ๊ทธ๋งŒํผ ํ”๋“ค๋ฆฌ๋Š” ๊ตฌ๊ฐ„์ด๋ผ,
411
+ ๊ทธ ํญ ์•ˆ์˜ ์ฐจ์ด๋Š” <b>์ˆœ์œ„ ์ฐจ์ด๋กœ ์ฃผ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค</b> (4์กฐ).
412
  ๊ธฐ์ค€์„  3์ข…์€ ํ‘œ์—์„œ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค (3์กฐ).</div>
413
+ ${ent.length?`<p class="dim" style="font-size:13px">์ˆœ์œ„ ๊ฐฑ์‹  ${new Date(d.updated*1000).toLocaleString("ko-KR")}</p>`
414
+ :`<p class="dim" style="font-size:13px">์•„์ง ์ฐธ๊ฐ€ ์ œ์ถœ์ด ์—†์Šต๋‹ˆ๋‹ค. ํ‘œ์— ์žˆ๋Š” ๊ฒƒ์€ ๊ธฐ์ค€์„ ๋ฟ์ž…๋‹ˆ๋‹ค.</p>`}`;
415
  }
416
 
417
  function paneInfo(b){
418
+ const nf=b.noise_floor,bl=b.baselines;
419
+ $("#pane").innerHTML=`
420
  <h3>๋“ฑ๊ธ‰</h3>
421
  <dl class="kv">
422
+ <dt>๋ถ„ํ•  ๋“ฑ๊ธ‰ <b>${b.split_grade}</b></dt><dd>${esc(b.split_grade_note||"")}</dd>
423
+ <dt>์ •๋‹ต ๋“ฑ๊ธ‰ <b>${b.answer_grade}</b></dt><dd>${esc(b.answer_grade_note||"")}</dd>
424
  </dl>
425
  <h3>ํ™€๋“œ์•„์›ƒ</h3>
426
  <dl class="kv">
427
  <dt>์ถœ์ฒ˜</dt><dd>${esc(b.source)} ยท ${esc(b.target)} ยท ${esc(b.type)}</dd>
428
+ <dt>๋ถ„ํ•  ๊ธฐ์ค€์ผ</dt><dd>${b.cut} โ€” ์ตœ์ดˆ ๋ณด๊ณ  ์—ฐ๋„</dd>
429
  <dt>ํ›ˆ๋ จ / ํ…Œ์ŠคํŠธ</dt><dd>${b.n_train.toLocaleString()} / ${b.n_test.toLocaleString()}</dd>
430
  <dt>๊ตฌ์กฐ ๊ฒน์นจ</dt><dd>0 (์‹œ๊ฐ„๋ถ„ํ•  ์ •์˜์ƒ)</dd>
431
  <dt>๋ถ„๋ฅ˜ ์ž„๊ณ„</dt><dd>p &gt; ${b.threshold} ยท ์–‘์„ฑ ${(b.positive_rate*100).toFixed(1)}%</dd>
432
  <dt>๋ฐ์ดํ„ฐ ์ง€๋ฌธ</dt><dd>${esc(b.dataset_sha)}</dd>
433
  </dl>
434
+ <h3>์žก์Œ ๋ฐ”๋‹ฅ <span class="dim" style="font-weight:400;font-size:14px">(4์กฐ)</span></h3>
435
  <div class="fig">
436
+ ๊ฐ™์€ ํ™”ํ•ฉ๋ฌผ์„ <b>์„œ๋กœ ๋‹ค๋ฅธ ๋…ผ๋ฌธ</b>์—์„œ ์žฐ ์ฐจ์ด<br>
437
+ ๋น„๊ต์Œ <span class="n">${nf.pairs.toLocaleString()}</span> ยท ํ™”ํ•ฉ๋ฌผ <span class="n">${nf.compounds.toLocaleString()}</span><br>
438
  ์ค‘์•™๊ฐ’ ${f(nf.median)} ยท ํ‰๊ท  ${f(nf.mean)} ยท 90๋ถ„์œ„ ${f(nf.p90)}<br>
439
  <b>๋‹จ์ผ ์ธก์ • SD ์ถ”์ • <span class="n">${f(nf.sd_single)}</span> log</b>
440
  </div>
441
+ <div class="note">๋ถ„๋ฅ˜ ์ž„๊ณ„์„ ์—์„œ ์‹คํ—˜ ์˜ค์ฐจ ํ•œ ์นธ ์•ˆ์— ์žˆ๋Š” ํ…Œ์ŠคํŠธ ํ™”ํ•ฉ๋ฌผ์ด <b>${b.near_pct}%</b> ์ž…๋‹ˆ๋‹ค.
442
+ ${b.near_pct>45?"์ ˆ๋ฐ˜ ๊ฐ€๊นŒ์ด๊ฐ€ ์žฌ์ธก์ •ํ•˜๋ฉด ๋ผ๋ฒจ์ด ๋’ค์ง‘ํž ์ˆ˜ ์žˆ์–ด <b>ํšŒ๊ท€๋ฅผ ์ฃผ์ง€ํ‘œ</b>๋กœ ๋‘ก๋‹ˆ๋‹ค."
443
+ :"๋ถ„๋ฅ˜ ์ง€ํ‘œ๋ฅผ ํ•จ๊ป˜ ์”๋‹ˆ๋‹ค."}</div>
444
+ <h3>๊ธฐ์ค€์„  <span class="dim" style="font-weight:400;font-size:14px">(3์กฐ)</span></h3>
445
+ ${bandHTML("๊ธฐ์ค€์„  ์œ„์น˜",nf.sd_single,
446
+ Object.entries(bl).map(([k,v])=>({l:k,v:v.mae,base:true})),"")}
447
+ <div class="tw"><table><thead><tr><th style="width:auto"></th><th>๊ธฐ์ค€์„ </th><th>MAE</th>
448
+ <th>AUROC</th><th>PR-AUC</th></tr></thead><tbody>
449
+ ${Object.entries(bl).map(([k,v])=>`<tr><td>ยท</td><td>${esc(k)}</td><td>${f(v.mae)}</td>
450
+ <td>${f(v.auroc)}</td><td>${f(v.prauc)}</td></tr>`).join("")}
451
+ <tr class="base"><td>ยท</td><td>์‹คํ—˜ ์žก์Œ ๋ฐ”๋‹ฅ</td><td>${f(nf.sd_single)}</td><td>โ€”</td><td>โ€”</td></tr>
452
  </tbody></table></div>
453
+ <div class="note ${b.beats_constant?"ok":""}">${b.beats_constant
454
  ? "ํ•™์Šต ๊ธฐ์ค€์„ ๏ฟฝ๏ฟฝ ์ƒ์ˆ˜ ์˜ˆ์ธก์„ ๋„˜์—ˆ์Šต๋‹ˆ๋‹ค."
455
+ : "<b>ํ•™์Šต ๊ธฐ์ค€์„ ์ด ์ƒ์ˆ˜ ์˜ˆ์ธก์„ ๋„˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.</b> ์•„๋ฌด๊ฒƒ๋„ ๋ฐฐ์šฐ์ง€ ์•Š์€ ์ชฝ์ด ๋” ์ •ํ™•ํ–ˆ์Šต๋‹ˆ๋‹ค."}</div>`;
456
  }
457
 
458
  function paneSubmit(b){
459
+ const url=`/api/board/${encodeURIComponent(b.board)}/testset`;
460
+ $("#pane").innerHTML=`
461
+ <ol class="steps">
462
+ <li><b>ํ…Œ์ŠคํŠธ์…‹ ๋ฐ›๊ธฐ</b>
463
+ <p class="dim" style="font-size:13.5px">ํ™”ํ•ฉ๋ฌผ <b>${b.n_test.toLocaleString()}๊ฐœ</b>์˜ ๊ตฌ์กฐ(SMILES)๋งŒ
464
+ ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ผ๋ฒจ์€ ์ด ์„œ๋ฒ„์— ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.</p>
465
+ <a class="btn" href="${url}" download>ํ…Œ์ŠคํŠธ์…‹ ๋‚ด๋ ค๋ฐ›๊ธฐ</a>
466
+ <span class="hint">&nbsp;${esc(b.board.toLowerCase())}_test.json</span></li>
467
+ <li><b>์˜ˆ์ธก ์˜ฌ๋ฆฌ๊ธฐ</b>
468
+ ${ME.user?`
469
+ <label>๋ฐฉ๋ฒ• ์ด๋ฆ„ <span class="hint">์ˆœ์œ„ํ‘œ์— ์ด๋ ‡๊ฒŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค</span></label>
470
+ <input type="text" id="m" placeholder="์˜ˆ: my-gnn-ensemble-v2" maxlength="60">
471
+ <label>ํ•™์Šต์— ์“ด ๋ฐ์ดํ„ฐ <span class="hint">5์กฐ ๋ˆ„์ถœ ๊ฒ€์‚ฌ์— ์”๋‹ˆ๋‹ค</span></label>
472
+ <input type="text" id="td" placeholder="์˜ˆ: ChEMBL34 + ์‚ฌ๋‚ด ๋ฐ์ดํ„ฐ">
473
+ <label>์˜ˆ์ธก๊ฐ’ <span class="hint">JSON {"LB-โ€ฆ": 6.42} ๋˜๋Š” CSV compound_id,prediction โ€” p ์Šค์ผ€์ผ(โˆ’log10 M)</span></label>
474
+ <div class="drop" id="drop">ํŒŒ์ผ์„ ์—ฌ๊ธฐ์— ๋Œ์–ด๋‹ค ๋†“๊ฑฐ๋‚˜ ๋ˆŒ๋Ÿฌ์„œ ๊ณ ๋ฅด์„ธ์š”
475
+ <input type="file" id="file" accept=".json,.csv,.txt" hidden></div>
476
+ <textarea id="p" rows="6" style="margin-top:10px"
477
+ placeholder='{"LB-A1B2C3D4E5F6": 6.42, "LB-โ€ฆ": 5.10}'></textarea>
478
+ <p style="margin-top:14px"><button class="btn" id="go">์ œ์ถœ</button>
479
+ <span class="hint" id="msg"></span></p>`
480
+ :`<div class="note">์ œ์ถœํ•˜๋ ค๋ฉด HF ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
481
+ <a href="/login?next=${encodeURIComponent(location.hash)}">๋กœ๊ทธ์ธ</a></div>`}</li>
482
+ <li><b>์ปจํ…Œ์ด๋„ˆ๊นŒ์ง€ ์˜ฌ๋ฆฌ๋ฉด (์„ ํƒ)</b>
483
+ <p class="dim" style="font-size:13.5px">๋ชจ๋ธ๊ณผ ์ถ”๋ก  ํ™˜๊ฒฝ์„ ํ•จ๊ป˜ ์ œ์ถœํ•˜๋ฉด <b>์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๋Œ๋ ค</b>
484
+ ๊ฐ™์€ ์ˆซ์ž๊ฐ€ ๋‚˜์˜ค๋Š”์ง€ ํ™•์ธํ•˜๊ณ  <span class="chip ok">๊ฒ€์ฆ๋จ</span> ํ‘œ์‹œ๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค (6์กฐ).</p></li>
485
+ </ol>
486
+ <div class="note"><b>8์กฐ.</b> ๋˜ํ’€์ด ์ œ์ถœ๋กœ๋Š” ์ˆœ์œ„๊ฐ€ ์˜ค๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
487
+ ์ง์ „ ์ตœ๊ณ ์ ์„ ์žก์Œ ๋ฐ”๋‹ฅ <b>${f(b.noise_floor.sd_single)}</b> ์ด์ƒ์œผ๋กœ ๋„˜์—ˆ์„ ๋•Œ๋งŒ
488
+ ์ƒˆ ์ ์ˆ˜๊ฐ€ ๊ณต๊ฐœ๋ฉ๋‹ˆ๋‹ค. ํ•˜๋ฃจ ์ œ์ถœ ํ•œ๋„ ${ME.daily_cap||5}ํšŒ.</div>`;
489
+ wireSubmit(b);
490
+ }
491
+
492
+ function parsePred(txt){
493
+ txt=txt.trim();
494
+ if(txt.startsWith("{"))return JSON.parse(txt);
495
+ const out={};
496
+ txt.split(/\r?\n/).forEach((ln,i)=>{
497
+ const p=ln.split(/[,\t]/); if(p.length<2)return;
498
+ const k=p[0].trim(); if(!k||(i===0&&isNaN(parseFloat(p[1]))))return;
499
+ const v=parseFloat(p[1]); if(!isNaN(v))out[k]=v;
500
+ });
501
+ if(!Object.keys(out).length)throw new Error("์ฝ์„ ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค");
502
+ return out;
503
+ }
504
+
505
+ function wireSubmit(b){
506
+ const drop=$("#drop"),file=$("#file"),ta=$("#p"),go=$("#go");
507
+ if(drop){
508
+ drop.onclick=()=>file.click();
509
+ file.onchange=e=>{const f0=e.target.files[0];if(!f0)return;
510
+ const r=new FileReader();r.onload=()=>{ta.value=r.result;
511
+ drop.textContent=`${f0.name} โ€” ${(f0.size/1024).toFixed(0)} KB ์ฝ์Œ`;};r.readAsText(f0);};
512
+ ["dragenter","dragover"].forEach(ev=>drop.addEventListener(ev,e=>{
513
+ e.preventDefault();drop.classList.add("hot");}));
514
+ ["dragleave","drop"].forEach(ev=>drop.addEventListener(ev,e=>{
515
+ e.preventDefault();drop.classList.remove("hot");}));
516
+ drop.addEventListener("drop",e=>{const f0=e.dataTransfer.files[0];if(!f0)return;
517
+ const r=new FileReader();r.onload=()=>{ta.value=r.result;
518
+ drop.textContent=`${f0.name} โ€” ${(f0.size/1024).toFixed(0)} KB ์ฝ์Œ`;};r.readAsText(f0);});
519
+ }
520
+ if(!go)return;
521
+ go.onclick=async()=>{
522
+ const msg=$("#msg");let preds;
523
+ try{preds=parsePred(ta.value);}catch(e){msg.textContent="์˜ˆ์ธก๊ฐ’์„ ์ฝ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: "+e.message;return;}
524
+ msg.textContent=`${Object.keys(preds).length.toLocaleString()}๊ฐœ ํ•ญ๋ชฉ ยท ๋ณด๋‚ด๋Š” ์ค‘โ€ฆ`;go.disabled=true;
525
  try{
526
+ const r=await j("/api/submit",{method:"POST",headers:{"Content-Type":"application/json"},
527
+ body:JSON.stringify({board:b.board,method:($("#m").value||"").trim(),
528
+ predictions:preds,training_data:($("#td").value||"").trim()})});
529
+ msg.innerHTML=`์ ‘์ˆ˜ ์™„๋ฃŒ ยท ์ œ์ถœ ๋ฒˆํ˜ธ <code>${esc(r.submission_id)}</code>`;
530
+ }catch(e){let t=String(e.message||e);try{t=JSON.parse(t).detail||t;}catch(_){}msg.textContent=t;}
531
+ go.disabled=false;
 
 
 
 
532
  };
533
  }
534
 
535
+ boot().catch(e=>{app.innerHTML=W(`<div class="note" style="margin-top:40px">
536
+ ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ${esc(e.message||e)}</div>`);});
537
  </script>
538
  </body>
539
  </html>