worldcup2026-gemma / index.html
lazyxcode's picture
Upload index.html with huggingface_hub
39c3862 verified
Raw
History Blame Contribute Delete
18 kB
<!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>