File size: 18,033 Bytes
1acfebf
 
 
 
 
 
3c69a0f
80dab3d
 
3c69a0f
 
 
39c3862
 
 
 
 
 
 
1acfebf
 
 
 
 
 
 
 
 
 
80dab3d
1acfebf
 
80dab3d
 
 
1acfebf
b59ad90
 
80dab3d
b59ad90
 
3c69a0f
80dab3d
b59ad90
 
 
 
 
1acfebf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c69a0f
 
 
 
 
 
 
 
 
 
1acfebf
 
 
 
 
 
80dab3d
 
 
1acfebf
 
 
80dab3d
3c69a0f
1acfebf
80dab3d
3c69a0f
1acfebf
 
80dab3d
 
 
 
 
 
 
 
 
 
 
4b3a447
 
80dab3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b3a447
 
80dab3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c69a0f
 
80dab3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b3a447
80dab3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b3a447
80dab3d
 
 
4b3a447
80dab3d
 
 
 
 
 
 
 
5294e66
 
 
 
 
 
 
 
 
 
4b3a447
5294e66
4b3a447
 
 
 
 
 
 
 
 
 
 
5294e66
 
4b3a447
5294e66
4b3a447
 
5294e66
 
 
 
 
4b3a447
5294e66
4b3a447
5294e66
4b3a447
 
 
5294e66
4b3a447
 
 
 
5294e66
4b3a447
 
 
 
 
 
 
5294e66
4b3a447
 
 
 
 
5294e66
4b3a447
 
 
 
 
 
 
 
 
 
 
 
5294e66
4b3a447
 
 
 
 
 
 
 
 
 
 
 
 
 
5294e66
 
 
4b3a447
5294e66
4b3a447
5294e66
 
 
4b3a447
5294e66
 
4b3a447
5294e66
4b3a447
5294e66
 
 
 
 
 
 
 
 
4b3a447
5294e66
3c69a0f
80dab3d
 
 
3c69a0f
80dab3d
 
 
 
3c69a0f
80dab3d
1acfebf
 
0bb38be
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>世界杯 2026 · Gemma 4 赛果预测</title>
<meta name="description" content="用 Gemma 4 预测世界杯 2026 每场比赛的比分与胜平负概率,Fast Gemma Challenge 展示项目。">
<meta property="og:title" content="⚽️ World Cup 2026 · Gemma 4 Predictions">
<meta property="og:description" content="AI-predicted scores and win/draw/lose probabilities for every group-stage match.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://lazyxcode-worldcup2026-gemma.static.hf.space">
<meta name="twitter:card" content="summary">
<script async src="https://www.googletagmanager.com/gtag/js?id=G-KLCFPG9NP7"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-KLCFPG9NP7');
</script>
<style>
  :root {
    --bg: #f4f6f9; --card: #ffffff; --line: #e6eaf0;
    --text: #1f2937; --muted: #6b7280;
    --green: #16a34a; --green-soft: #dcfce7;
    --amber: #d97706; --amber-soft: #fef3c7;
    --red: #dc2626; --red-soft: #fee2e2;
  }
  * { box-sizing: border-box; margin: 0; }
  body { background: var(--bg); color: var(--text); font: 15px/1.55 -apple-system, "PingFang SC", "Segoe UI", sans-serif; }
  header { background: linear-gradient(135deg, #15803d, #16a34a 55%, #4ade80); color: #fff; padding: 44px 16px 56px; text-align: center; position: relative; }
  header h1 { font-size: 30px; letter-spacing: 1px; }
  header p { margin-top: 8px; opacity: .9; font-size: 13px; }
  .lang { position: absolute; top: 14px; right: 14px; background: rgba(255,255,255,.18); border: 1px solid rgba(255,255,255,.45);
          color: #fff; font: 600 13px/1 inherit; padding: 7px 14px; border-radius: 99px; cursor: pointer; }
  .lang:hover { background: rgba(255,255,255,.3); }
  .wrap { max-width: 920px; margin: -28px auto 60px; padding: 0 16px; }
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr)); gap: 16px; }
  @media (max-width: 480px) {
    header { padding: 36px 12px 44px; }
    header h1 { font-size: 22px; }
    .card { padding: 14px; border-radius: 12px; }
    .team { font-size: 13px; gap: 6px; }
    .card .team span { white-space: normal; line-height: 1.25; hyphens: auto; overflow-wrap: anywhere; }
    .card .team.away { text-align: right; }
    .teams img { width: 20px; }
    .score { font-size: 18px; padding: 2px 10px; }
    .meta { flex-wrap: wrap; gap: 4px; }
  }
  .card { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 18px 20px;
          box-shadow: 0 1px 3px rgba(16,24,40,.06); }
  .meta { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); margin-bottom: 12px; }
  .badge { background: var(--green-soft); color: var(--green); font-weight: 600; padding: 1px 8px; border-radius: 99px; }
  .teams { display: flex; align-items: center; gap: 10px; }
  .team { flex: 1; display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 16px; min-width: 0; }
  .team span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .team.away { justify-content: flex-end; }
  .teams img { width: 26px; border-radius: 3px; box-shadow: 0 0 0 1px var(--line); flex-shrink: 0; }
  .score { font-size: 22px; font-weight: 800; color: var(--green); background: var(--green-soft);
           padding: 3px 14px; border-radius: 10px; white-space: nowrap; }
  .bar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; margin: 14px 0 8px; background: var(--line); }
  .bar .w { background: var(--green); } .bar .d { background: #f59e0b; } .bar .l { background: #ef4444; }
  .probs { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); gap: 8px; }
  .probs b { font-size: 13px; }
  .probs .w b { color: var(--green); } .probs .d b { color: var(--amber); } .probs .l b { color: var(--red); }
  .reason { margin-top: 12px; font-size: 13px; color: var(--muted); background: var(--bg);
            border-radius: 10px; padding: 8px 12px; }
  .empty { text-align: center; color: var(--muted); padding: 80px 0; }
  .share { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin: 28px 0 8px; }
  .share button, .share a { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
    border: 1px solid var(--line); background: var(--card); color: var(--text); text-decoration: none;
    font: 600 13px/1 inherit; padding: 9px 16px; border-radius: 99px;
    box-shadow: 0 1px 2px rgba(16,24,40,.05); transition: transform .1s; }
  .share button:hover, .share a:hover { transform: translateY(-1px); border-color: var(--green); }
  .toast { position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%);
    background: #1f2937; color: #fff; font-size: 13px; padding: 10px 18px; border-radius: 99px;
    opacity: 0; pointer-events: none; transition: opacity .25s; z-index: 9; }
  .toast.show { opacity: 1; }
  footer { text-align: center; font-size: 12px; color: var(--muted); padding-bottom: 40px; }
  footer a { color: var(--green); }
</style>
</head>
<body>
<header>
  <button class="lang" id="langBtn"></button>
  <h1 id="title"></h1>
  <p id="sub"></p>
</header>
<div class="wrap">
  <div class="grid" id="list"></div>
  <div class="empty" id="empty" hidden></div>
  <div class="share" id="share"></div>
</div>
<footer id="foot"></footer>
<div class="toast" id="toast"></div>
<script src="predictions.js"></script>
<script>
  // ---- 多语言资源 ----
  const I18N = {
    zh: {
      title: "⚽️ 世界杯 2026 赛果预测",
      sub: g => `Gemma 4 驱动 · 数据来源 worldcup26.ir${g ? ` · 生成于 ${g}` : ""}`,
      badge: p => `${p.group} 组 · 第 ${p.matchday} 轮`,
      gameNo: p => `第 ${p.game_id} 场 · ${p.date}`,
      home: "主胜", draw: "平局", away: "客胜",
      empty: "没有预测数据 —— 先运行 batch.py 生成 predictions.js",
      copied: "文案已复制,去粘贴分享吧 ✅", copyFail: "复制失败,请手动复制",
      copyBtn: "📋 复制文案", sysShare: "📤 分享…", xShare: "𝕏 分享",
      snap: "🖼 生成分享图", snapMaking: "正在生成分享图…", snapDone: "分享图已保存 ✅",
      imgSub: (n, md) => `小组赛第 ${md} 轮 · ${n} 场 AI 预测`,
      foot: `Fast Gemma Challenge 展示项目 · 预测仅供娱乐 · <a href="https://huggingface.co/spaces/gemma-challenge/gemma-dashboard" target="_blank">挑战赛排行榜</a>`,
      langBtn: "English",
      reason: p => p.reason_zh || p.reason,
      teamName: en => TEAM_ZH[en] || en,
      shareText: (n, p) => `⚽️ 我用 Gemma 4 把世界杯 2026 小组赛 ${n} 场比赛全预测了一遍:${p ? `揭幕战 AI 押 ${TEAM_ZH[p.home] || p.home} ${p.score} ${TEAM_ZH[p.away] || p.away}(胜率 ${Math.round(p.win_prob * 100)}%)!` : ""}来看看 AI 的眼光准不准 👉 `,
    },
    en: {
      title: "⚽️ World Cup 2026 Predictions",
      sub: g => `Powered by Gemma 4 · Data from worldcup26.ir${g ? ` · Generated ${g}` : ""}`,
      badge: p => `Group ${p.group} · MD ${p.matchday}`,
      gameNo: p => `Match ${p.game_id} · ${p.date}`,
      home: "Home win", draw: "Draw", away: "Away win",
      empty: "No predictions yet — run batch.py to generate predictions.js",
      copied: "Copied! Go paste & share ✅", copyFail: "Copy failed, please copy manually",
      copyBtn: "📋 Copy text", sysShare: "📤 Share…", xShare: "𝕏 Post",
      snap: "🖼 Make image", snapMaking: "Generating image…", snapDone: "Image saved ✅",
      imgSub: (n, md) => `Group stage · Matchday ${md} · ${n} AI predictions`,
      foot: `A Fast Gemma Challenge showcase · For fun only · <a href="https://huggingface.co/spaces/gemma-challenge/gemma-dashboard" target="_blank">Leaderboard</a>`,
      langBtn: "中文",
      reason: p => p.reason,
      teamName: en => en,
      shareText: (n, p) => `⚽️ I asked Gemma 4 to predict all ${n} World Cup 2026 group-stage openers. ${p ? `It picks ${p.home} ${p.score} ${p.away} in the opener (${Math.round(p.win_prob * 100)}% win)! ` : ""}See the AI's calls 👉 `,
    },
  };
  const TEAM_ZH = {
    "Mexico": "墨西哥", "South Africa": "南非", "South Korea": "韩国", "Czech Republic": "捷克",
    "Canada": "加拿大", "Bosnia and Herzegovina": "波黑", "United States": "美国", "Paraguay": "巴拉圭",
    "Haiti": "海地", "Scotland": "苏格兰", "Australia": "澳大利亚", "Turkey": "土耳其",
    "Brazil": "巴西", "Morocco": "摩洛哥", "Qatar": "卡塔尔", "Switzerland": "瑞士",
    "Ivory Coast": "科特迪瓦", "Ecuador": "厄瓜多尔", "Germany": "德国", "Curaçao": "库拉索",
    "Netherlands": "荷兰", "Japan": "日本", "Sweden": "瑞典", "Tunisia": "突尼斯",
    "Iran": "伊朗", "New Zealand": "新西兰", "Spain": "西班牙", "Cape Verde": "佛得角",
    "Belgium": "比利时", "Egypt": "埃及", "Saudi Arabia": "沙特阿拉伯", "Uruguay": "乌拉圭",
    "France": "法国", "Senegal": "塞内加尔", "Iraq": "伊拉克", "Norway": "挪威",
    "Argentina": "阿根廷", "Algeria": "阿尔及利亚", "Austria": "奥地利", "Jordan": "约旦",
    "Portugal": "葡萄牙", "Democratic Republic of the Congo": "刚果(金)", "England": "英格兰", "Croatia": "克罗地亚",
    "Uzbekistan": "乌兹别克斯坦", "Colombia": "哥伦比亚", "Ghana": "加纳", "Panama": "巴拿马",
  };

  const SITE = "https://lazyxcode-worldcup2026-gemma.static.hf.space";
  const preds = window.PREDICTIONS || [];
  let lang = localStorage.getItem("wc-lang") ||
             ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en");

  function render() {
    const t = I18N[lang];
    document.documentElement.lang = lang;
    document.title = t.title.replace("⚽️ ", "");
    document.getElementById("langBtn").textContent = t.langBtn;
    document.getElementById("title").textContent = t.title;
    document.getElementById("sub").textContent = t.sub(window.GENERATED_AT);
    document.getElementById("empty").textContent = t.empty;
    document.getElementById("empty").hidden = preds.length > 0;
    document.getElementById("foot").innerHTML = t.foot;

    const pct = x => Math.round(x * 100) + "%";
    document.getElementById("list").innerHTML = preds.map(p => `
      <div class="card">
        <div class="meta"><span class="badge">${t.badge(p)}</span><span>${t.gameNo(p)}</span></div>
        <div class="teams">
          <div class="team">${p.home_flag ? `<img src="${p.home_flag}" alt="">` : ""}<span>${t.teamName(p.home)}</span></div>
          <div class="score">${p.score}</div>
          <div class="team away"><span>${t.teamName(p.away)}</span>${p.away_flag ? `<img src="${p.away_flag}" alt="">` : ""}</div>
        </div>
        <div class="bar">
          <div class="w" style="flex:${p.win_prob}"></div>
          <div class="d" style="flex:${p.draw_prob}"></div>
          <div class="l" style="flex:${p.lose_prob}"></div>
        </div>
        <div class="probs">
          <span class="w"><b>${pct(p.win_prob)}</b> ${t.home}</span>
          <span class="d"><b>${pct(p.draw_prob)}</b> ${t.draw}</span>
          <span class="l"><b>${pct(p.lose_prob)}</b> ${t.away}</span>
        </div>
        <div class="reason">💡 ${t.reason(p)}</div>
      </div>`).join("");
    const copy = t.shareText(preds.length, preds[0]) + SITE;
    const enc = encodeURIComponent;
    const links = [
      [t.xShare, `https://twitter.com/intent/tweet?text=${enc(copy)}`],
      ["微博", `https://service.weibo.com/share/share.php?url=${enc(SITE)}&title=${enc(copy)}`],
      ["Telegram", `https://t.me/share/url?url=${enc(SITE)}&text=${enc(copy)}`],
      ["WhatsApp", `https://wa.me/?text=${enc(copy)}`],
    ];
    document.getElementById("share").innerHTML =
      `<button id="imgBtn">${t.snap}</button>` +
      links.map(([n, u]) => `<a href="${u}" target="_blank" rel="noopener">${n}</a>`).join("") +
      `<button id="copyBtn">${t.copyBtn}</button>` +
      (navigator.share ? `<button id="sysShare">${t.sysShare}</button>` : "");
    document.getElementById("imgBtn").onclick = shareImage;
    document.getElementById("copyBtn").onclick = async () => {
      try { await navigator.clipboard.writeText(copy); toast(t.copied); }
      catch { toast(t.copyFail); }
    };
    const sys = document.getElementById("sysShare");
    if (sys) sys.onclick = () => navigator.share({ title: t.title, text: copy, url: SITE }).catch(() => {});
  }

  // ---- 分享卡片图片 (Canvas) ----
  const loadImg = src => new Promise(res => {
    const i = new Image(); i.crossOrigin = "anonymous";
    i.onload = () => res(i); i.onerror = () => res(null); i.src = src;
  });

  function rr(ctx, x, y, w, h, r) {
    ctx.beginPath(); ctx.roundRect(x, y, w, h, r); ctx.closePath();
  }

  const FONT = "-apple-system, 'PingFang SC', sans-serif";

  function fitText(ctx, text, maxW, size, minSize) {
    let s = size;
    while (s > minSize) {
      ctx.font = `bold ${s}px ${FONT}`;
      if (ctx.measureText(text).width <= maxW) return text;
      s -= 2;
    }
    ctx.font = `bold ${minSize}px ${FONT}`;
    let txt = text;
    while (txt.length > 1 && ctx.measureText(txt + "…").width > maxW) txt = txt.slice(0, -1);
    return txt === text ? text : txt + "…";
  }

  async function makeSummaryCanvas() {
    const t = I18N[lang];
    const W = 1080, rowH = 84, gap = 14, top = 300;
    const H = top + preds.length * (rowH + gap) + 140;
    const c = document.createElement("canvas"); c.width = W; c.height = H;
    const ctx = c.getContext("2d");

    // 背景 + 头部渐变
    ctx.fillStyle = "#f4f6f9"; ctx.fillRect(0, 0, W, H);
    const grad = ctx.createLinearGradient(0, 0, W, 250);
    grad.addColorStop(0, "#15803d"); grad.addColorStop(.55, "#16a34a"); grad.addColorStop(1, "#4ade80");
    ctx.fillStyle = grad; ctx.fillRect(0, 0, W, 250);
    ctx.fillStyle = "#fff"; ctx.textAlign = "center";
    ctx.font = `bold 54px ${FONT}`; ctx.fillText(t.title, W / 2, 115);
    ctx.font = `30px ${FONT}`; ctx.globalAlpha = .92;
    ctx.fillText(t.imgSub(preds.length, preds[0]?.matchday || 1), W / 2, 180); ctx.globalAlpha = 1;

    // 预加载全部国旗
    const flagUrls = [...new Set(preds.flatMap(p => [p.home_flag, p.away_flag]).filter(Boolean))];
    const flagImgs = {};
    await Promise.all(flagUrls.map(async u => { flagImgs[u] = await loadImg(u); }));

    const drawFlag = (u, x, y) => {
      const img = flagImgs[u];
      if (img) ctx.drawImage(img, x, y, 48, 32);
      else { ctx.fillStyle = "#e6eaf0"; rr(ctx, x, y, 48, 32, 4); ctx.fill(); }
      ctx.strokeStyle = "rgba(0,0,0,.08)"; ctx.lineWidth = 1.5;
      rr(ctx, x, y, 48, 32, 3); ctx.stroke();
    };

    preds.forEach((p, i) => {
      const ry = top + i * (rowH + gap);
      const cy = ry + 40;  // 文本/旗帜的垂直中心
      ctx.fillStyle = "#fff"; rr(ctx, 60, ry, W - 120, rowH, 16); ctx.fill();
      ctx.strokeStyle = "#e6eaf0"; ctx.lineWidth = 1.5; rr(ctx, 60, ry, W - 120, rowH, 16); ctx.stroke();

      // 小组徽章
      ctx.fillStyle = "#dcfce7"; rr(ctx, 84, cy - 20, 52, 40, 12); ctx.fill();
      ctx.fillStyle = "#16a34a"; ctx.font = `bold 26px ${FONT}`; ctx.textAlign = "center";
      ctx.fillText(p.group, 110, cy + 9);

      // 主队 (左) / 客队 (右)
      drawFlag(p.home_flag, 160, cy - 16);
      ctx.fillStyle = "#1f2937"; ctx.textAlign = "left";
      ctx.fillText(fitText(ctx, t.teamName(p.home), 230, 28, 20), 222, cy + 9);
      drawFlag(p.away_flag, W - 208, cy - 16);
      ctx.fillStyle = "#1f2937"; ctx.textAlign = "right";
      ctx.fillText(fitText(ctx, t.teamName(p.away), 230, 28, 20), W - 222, cy + 9);

      // 比分
      ctx.fillStyle = "#dcfce7"; rr(ctx, W / 2 - 60, cy - 24, 120, 48, 12); ctx.fill();
      ctx.fillStyle = "#16a34a"; ctx.font = `bold 30px ${FONT}`; ctx.textAlign = "center";
      ctx.fillText(p.score, W / 2, cy + 10);

      // 概率细条
      const bx = 160, bw = W - 368, by = ry + 64, bh = 6;
      ctx.save(); rr(ctx, bx, by, bw, bh, 3); ctx.clip();
      const wW = bw * p.win_prob, dW = bw * p.draw_prob;
      ctx.fillStyle = "#16a34a"; ctx.fillRect(bx, by, wW, bh);
      ctx.fillStyle = "#f59e0b"; ctx.fillRect(bx + wW, by, dW, bh);
      ctx.fillStyle = "#ef4444"; ctx.fillRect(bx + wW + dW, by, bw - wW - dW, bh);
      ctx.restore();
    });

    // 页脚
    ctx.textAlign = "center"; ctx.fillStyle = "#9ca3af"; ctx.font = `26px ${FONT}`;
    ctx.fillText(SITE.replace("https://", ""), W / 2, H - 80);
    ctx.fillStyle = "#16a34a"; ctx.font = `bold 26px ${FONT}`;
    ctx.fillText("⚡ Fast Gemma Challenge", W / 2, H - 38);
    return c;
  }

  async function shareImage() {
    const t = I18N[lang];
    toast(t.snapMaking);
    const canvas = await makeSummaryCanvas();
    const blob = await new Promise(res => canvas.toBlob(res, "image/png"));
    const file = new File([blob], `wc2026-md${preds[0]?.matchday || 1}-${lang}.png`, { type: "image/png" });
    if (navigator.canShare && navigator.canShare({ files: [file] })) {
      try { await navigator.share({ files: [file], title: t.title, url: SITE }); return; }
      catch (e) { if (e.name === "AbortError") return; }
    }
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob); a.download = file.name; a.click();
    URL.revokeObjectURL(a.href);
    toast(t.snapDone);
  }
  window.__makeSummaryCanvas = makeSummaryCanvas; // 调试用

  const toast = msg => {
    const el = document.getElementById("toast");
    el.textContent = msg; el.classList.add("show");
    setTimeout(() => el.classList.remove("show"), 1800);
  };
  document.getElementById("langBtn").onclick = () => {
    lang = lang === "zh" ? "en" : "zh";
    localStorage.setItem("wc-lang", lang);
    render();
  };
  render();
</script>
</body>
</html>