Skip to content

Commit f33d33f

Browse files
committed
Tighten the daily questions, stamps, and lab after review
Daily question - Reorder the pool so answers no longer alternate day by day, and add three trap items that use design words but are correlational (attendance after a lottery, diverging pre-trends, manipulable cutoff). 17 items, 7 causal. - Fix the training, premium and loan explanations; state precision and units in the causal items. - The answered card keeps the claim and shows the visitor's own pick; the streak chip says how many days remain for the stamp. Passport - 첫 걸음 is not granted by a #station= deep link. - Builder 탐방 counts distinct Builders, not page reloads. - Shorter toast, wider on phones and shown at the top there, paused on hover or focus. Passport button on article and 404 pages too. - passport_open carries its source. Lab - Confounding slope 0.4 -> 0.12 so the slider stays meaningful across its range; seed 9 gives representative defaults. - Only randomizing completes the challenge; a near miss with randomization on points to resampling, and a hint appears after repeated slider moves. - While randomized the confounding slider is dimmed and the result line calls the gap sampling error. - Bar colours no longer reuse the group colours; the chart draws the two fitted lines and labels the adjusted gap. Chart first on phones. - read_chapter {source, chapter} on daily, game and lab chapter links; lab_randomize fires before lab_challenge_complete.
1 parent 544531f commit f33d33f

9 files changed

Lines changed: 193 additions & 111 deletions

File tree

‎404.html‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<header class="site-header">
1515
<div class="wrap header-row">
1616
<a class="brand" href="/"><img class="brand-logo" src="/assets/img/team-mark.png" alt="" width="34" height="34"><span class="brand-text"><span class="brand-name">Causal Inference Hub</span><span class="brand-sub">가짜연구소 인과추론팀</span></span></a>
17+
<button type="button" class="pass-btn" data-passport aria-label="학습 여권 열기">여권 <b data-passport-count>0/9</b></button>
1718
</div>
1819
</header>
1920
<main class="reader">
@@ -23,5 +24,7 @@ <h1>페이지를 찾을 수 없습니다</h1>
2324
<p><a class="btn" href="/">허브로 돌아가기</a></p>
2425
</div>
2526
</main>
27+
<script src="/assets/js/track.js" defer></script>
28+
<script src="/assets/js/passport.js" defer></script>
2629
</body>
2730
</html>

‎article.html‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<a href="./#reading">읽을거리</a>
2828
<a href="./#join">참여하기</a>
2929
</nav>
30+
<button type="button" class="pass-btn" data-passport aria-label="학습 여권 열기">여권 <b data-passport-count>0/9</b></button>
3031
</div>
3132
</header>
3233

@@ -57,5 +58,7 @@ <h2 id="more-title">다른 글</h2>
5758

5859
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
5960
<script src="assets/js/article.js" defer></script>
61+
<script src="assets/js/track.js" defer></script>
62+
<script src="assets/js/passport.js" defer></script>
6063
</body>
6164
</html>

‎assets/css/hub.css‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ button.btn.sm { font-size: 14px; }
806806
.stamp { display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%; border: 2px dashed var(--rule); color: var(--muted); font-family: var(--f-display); font-weight: 800; font-size: 18px; flex-shrink: 0; }
807807
.stamp.on { border: 2px solid var(--accent); background: var(--accent); color: var(--surface); box-shadow: 0 0 0 3px var(--accent-soft); transform: rotate(-8deg); }
808808
.stamp.sm { width: 36px; height: 36px; font-size: 15px; }
809-
.toast { position: fixed; left: 50%; bottom: 20px; z-index: 50; display: flex; align-items: center; gap: 12px; max-width: calc(100vw - 32px); padding: 12px 14px; border-radius: 14px; background: var(--ink); color: var(--paper); box-shadow: 0 12px 32px rgba(0, 0, 0, .25); transform: translate(-50%, 140%); transition: transform .3s ease; }
809+
.toast { position: fixed; left: 50%; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); z-index: 50; display: flex; align-items: center; gap: 12px; width: min(440px, calc(100vw - 32px)); box-sizing: border-box; padding: 12px 14px; border-radius: 14px; background: var(--ink); color: var(--paper); box-shadow: 0 12px 32px rgba(0, 0, 0, .25); transform: translate(-50%, 140%); transition: transform .3s ease; }
810810
.toast.show { transform: translate(-50%, 0); }
811811
.toast b { display: block; font-size: 14px; }
812812
.toast span span { font-size: 12.5px; opacity: .75; }
@@ -874,8 +874,8 @@ button.btn.sm { font-size: 14px; }
874874
.lab-row .bar { height: 14px; border-radius: 4px; background: var(--surface); overflow: hidden; }
875875
.lab-row .bar i { display: block; height: 100%; border-radius: 0 4px 4px 0; transition: width .2s ease; }
876876
.lab-row.truth .bar i { background: var(--ink); }
877-
.lab-row.naive .bar i { background: var(--l-exp); }
878-
.lab-row.adj .bar i { background: var(--l-main); }
877+
.lab-row.naive .bar i { background: #B7791F; }
878+
.lab-row.adj .bar i { background: var(--live); }
879879
.lab-row b { text-align: right; font-variant-numeric: tabular-nums; font-size: 15px; }
880880
.lab-bias { font-size: 13.5px; }
881881
.lab-challenge { padding: 14px; border-radius: 12px; border: 1.5px dashed color-mix(in srgb, var(--accent) 50%, var(--rule)); font-size: 14px; }
@@ -905,3 +905,14 @@ button.btn.sm { font-size: 14px; }
905905
.lab-learn ol { margin: 0 0 18px; padding-left: 20px; display: grid; gap: 10px; font-size: 15px; }
906906
.lab-learn a { font-weight: 700; margin-left: 4px; }
907907
@media (max-width: 900px) { .lab-grid { grid-template-columns: minmax(0, 1fr); } }
908+
.toast > span:nth-child(2) { flex: 1; min-width: 0; }
909+
@media (max-width: 900px) { .toast { bottom: auto; top: 72px; transform: translate(-50%, -180%); } .toast.show { transform: translate(-50%, 0); } }
910+
.lab-ctl.off input[type=range] { opacity: .45; }
911+
.lab-ctl.off small { color: var(--live); font-weight: 600; }
912+
.lab-note { font-size: 12.5px; color: var(--muted); }
913+
#lab-svg .fit-l { stroke-width: 1.5; opacity: .9; }
914+
#lab-svg .fit-l.on { stroke: var(--l-main); }
915+
#lab-svg .fit-l.off { stroke: var(--l-exp); }
916+
#lab-svg .fit-gap { stroke: var(--live); stroke-width: 3; }
917+
#lab-svg .fit-t { fill: var(--live); font-size: 12px; font-weight: 700; paint-order: stroke; stroke: var(--surface); stroke-width: 4px; }
918+
@media (max-width: 900px) { .lab-chart { order: -1; } }

‎assets/js/game.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
$("#gm-feedback").innerHTML = `
5252
<p class="gm-verdict ${right ? "ok" : "no"}">${right ? "맞았습니다" : "아쉽습니다"}<span>정답: ${LABEL[q.answer]}</span></p>
5353
<p>${esc(q.why)}</p>
54-
<p class="gm-read"><a href="${esc(q.chapter.url)}" target="_blank" rel="noopener">${esc(q.chapter.title)} 읽기</a></p>
54+
<p class="gm-read"><a href="${esc(q.chapter.url)}" target="_blank" rel="noopener" data-event="read_chapter" data-event-params='${esc(JSON.stringify({ source: "game", chapter: q.chapter.url.split("/").pop().slice(0, 2) }))}'>${esc(q.chapter.title)} 읽기</a></p>
5555
<button type="button" class="btn" data-next>${last ? "결과 보기" : "다음 문제"}</button>`;
5656
$("[data-next]", root).focus();
5757
}
@@ -70,7 +70,7 @@
7070
<p class="gm-split">인과라고 말할 수 있는 문제 ${state.right.cause} / ${kinds.cause} · 상관까지만인 문제 ${state.right.corr} / ${kinds.corr}</p>
7171
<h2>${esc(t.head)}</h2>
7272
<p class="gm-lede">${t.html ? t.body : esc(t.body)}</p>
73-
${chapters.length ? `<div class="gm-review"><h3>다시 볼 장</h3><ul>${chapters.map((c) => `<li><a href="${esc(c.url)}" target="_blank" rel="noopener">${esc(c.title)}</a></li>`).join("")}</ul></div>` : ""}
73+
${chapters.length ? `<div class="gm-review"><h3>다시 볼 장</h3><ul>${chapters.map((c) => `<li><a href="${esc(c.url)}" target="_blank" rel="noopener" data-event="read_chapter" data-event-params='${esc(JSON.stringify({ source: "game_review", chapter: c.url.split("/").pop().slice(0, 2) }))}'>${esc(c.title)}</a></li>`).join("")}</ul></div>` : ""}
7474
<div class="gm-cta">
7575
<a class="btn" href="./?quiz=1">3문항으로 내 시작점 찾기</a>
7676
<button type="button" class="btn ghost" data-share-game>${navigator.share ? "공유하기" : "결과와 링크 복사"}</button>

‎assets/js/hub.js‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@
530530
if (!p) return;
531531
const info = builderInfo(name);
532532
track("open_builder", { builder: info.slug || name, source: source || "hub" });
533-
if (window.hubCount) window.hubCount("builder_views", 3, "builder");
533+
if (window.hubSeen) window.hubSeen("builders", info.slug || name, 3, "builder");
534534
const runnerItems = p.runnerOf.map((x) => {
535535
const proj = state.projects.find((q) => q.cohort === x.cohort && q.name === x.project);
536536
const r = proj && proj.repo && state.repos.get(proj.repo);
@@ -814,7 +814,7 @@
814814
const now = new Date();
815815
const streak = rec.last === today || rec.last === today - 1 ? rec.streak || 0 : 0;
816816
const head = `<div class="daily-head"><h2 id="daily-title">오늘의 한 문제</h2><p class="daily-date">${now.getMonth() + 1}월 ${now.getDate()}일 · 매일 바뀝니다</p>
817-
${streak ? `<p class="daily-streak"><b>${streak}일</b> 연속${rec.best > streak ? ` · 최고 ${rec.best}일` : ""}</p>` : `<p class="daily-streak quiet">사흘 연속 풀면 스탬프</p>`}</div>`;
817+
${streak ? `<p class="daily-streak"><b>${streak}일</b> 연속${streak < 3 ? ` · ${3 - streak}일 더 풀면 스탬프` : rec.best > streak ? ` · 최고 ${rec.best}일` : ""}</p>` : `<p class="daily-streak quiet">사흘 연속 풀면 스탬프</p>`}</div>`;
818818
if (!ans) {
819819
box.innerHTML = `${head}<div class="daily-body">
820820
<p class="daily-q">${esc(q.text)}</p>
@@ -824,10 +824,11 @@
824824
}
825825
box.innerHTML = `${head}<div class="daily-body">
826826
<p class="daily-q">${esc(q.text)}</p>
827-
<p class="daily-verdict ${ans.correct ? "ok" : "no"}">${ans.correct ? "맞았습니다" : "아쉽습니다"}<span>정답: ${DAILY_LABEL[q.answer]}</span></p>
827+
<p class="daily-claim"><span>이렇게 말해도 될까요?</span>“${esc(q.claim)}”</p>
828+
<p class="daily-verdict ${ans.correct ? "ok" : "no"}">${ans.correct ? "맞았습니다" : "아쉽습니다"}<span>내 답: ${DAILY_LABEL[ans.pick] || ""} · 정답: ${DAILY_LABEL[q.answer]}</span></p>
828829
<p class="daily-why">${esc(q.why)}</p>
829-
<p class="daily-more"><a href="${esc(q.chapter.url)}" target="_blank" rel="noopener">${esc(q.chapter.title)} 읽기</a><a href="game.html">10문제 더 풀기</a><a href="lab.html">실험실에서 직접 해 보기</a></p>
830-
<p class="daily-next">내일 새 문제가 열립니다.</p></div>`;
830+
<p class="daily-more"><a href="${esc(q.chapter.url)}" target="_blank" rel="noopener" data-event="read_chapter" data-event-params='${esc(JSON.stringify({ source: "daily", chapter: q.chapter.url.split("/").pop().slice(0, 2) }))}'>${esc(q.chapter.title)} 읽기</a><a href="game.html">10문제 더 풀기</a><a href="lab.html">실험실에서 직접 해 보기</a></p>
831+
<p class="daily-next">자정에 새 문제가 열립니다.</p></div>`;
831832
}
832833
function answerDaily(pick) {
833834
const pool = (state.daily && state.daily.questions) || [];
@@ -1013,7 +1014,7 @@
10131014
const r = state.repos.get(id);
10141015
if (!r) return;
10151016
state.openRepo = r;
1016-
if (!silent) { track("open_repo", { repo: r.repo, level: r.level || "", source: source || "hub" }); stamp("first_open"); }
1017+
if (!silent) { track("open_repo", { repo: r.repo, level: r.level || "", source: source || "hub" }); if (source !== "deeplink") stamp("first_open"); }
10171018
if (!silent && r.kind !== "paid") {
10181019
state.recent = [{ id, t: Date.now() }, ...state.recent.filter((x) => x.id !== id)].slice(0, 6);
10191020
saveRecent(); renderWeek();

‎assets/js/lab.js‎

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
const $ = (s) => document.querySelector(s);
77
const track = (n, p) => window.hubTrack && window.hubTrack(n, p);
88
const N = 800, SVG_NS = "http://www.w3.org/2000/svg";
9-
let seed = 7, base = null, started = false, solved = false;
9+
let seed = 9, base = null, started = false, solved = false;
1010

1111
function rng(a) { return () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
1212
function draw(s) {
@@ -24,18 +24,19 @@
2424
if (Math.abs(A[c][c]) < 1e-9) return NaN;
2525
for (let r2 = 0; r2 < 3; r2++) if (r2 !== c) { const f = A[r2][c] / A[c][c]; for (let k = c; k < 3; k++) A[r2][k] -= f * A[c][k]; b[r2] -= f * b[c]; }
2626
}
27-
return b[1] / A[1][1];
27+
return [b[0] / A[0][0], b[1] / A[1][1], b[2] / A[2][2]];
2828
}
2929
const fmt = (v) => (Number.isFinite(v) ? `${v >= 0 ? "+" : "−"}${Math.abs(v).toFixed(1)}` : "계산 불가");
3030

3131
function compute() {
3232
const conf = Number($("#in-conf").value), eff = Number($("#in-eff").value), rand = $("#in-rand").getAttribute("aria-pressed") === "true";
33-
const k = conf * 0.4;
33+
const k = conf * 0.12;
3434
const rows = base.map((d) => { const p = rand ? 0.5 : 1 / (1 + Math.exp(-k * d.z)); const t = d.u < p ? 1 : 0; return { z: d.z, t, y: 50 + 6 * d.z + eff * t + d.e }; });
3535
const on = rows.filter((d) => d.t), off = rows.filter((d) => !d.t);
3636
const mean = (a) => a.reduce((s, d) => s + d.y, 0) / (a.length || 1);
3737
const mOn = mean(on), mOff = mean(off);
38-
return { conf, eff, rand, rows, mOn, mOff, naive: mOn - mOff, adj: ols(rows), nOn: on.length };
38+
const fit = ols(rows);
39+
return { conf, eff, rand, rows, mOn, mOff, naive: mOn - mOff, adj: Array.isArray(fit) ? fit[1] : NaN, fit, nOn: on.length };
3940
}
4041

4142
function bars(r) {
@@ -44,16 +45,23 @@
4445
$("#bar-true").style.width = w(r.eff); $("#bar-naive").style.width = w(r.naive); $("#bar-adj").style.width = w(r.adj);
4546
$("#v-true").textContent = fmt(r.eff); $("#v-naive").textContent = fmt(r.naive); $("#v-adj").textContent = fmt(r.adj);
4647
const bias = r.naive - r.eff;
47-
$("#lab-bias").innerHTML = Math.abs(bias) <= 1
48-
? `단순 비교의 편향 <b>${fmt(bias)}</b>: 진짜 효과에 거의 붙었습니다.`
49-
: `단순 비교의 편향 <b>${fmt(bias)}</b>: 진짜 효과보다 ${Math.abs(bias).toFixed(1)}점 ${bias > 0 ? "부풀려졌습니다" : "작게 나왔습니다"}.`;
48+
$("#lab-bias").innerHTML = r.rand
49+
? `단순 비교와 진짜 효과의 차이 <b>${fmt(bias)}</b>: 800명 표본의 우연 오차 범위(±1 안팎)입니다.`
50+
: Math.abs(bias) <= 1
51+
? `단순 비교의 편향 <b>${fmt(bias)}</b>: 진짜 효과에 거의 붙었습니다.`
52+
: `단순 비교의 편향 <b>${fmt(bias)}</b>: 진짜 효과보다 ${Math.abs(bias).toFixed(1)}점 ${bias > 0 ? "부풀려졌습니다" : "작게 나왔습니다"}.`;
5053
$("#out-conf").textContent = r.conf; $("#out-eff").textContent = `+${r.eff}`;
54+
/* while randomized, loyalty no longer decides who turns notifications on */
55+
const ctl = $("#in-conf").closest(".lab-ctl");
56+
ctl.classList.toggle("off", r.rand);
57+
$("#in-conf").setAttribute("aria-disabled", String(r.rand));
58+
ctl.querySelector("small").textContent = r.rand ? "무작위 배정 중: 충성도가 알림을 정하지 못합니다" : "충성도가 알림 켜기에 얼마나 영향을 주나";
5159
}
5260

5361
function chart(r) {
5462
const svg = $("#lab-svg"); while (svg.lastChild && svg.lastChild.nodeName !== "desc") svg.removeChild(svg.lastChild);
5563
/* draw in the chart's own pixel size so labels stay readable on phones */
56-
const W = Math.max(300, Math.round(svg.parentElement.clientWidth - 40)), H = Math.round(Math.min(360, Math.max(260, W * 0.62)));
64+
const W = Math.max(300, Math.round(svg.parentElement.clientWidth - 40)), H = W < 480 ? 220 : Math.round(Math.min(360, Math.max(260, W * 0.62)));
5765
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
5866
const small = W < 480, dot = small ? 2 : 2.6;
5967
const L = 40, R = W - 16, T = 14, B = H - 40, X = (z) => L + ((Math.max(-3, Math.min(3, z)) + 3) / 6) * (R - L), Y = (y) => B - ((Math.max(15, Math.min(85, y)) - 15) / 70) * (B - T);
@@ -64,27 +72,42 @@
6472
el("text", { x: R, y: B + 34, class: "axis-t", "text-anchor": "end" }, g).textContent = "충성도 →";
6573
const dots = el("g", { class: "dots" });
6674
r.rows.forEach((d) => el("circle", { cx: X(d.z).toFixed(1), cy: Y(d.y).toFixed(1), r: dot, class: d.t ? "on" : "off" }, dots));
75+
/* the adjusted estimate: two parallel fitted lines, one per group, at the same loyalty */
76+
if (Array.isArray(r.fit)) {
77+
const [a, tau, bz] = r.fit, fitG = el("g", { class: "fit" });
78+
[[0, "off"], [1, "on"]].forEach(([tt, cls]) => el("line", { x1: X(-3), x2: X(3), y1: Y(a + bz * -3 + tau * tt), y2: Y(a + bz * 3 + tau * tt), class: `fit-l ${cls}` }, fitG));
79+
const zl = -2.2, yOn = Y(a + bz * zl + tau), yOff = Y(a + bz * zl);
80+
el("line", { x1: X(zl), x2: X(zl), y1: yOn, y2: yOff, class: "fit-gap" }, fitG);
81+
el("text", { x: X(zl) + 6, y: (yOn + yOff) / 2 + 4, class: "fit-t" }, fitG).textContent = `같은 충성도에서의 간격 = 보정 ${fmt(tau)}`;
82+
}
6783
const means = el("g", { class: "means" });
6884
[["on", r.mOn, "켬 평균"], ["off", r.mOff, "끔 평균"]].forEach(([cls, m, label]) => {
6985
el("line", { x1: L, x2: R, y1: Y(m), y2: Y(m), class: `mean ${cls}` }, means);
7086
const t = el("text", { x: R - 4, y: Y(m) + (cls === "on" ? -6 : 14), class: `mean-t ${cls}`, "text-anchor": "end" }, means); t.textContent = `${label} ${m.toFixed(1)}`;
7187
});
7288
}
7389

90+
let moves = 0;
7491
function challenge(r) {
7592
if (solved) return;
76-
const ok = r.conf >= 5 && Math.abs(r.naive - r.eff) <= 1;
77-
if (!ok) return;
78-
solved = true;
79-
$("#lab-challenge").classList.add("done");
80-
$("#lab-ch-text").innerHTML = r.rand
81-
? "해냈습니다. 무작위로 켜면 충성도와 알림이 따로 움직여서, 단순 비교만으로도 진짜 효과에 가까워집니다. 이것이 무작위 실험이 하는 일입니다."
82-
: "해냈습니다. 이번 표본에서는 우연히 편향이 작게 나왔습니다. ‘알림을 무작위로 켜기’를 눌러 보면 왜 실험이 확실한 방법인지 보입니다.";
83-
track("lab_challenge_complete", { randomized: r.rand, conf: r.conf });
84-
if (window.hubStamp) window.hubStamp("lab");
93+
const close = Math.abs(r.naive - r.eff) <= 1;
94+
if (r.conf >= 5 && r.rand && close) {
95+
solved = true;
96+
$("#lab-challenge").classList.add("done");
97+
$("#lab-ch-text").textContent = "해냈습니다. 무작위로 켜면 충성도와 알림이 따로 움직여서, 단순 비교만으로도 진짜 효과에 가까워집니다. 이것이 무작위 실험이 하는 일입니다.";
98+
track("lab_challenge_complete", { randomized: true, conf: r.conf });
99+
if (window.hubStamp) window.hubStamp("lab");
100+
return;
101+
}
102+
if (r.rand && r.conf >= 5) {
103+
$("#lab-ch-text").textContent = "무작위 배정이 맞는 답입니다. 지금 남은 차이는 800명 표본의 우연 오차입니다. ‘새 사용자 800명 뽑기’를 눌러 보세요.";
104+
} else if (moves >= 8) {
105+
$("#lab-ch-text").textContent = "힌트: 교란 강도는 5 이상으로 그대로 두고, 슬라이더가 아닌 다른 버튼을 찾아보세요.";
106+
}
85107
}
86108

87109
function update(source) {
110+
if (source === "confounding" || source === "effect") moves += 1;
88111
const r = compute();
89112
bars(r); chart(r); challenge(r);
90113
if (source && !started) { started = true; track("lab_start", { control: source }); }
@@ -94,7 +117,7 @@
94117
base = draw(seed);
95118
$("#in-conf").addEventListener("input", () => update("confounding"));
96119
$("#in-eff").addEventListener("input", () => update("effect"));
97-
$("#in-rand").addEventListener("click", (e) => { const b = e.currentTarget; b.setAttribute("aria-pressed", String(b.getAttribute("aria-pressed") !== "true")); update("randomize"); track("lab_randomize", { on: b.getAttribute("aria-pressed") === "true" }); });
120+
$("#in-rand").addEventListener("click", (e) => { const b = e.currentTarget; b.setAttribute("aria-pressed", String(b.getAttribute("aria-pressed") !== "true")); track("lab_randomize", { on: b.getAttribute("aria-pressed") === "true" }); update("randomize"); });
98121
$("#in-seed").addEventListener("click", () => { seed += 1; base = draw(seed); update("reseed"); });
99122
$("#lab-share").addEventListener("click", async (e) => {
100123
const url = window.hubShareUrl ? window.hubShareUrl("/lab.html", "lab") : location.href;

0 commit comments

Comments
 (0)