Spaces:
Running
Running
Bilingual UI (ko/en) with browser detection and a language switch
Browse filesAll copy moves into a string dictionary served at /i18n.js, so the English edition cannot silently fall behind. Language resolves from a stored choice, then the browser locale. Page order now leads with disciplines, then the measurement-limit evidence, then the spec.
- app.py +7 -0
- categories.json +22 -11
- i18n.js +218 -0
- index.html +190 -211
app.py
CHANGED
|
@@ -183,6 +183,13 @@ def index():
|
|
| 183 |
return FileResponse(os.path.join(HERE, "index.html"))
|
| 184 |
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
@app.get("/api/categories")
|
| 187 |
def api_categories():
|
| 188 |
c = categories()
|
|
|
|
| 183 |
return FileResponse(os.path.join(HERE, "index.html"))
|
| 184 |
|
| 185 |
|
| 186 |
+
@app.get("/i18n.js")
|
| 187 |
+
def i18n():
|
| 188 |
+
"""문자열 사전. 화면 코드와 분리해 두어야 영어판이 조용히 뒤처지지 않는다."""
|
| 189 |
+
return FileResponse(os.path.join(HERE, "i18n.js"),
|
| 190 |
+
media_type="application/javascript")
|
| 191 |
+
|
| 192 |
+
|
| 193 |
@app.get("/api/categories")
|
| 194 |
def api_categories():
|
| 195 |
c = categories()
|
categories.json
CHANGED
|
@@ -10,7 +10,8 @@
|
|
| 10 |
"planned": 8,
|
| 11 |
"boards": [
|
| 12 |
"Solubility"
|
| 13 |
-
]
|
|
|
|
| 14 |
},
|
| 15 |
{
|
| 16 |
"id": "D",
|
|
@@ -19,7 +20,8 @@
|
|
| 19 |
"icon": "🩸",
|
| 20 |
"desc": "혈액 단백질에 붙는가 · 뇌까지 가는가",
|
| 21 |
"planned": 5,
|
| 22 |
-
"boards": []
|
|
|
|
| 23 |
},
|
| 24 |
{
|
| 25 |
"id": "M",
|
|
@@ -32,7 +34,8 @@
|
|
| 32 |
"CYP3A4",
|
| 33 |
"CYP2D6",
|
| 34 |
"CYP2C9"
|
| 35 |
-
]
|
|
|
|
| 36 |
},
|
| 37 |
{
|
| 38 |
"id": "E",
|
|
@@ -41,7 +44,8 @@
|
|
| 41 |
"icon": "💧",
|
| 42 |
"desc": "몸 밖으로 언제 나가는가",
|
| 43 |
"planned": 2,
|
| 44 |
-
"boards": []
|
|
|
|
| 45 |
},
|
| 46 |
{
|
| 47 |
"id": "T",
|
|
@@ -52,7 +56,8 @@
|
|
| 52 |
"planned": 7,
|
| 53 |
"boards": [
|
| 54 |
"hERG"
|
| 55 |
-
]
|
|
|
|
| 56 |
},
|
| 57 |
{
|
| 58 |
"id": "P",
|
|
@@ -65,7 +70,8 @@
|
|
| 65 |
"AChE",
|
| 66 |
"MAOB",
|
| 67 |
"COX2"
|
| 68 |
-
]
|
|
|
|
| 69 |
},
|
| 70 |
{
|
| 71 |
"id": "K",
|
|
@@ -86,7 +92,8 @@
|
|
| 86 |
"BRAF",
|
| 87 |
"KIT",
|
| 88 |
"ALK"
|
| 89 |
-
]
|
|
|
|
| 90 |
},
|
| 91 |
{
|
| 92 |
"id": "S",
|
|
@@ -95,7 +102,8 @@
|
|
| 95 |
"icon": "🧬",
|
| 96 |
"desc": "어떤 자세로 붙는가 · 그 자세가 물리적으로 말이 되는가",
|
| 97 |
"planned": 4,
|
| 98 |
-
"boards": []
|
|
|
|
| 99 |
},
|
| 100 |
{
|
| 101 |
"id": "X",
|
|
@@ -104,7 +112,8 @@
|
|
| 104 |
"icon": "🔬",
|
| 105 |
"desc": "약이 실제 세포에 무슨 짓을 하는가",
|
| 106 |
"planned": 5,
|
| 107 |
-
"boards": []
|
|
|
|
| 108 |
},
|
| 109 |
{
|
| 110 |
"id": "C",
|
|
@@ -113,7 +122,8 @@
|
|
| 113 |
"icon": "🏥",
|
| 114 |
"desc": "사람 몸에서의 농도 · 약물 충돌 · 용량. 겨룰 표가 아직 없는 자리",
|
| 115 |
"planned": 6,
|
| 116 |
-
"boards": []
|
|
|
|
| 117 |
},
|
| 118 |
{
|
| 119 |
"id": "G",
|
|
@@ -122,7 +132,8 @@
|
|
| 122 |
"icon": "⚗️",
|
| 123 |
"desc": "새로 만들어 내는 쪽 · 소분자를 넘어서는 쪽",
|
| 124 |
"planned": 5,
|
| 125 |
-
"boards": []
|
|
|
|
| 126 |
}
|
| 127 |
]
|
| 128 |
}
|
|
|
|
| 10 |
"planned": 8,
|
| 11 |
"boards": [
|
| 12 |
"Solubility"
|
| 13 |
+
],
|
| 14 |
+
"desc_en": "Whether an orally dosed compound enters the body at all"
|
| 15 |
},
|
| 16 |
{
|
| 17 |
"id": "D",
|
|
|
|
| 20 |
"icon": "🩸",
|
| 21 |
"desc": "혈액 단백질에 붙는가 · 뇌까지 가는가",
|
| 22 |
"planned": 5,
|
| 23 |
+
"boards": [],
|
| 24 |
+
"desc_en": "Binding to plasma protein; passage across the blood-brain barrier"
|
| 25 |
},
|
| 26 |
{
|
| 27 |
"id": "M",
|
|
|
|
| 34 |
"CYP3A4",
|
| 35 |
"CYP2D6",
|
| 36 |
"CYP2C9"
|
| 37 |
+
],
|
| 38 |
+
"desc_en": "How quickly the liver clears it, and which enzymes act on it"
|
| 39 |
},
|
| 40 |
{
|
| 41 |
"id": "E",
|
|
|
|
| 44 |
"icon": "💧",
|
| 45 |
"desc": "몸 밖으로 언제 나가는가",
|
| 46 |
"planned": 2,
|
| 47 |
+
"boards": [],
|
| 48 |
+
"desc_en": "When the compound leaves the body"
|
| 49 |
},
|
| 50 |
{
|
| 51 |
"id": "T",
|
|
|
|
| 56 |
"planned": 7,
|
| 57 |
"boards": [
|
| 58 |
"hERG"
|
| 59 |
+
],
|
| 60 |
+
"desc_en": "Cardiac, hepatic and mutagenic liability — where candidates most often fail"
|
| 61 |
},
|
| 62 |
{
|
| 63 |
"id": "P",
|
|
|
|
| 70 |
"AChE",
|
| 71 |
"MAOB",
|
| 72 |
"COX2"
|
| 73 |
+
],
|
| 74 |
+
"desc_en": "How tightly a compound binds its intended target"
|
| 75 |
},
|
| 76 |
{
|
| 77 |
"id": "K",
|
|
|
|
| 92 |
"BRAF",
|
| 93 |
"KIT",
|
| 94 |
"ALK"
|
| 95 |
+
],
|
| 96 |
+
"desc_en": "Which of several hundred kinases are engaged, and which are spared"
|
| 97 |
},
|
| 98 |
{
|
| 99 |
"id": "S",
|
|
|
|
| 102 |
"icon": "🧬",
|
| 103 |
"desc": "어떤 자세로 붙는가 · 그 자세가 물리적으로 말이 되는가",
|
| 104 |
"planned": 4,
|
| 105 |
+
"boards": [],
|
| 106 |
+
"desc_en": "The binding pose, and whether that pose is physically admissible"
|
| 107 |
},
|
| 108 |
{
|
| 109 |
"id": "X",
|
|
|
|
| 112 |
"icon": "🔬",
|
| 113 |
"desc": "약이 실제 세포에 무슨 짓을 하는가",
|
| 114 |
"planned": 5,
|
| 115 |
+
"boards": [],
|
| 116 |
+
"desc_en": "What a compound actually does to a living cell"
|
| 117 |
},
|
| 118 |
{
|
| 119 |
"id": "C",
|
|
|
|
| 122 |
"icon": "🏥",
|
| 123 |
"desc": "사람 몸에서의 농도 · 약물 충돌 · 용량. 겨룰 표가 아직 없는 자리",
|
| 124 |
"planned": 6,
|
| 125 |
+
"boards": [],
|
| 126 |
+
"desc_en": "Human exposure, drug-drug interaction and dose — endpoints with no leaderboard yet"
|
| 127 |
},
|
| 128 |
{
|
| 129 |
"id": "G",
|
|
|
|
| 132 |
"icon": "⚗️",
|
| 133 |
"desc": "새로 만들어 내는 쪽 · 소분자를 넘어서는 쪽",
|
| 134 |
"planned": 5,
|
| 135 |
+
"boards": [],
|
| 136 |
+
"desc_en": "Generating new matter, and modalities beyond small molecules"
|
| 137 |
}
|
| 138 |
]
|
| 139 |
}
|
i18n.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* LEADBOARD 문자열 사전.
|
| 2 |
+
*
|
| 3 |
+
* 문자열은 전부 여기에만 둔다. 화면 코드에 한국어가 섞이면 영어판이 조용히 뒤처진다.
|
| 4 |
+
* 작은따옴표를 쓰지 않는다 - 영문 축약형(don't) 하나가 문자열을 끊어 사전 전체를
|
| 5 |
+
* 무너뜨린 전례가 있다. 축약형은 풀어 쓴다.
|
| 6 |
+
*/
|
| 7 |
+
window.T = {
|
| 8 |
+
ko: {
|
| 9 |
+
htmlLang: "ko",
|
| 10 |
+
title: "LEADBOARD — 신약 예측 도구 시험대",
|
| 11 |
+
navSpec: "규격",
|
| 12 |
+
login: "HF 로그인", logout: "로그아웃",
|
| 13 |
+
langBtn: "EN",
|
| 14 |
+
|
| 15 |
+
heroKicker: "AI 신약 개발 벤치마크",
|
| 16 |
+
heroTag: "신약 예측 도구를<br>분야별로, 동일한 기준으로 평가합니다",
|
| 17 |
+
heroLede: "본 시험대는 정답을 배포하지 않습니다. 대신 <b>학습하지 않은 기준선</b>과 " +
|
| 18 |
+
"<b>해당 종목의 실험실 간 재현 오차</b>를 사전에 공시하여, 보고된 성능이 " +
|
| 19 |
+
"측정 한계 대비 어디에 위치하는지 판정할 수 있게 합니다.",
|
| 20 |
+
|
| 21 |
+
statBoards: "개설 부문", statCompounds: "테스트 화합물", statFields: "분야",
|
| 22 |
+
statClauses: "규격 조항", statPlanned: "계획 부문",
|
| 23 |
+
|
| 24 |
+
secBrowse: "분야", secBrowseLead: "분야를 선택하면 소속 부문의 순위표와 제출 창구로 이동합니다.",
|
| 25 |
+
secEvidence: "측정 한계 지형",
|
| 26 |
+
secEvidenceLead: "가로축은 <b>기준선 오차 ÷ 실험 재현 오차</b>입니다. 1.0 에 근접할수록 " +
|
| 27 |
+
"모델을 개선해도 <b>측정의 산포를 예측하는 일</b>에 가까워집니다.",
|
| 28 |
+
secMethod: "채점 규격 8개 조항",
|
| 29 |
+
secMethodLead: "참가자에게 부과하는 요건이 아니라, 채점자가 스스로에게 부과하는 제약입니다.",
|
| 30 |
+
secData: "데이터와 라이선스",
|
| 31 |
+
|
| 32 |
+
terrLeft: "실험 오차 안쪽", terrRight: "모델 오차가 더 큼",
|
| 33 |
+
insideCount: n => `${n}개 부문이 1.3배 안쪽에 위치합니다.`,
|
| 34 |
+
lostNote: n => `<b>${n}개 부문에서 학습 기준선이 상수 예측에 미달했습니다.</b> ` +
|
| 35 |
+
"학습하지 않은 예측이 더 정확했다는 의미입니다. 기준선을 순위표에 상주시키지 " +
|
| 36 |
+
"않으면(3조) 이 사실은 어떤 지표에도 나타나지 않습니다.",
|
| 37 |
+
|
| 38 |
+
openN: n => `${n} 개설`, prep: "준비 중",
|
| 39 |
+
cardBoards: (o, p) => `부문 ${o}/${p}`,
|
| 40 |
+
cardCompounds: n => `${n} 화합물`,
|
| 41 |
+
|
| 42 |
+
clauses: [
|
| 43 |
+
["정답은 배포하지 않는다", "부문마다 분할 등급과 정답 등급을 공시한다"],
|
| 44 |
+
["점수마다 두 개의 지문", "데이터 판본과 채점 코드의 SHA-256 을 각인한다"],
|
| 45 |
+
["기준선은 순위표에서 내려가지 않는다", "상수 예측 · 최근접 이웃 · Morgan+LightGBM 영구 등재"],
|
| 46 |
+
["실험 오차를 사전 공시한다", "동일 화합물의 논문 간 측정 차이로 산출한다"],
|
| 47 |
+
["누출은 매 제출마다 검사한다", "정답과 소수 자리까지 일치하는 항목의 과밀도를 공시한다"],
|
| 48 |
+
["결과 제출과 모델 제출을 구분한다", "직접 재실행하여 확인한 제출에만 [검증됨]을 부여한다"],
|
| 49 |
+
["미산출은 0점이 아니다", "측정하지 못한 것과 측정하여 나쁜 것은 다르다"],
|
| 50 |
+
["반복 제출로 순위가 오르지 않는다", "Ladder · Blum & Hardt, ICML 2015"]
|
| 51 |
+
],
|
| 52 |
+
|
| 53 |
+
all: "전체",
|
| 54 |
+
tabRank: "순위", tabSubmit: "제출", tabInfo: "부문 정보",
|
| 55 |
+
thRank: "#", thMethod: "방법", thUser: "제출자", thLeak: "누출", thGrade: "등급",
|
| 56 |
+
baseline: "기준선", submitted: "제출됨", verified: "검증됨",
|
| 57 |
+
leakHigh: "조회 흔적", leakElev: "근접 과밀",
|
| 58 |
+
noiseNote: v => `<b>실험 재현 오차 ${v}.</b> 이 폭 안의 차이는 순위 차이로 주장하지 ` +
|
| 59 |
+
"않습니다 (4조). 기준선 3종은 순위표에서 내려가지 않습니다 (3조).",
|
| 60 |
+
noEntries: "참가 제출이 아직 없습니다. 표에 표시된 것은 기준선입니다.",
|
| 61 |
+
updated: t => `순위 갱신 ${t}`,
|
| 62 |
+
|
| 63 |
+
gradeH: "등급", holdoutH: "홀드아웃", noiseH: "실험 재현 오차", baseH: "기준선",
|
| 64 |
+
splitGrade: "분할 등급", answerGrade: "정답 등급",
|
| 65 |
+
gradeNote: {
|
| 66 |
+
T: "시간 분할 — 최초 보고 연도 기준. 기준일 이후 처음 등장한 화합물만 평가한다",
|
| 67 |
+
S: "스캐폴드 분할 — 골격이 훈련에 없는 화합물만 평가한다",
|
| 68 |
+
P1: "정답이 공개 데이터베이스에 존재한다. 조회 방어는 8조가 담당한다",
|
| 69 |
+
P2: "원자료는 공개지만 선별과 환산이 본 판본을 구성한다. 공개 DB 조회로는 동일 값이 나오지 않는다",
|
| 70 |
+
P3: "파트너로부터 제공받은 미공표 실측",
|
| 71 |
+
P4: "부문 개설 이후 생성된 측정. 개설 시점에 존재하지 않았다"
|
| 72 |
+
},
|
| 73 |
+
kSource: "출처", kCut: "분할 기준일", kTrainTest: "훈련 / 테스트",
|
| 74 |
+
kOverlap: "구조 겹침", kThr: "분류 임계", kSha: "데이터 지문", kLicense: "라이선스",
|
| 75 |
+
cutNote: "최초 보고 연도 기준", overlapNote: "0 (시간 분할 정의상)",
|
| 76 |
+
posRate: p => `양성 ${p}%`,
|
| 77 |
+
noisePairs: (p, c) => `비교쌍 ${p} · 화합물 ${c}`,
|
| 78 |
+
noiseDesc: "동일 화합물을 <b>서로 다른 논문</b>에서 측정한 값의 차이",
|
| 79 |
+
noiseSd: "단일 측정 표준편차 추정",
|
| 80 |
+
nearNote: (p, hi) => `분류 임계선에서 실험 오차 1 표준편차 안에 있는 테스트 화합물이 ` +
|
| 81 |
+
`<b>${p}%</b> 입니다. ` + (hi ? "재측정 시 라벨이 반전될 수 있어 <b>회귀를 주지표</b>로 둡니다."
|
| 82 |
+
: "분류 지표를 함께 사용합니다."),
|
| 83 |
+
beatsYes: "학습 기준선이 상수 예측을 상회했습니다.",
|
| 84 |
+
beatsNo: "<b>학습 기준선이 상수 예측에 미달했습니다.</b> 학습하지 않은 예측이 더 정확했습니다.",
|
| 85 |
+
noiseFloorRow: "실험 오차 바닥",
|
| 86 |
+
|
| 87 |
+
step1: "테스트셋 수령", step2: "예측 제출", step3: "컨테이너 제출 (선택)",
|
| 88 |
+
step1d: n => `화합물 <b>${n}개</b>의 구조(SMILES)만 포함합니다. 라벨은 본 서버에 존재하지 않습니다.`,
|
| 89 |
+
licLine: "ChEMBL 파생 · CC BY-SA 3.0 — 재배포 시 출처와 동일 라이선스를 유지하십시오.",
|
| 90 |
+
dl: "테스트셋 내려받기",
|
| 91 |
+
fMethod: "방법 이름", fMethodH: "순위표에 표시될 명칭",
|
| 92 |
+
fTrain: "학습에 사용한 데이터", fTrainH: "5조 누출 검사에 사용합니다",
|
| 93 |
+
fPred: "예측값", fPredH: "JSON 또는 CSV (compound_id, prediction)",
|
| 94 |
+
drop: "파일을 끌어다 놓거나 클릭하여 선택",
|
| 95 |
+
send: "제출", needLogin: "제출하려면 HF 계정 로그인이 필요합니다.",
|
| 96 |
+
step3d: "모델과 추론 환경을 함께 제출하면 직접 재실행하여 동일 결과를 확인하고 " +
|
| 97 |
+
"[검증됨]을 부여합니다 (6조).",
|
| 98 |
+
ladderNote: (v, c) => `<b>8조.</b> 반복 제출로는 순위가 오르지 않습니다. 직전 최고 점수를 ` +
|
| 99 |
+
`실험 오차 <b>${v}</b> 이상 개선한 경우에만 새 점수가 공개됩니다. 일일 제출 한도 ${c}회.`,
|
| 100 |
+
accepted: id => `접수 완료 · 제출 번호 ${id}`,
|
| 101 |
+
parseFail: m => `예측값을 해석하지 못했습니다: ${m}`,
|
| 102 |
+
sending: n => `${n}개 항목 전송 중`,
|
| 103 |
+
emptyCat: "본 분야는 아직 개설된 부문이 없습니다.",
|
| 104 |
+
loadFail: "화면을 불러오지 못했습니다.",
|
| 105 |
+
dataNote: "모든 부문은 ChEMBL_37 에서 구축했으며 최초 보고 연도를 기준으로 시간 분할했습니다. " +
|
| 106 |
+
"배포되는 테스트셋은 구조만 포함하며 라벨은 포함하지 않습니다.",
|
| 107 |
+
srcLabel: "데이터 출처"
|
| 108 |
+
},
|
| 109 |
+
|
| 110 |
+
en: {
|
| 111 |
+
htmlLang: "en",
|
| 112 |
+
title: "LEADBOARD — Benchmark for Drug Prediction Tools",
|
| 113 |
+
navSpec: "spec",
|
| 114 |
+
login: "Sign in with HF", logout: "Sign out",
|
| 115 |
+
langBtn: "한국어",
|
| 116 |
+
|
| 117 |
+
heroKicker: "AI Drug Discovery Benchmark",
|
| 118 |
+
heroTag: "One yardstick, per discipline,<br>for drug prediction tools",
|
| 119 |
+
heroLede: "We do not release the answer key. Instead we publish, in advance, " +
|
| 120 |
+
"<b>baselines that learned nothing</b> and <b>the inter-laboratory reproducibility " +
|
| 121 |
+
"of each endpoint</b> — so that any reported score can be judged against the " +
|
| 122 |
+
"measurement limit itself.",
|
| 123 |
+
|
| 124 |
+
statBoards: "open boards", statCompounds: "test compounds", statFields: "disciplines",
|
| 125 |
+
statClauses: "spec clauses", statPlanned: "planned boards",
|
| 126 |
+
|
| 127 |
+
secBrowse: "Disciplines",
|
| 128 |
+
secBrowseLead: "Select a discipline to reach its boards, leaderboards and submission desk.",
|
| 129 |
+
secEvidence: "Measurement-limit landscape",
|
| 130 |
+
secEvidenceLead: "The axis is <b>baseline error divided by experimental error</b>. " +
|
| 131 |
+
"The closer to 1.0, the more that further tuning amounts to " +
|
| 132 |
+
"<b>predicting measurement scatter</b>.",
|
| 133 |
+
secMethod: "Eight clauses the scorer imposes on itself",
|
| 134 |
+
secMethodLead: "These are not requirements placed on entrants. They are constraints we accept.",
|
| 135 |
+
secData: "Data and licence",
|
| 136 |
+
|
| 137 |
+
terrLeft: "inside experimental error", terrRight: "model error larger",
|
| 138 |
+
insideCount: n => `${n} boards sit within 1.3x.`,
|
| 139 |
+
lostNote: n => `<b>On ${n} boards the trained baseline failed to beat constant prediction.</b> ` +
|
| 140 |
+
"A prediction that learned nothing was more accurate. Without baselines resident on the " +
|
| 141 |
+
"leaderboard (clause 3), this fact appears in no metric.",
|
| 142 |
+
|
| 143 |
+
openN: n => `${n} open`, prep: "in preparation",
|
| 144 |
+
cardBoards: (o, p) => `boards ${o}/${p}`,
|
| 145 |
+
cardCompounds: n => `${n} compounds`,
|
| 146 |
+
|
| 147 |
+
clauses: [
|
| 148 |
+
["The answer key is not distributed", "Each board declares its split grade and answer grade"],
|
| 149 |
+
["Every score carries two fingerprints", "SHA-256 of the data revision and of the scoring code"],
|
| 150 |
+
["Baselines never leave the table", "Constant, nearest-neighbour and Morgan+LightGBM stay resident"],
|
| 151 |
+
["Experimental error is published first", "Derived from cross-publication measurements of the same compound"],
|
| 152 |
+
["Leakage is screened on every submission", "We report the density of predictions matching labels to the decimal"],
|
| 153 |
+
["Result submission and model submission are distinct", "Only re-executed submissions receive [verified]"],
|
| 154 |
+
["An unmeasured axis is not a zero", "Failing to measure differs from measuring a poor result"],
|
| 155 |
+
["Repeated submission does not raise rank", "Ladder · Blum & Hardt, ICML 2015"]
|
| 156 |
+
],
|
| 157 |
+
|
| 158 |
+
all: "All",
|
| 159 |
+
tabRank: "Leaderboard", tabSubmit: "Submit", tabInfo: "Board detail",
|
| 160 |
+
thRank: "#", thMethod: "Method", thUser: "Entrant", thLeak: "Leakage", thGrade: "Grade",
|
| 161 |
+
baseline: "baseline", submitted: "submitted", verified: "verified",
|
| 162 |
+
leakHigh: "lookup trace", leakElev: "near-exact density",
|
| 163 |
+
noiseNote: v => `<b>Experimental error ${v}.</b> Differences within this band are not ` +
|
| 164 |
+
"claimed as rank differences (clause 4). The three baselines stay resident (clause 3).",
|
| 165 |
+
noEntries: "No entrant submissions yet. The rows shown are baselines.",
|
| 166 |
+
updated: t => `Leaderboard updated ${t}`,
|
| 167 |
+
|
| 168 |
+
gradeH: "Grades", holdoutH: "Holdout", noiseH: "Experimental error", baseH: "Baselines",
|
| 169 |
+
splitGrade: "Split grade", answerGrade: "Answer grade",
|
| 170 |
+
gradeNote: {
|
| 171 |
+
T: "Time split by year of first report. Only compounds first reported after the cut are scored",
|
| 172 |
+
S: "Scaffold split. Only compounds whose scaffold is absent from training are scored",
|
| 173 |
+
P1: "The answer exists in a public database. Clause 8 carries the defence against lookup",
|
| 174 |
+
P2: "The source is public, but selection and unit conversion define this revision. " +
|
| 175 |
+
"Querying the public database does not reproduce these values",
|
| 176 |
+
P3: "Unpublished measurements provided by a partner",
|
| 177 |
+
P4: "Measured after the board opened. It did not exist at opening"
|
| 178 |
+
},
|
| 179 |
+
kSource: "Source", kCut: "Split year", kTrainTest: "Train / test",
|
| 180 |
+
kOverlap: "Structure overlap", kThr: "Classification threshold", kSha: "Data fingerprint",
|
| 181 |
+
kLicense: "Licence",
|
| 182 |
+
cutNote: "by year of first report", overlapNote: "0 (by construction of the time split)",
|
| 183 |
+
posRate: p => `positives ${p}%`,
|
| 184 |
+
noisePairs: (p, c) => `${p} pairs · ${c} compounds`,
|
| 185 |
+
noiseDesc: "Difference between measurements of the same compound in <b>different publications</b>",
|
| 186 |
+
noiseSd: "Estimated single-measurement SD",
|
| 187 |
+
nearNote: (p, hi) => `<b>${p}%</b> of test compounds lie within one experimental SD of the ` +
|
| 188 |
+
"classification threshold. " + (hi ? "Labels there can invert on re-measurement, so " +
|
| 189 |
+
"<b>regression is the primary metric</b>." : "Classification metrics are reported alongside."),
|
| 190 |
+
beatsYes: "The trained baseline beat constant prediction.",
|
| 191 |
+
beatsNo: "<b>The trained baseline failed to beat constant prediction.</b> " +
|
| 192 |
+
"A prediction that learned nothing was more accurate.",
|
| 193 |
+
noiseFloorRow: "experimental error floor",
|
| 194 |
+
|
| 195 |
+
step1: "Obtain the test set", step2: "Submit predictions", step3: "Submit a container (optional)",
|
| 196 |
+
step1d: n => `Structures (SMILES) for <b>${n} compounds</b>. No labels — they do not exist on this server.`,
|
| 197 |
+
licLine: "Derived from ChEMBL · CC BY-SA 3.0 — retain attribution and the same licence on redistribution.",
|
| 198 |
+
dl: "Download test set",
|
| 199 |
+
fMethod: "Method name", fMethodH: "as it will appear on the leaderboard",
|
| 200 |
+
fTrain: "Training data used", fTrainH: "used for the clause 5 leakage screen",
|
| 201 |
+
fPred: "Predictions", fPredH: "JSON or CSV (compound_id, prediction)",
|
| 202 |
+
drop: "Drop a file here, or click to choose",
|
| 203 |
+
send: "Submit", needLogin: "Signing in with a Hugging Face account is required to submit.",
|
| 204 |
+
step3d: "Submit the model with its inference environment and we re-execute it, confirm the " +
|
| 205 |
+
"same numbers, and mark it [verified] (clause 6).",
|
| 206 |
+
ladderNote: (v, c) => `<b>Clause 8.</b> Repeated submission does not raise rank. A new score is ` +
|
| 207 |
+
`released only when it improves on your previous best by more than the experimental error ` +
|
| 208 |
+
`<b>${v}</b>. Daily limit ${c}.`,
|
| 209 |
+
accepted: id => `Accepted · submission ${id}`,
|
| 210 |
+
parseFail: m => `Could not parse the predictions: ${m}`,
|
| 211 |
+
sending: n => `sending ${n} entries`,
|
| 212 |
+
emptyCat: "No boards are open in this discipline yet.",
|
| 213 |
+
loadFail: "The page could not be loaded.",
|
| 214 |
+
dataNote: "Every board is built from ChEMBL_37 and split by year of first report. " +
|
| 215 |
+
"Distributed test sets contain structures only; labels are not included.",
|
| 216 |
+
srcLabel: "Data source"
|
| 217 |
+
}
|
| 218 |
+
};
|
index.html
CHANGED
|
@@ -4,27 +4,26 @@
|
|
| 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:#fbfcfd;
|
| 13 |
-
--muted:#6d7585;
|
| 14 |
-
--acc:#1f4fa3;
|
| 15 |
-
--warn:#8a5c00;
|
| 16 |
-
--good:#0f6b43;
|
| 17 |
--m:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
| 18 |
--sh:0 1px 2px rgba(13,18,30,.05),0 2px 6px rgba(13,18,30,.05);
|
| 19 |
--sh2:0 8px 16px -6px rgba(13,18,30,.14),0 20px 36px -12px rgba(13,18,30,.14);
|
| 20 |
}
|
| 21 |
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
|
| 22 |
-
--bg:#090b0f;
|
| 23 |
-
--muted:#8b94a5;
|
| 24 |
-
--
|
| 25 |
-
--
|
| 26 |
-
--
|
| 27 |
-
--sh:0 1px 2px rgba(0,0,0,.4); --sh2:0 12px 32px rgba(0,0,0,.55);}}
|
| 28 |
:root[data-theme="dark"]{
|
| 29 |
--bg:#090b0f;--bg2:#101319;--card:#141821;--ink:#f1f4f8;--ink2:#c8cfdb;
|
| 30 |
--muted:#8b94a5;--line:#212734;--line2:#2e3646;--acc:#8cb0f0;--acc2:#6a95e4;
|
|
@@ -33,9 +32,9 @@
|
|
| 33 |
--sh:0 1px 2px rgba(0,0,0,.4);--sh2:0 12px 32px rgba(0,0,0,.55);}
|
| 34 |
*{box-sizing:border-box}
|
| 35 |
/* 히어로 뒤 그라디언트가 좌우로 음수 inset 을 쓴다. z-index 를 내려도 스크롤 범위에는
|
| 36 |
-
남아
|
| 37 |
html{overflow-x:clip}
|
| 38 |
-
body{margin:0;background:var(--bg);color:var(--ink);line-height:1.62;font-size:15px;
|
| 39 |
font-family:-apple-system,BlinkMacSystemFont,"Pretendard","Apple SD Gothic Neo",
|
| 40 |
"Malgun Gothic",Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
| 41 |
.wrap{max-width:1060px;margin:0 auto;padding:0 24px}
|
|
@@ -45,7 +44,7 @@
|
|
| 45 |
|
| 46 |
nav{position:sticky;top:0;z-index:30;background:color-mix(in srgb,var(--bg) 78%,transparent);
|
| 47 |
backdrop-filter:saturate(180%) blur(16px);border-bottom:1px solid var(--line)}
|
| 48 |
-
nav .in{max-width:1060px;margin:0 auto;padding:12px 24px;display:flex;align-items:center;gap:
|
| 49 |
.lg{font-weight:850;letter-spacing:-.04em;font-size:18px;color:var(--ink)}
|
| 50 |
.lg:hover{text-decoration:none}.lg i{color:var(--acc);font-style:normal}
|
| 51 |
.sp{flex:1}
|
|
@@ -55,42 +54,40 @@
|
|
| 55 |
.chip.ok{background:var(--goodbg);color:var(--good)}
|
| 56 |
.chip.wr{background:var(--warnbg);color:var(--warn)}
|
| 57 |
.chip.bd{background:var(--badbg);color:var(--bad)}
|
|
|
|
|
|
|
|
|
|
| 58 |
.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 19px;border-radius:11px;
|
| 59 |
background:linear-gradient(180deg,var(--acc2),var(--acc));color:#fff;font-size:14px;
|
| 60 |
font-weight:650;border:0;cursor:pointer;box-shadow:var(--sh);transition:transform .1s,filter .15s}
|
| 61 |
.btn:hover{filter:brightness(1.08);text-decoration:none}
|
| 62 |
.btn:active{transform:translateY(1px)}.btn:disabled{opacity:.45;cursor:default}
|
| 63 |
|
| 64 |
-
/* hero */
|
| 65 |
.hero{padding:78px 0 46px;position:relative}
|
| 66 |
.hero::before{content:"";position:absolute;inset:-70% -30% auto -30%;height:520px;z-index:-1;
|
| 67 |
background:radial-gradient(ellipse 55% 50% at 32% 55%,color-mix(in srgb,var(--acc) 16%,transparent),transparent 70%)}
|
| 68 |
.brand{font-size:clamp(46px,9vw,80px);font-weight:900;letter-spacing:-.05em;margin:0;line-height:.96}
|
| 69 |
.brand i{color:var(--acc);font-style:normal}
|
| 70 |
-
.subttl{font-size:
|
| 71 |
-
.tag{font-size:clamp(23px,
|
| 72 |
-
letter-spacing:-.03em;line-height:1.
|
| 73 |
-
.lede{font-size:16.5px;color:var(--ink2);margin:0;max-width:
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
margin:42px 0 0;background:var(--line);border:1px solid var(--line);border-radius:14px;overflow:hidden}
|
| 77 |
.stat{background:var(--card);padding:17px 19px}
|
| 78 |
-
.stat b{display:block;font-size:30px;font-weight:850;font-family:var(--m);
|
| 79 |
-
letter-spacing:-.045em;line-height:1.1}
|
| 80 |
.stat span{font-size:11.5px;color:var(--muted)}
|
| 81 |
|
| 82 |
-
.sec{margin:
|
| 83 |
.sec h2{font-size:11.5px;font-weight:750;letter-spacing:.17em;text-transform:uppercase;
|
| 84 |
-
color:var(--acc);margin:0 0
|
| 85 |
-
.sec .lead2{font-size:15.5px;color:var(--ink2);margin:0 0 22px;max-width:
|
| 86 |
h3{font-size:18.5px;font-weight:720;margin:30px 0 9px;letter-spacing:-.018em}
|
| 87 |
p{margin:0 0 15px}
|
| 88 |
|
| 89 |
-
/* ── 서명 차트: 측정 한계 지형 ── */
|
| 90 |
.terr{background:var(--card);border:1px solid var(--line);border-radius:16px;
|
| 91 |
padding:22px 24px 18px;box-shadow:var(--sh)}
|
| 92 |
-
.trow{display:grid;grid-template-columns:
|
| 93 |
-
padding:4px 0;font-size:13px}
|
| 94 |
.trow .nm{font-weight:650;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 95 |
.trow .nm a{color:var(--ink)}
|
| 96 |
.trk{position:relative;height:19px;background:var(--bg2);border-radius:5px;overflow:hidden}
|
|
@@ -100,11 +97,10 @@
|
|
| 100 |
.trk .one{position:absolute;top:-3px;bottom:-3px;width:2px;background:var(--ink);opacity:.42}
|
| 101 |
.trow .v{font-family:var(--m);font-size:12px;color:var(--muted);text-align:right}
|
| 102 |
.trow .v.in{color:var(--warn);font-weight:700}
|
| 103 |
-
.taxis{display:grid;grid-template-columns:
|
| 104 |
font-size:11px;color:var(--muted);font-family:var(--m)}
|
| 105 |
-
.taxis .lbl{display:flex;justify-content:space-between}
|
| 106 |
|
| 107 |
-
/* cards */
|
| 108 |
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(252px,1fr));gap:15px}
|
| 109 |
.cc{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px;
|
| 110 |
display:block;box-shadow:var(--sh);transition:box-shadow .2s,transform .13s,border-color .2s}
|
|
@@ -131,7 +127,7 @@
|
|
| 131 |
box-shadow:var(--sh);margin:18px 0}
|
| 132 |
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
| 133 |
th{text-align:right;font-weight:650;color:var(--muted);font-size:10.5px;letter-spacing:.08em;
|
| 134 |
-
text-transform:uppercase;padding:13px
|
| 135 |
th:first-child,td:first-child{text-align:center;width:48px}
|
| 136 |
th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3){text-align:left}
|
| 137 |
td{padding:13px;border-bottom:1px solid var(--line);text-align:right;
|
|
@@ -153,7 +149,8 @@
|
|
| 153 |
.note{background:var(--warnbg);border:1px solid var(--warnln);border-radius:13px;
|
| 154 |
padding:15px 18px;font-size:14.5px;margin:18px 0;color:var(--ink2);line-height:1.65}
|
| 155 |
.note.ok{background:var(--goodbg);border-color:color-mix(in srgb,var(--good) 30%,transparent)}
|
| 156 |
-
.
|
|
|
|
| 157 |
.kv dt{color:var(--muted)}.kv dd{margin:0;font-family:var(--m);font-size:13px;word-break:break-word}
|
| 158 |
label{display:block;font-size:13.5px;font-weight:680;margin:17px 0 6px}
|
| 159 |
.hint{font-size:12px;color:var(--muted);font-weight:400}
|
|
@@ -172,7 +169,7 @@
|
|
| 172 |
display:flex;align-items:center;justify-content:center;font-family:var(--m);border:3px solid var(--bg)}
|
| 173 |
.steps b{display:block;font-size:16px;margin-bottom:4px}
|
| 174 |
ol.cl{list-style:none;counter-reset:c;padding:0;margin:18px 0;
|
| 175 |
-
display:grid;grid-template-columns:repeat(auto-fill,minmax(
|
| 176 |
ol.cl li{counter-increment:c;position:relative;padding:16px 16px 16px 52px;border-top:1px solid var(--line)}
|
| 177 |
ol.cl li::before{content:counter(c);position:absolute;left:8px;top:16px;width:30px;height:30px;
|
| 178 |
border-radius:9px;background:var(--soft);color:var(--acc);font-weight:750;font-size:13px;
|
|
@@ -186,7 +183,7 @@
|
|
| 186 |
font-size:13px;color:var(--muted);display:flex;gap:20px;flex-wrap:wrap;align-items:center}
|
| 187 |
@media(max-width:640px){
|
| 188 |
.kv{grid-template-columns:1fr}.kv dt{margin-top:8px}
|
| 189 |
-
.trow,.taxis{grid-template-columns:
|
| 190 |
</style>
|
| 191 |
</head>
|
| 192 |
<body>
|
|
@@ -195,65 +192,76 @@
|
|
| 195 |
<a class="lg" href="#/">LEAD<i>BOARD</i></a>
|
| 196 |
<span class="chip g" id="spec">v1.1</span>
|
| 197 |
<span class="sp"></span>
|
|
|
|
| 198 |
<span id="who" style="font-size:13px;color:var(--muted)"></span>
|
| 199 |
</div></nav>
|
| 200 |
|
| 201 |
<div id="app"><div class="wrap"><div class="skel" style="margin-top:70px"></div></div></div>
|
|
|
|
| 202 |
|
| 203 |
-
<
|
| 204 |
-
<span>LEADBOARD · FINAL-Bench / VIDRAFT</span>
|
| 205 |
-
<span class="dim">데이터 출처 <a href="https://www.ebi.ac.uk/chembl/">ChEMBL</a> ·
|
| 206 |
-
CC BY-SA 3.0</span>
|
| 207 |
-
<span class="sp"></span>
|
| 208 |
-
<a href="/api/health">API</a>
|
| 209 |
-
<a href="https://huggingface.co/spaces/FINAL-Bench/open-discovery-challenge">ODC</a>
|
| 210 |
-
<a href="https://huggingface.co/spaces/FINAL-Bench/open-materials-challenge">OMC</a>
|
| 211 |
-
</footer>
|
| 212 |
-
|
| 213 |
<script>
|
| 214 |
const $=s=>document.querySelector(s), app=$("#app");
|
| 215 |
const f=(x,d=3)=>(x==null||isNaN(x))?"—":Number(x).toFixed(d);
|
| 216 |
const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
| 217 |
const W=h=>`<div class="wrap">${h}</div>`;
|
| 218 |
-
let CATS=null, ME={user:null}, BOARDS={};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
async function j(u,o){const r=await fetch(u,o);if(!r.ok)throw new Error(await r.text());return r.json();}
|
| 220 |
|
| 221 |
-
/* ═══
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
<div class="trk">
|
| 235 |
-
<div class="fill ${inside?"in":""}" style="width:${pc(b.ratio)}%"></div>
|
| 236 |
-
<div class="one" style="left:${pc(1)}%"></div>
|
| 237 |
-
</div>
|
| 238 |
-
<div class="v ${inside?"in":""}">${f(b.ratio,2)}×</div></div>`;}).join("")}
|
| 239 |
-
<div class="taxis"><span></span>
|
| 240 |
-
<span class="lbl"><i>0</i><i>← 실험 오차 안쪽 · 1.0 · 모델이 더 나쁨 →</i><i>${MAX.toFixed(1)}</i></span>
|
| 241 |
-
<span></span></div></div>`;
|
| 242 |
}
|
| 243 |
-
const ratioOf=b=>
|
| 244 |
-
return Math.min(...Object.values(bl).map(v=>v.mae))/nf;};
|
| 245 |
|
| 246 |
async function boot(){
|
| 247 |
try{ME=await j("/api/me");}catch(e){}
|
| 248 |
-
$("#who").innerHTML=ME.user?`${esc(ME.user.user)} · <a href="/logout">로그아웃</a>`
|
| 249 |
-
:(ME.login_enabled?`<a href="/login?next=${encodeURIComponent(location.hash||"#/")}">HF 로그인</a>`:"");
|
| 250 |
CATS=await j("/api/categories");
|
| 251 |
-
$("#spec").textContent=CATS.spec;
|
| 252 |
CATS.categories.forEach(c=>(c.board_cards||[]).forEach(b=>BOARDS[b.board]=b));
|
| 253 |
-
|
|
|
|
|
|
|
| 254 |
}
|
| 255 |
addEventListener("hashchange",route);
|
| 256 |
function route(){
|
|
|
|
| 257 |
const p=(location.hash||"#/").slice(2).split("/").filter(Boolean);scrollTo(0,0);
|
| 258 |
if(!p.length)return hub();
|
| 259 |
if(p[0]==="c")return category(p[1]);
|
|
@@ -263,70 +271,51 @@ function route(){
|
|
| 263 |
|
| 264 |
/* ═══ 허브 ═══ */
|
| 265 |
function hub(){
|
| 266 |
-
const
|
| 267 |
const items=bs.map(b=>({board:b.board,ratio:ratioOf(b)})).sort((a,b)=>a.ratio-b.ratio);
|
| 268 |
const inside=items.filter(x=>x.ratio<1.3).length;
|
| 269 |
-
const cmp=bs.reduce((s,b)=>s+b.n_test,0);
|
| 270 |
const lost=bs.filter(b=>b.beats_constant===false).length;
|
| 271 |
app.innerHTML=W(`
|
| 272 |
<div class="hero">
|
| 273 |
<h1 class="brand">LEAD<i>BOARD</i></h1>
|
| 274 |
-
<p class="subttl">
|
| 275 |
-
<p class="tag">
|
| 276 |
-
<p class="lede">
|
| 277 |
-
<b>그 분야를 실험으로 다시 재도 남는 오차</b>를 먼저 공개합니다.
|
| 278 |
-
믿어달라고 하지 않고, 확인하게 합니다.</p>
|
| 279 |
<div class="stats">
|
| 280 |
-
<div class="stat"><b>${
|
| 281 |
-
<div class="stat"><b>${
|
| 282 |
-
<div class="stat"><b>${CATS.categories.filter(c=>c.open).length}</b><span>
|
| 283 |
-
<div class="stat"><b>8</b><span>
|
| 284 |
-
<div class="stat"><b>${
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
|
| 288 |
-
<div class="sec">
|
| 289 |
-
<
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
${terrain(items,true)}
|
| 294 |
-
${lost?`<div class="note">
|
| 295 |
-
아무것도 배우지 않은 쪽이 더 정확했다는 뜻입니다.
|
| 296 |
-
기준선을 표에 상주시키지 않으면(3조) 이 사실은 어디에도 나타나지 않습니다.</div>`:""}
|
| 297 |
-
</div>
|
| 298 |
|
| 299 |
-
<div class="sec">
|
| 300 |
-
<
|
| 301 |
-
<p class="lead2">분야를 고르면 그 안의 부문과 순위·제출로 들어갑니다.</p>
|
| 302 |
-
<div class="grid">${CATS.categories.map(cardHTML).join("")}</div>
|
| 303 |
-
</div>
|
| 304 |
|
| 305 |
-
<div class="sec">
|
| 306 |
-
<
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
${[["정답은 배포하지 않는다","부문마다 분할 등급과 정답 등급을 공시한다"],
|
| 310 |
-
["점수마다 두 개의 지문","데이터 판본과 채점 코드의 SHA-256"],
|
| 311 |
-
["기준선은 표에서 내려가지 않는다","상수 예측 · 최근접 이웃 · Morgan+LightGBM 영구 등재"],
|
| 312 |
-
["잡음 바닥을 먼저 공시한다","같은 화합물을 다른 논문에서 잰 차이로 산출한다"],
|
| 313 |
-
["누출은 매 제출마다 검사한다","정답과 소수점까지 같은 항목이 무더기면 표에 적는다"],
|
| 314 |
-
["결과 제출과 모델 제출을 구분한다","우리가 직접 돌려본 것만 [검증됨]"],
|
| 315 |
-
["미산출은 0점이 아니다","측정하지 못한 것과 측정해서 나쁜 것은 다르다"],
|
| 316 |
-
["되풀이 제출로는 순위가 오르지 않는다","Ladder · Blum & Hardt, ICML 2015"]]
|
| 317 |
-
.map(([a,b])=>`<li><b>${a}</b><span>${b}</span></li>`).join("")}
|
| 318 |
-
</ol>
|
| 319 |
-
</div>`);
|
| 320 |
}
|
| 321 |
|
| 322 |
function cardHTML(c){
|
| 323 |
const on=c.open>0, bs=c.board_cards||[];
|
| 324 |
const tot=bs.reduce((s,b)=>s+b.n_test,0);
|
| 325 |
const inner=`<div class="ico">${c.icon}</div>
|
| 326 |
-
<div class="nm">${esc(
|
| 327 |
-
|
| 328 |
-
<div class="
|
| 329 |
-
|
|
|
|
| 330 |
return on?`<a class="cc" href="#/c/${c.id}">${inner}</a>`:`<div class="cc off">${inner}</div>`;
|
| 331 |
}
|
| 332 |
|
|
@@ -335,28 +324,27 @@ function category(id){
|
|
| 335 |
const c=CATS.categories.find(x=>x.id===id); if(!c)return hub();
|
| 336 |
const bs=(c.board_cards||[]).slice().sort((a,b)=>b.n_test-a.n_test);
|
| 337 |
const items=bs.map(b=>({board:b.board,ratio:ratioOf(b)})).sort((a,b)=>a.ratio-b.ratio);
|
|
|
|
| 338 |
app.innerHTML=W(`
|
| 339 |
-
<div class="crumb"><a href="#/">
|
| 340 |
-
<h1 class="ttl">${c.icon} ${esc(
|
| 341 |
-
<p class="lede">${esc(c.desc)}</p>
|
| 342 |
<div class="stats" style="margin-top:26px">
|
| 343 |
-
<div class="stat"><b>${c.open}</b><span>
|
| 344 |
-
<div class="stat"><b>${bs.reduce((s,b)=>s+b.n_test,0).toLocaleString()}</b><span>
|
| 345 |
-
<div class="stat"><b>${c.planned}</b><span>
|
| 346 |
-
</div>
|
| 347 |
${bs.length?`
|
| 348 |
-
<div class="sec"><h2>
|
| 349 |
-
|
| 350 |
-
${terrain(items,true)}</div>
|
| 351 |
-
|
| 352 |
-
:`<div class="note">이 분야는 아직 개설된 부문이 없습니다.</div>`}`);
|
| 353 |
}
|
| 354 |
function boardMini(b){
|
| 355 |
const bl=b.baselines[b.best_baseline]||{};
|
| 356 |
return `<a class="cc" href="#/b/${encodeURIComponent(b.board)}">
|
| 357 |
<div class="nm">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></div>
|
| 358 |
-
<div class="ds">
|
| 359 |
-
|
| 360 |
<div class="mt"><span>${esc(b.source)}</span><span>${f(ratioOf(b),2)}×</span></div></a>`;
|
| 361 |
}
|
| 362 |
|
|
@@ -364,13 +352,14 @@ function boardMini(b){
|
|
| 364 |
async function board(name,tab){
|
| 365 |
const b=BOARDS[name]||await j("/api/board/"+encodeURIComponent(name));
|
| 366 |
const cat=CATS.categories.find(c=>(c.boards||[]).includes(name));
|
| 367 |
-
const
|
|
|
|
| 368 |
app.innerHTML=W(`
|
| 369 |
-
<div class="crumb"><a href="#/">
|
| 370 |
<h1 class="ttl">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></h1>
|
| 371 |
<p class="lede">${esc(b.source)} · ${esc(b.type)} ·
|
| 372 |
-
|
| 373 |
-
<div class="pills">${
|
| 374 |
`<a class="pilltab ${k===tab?"on":""}" href="#/b/${encodeURIComponent(name)}/${k}">${l}</a>`).join("")}</div>
|
| 375 |
<div id="pane"><div class="skel" style="margin-top:20px"></div></div>`);
|
| 376 |
if(tab==="rank")return paneRank(b);
|
|
@@ -382,8 +371,8 @@ async function paneRank(b){
|
|
| 382 |
const d=await j(`/api/board/${encodeURIComponent(b.board)}/leaderboard`);
|
| 383 |
const nf=d.noise_floor, ent=d.rows.filter(r=>r.kind!=="baseline");
|
| 384 |
const worst=Math.max(...d.rows.map(r=>r.mae||0))||1;
|
| 385 |
-
const leak=l=>
|
| 386 |
-
|
| 387 |
const rows=d.rows.map(r=>{
|
| 388 |
const g=r.rank&&r.rank<=3?` g${r.rank}`:"";
|
| 389 |
const w=Math.max(4,58*(r.mae||0)/worst);
|
|
@@ -392,102 +381,92 @@ async function paneRank(b){
|
|
| 392 |
<td>${esc(r.method)}</td><td>${esc(r.user)}</td>
|
| 393 |
<td><span class="mini" style="width:${w}px"></span>${f(r.mae)}</td>
|
| 394 |
<td>${f(r.auroc)}</td><td>${f(r.prauc)}</td>
|
| 395 |
-
<td>${r.kind==="baseline"?
|
| 396 |
-
<td>${r.kind==="baseline"?"—":(r.verified?
|
| 397 |
-
:
|
| 398 |
$("#pane").innerHTML=`
|
| 399 |
-
<div class="tw"><table>
|
| 400 |
-
<
|
| 401 |
-
<
|
| 402 |
-
<div class="note">
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
:
|
| 406 |
}
|
| 407 |
|
| 408 |
function paneInfo(b){
|
| 409 |
-
const nf=b.noise_floor,bl=b.baselines;
|
| 410 |
$("#pane").innerHTML=`
|
| 411 |
-
<h3>
|
| 412 |
<dl class="kv">
|
| 413 |
-
<dt>
|
| 414 |
-
<
|
| 415 |
-
|
|
|
|
|
|
|
| 416 |
<dl class="kv">
|
| 417 |
-
<dt>
|
| 418 |
-
<dt>
|
| 419 |
-
<dt>
|
| 420 |
-
<dt>
|
| 421 |
-
<dt>
|
| 422 |
-
<dt>
|
| 423 |
-
<dt>
|
| 424 |
-
<h3>
|
| 425 |
-
<div class="fig">
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
<b>
|
| 429 |
-
<div class="note">
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
<h3>기준선 <span class="dim" style="font-weight:400;font-size:14px">(3조)</span></h3>
|
| 433 |
-
<div class="tw"><table><thead><tr><th style="width:auto">·</th><th>기준선</th><th>MAE</th>
|
| 434 |
<th>AUROC</th><th>PR-AUC</th></tr></thead><tbody>
|
| 435 |
${Object.entries(bl).map(([k,v])=>`<tr><td>·</td><td>${esc(k)}</td><td>${f(v.mae)}</td>
|
| 436 |
<td>${f(v.auroc)}</td><td>${f(v.prauc)}</td></tr>`).join("")}
|
| 437 |
-
<tr class="base"><td>·</td><td>
|
| 438 |
</tbody></table></div>
|
| 439 |
-
<div class="note ${b.beats_constant?"ok":""}">${b.beats_constant
|
| 440 |
-
?"학습 기준선이 상수 예측을 넘었습니다."
|
| 441 |
-
:"<b>학습 기준선이 상수 예측을 넘지 못했습니다.</b> 아무것도 배우지 않은 쪽이 더 정확했습니다."}</div>`;
|
| 442 |
}
|
| 443 |
|
| 444 |
function paneSubmit(b){
|
| 445 |
const url=`/api/board/${encodeURIComponent(b.board)}/testset`;
|
| 446 |
$("#pane").innerHTML=`
|
| 447 |
<ol class="steps">
|
| 448 |
-
<li><b>
|
| 449 |
-
<p class="dim" style="font-size:13.5px">
|
| 450 |
-
|
| 451 |
-
ChEMBL 파생 · CC BY-SA 3.0 — 재배포 시 출처와 같은 라이선스를 유지해 주십시오.</p>
|
| 452 |
-
<a class="btn" href="${url}" download>테스트셋 내려받기</a>
|
| 453 |
<span class="hint"> ${esc(b.board.toLowerCase())}_test.json</span></li>
|
| 454 |
-
<li><b>
|
| 455 |
${ME.user?`
|
| 456 |
-
<label>
|
| 457 |
-
<input type="text" id="m"
|
| 458 |
-
<label>
|
| 459 |
-
<input type="text" id="td"
|
| 460 |
-
<label>
|
| 461 |
-
<div class="drop" id="drop">
|
| 462 |
-
|
| 463 |
-
<
|
| 464 |
-
placeholder='{"LB-A1B2C3D4E5F6": 6.42}'></textarea>
|
| 465 |
-
<p style="margin-top:15px"><button class="btn" id="go">제출</button>
|
| 466 |
<span class="hint" id="msg"></span></p>`
|
| 467 |
-
:`<div class="note">
|
| 468 |
-
<a href="/login?next=${encodeURIComponent(location.hash)}">
|
| 469 |
-
<li><b>
|
| 470 |
-
<p class="dim" style="font-size:13.5px">모델과 추론 환경을 함께 제출하면 <b>우리가 직접 돌려</b>
|
| 471 |
-
같은 숫자가 나오는지 확인하고 <span class="chip ok">검증됨</span> 표시를 붙입니다 (6조).</p></li>
|
| 472 |
</ol>
|
| 473 |
-
<div class="note">
|
| 474 |
-
직전 최고점을 실험 오차 <b>${f(b.noise_floor.sd_single)}</b> 이상으로 넘었을 때만
|
| 475 |
-
새 점수가 공개됩니다. 하루 제출 한도 ${ME.daily_cap||5}회.</div>`;
|
| 476 |
wire(b);
|
| 477 |
}
|
| 478 |
-
function parsePred(
|
| 479 |
-
|
| 480 |
const o={};
|
| 481 |
-
|
| 482 |
const k=p[0].trim();if(!k||(i===0&&isNaN(parseFloat(p[1]))))return;
|
| 483 |
const v=parseFloat(p[1]);if(!isNaN(v))o[k]=v;});
|
| 484 |
-
if(!Object.keys(o).length)throw new Error("
|
| 485 |
return o;
|
| 486 |
}
|
| 487 |
function wire(b){
|
| 488 |
const drop=$("#drop"),file=$("#file"),ta=$("#p"),go=$("#go");
|
| 489 |
const load=f0=>{const r=new FileReader();r.onload=()=>{ta.value=r.result;
|
| 490 |
-
drop.textContent=`${f0.name} — ${(f0.size/1024).toFixed(0)} KB
|
| 491 |
if(drop){
|
| 492 |
drop.onclick=()=>file.click();
|
| 493 |
file.onchange=e=>e.target.files[0]&&load(e.target.files[0]);
|
|
@@ -498,18 +477,18 @@ function wire(b){
|
|
| 498 |
if(!go)return;
|
| 499 |
go.onclick=async()=>{
|
| 500 |
const msg=$("#msg");let preds;
|
| 501 |
-
try{preds=parsePred(ta.value);}catch(e){msg.textContent=
|
| 502 |
-
msg.textContent=
|
| 503 |
try{const r=await j("/api/submit",{method:"POST",headers:{"Content-Type":"application/json"},
|
| 504 |
body:JSON.stringify({board:b.board,method:($("#m").value||"").trim(),
|
| 505 |
predictions:preds,training_data:($("#td").value||"").trim()})});
|
| 506 |
-
msg.
|
| 507 |
-
}catch(e){let
|
| 508 |
go.disabled=false;
|
| 509 |
};
|
| 510 |
}
|
| 511 |
boot().catch(e=>{app.innerHTML=W(`<div class="note" style="margin-top:50px">
|
| 512 |
-
|
| 513 |
</script>
|
| 514 |
</body>
|
| 515 |
</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="One yardstick, per discipline, for drug prediction tools. Baselines and experimental error published in advance.">
|
| 8 |
<!-- <head> 는 선택이 아니다: 플랫폼이 자기 태그를 문서에 주입하는데, 받을 head 가 없으면
|
| 9 |
그것들이 doctype 앞에 놓이고 페이지 전체가 쿼크 모드로 떨어진다. -->
|
| 10 |
<style>
|
| 11 |
:root{
|
| 12 |
+
--bg:#fbfcfd;--bg2:#f0f3f7;--card:#fff;--ink:#0b0d12;--ink2:#3a4150;
|
| 13 |
+
--muted:#6d7585;--line:#e6e9ef;--line2:#d0d6e0;
|
| 14 |
+
--acc:#1f4fa3;--acc2:#3d7ae0;--soft:#eaf0fb;
|
| 15 |
+
--warn:#8a5c00;--warnbg:#fff6e2;--warnln:#f0d9a0;
|
| 16 |
+
--good:#0f6b43;--goodbg:#e6f6ee;--bad:#9c2f24;--badbg:#fdeceb;
|
| 17 |
--m:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
| 18 |
--sh:0 1px 2px rgba(13,18,30,.05),0 2px 6px rgba(13,18,30,.05);
|
| 19 |
--sh2:0 8px 16px -6px rgba(13,18,30,.14),0 20px 36px -12px rgba(13,18,30,.14);
|
| 20 |
}
|
| 21 |
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
|
| 22 |
+
--bg:#090b0f;--bg2:#101319;--card:#141821;--ink:#f1f4f8;--ink2:#c8cfdb;
|
| 23 |
+
--muted:#8b94a5;--line:#212734;--line2:#2e3646;--acc:#8cb0f0;--acc2:#6a95e4;
|
| 24 |
+
--soft:#111a2b;--warn:#e8bd5e;--warnbg:#241d0e;--warnln:#4a3c1a;
|
| 25 |
+
--good:#4fd396;--goodbg:#0c2418;--bad:#f59285;--badbg:#2b1411;
|
| 26 |
+
--sh:0 1px 2px rgba(0,0,0,.4);--sh2:0 12px 32px rgba(0,0,0,.55);}}
|
|
|
|
| 27 |
:root[data-theme="dark"]{
|
| 28 |
--bg:#090b0f;--bg2:#101319;--card:#141821;--ink:#f1f4f8;--ink2:#c8cfdb;
|
| 29 |
--muted:#8b94a5;--line:#212734;--line2:#2e3646;--acc:#8cb0f0;--acc2:#6a95e4;
|
|
|
|
| 32 |
--sh:0 1px 2px rgba(0,0,0,.4);--sh2:0 12px 32px rgba(0,0,0,.55);}
|
| 33 |
*{box-sizing:border-box}
|
| 34 |
/* 히어로 뒤 그라디언트가 좌우로 음수 inset 을 쓴다. z-index 를 내려도 스크롤 범위에는
|
| 35 |
+
남아 가로 스크롤이 생긴다. clip 은 hidden 과 달리 sticky 헤더를 깨뜨리지 않는다. */
|
| 36 |
html{overflow-x:clip}
|
| 37 |
+
body{margin:0;overflow-x:clip;background:var(--bg);color:var(--ink);line-height:1.62;font-size:15px;
|
| 38 |
font-family:-apple-system,BlinkMacSystemFont,"Pretendard","Apple SD Gothic Neo",
|
| 39 |
"Malgun Gothic",Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
| 40 |
.wrap{max-width:1060px;margin:0 auto;padding:0 24px}
|
|
|
|
| 44 |
|
| 45 |
nav{position:sticky;top:0;z-index:30;background:color-mix(in srgb,var(--bg) 78%,transparent);
|
| 46 |
backdrop-filter:saturate(180%) blur(16px);border-bottom:1px solid var(--line)}
|
| 47 |
+
nav .in{max-width:1060px;margin:0 auto;padding:12px 24px;display:flex;align-items:center;gap:11px}
|
| 48 |
.lg{font-weight:850;letter-spacing:-.04em;font-size:18px;color:var(--ink)}
|
| 49 |
.lg:hover{text-decoration:none}.lg i{color:var(--acc);font-style:normal}
|
| 50 |
.sp{flex:1}
|
|
|
|
| 54 |
.chip.ok{background:var(--goodbg);color:var(--good)}
|
| 55 |
.chip.wr{background:var(--warnbg);color:var(--warn)}
|
| 56 |
.chip.bd{background:var(--badbg);color:var(--bad)}
|
| 57 |
+
.lang{padding:5px 11px;border-radius:8px;border:1px solid var(--line2);background:var(--card);
|
| 58 |
+
color:var(--ink2);font-size:12px;font-weight:650;cursor:pointer;font-family:inherit}
|
| 59 |
+
.lang:hover{border-color:var(--acc);color:var(--acc)}
|
| 60 |
.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 19px;border-radius:11px;
|
| 61 |
background:linear-gradient(180deg,var(--acc2),var(--acc));color:#fff;font-size:14px;
|
| 62 |
font-weight:650;border:0;cursor:pointer;box-shadow:var(--sh);transition:transform .1s,filter .15s}
|
| 63 |
.btn:hover{filter:brightness(1.08);text-decoration:none}
|
| 64 |
.btn:active{transform:translateY(1px)}.btn:disabled{opacity:.45;cursor:default}
|
| 65 |
|
|
|
|
| 66 |
.hero{padding:78px 0 46px;position:relative}
|
| 67 |
.hero::before{content:"";position:absolute;inset:-70% -30% auto -30%;height:520px;z-index:-1;
|
| 68 |
background:radial-gradient(ellipse 55% 50% at 32% 55%,color-mix(in srgb,var(--acc) 16%,transparent),transparent 70%)}
|
| 69 |
.brand{font-size:clamp(46px,9vw,80px);font-weight:900;letter-spacing:-.05em;margin:0;line-height:.96}
|
| 70 |
.brand i{color:var(--acc);font-style:normal}
|
| 71 |
+
.subttl{font-size:12.5px;color:var(--muted);margin:9px 0 0;letter-spacing:.11em;text-transform:uppercase}
|
| 72 |
+
.tag{font-size:clamp(23px,3.9vw,34px);font-weight:750;margin:30px 0 14px;
|
| 73 |
+
letter-spacing:-.03em;line-height:1.32;max-width:17em}
|
| 74 |
+
.lede{font-size:16.5px;color:var(--ink2);margin:0;max-width:38em;line-height:1.72}
|
| 75 |
+
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(128px,1fr));gap:1px;
|
| 76 |
+
margin:44px 0 0;background:var(--line);border:1px solid var(--line);border-radius:14px;overflow:hidden}
|
|
|
|
| 77 |
.stat{background:var(--card);padding:17px 19px}
|
| 78 |
+
.stat b{display:block;font-size:30px;font-weight:850;font-family:var(--m);letter-spacing:-.045em;line-height:1.1}
|
|
|
|
| 79 |
.stat span{font-size:11.5px;color:var(--muted)}
|
| 80 |
|
| 81 |
+
.sec{margin:76px 0 0}
|
| 82 |
.sec h2{font-size:11.5px;font-weight:750;letter-spacing:.17em;text-transform:uppercase;
|
| 83 |
+
color:var(--acc);margin:0 0 8px}
|
| 84 |
+
.sec .lead2{font-size:15.5px;color:var(--ink2);margin:0 0 22px;max-width:46em}
|
| 85 |
h3{font-size:18.5px;font-weight:720;margin:30px 0 9px;letter-spacing:-.018em}
|
| 86 |
p{margin:0 0 15px}
|
| 87 |
|
|
|
|
| 88 |
.terr{background:var(--card);border:1px solid var(--line);border-radius:16px;
|
| 89 |
padding:22px 24px 18px;box-shadow:var(--sh)}
|
| 90 |
+
.trow{display:grid;grid-template-columns:104px 1fr 60px;align-items:center;gap:12px;padding:4px 0;font-size:13px}
|
|
|
|
| 91 |
.trow .nm{font-weight:650;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 92 |
.trow .nm a{color:var(--ink)}
|
| 93 |
.trk{position:relative;height:19px;background:var(--bg2);border-radius:5px;overflow:hidden}
|
|
|
|
| 97 |
.trk .one{position:absolute;top:-3px;bottom:-3px;width:2px;background:var(--ink);opacity:.42}
|
| 98 |
.trow .v{font-family:var(--m);font-size:12px;color:var(--muted);text-align:right}
|
| 99 |
.trow .v.in{color:var(--warn);font-weight:700}
|
| 100 |
+
.taxis{display:grid;grid-template-columns:104px 1fr 60px;gap:12px;margin-top:11px;
|
| 101 |
font-size:11px;color:var(--muted);font-family:var(--m)}
|
| 102 |
+
.taxis .lbl{display:flex;justify-content:space-between;gap:8px}
|
| 103 |
|
|
|
|
| 104 |
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(252px,1fr));gap:15px}
|
| 105 |
.cc{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px;
|
| 106 |
display:block;box-shadow:var(--sh);transition:box-shadow .2s,transform .13s,border-color .2s}
|
|
|
|
| 127 |
box-shadow:var(--sh);margin:18px 0}
|
| 128 |
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
| 129 |
th{text-align:right;font-weight:650;color:var(--muted);font-size:10.5px;letter-spacing:.08em;
|
| 130 |
+
text-transform:uppercase;padding:13px;border-bottom:1px solid var(--line);white-space:nowrap}
|
| 131 |
th:first-child,td:first-child{text-align:center;width:48px}
|
| 132 |
th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3){text-align:left}
|
| 133 |
td{padding:13px;border-bottom:1px solid var(--line);text-align:right;
|
|
|
|
| 149 |
.note{background:var(--warnbg);border:1px solid var(--warnln);border-radius:13px;
|
| 150 |
padding:15px 18px;font-size:14.5px;margin:18px 0;color:var(--ink2);line-height:1.65}
|
| 151 |
.note.ok{background:var(--goodbg);border-color:color-mix(in srgb,var(--good) 30%,transparent)}
|
| 152 |
+
.note.n{background:var(--card);border-color:var(--line)}
|
| 153 |
+
.kv{display:grid;grid-template-columns:190px 1fr;gap:10px 18px;font-size:14px;margin:15px 0}
|
| 154 |
.kv dt{color:var(--muted)}.kv dd{margin:0;font-family:var(--m);font-size:13px;word-break:break-word}
|
| 155 |
label{display:block;font-size:13.5px;font-weight:680;margin:17px 0 6px}
|
| 156 |
.hint{font-size:12px;color:var(--muted);font-weight:400}
|
|
|
|
| 169 |
display:flex;align-items:center;justify-content:center;font-family:var(--m);border:3px solid var(--bg)}
|
| 170 |
.steps b{display:block;font-size:16px;margin-bottom:4px}
|
| 171 |
ol.cl{list-style:none;counter-reset:c;padding:0;margin:18px 0;
|
| 172 |
+
display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:0}
|
| 173 |
ol.cl li{counter-increment:c;position:relative;padding:16px 16px 16px 52px;border-top:1px solid var(--line)}
|
| 174 |
ol.cl li::before{content:counter(c);position:absolute;left:8px;top:16px;width:30px;height:30px;
|
| 175 |
border-radius:9px;background:var(--soft);color:var(--acc);font-weight:750;font-size:13px;
|
|
|
|
| 183 |
font-size:13px;color:var(--muted);display:flex;gap:20px;flex-wrap:wrap;align-items:center}
|
| 184 |
@media(max-width:640px){
|
| 185 |
.kv{grid-template-columns:1fr}.kv dt{margin-top:8px}
|
| 186 |
+
.trow,.taxis{grid-template-columns:78px 1fr 48px;gap:8px;font-size:12px}}
|
| 187 |
</style>
|
| 188 |
</head>
|
| 189 |
<body>
|
|
|
|
| 192 |
<a class="lg" href="#/">LEAD<i>BOARD</i></a>
|
| 193 |
<span class="chip g" id="spec">v1.1</span>
|
| 194 |
<span class="sp"></span>
|
| 195 |
+
<button class="lang" id="lang"></button>
|
| 196 |
<span id="who" style="font-size:13px;color:var(--muted)"></span>
|
| 197 |
</div></nav>
|
| 198 |
|
| 199 |
<div id="app"><div class="wrap"><div class="skel" style="margin-top:70px"></div></div></div>
|
| 200 |
+
<footer class="wrap" id="foot"></footer>
|
| 201 |
|
| 202 |
+
<script src="i18n.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
<script>
|
| 204 |
const $=s=>document.querySelector(s), app=$("#app");
|
| 205 |
const f=(x,d=3)=>(x==null||isNaN(x))?"—":Number(x).toFixed(d);
|
| 206 |
const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
| 207 |
const W=h=>`<div class="wrap">${h}</div>`;
|
| 208 |
+
let CATS=null, ME={user:null}, BOARDS={}, L=null, t=null;
|
| 209 |
+
|
| 210 |
+
/* 언어 결정 순서: 저장된 선택 → 브라우저 설정 → 영어.
|
| 211 |
+
브라우저를 먼저 보면 한 번 고른 선택이 새로고침마다 뒤집힌다. */
|
| 212 |
+
function pickLang(){
|
| 213 |
+
const saved=localStorage.getItem("lb-lang");
|
| 214 |
+
if(saved && window.T[saved]) return saved;
|
| 215 |
+
return (navigator.language||"").toLowerCase().startsWith("ko") ? "ko" : "en";
|
| 216 |
+
}
|
| 217 |
+
function setLang(l){
|
| 218 |
+
L=l; t=window.T[l];
|
| 219 |
+
localStorage.setItem("lb-lang", l);
|
| 220 |
+
document.documentElement.lang=t.htmlLang;
|
| 221 |
+
document.title=t.title;
|
| 222 |
+
$("#lang").textContent=t.langBtn;
|
| 223 |
+
paintChrome();
|
| 224 |
+
route();
|
| 225 |
+
}
|
| 226 |
+
function paintChrome(){
|
| 227 |
+
$("#who").innerHTML=ME.user?`${esc(ME.user.user)} · <a href="/logout">${t.logout}</a>`
|
| 228 |
+
:(ME.login_enabled?`<a href="/login?next=${encodeURIComponent(location.hash||"#/")}">${t.login}</a>`:"");
|
| 229 |
+
$("#foot").innerHTML=`<span>LEADBOARD · FINAL-Bench / VIDRAFT</span>
|
| 230 |
+
<span class="dim">${t.srcLabel} <a href="https://www.ebi.ac.uk/chembl/">ChEMBL</a> ·
|
| 231 |
+
<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC BY-SA 3.0</a></span>
|
| 232 |
+
<span class="sp"></span><a href="/api/health">API</a>
|
| 233 |
+
<a href="https://huggingface.co/spaces/FINAL-Bench/open-discovery-challenge">ODC</a>
|
| 234 |
+
<a href="https://huggingface.co/spaces/FINAL-Bench/open-materials-challenge">OMC</a>`;
|
| 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 |
|
| 238 |
+
/* ═══ 측정 한계 지형 ═══ */
|
| 239 |
+
function terrain(items,link){
|
| 240 |
+
const MAX=3.0, pc=v=>Math.min(100,100*v/MAX);
|
| 241 |
+
return `<div class="terr">${items.map(b=>{
|
| 242 |
+
const inside=b.ratio<1.0;
|
| 243 |
+
const nm=link?`<a href="#/b/${encodeURIComponent(b.board)}">${esc(b.board)}</a>`:esc(b.board);
|
| 244 |
+
return `<div class="trow"><div class="nm">${nm}</div>
|
| 245 |
+
<div class="trk"><div class="fill ${inside?"in":""}" style="width:${pc(b.ratio)}%"></div>
|
| 246 |
+
<div class="one" style="left:${pc(1)}%"></div></div>
|
| 247 |
+
<div class="v ${inside?"in":""}">${f(b.ratio,2)}×</div></div>`;}).join("")}
|
| 248 |
+
<div class="taxis"><span></span><span class="lbl">
|
| 249 |
+
<i>0</i><i>← ${t.terrLeft} · 1.0 · ${t.terrRight} →</i><i>${MAX.toFixed(1)}</i>
|
| 250 |
+
</span><span></span></div></div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
}
|
| 252 |
+
const ratioOf=b=>Math.min(...Object.values(b.baselines).map(v=>v.mae))/b.noise_floor.sd_single;
|
|
|
|
| 253 |
|
| 254 |
async function boot(){
|
| 255 |
try{ME=await j("/api/me");}catch(e){}
|
|
|
|
|
|
|
| 256 |
CATS=await j("/api/categories");
|
|
|
|
| 257 |
CATS.categories.forEach(c=>(c.board_cards||[]).forEach(b=>BOARDS[b.board]=b));
|
| 258 |
+
$("#spec").textContent=CATS.spec;
|
| 259 |
+
$("#lang").onclick=()=>setLang(L==="ko"?"en":"ko");
|
| 260 |
+
setLang(pickLang());
|
| 261 |
}
|
| 262 |
addEventListener("hashchange",route);
|
| 263 |
function route(){
|
| 264 |
+
if(!t)return;
|
| 265 |
const p=(location.hash||"#/").slice(2).split("/").filter(Boolean);scrollTo(0,0);
|
| 266 |
if(!p.length)return hub();
|
| 267 |
if(p[0]==="c")return category(p[1]);
|
|
|
|
| 271 |
|
| 272 |
/* ═══ 허브 ═══ */
|
| 273 |
function hub(){
|
| 274 |
+
const T0=CATS.totals, bs=Object.values(BOARDS);
|
| 275 |
const items=bs.map(b=>({board:b.board,ratio:ratioOf(b)})).sort((a,b)=>a.ratio-b.ratio);
|
| 276 |
const inside=items.filter(x=>x.ratio<1.3).length;
|
|
|
|
| 277 |
const lost=bs.filter(b=>b.beats_constant===false).length;
|
| 278 |
app.innerHTML=W(`
|
| 279 |
<div class="hero">
|
| 280 |
<h1 class="brand">LEAD<i>BOARD</i></h1>
|
| 281 |
+
<p class="subttl">${t.heroKicker}</p>
|
| 282 |
+
<p class="tag">${t.heroTag}</p>
|
| 283 |
+
<p class="lede">${t.heroLede}</p>
|
|
|
|
|
|
|
| 284 |
<div class="stats">
|
| 285 |
+
<div class="stat"><b>${T0.open}</b><span>${t.statBoards}</span></div>
|
| 286 |
+
<div class="stat"><b>${bs.reduce((s,b)=>s+b.n_test,0).toLocaleString()}</b><span>${t.statCompounds}</span></div>
|
| 287 |
+
<div class="stat"><b>${CATS.categories.filter(c=>c.open).length}</b><span>${t.statFields}</span></div>
|
| 288 |
+
<div class="stat"><b>8</b><span>${t.statClauses}</span></div>
|
| 289 |
+
<div class="stat"><b>${T0.planned}</b><span>${t.statPlanned}</span></div>
|
| 290 |
</div>
|
| 291 |
</div>
|
| 292 |
|
| 293 |
+
<div class="sec"><h2>${t.secBrowse}</h2><p class="lead2">${t.secBrowseLead}</p>
|
| 294 |
+
<div class="grid">${CATS.categories.map(cardHTML).join("")}</div></div>
|
| 295 |
+
|
| 296 |
+
<div class="sec"><h2>${t.secEvidence}</h2>
|
| 297 |
+
<p class="lead2">${t.secEvidenceLead} ${t.insideCount(inside)}</p>
|
| 298 |
${terrain(items,true)}
|
| 299 |
+
${lost?`<div class="note">${t.lostNote(lost)}</div>`:""}</div>
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
+
<div class="sec"><h2>${t.secMethod}</h2><p class="lead2">${t.secMethodLead}</p>
|
| 302 |
+
<ol class="cl">${t.clauses.map(([a,b])=>`<li><b>${a}</b><span>${b}</span></li>`).join("")}</ol></div>
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
+
<div class="sec"><h2>${t.secData}</h2>
|
| 305 |
+
<div class="note n">${t.dataNote}<br><br>
|
| 306 |
+
${t.srcLabel} · <a href="https://www.ebi.ac.uk/chembl/">ChEMBL_37 (EMBL-EBI)</a> ·
|
| 307 |
+
<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC BY-SA 3.0 Unported</a></div></div>`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
}
|
| 309 |
|
| 310 |
function cardHTML(c){
|
| 311 |
const on=c.open>0, bs=c.board_cards||[];
|
| 312 |
const tot=bs.reduce((s,b)=>s+b.n_test,0);
|
| 313 |
const inner=`<div class="ico">${c.icon}</div>
|
| 314 |
+
<div class="nm">${esc(L==="ko"?c.name:c.en)}
|
| 315 |
+
<span class="chip ${on?"ok":"g"}">${on?t.openN(c.open):t.prep}</span></div>
|
| 316 |
+
<div class="ds">${esc(L==="ko"?c.desc:(c.desc_en||c.desc))}</div>
|
| 317 |
+
<div class="mt"><span>${t.cardBoards(c.open,c.planned)}</span>
|
| 318 |
+
<span>${tot?t.cardCompounds(tot.toLocaleString()):"—"}</span></div>`;
|
| 319 |
return on?`<a class="cc" href="#/c/${c.id}">${inner}</a>`:`<div class="cc off">${inner}</div>`;
|
| 320 |
}
|
| 321 |
|
|
|
|
| 324 |
const c=CATS.categories.find(x=>x.id===id); if(!c)return hub();
|
| 325 |
const bs=(c.board_cards||[]).slice().sort((a,b)=>b.n_test-a.n_test);
|
| 326 |
const items=bs.map(b=>({board:b.board,ratio:ratioOf(b)})).sort((a,b)=>a.ratio-b.ratio);
|
| 327 |
+
const nm=L==="ko"?c.name:c.en;
|
| 328 |
app.innerHTML=W(`
|
| 329 |
+
<div class="crumb"><a href="#/">${t.all}</a> › ${esc(nm)}</div>
|
| 330 |
+
<h1 class="ttl">${c.icon} ${esc(nm)}</h1>
|
| 331 |
+
<p class="lede">${esc(L==="ko"?c.desc:(c.desc_en||c.desc))}</p>
|
| 332 |
<div class="stats" style="margin-top:26px">
|
| 333 |
+
<div class="stat"><b>${c.open}</b><span>${t.statBoards}</span></div>
|
| 334 |
+
<div class="stat"><b>${bs.reduce((s,b)=>s+b.n_test,0).toLocaleString()}</b><span>${t.statCompounds}</span></div>
|
| 335 |
+
<div class="stat"><b>${c.planned}</b><span>${t.statPlanned}</span></div></div>
|
|
|
|
| 336 |
${bs.length?`
|
| 337 |
+
<div class="sec"><h2>${t.secBrowse}</h2><div class="grid">${bs.map(boardMini).join("")}</div></div>
|
| 338 |
+
<div class="sec"><h2>${t.secEvidence}</h2><p class="lead2">${t.secEvidenceLead}</p>
|
| 339 |
+
${terrain(items,true)}</div>`
|
| 340 |
+
:`<div class="note">${t.emptyCat}</div>`}`);
|
|
|
|
| 341 |
}
|
| 342 |
function boardMini(b){
|
| 343 |
const bl=b.baselines[b.best_baseline]||{};
|
| 344 |
return `<a class="cc" href="#/b/${encodeURIComponent(b.board)}">
|
| 345 |
<div class="nm">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></div>
|
| 346 |
+
<div class="ds">${t.cardCompounds(b.n_test.toLocaleString())} · ${t.noiseH} ${f(b.noise_floor.sd_single)}<br>
|
| 347 |
+
${esc(b.best_baseline)} ${f(bl.mae)}</div>
|
| 348 |
<div class="mt"><span>${esc(b.source)}</span><span>${f(ratioOf(b),2)}×</span></div></a>`;
|
| 349 |
}
|
| 350 |
|
|
|
|
| 352 |
async function board(name,tab){
|
| 353 |
const b=BOARDS[name]||await j("/api/board/"+encodeURIComponent(name));
|
| 354 |
const cat=CATS.categories.find(c=>(c.boards||[]).includes(name));
|
| 355 |
+
const cn=cat?(L==="ko"?cat.name:cat.en):"";
|
| 356 |
+
const TT=[["rank",t.tabRank],["submit",t.tabSubmit],["info",t.tabInfo]];
|
| 357 |
app.innerHTML=W(`
|
| 358 |
+
<div class="crumb"><a href="#/">${t.all}</a> › <a href="#/c/${cat?cat.id:""}">${esc(cn)}</a> › ${esc(b.board)}</div>
|
| 359 |
<h1 class="ttl">${esc(b.board)}<span class="chip">${b.split_grade}/${b.answer_grade}</span></h1>
|
| 360 |
<p class="lede">${esc(b.source)} · ${esc(b.type)} ·
|
| 361 |
+
${t.cardCompounds(b.n_test.toLocaleString())} · ${t.noiseH} <b>${f(b.noise_floor.sd_single)}</b></p>
|
| 362 |
+
<div class="pills">${TT.map(([k,l])=>
|
| 363 |
`<a class="pilltab ${k===tab?"on":""}" href="#/b/${encodeURIComponent(name)}/${k}">${l}</a>`).join("")}</div>
|
| 364 |
<div id="pane"><div class="skel" style="margin-top:20px"></div></div>`);
|
| 365 |
if(tab==="rank")return paneRank(b);
|
|
|
|
| 371 |
const d=await j(`/api/board/${encodeURIComponent(b.board)}/leaderboard`);
|
| 372 |
const nf=d.noise_floor, ent=d.rows.filter(r=>r.kind!=="baseline");
|
| 373 |
const worst=Math.max(...d.rows.map(r=>r.mae||0))||1;
|
| 374 |
+
const leak=l=>l==="high"?`<span class="chip bd">${t.leakHigh}</span>`
|
| 375 |
+
:l==="elevated"?`<span class="chip wr">${t.leakElev}</span>`:`<span class="chip g">—</span>`;
|
| 376 |
const rows=d.rows.map(r=>{
|
| 377 |
const g=r.rank&&r.rank<=3?` g${r.rank}`:"";
|
| 378 |
const w=Math.max(4,58*(r.mae||0)/worst);
|
|
|
|
| 381 |
<td>${esc(r.method)}</td><td>${esc(r.user)}</td>
|
| 382 |
<td><span class="mini" style="width:${w}px"></span>${f(r.mae)}</td>
|
| 383 |
<td>${f(r.auroc)}</td><td>${f(r.prauc)}</td>
|
| 384 |
+
<td>${r.kind==="baseline"?`<span class="chip g">${t.baseline}</span>`:leak(r.leak)}</td>
|
| 385 |
+
<td>${r.kind==="baseline"?"—":(r.verified?`<span class="chip ok">${t.verified}</span>`
|
| 386 |
+
:`<span class="chip g">${t.submitted}</span>`)}</td></tr>`;}).join("");
|
| 387 |
$("#pane").innerHTML=`
|
| 388 |
+
<div class="tw"><table><thead><tr><th>${t.thRank}</th><th>${t.thMethod}</th><th>${t.thUser}</th>
|
| 389 |
+
<th>MAE</th><th>AUROC</th><th>PR-AUC</th><th>${t.thLeak}</th><th>${t.thGrade}</th></tr></thead>
|
| 390 |
+
<tbody>${rows}</tbody></table></div>
|
| 391 |
+
<div class="note">${t.noiseNote(f(nf))}</div>
|
| 392 |
+
<p class="dim" style="font-size:13px">${ent.length
|
| 393 |
+
? t.updated(new Date(d.updated*1000).toLocaleString(L==="ko"?"ko-KR":"en-GB"))
|
| 394 |
+
: t.noEntries}</p>`;
|
| 395 |
}
|
| 396 |
|
| 397 |
function paneInfo(b){
|
| 398 |
+
const nf=b.noise_floor, bl=b.baselines;
|
| 399 |
$("#pane").innerHTML=`
|
| 400 |
+
<h3>${t.gradeH}</h3>
|
| 401 |
<dl class="kv">
|
| 402 |
+
<dt>${t.splitGrade} <b>${b.split_grade}</b></dt>
|
| 403 |
+
<dd>${esc(t.gradeNote[b.split_grade]||b.split_grade_note||"")}</dd>
|
| 404 |
+
<dt>${t.answerGrade} <b>${b.answer_grade}</b></dt>
|
| 405 |
+
<dd>${esc(t.gradeNote[b.answer_grade]||b.answer_grade_note||"")}</dd></dl>
|
| 406 |
+
<h3>${t.holdoutH}</h3>
|
| 407 |
<dl class="kv">
|
| 408 |
+
<dt>${t.kSource}</dt><dd>${esc(b.source)} · ${esc(b.target)} · ${esc(b.type)}</dd>
|
| 409 |
+
<dt>${t.kCut}</dt><dd>${b.cut} — ${t.cutNote}</dd>
|
| 410 |
+
<dt>${t.kTrainTest}</dt><dd>${b.n_train.toLocaleString()} / ${b.n_test.toLocaleString()}</dd>
|
| 411 |
+
<dt>${t.kOverlap}</dt><dd>${t.overlapNote}</dd>
|
| 412 |
+
<dt>${t.kThr}</dt><dd>${b.threshold} · ${t.posRate((b.positive_rate*100).toFixed(1))}</dd>
|
| 413 |
+
<dt>${t.kSha}</dt><dd>${esc(b.dataset_sha)}</dd>
|
| 414 |
+
<dt>${t.kLicense}</dt><dd>ChEMBL · CC BY-SA 3.0</dd></dl>
|
| 415 |
+
<h3>${t.noiseH} <span class="dim" style="font-weight:400;font-size:14px">(4)</span></h3>
|
| 416 |
+
<div class="fig">${t.noiseDesc}<br>
|
| 417 |
+
${t.noisePairs(nf.pairs.toLocaleString(),nf.compounds.toLocaleString())}<br>
|
| 418 |
+
median ${f(nf.median)} · mean ${f(nf.mean)} · p90 ${f(nf.p90)}<br>
|
| 419 |
+
<b>${t.noiseSd} <span class="n">${f(nf.sd_single)}</span></b></div>
|
| 420 |
+
<div class="note">${t.nearNote(b.near_pct,b.near_pct>45)}</div>
|
| 421 |
+
<h3>${t.baseH} <span class="dim" style="font-weight:400;font-size:14px">(3)</span></h3>
|
| 422 |
+
<div class="tw"><table><thead><tr><th style="width:auto">·</th><th>${t.baseH}</th><th>MAE</th>
|
|
|
|
|
|
|
| 423 |
<th>AUROC</th><th>PR-AUC</th></tr></thead><tbody>
|
| 424 |
${Object.entries(bl).map(([k,v])=>`<tr><td>·</td><td>${esc(k)}</td><td>${f(v.mae)}</td>
|
| 425 |
<td>${f(v.auroc)}</td><td>${f(v.prauc)}</td></tr>`).join("")}
|
| 426 |
+
<tr class="base"><td>·</td><td>${t.noiseFloorRow}</td><td>${f(nf.sd_single)}</td><td>—</td><td>—</td></tr>
|
| 427 |
</tbody></table></div>
|
| 428 |
+
<div class="note ${b.beats_constant?"ok":""}">${b.beats_constant?t.beatsYes:t.beatsNo}</div>`;
|
|
|
|
|
|
|
| 429 |
}
|
| 430 |
|
| 431 |
function paneSubmit(b){
|
| 432 |
const url=`/api/board/${encodeURIComponent(b.board)}/testset`;
|
| 433 |
$("#pane").innerHTML=`
|
| 434 |
<ol class="steps">
|
| 435 |
+
<li><b>${t.step1}</b>
|
| 436 |
+
<p class="dim" style="font-size:13.5px">${t.step1d(b.n_test.toLocaleString())}<br>${t.licLine}</p>
|
| 437 |
+
<a class="btn" href="${url}" download>${t.dl}</a>
|
|
|
|
|
|
|
| 438 |
<span class="hint"> ${esc(b.board.toLowerCase())}_test.json</span></li>
|
| 439 |
+
<li><b>${t.step2}</b>
|
| 440 |
${ME.user?`
|
| 441 |
+
<label>${t.fMethod} <span class="hint">${t.fMethodH}</span></label>
|
| 442 |
+
<input type="text" id="m" maxlength="60">
|
| 443 |
+
<label>${t.fTrain} <span class="hint">${t.fTrainH}</span></label>
|
| 444 |
+
<input type="text" id="td">
|
| 445 |
+
<label>${t.fPred} <span class="hint">${t.fPredH}</span></label>
|
| 446 |
+
<div class="drop" id="drop">${t.drop}<input type="file" id="file" accept=".json,.csv,.txt" hidden></div>
|
| 447 |
+
<textarea id="p" rows="6" style="margin-top:11px"></textarea>
|
| 448 |
+
<p style="margin-top:15px"><button class="btn" id="go">${t.send}</button>
|
|
|
|
|
|
|
| 449 |
<span class="hint" id="msg"></span></p>`
|
| 450 |
+
:`<div class="note">${t.needLogin}
|
| 451 |
+
<a href="/login?next=${encodeURIComponent(location.hash)}">${t.login}</a></div>`}</li>
|
| 452 |
+
<li><b>${t.step3}</b><p class="dim" style="font-size:13.5px">${t.step3d}</p></li>
|
|
|
|
|
|
|
| 453 |
</ol>
|
| 454 |
+
<div class="note">${t.ladderNote(f(b.noise_floor.sd_single),ME.daily_cap||5)}</div>`;
|
|
|
|
|
|
|
| 455 |
wire(b);
|
| 456 |
}
|
| 457 |
+
function parsePred(x){
|
| 458 |
+
x=x.trim(); if(x.startsWith("{"))return JSON.parse(x);
|
| 459 |
const o={};
|
| 460 |
+
x.split(/\r?\n/).forEach((ln,i)=>{const p=ln.split(/[,\t]/);if(p.length<2)return;
|
| 461 |
const k=p[0].trim();if(!k||(i===0&&isNaN(parseFloat(p[1]))))return;
|
| 462 |
const v=parseFloat(p[1]);if(!isNaN(v))o[k]=v;});
|
| 463 |
+
if(!Object.keys(o).length)throw new Error("no readable entries");
|
| 464 |
return o;
|
| 465 |
}
|
| 466 |
function wire(b){
|
| 467 |
const drop=$("#drop"),file=$("#file"),ta=$("#p"),go=$("#go");
|
| 468 |
const load=f0=>{const r=new FileReader();r.onload=()=>{ta.value=r.result;
|
| 469 |
+
drop.textContent=`${f0.name} — ${(f0.size/1024).toFixed(0)} KB`;};r.readAsText(f0);};
|
| 470 |
if(drop){
|
| 471 |
drop.onclick=()=>file.click();
|
| 472 |
file.onchange=e=>e.target.files[0]&&load(e.target.files[0]);
|
|
|
|
| 477 |
if(!go)return;
|
| 478 |
go.onclick=async()=>{
|
| 479 |
const msg=$("#msg");let preds;
|
| 480 |
+
try{preds=parsePred(ta.value);}catch(e){msg.textContent=t.parseFail(e.message);return;}
|
| 481 |
+
msg.textContent=t.sending(Object.keys(preds).length.toLocaleString());go.disabled=true;
|
| 482 |
try{const r=await j("/api/submit",{method:"POST",headers:{"Content-Type":"application/json"},
|
| 483 |
body:JSON.stringify({board:b.board,method:($("#m").value||"").trim(),
|
| 484 |
predictions:preds,training_data:($("#td").value||"").trim()})});
|
| 485 |
+
msg.textContent=t.accepted(r.submission_id);
|
| 486 |
+
}catch(e){let x=String(e.message||e);try{x=JSON.parse(x).detail||x;}catch(_){}msg.textContent=x;}
|
| 487 |
go.disabled=false;
|
| 488 |
};
|
| 489 |
}
|
| 490 |
boot().catch(e=>{app.innerHTML=W(`<div class="note" style="margin-top:50px">
|
| 491 |
+
${(window.T&&window.T.en.loadFail)||"Load failed."} ${esc(e.message||e)}</div>`);});
|
| 492 |
</script>
|
| 493 |
</body>
|
| 494 |
</html>
|