780 lines
24 KiB
HTML
780 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>CHUCK NORRIS FACT GENERATOR — Gemma製・自己流ファクトエンジン</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Zen+Kaku+Gothic+New:wght@400;500;700;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--ink:#121014;
|
||
--panel:#1C1820;
|
||
--panel-line: rgba(243,238,226,0.08);
|
||
--gold:#E8B23D;
|
||
--crimson:#C1442E;
|
||
--bone:#F3EEE2;
|
||
--ash:#8B8478;
|
||
}
|
||
*{ box-sizing:border-box; }
|
||
html,body{ margin:0; padding:0; }
|
||
body{
|
||
background:
|
||
radial-gradient(circle at 1px 1px, rgba(243,238,226,0.045) 1.5px, transparent 1.5px) 0 0/26px 26px,
|
||
var(--ink);
|
||
color:var(--bone);
|
||
font-family:'Zen Kaku Gothic New', sans-serif;
|
||
min-height:100vh;
|
||
display:flex;
|
||
justify-content:center;
|
||
padding:56px 20px 80px;
|
||
}
|
||
.stage{ width:100%; max-width:640px; }
|
||
|
||
.top-row{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
margin-bottom:14px;
|
||
}
|
||
.eyebrow{
|
||
font-family:'JetBrains Mono', monospace;
|
||
font-size:12px;
|
||
letter-spacing:0.14em;
|
||
color:var(--gold);
|
||
text-transform:uppercase;
|
||
display:flex;
|
||
align-items:center;
|
||
gap:8px;
|
||
}
|
||
.eyebrow::before{
|
||
content:"";
|
||
width:7px; height:7px;
|
||
background:var(--gold);
|
||
display:inline-block;
|
||
transform:rotate(45deg);
|
||
}
|
||
.lang-toggle{
|
||
display:flex;
|
||
border:1px solid var(--panel-line);
|
||
border-radius:2px;
|
||
overflow:hidden;
|
||
}
|
||
.lang-toggle button{
|
||
background:transparent;
|
||
color:var(--ash);
|
||
font-family:'JetBrains Mono', monospace;
|
||
font-size:12px;
|
||
font-weight:700;
|
||
padding:6px 12px;
|
||
border:none;
|
||
cursor:pointer;
|
||
}
|
||
.lang-toggle button.active{ background:var(--gold); color:var(--ink); }
|
||
|
||
h1{
|
||
font-family:'Anton', sans-serif;
|
||
font-weight:400;
|
||
font-size:clamp(34px, 6vw, 54px);
|
||
line-height:0.98;
|
||
letter-spacing:0.01em;
|
||
margin:0 0 10px;
|
||
color:var(--bone);
|
||
}
|
||
h1 span{ color:var(--crimson); }
|
||
.sub{
|
||
color:var(--ash);
|
||
font-size:15px;
|
||
line-height:1.7;
|
||
margin:0 0 36px;
|
||
max-width:52ch;
|
||
}
|
||
.panel{
|
||
background:var(--panel);
|
||
border:1px solid var(--panel-line);
|
||
border-radius:2px;
|
||
padding:28px;
|
||
margin-bottom:22px;
|
||
}
|
||
.row{ display:flex; gap:10px; }
|
||
input[type=text]{
|
||
flex:1;
|
||
background:var(--ink);
|
||
border:1px solid var(--panel-line);
|
||
color:var(--bone);
|
||
font-family:'Zen Kaku Gothic New', sans-serif;
|
||
font-size:15px;
|
||
padding:14px 16px;
|
||
border-radius:1px;
|
||
outline:none;
|
||
transition:border-color .15s ease;
|
||
}
|
||
input[type=text]:focus{ border-color:var(--gold); }
|
||
input[type=text]::placeholder{ color:var(--ash); }
|
||
|
||
button{
|
||
font-family:'Zen Kaku Gothic New', sans-serif;
|
||
font-weight:700;
|
||
font-size:15px;
|
||
border:none;
|
||
border-radius:1px;
|
||
cursor:pointer;
|
||
transition:transform .12s ease, opacity .12s ease;
|
||
}
|
||
button:active{ transform:scale(0.97); }
|
||
button:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
|
||
button:disabled{ cursor:default; opacity:0.4; transform:none; }
|
||
|
||
.kick{
|
||
background:var(--crimson);
|
||
color:var(--bone);
|
||
padding:14px 22px;
|
||
white-space:nowrap;
|
||
}
|
||
.kick:hover:not(:disabled){ opacity:0.9; }
|
||
|
||
.result{
|
||
margin-top:24px;
|
||
padding-top:22px;
|
||
border-top:1px dashed var(--panel-line);
|
||
min-height:64px;
|
||
}
|
||
.result-empty{ color:var(--ash); font-size:14px; }
|
||
.fact{
|
||
font-family:'Anton', sans-serif;
|
||
font-weight:400;
|
||
font-size:clamp(20px, 3.4vw, 27px);
|
||
line-height:1.35;
|
||
letter-spacing:0.005em;
|
||
}
|
||
.fact::before{ content:"“ "; color:var(--gold); }
|
||
.fact::after{ content:" ”"; color:var(--gold); }
|
||
|
||
.rate-grid{
|
||
margin-top:18px;
|
||
display:grid;
|
||
grid-template-columns:1fr 1fr;
|
||
gap:8px;
|
||
}
|
||
.rate-btn{
|
||
background:transparent;
|
||
border:1px solid var(--panel-line);
|
||
color:var(--bone);
|
||
padding:10px 12px;
|
||
font-size:13px;
|
||
border-radius:1px;
|
||
text-align:left;
|
||
}
|
||
.rate-btn .emoji{ margin-right:6px; }
|
||
.rate-btn[data-rating="love"]:hover:not(:disabled){ border-color:var(--gold); color:var(--gold); }
|
||
.rate-btn[data-rating="dislike"]:hover:not(:disabled){ border-color:var(--crimson); color:var(--crimson); }
|
||
.rate-btn[data-rating="too_much"]:hover:not(:disabled),
|
||
.rate-btn[data-rating="not_enough"]:hover:not(:disabled){ border-color:var(--bone); }
|
||
.rate-btn.selected{ border-color:var(--gold); background:rgba(232,178,61,0.12); opacity:1; }
|
||
|
||
.rate-note{
|
||
margin-top:10px;
|
||
font-family:'JetBrains Mono', monospace;
|
||
font-size:12px;
|
||
color:var(--ash);
|
||
}
|
||
|
||
.belt-section .label-row{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:baseline;
|
||
margin-bottom:6px;
|
||
}
|
||
.belt-section .label{
|
||
font-family:'JetBrains Mono', monospace;
|
||
font-size:12px;
|
||
letter-spacing:0.1em;
|
||
color:var(--ash);
|
||
text-transform:uppercase;
|
||
}
|
||
.belt-explainer{
|
||
font-size:12px;
|
||
color:var(--ash);
|
||
margin:0 0 14px;
|
||
line-height:1.6;
|
||
}
|
||
.belt-name{
|
||
font-family:'Anton', sans-serif;
|
||
font-size:20px;
|
||
color:var(--gold);
|
||
}
|
||
.belt-strap{
|
||
position:relative;
|
||
height:26px;
|
||
background:var(--ink);
|
||
border:1px solid var(--panel-line);
|
||
border-radius:2px;
|
||
overflow:hidden;
|
||
}
|
||
.belt-fill{
|
||
height:100%;
|
||
width:0%;
|
||
background:linear-gradient(90deg, var(--crimson), var(--gold));
|
||
transition:width .5s ease;
|
||
}
|
||
.belt-stats{
|
||
margin-top:10px;
|
||
font-family:'JetBrains Mono', monospace;
|
||
font-size:11px;
|
||
color:var(--ash);
|
||
display:flex;
|
||
gap:14px;
|
||
flex-wrap:wrap;
|
||
}
|
||
.belt-stats b{ color:var(--bone); }
|
||
|
||
.hypothesis-box{
|
||
margin-top:18px;
|
||
padding-top:16px;
|
||
border-top:1px dashed var(--panel-line);
|
||
}
|
||
.hypothesis-box p{
|
||
margin:8px 0 0;
|
||
font-size:14px;
|
||
line-height:1.6;
|
||
color:var(--bone);
|
||
}
|
||
.hypothesis-box p.updating{ color:var(--ash); font-style:italic; }
|
||
.hypothesis-box ul{
|
||
list-style:none;
|
||
margin:8px 0 0;
|
||
padding:0;
|
||
display:flex;
|
||
flex-direction:column;
|
||
gap:6px;
|
||
}
|
||
.hypothesis-box li{
|
||
font-size:14px;
|
||
line-height:1.5;
|
||
color:var(--bone);
|
||
padding-left:14px;
|
||
position:relative;
|
||
}
|
||
.hypothesis-box li::before{
|
||
content:"—";
|
||
position:absolute;
|
||
left:0;
|
||
color:var(--gold);
|
||
}
|
||
.hypothesis-box li.placeholder{
|
||
color:var(--ash);
|
||
font-style:italic;
|
||
}
|
||
.hypothesis-box li.placeholder::before{ content:""; }
|
||
.hypothesis-box.updating li{ color:var(--ash); font-style:italic; }
|
||
.hypothesis-box li .note{
|
||
display:block;
|
||
color:var(--gold);
|
||
font-size:11px;
|
||
font-family:'JetBrains Mono', monospace;
|
||
margin-top:2px;
|
||
}
|
||
|
||
footer{
|
||
margin-top:34px;
|
||
color:var(--ash);
|
||
font-size:12px;
|
||
line-height:1.7;
|
||
}
|
||
footer code{ color:var(--gold); font-family:'JetBrains Mono', monospace; }
|
||
|
||
.unlock-panel{ padding:18px 28px; }
|
||
.unlock-row{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
gap:10px;
|
||
flex-wrap:wrap;
|
||
}
|
||
.unlock-row .label{
|
||
font-family:'JetBrains Mono', monospace;
|
||
font-size:11px;
|
||
letter-spacing:0.1em;
|
||
color:var(--ash);
|
||
}
|
||
.unlock-row .label.gemini{ color:var(--gold); }
|
||
.unlock-form{ display:flex; gap:6px; }
|
||
.unlock-form input{
|
||
background:var(--ink);
|
||
border:1px solid var(--panel-line);
|
||
color:var(--bone);
|
||
font-family:'JetBrains Mono', monospace;
|
||
font-size:12px;
|
||
padding:8px 10px;
|
||
border-radius:1px;
|
||
outline:none;
|
||
width:140px;
|
||
}
|
||
.unlock-form input:focus{ border-color:var(--gold); }
|
||
.unlock-form button{
|
||
background:transparent;
|
||
border:1px solid var(--panel-line);
|
||
color:var(--bone);
|
||
font-size:12px;
|
||
padding:8px 12px;
|
||
border-radius:1px;
|
||
}
|
||
.unlock-form button:hover:not(:disabled){ border-color:var(--gold); color:var(--gold); }
|
||
.unlock-note{
|
||
margin:8px 0 0;
|
||
font-size:11px;
|
||
color:var(--ash);
|
||
font-family:'JetBrains Mono', monospace;
|
||
}
|
||
|
||
.rick-overlay{
|
||
display:none;
|
||
position:fixed;
|
||
inset:0;
|
||
background:rgba(0,0,0,0.85);
|
||
z-index:999;
|
||
align-items:center;
|
||
justify-content:center;
|
||
padding:20px;
|
||
}
|
||
.rick-overlay.open{ display:flex; }
|
||
.rick-modal{
|
||
background:var(--panel);
|
||
border:1px solid var(--gold);
|
||
border-radius:2px;
|
||
padding:28px;
|
||
max-width:520px;
|
||
width:100%;
|
||
position:relative;
|
||
box-shadow:0 0 60px rgba(232,178,61,0.15);
|
||
}
|
||
.rick-close{
|
||
position:absolute;
|
||
top:10px; right:10px;
|
||
background:transparent;
|
||
border:none;
|
||
color:var(--ash);
|
||
font-size:22px;
|
||
line-height:1;
|
||
padding:4px 8px;
|
||
}
|
||
.rick-close:hover{ color:var(--bone); }
|
||
.rick-text{
|
||
font-family:'Anton', sans-serif;
|
||
font-size:clamp(22px, 5vw, 32px);
|
||
text-align:center;
|
||
margin:0 0 20px;
|
||
color:var(--bone);
|
||
}
|
||
.rick-text span{ color:var(--crimson); }
|
||
.rick-video-wrap{
|
||
position:relative;
|
||
width:100%;
|
||
padding-top:56.25%;
|
||
background:#000;
|
||
}
|
||
.rick-video-wrap iframe{
|
||
position:absolute;
|
||
top:0; left:0;
|
||
width:100%; height:100%;
|
||
border:0;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce){
|
||
.belt-fill, button{ transition:none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="stage">
|
||
<div class="top-row">
|
||
<div class="eyebrow" id="eyebrow">Gemma 4 — self-tuning nonsense engine</div>
|
||
<div class="lang-toggle">
|
||
<button id="langJa" class="active">JA</button>
|
||
<button id="langEn">EN</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h1 id="titleMain">CHUCK NORRIS<br><span>FACT</span> GENERATOR</h1>
|
||
<p class="sub" id="subText">
|
||
好きなテーマを入れると、Gemmaが不条理チャック・ノリス・ファクトを1つ生成します。
|
||
評価を返すほど、あなた好みのテイストに寄っていきます。
|
||
</p>
|
||
|
||
<section class="panel">
|
||
<div class="row">
|
||
<input id="topic" type="text" placeholder="例:ラーメン、締め切り、猫" maxlength="40" />
|
||
<button class="kick" id="genBtn">喰らえ</button>
|
||
</div>
|
||
|
||
<div class="result" id="result">
|
||
<p class="result-empty" id="resultEmpty">まだ何も生成していません。テーマを入れて「喰らえ」を押してください。</p>
|
||
</div>
|
||
|
||
<div class="rate-grid" id="rateGrid" style="display:none;">
|
||
<button class="rate-btn" data-rating="love"><span class="emoji">😍</span><span class="rlabel">大好き</span></button>
|
||
<button class="rate-btn" data-rating="too_much"><span class="emoji">🔥</span><span class="rlabel">やりすぎ!</span></button>
|
||
<button class="rate-btn" data-rating="not_enough"><span class="emoji">😐</span><span class="rlabel">まだまだ</span></button>
|
||
<button class="rate-btn" data-rating="dislike"><span class="emoji">👎</span><span class="rlabel">きらい</span></button>
|
||
</div>
|
||
<div class="rate-note" id="rateNote"></div>
|
||
</section>
|
||
|
||
<section class="panel belt-section">
|
||
<div class="label-row">
|
||
<span class="label" id="rankLabel">Personalization Rank</span>
|
||
<span class="belt-name" id="beltName">白帯</span>
|
||
</div>
|
||
<p class="belt-explainer" id="beltExplainer">
|
||
帯は「評価を何回返したか」の目安です。評価が増えるほど、Gemmaがあなたの好みについて
|
||
より具体的な仮説を持つようになります。
|
||
</p>
|
||
<div class="belt-strap"><div class="belt-fill" id="beltFill"></div></div>
|
||
<div class="belt-stats" id="beltStats">
|
||
<span>😍 <b id="cLove">0</b></span>
|
||
<span>🔥 <b id="cTooMuch">0</b></span>
|
||
<span>😐 <b id="cNotEnough">0</b></span>
|
||
<span>👎 <b id="cDislike">0</b></span>
|
||
</div>
|
||
<div class="hypothesis-box">
|
||
<span class="label" id="hypLabel">Current Hypothesis</span>
|
||
<ul id="traitsList"><li class="placeholder">まだ十分なデータがない。特定の好みの傾向は未確認。</li></ul>
|
||
</div>
|
||
<div class="hypothesis-box">
|
||
<span class="label" id="bestLabel">Best Of Collection</span>
|
||
<ul id="examplesList"></ul>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel unlock-panel">
|
||
<div class="unlock-row">
|
||
<span class="label" id="tierLabel">MODEL: GEMMA</span>
|
||
<div class="unlock-form">
|
||
<input id="unlockCode" type="text" placeholder="unlock code" maxlength="40" />
|
||
<button id="unlockBtn">Unlock</button>
|
||
</div>
|
||
</div>
|
||
<p class="unlock-note" id="unlockNote"></p>
|
||
</section>
|
||
|
||
<footer id="footerText">
|
||
モデル: <code>gemma-4-26b-a4b-it</code>(Gemini API経由)。
|
||
好み調整はモデルの重み更新ではなく、評価結果をもとにGemma自身が仮説を書き直す
|
||
サーバーサイドのプロンプトベース・パーソナライズです。
|
||
</footer>
|
||
</main>
|
||
|
||
<div class="rick-overlay" id="rickOverlay">
|
||
<div class="rick-modal">
|
||
<button class="rick-close" id="rickClose" aria-label="close">×</button>
|
||
<p class="rick-text">No, I am <span>CHUCK NORRIS.</span></p>
|
||
<div class="rick-video-wrap">
|
||
<iframe id="rickIframe" src="" title="???" frameborder="0"
|
||
allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const BELT_MAX = 20;
|
||
|
||
const UI = {
|
||
ja: {
|
||
eyebrow: "Gemma 4 — 自己流ネタ生成エンジン",
|
||
sub: "好きなテーマを入れると、Gemmaが不条理チャック・ノリス・ファクトを1つ生成します。評価を返すほど、あなた好みのテイストに寄っていきます。",
|
||
placeholder: "例:ラーメン、締め切り、猫",
|
||
genBtn: "喰らえ",
|
||
genBtnLoading: "生成中…",
|
||
resultEmpty: "まだ何も生成していません。テーマを入れて「喰らえ」を押してください。",
|
||
rankLabel: "パーソナライズ・ランク",
|
||
beltExplainer: "帯は「評価を何回返したか」の目安です。評価が増えるほど、Gemmaがあなたの好みについてより具体的な仮説を持つようになります。",
|
||
hypLabel: "現在の仮説",
|
||
bestLabel: "ベストネタ集",
|
||
noTraits: "まだ十分なデータがない。特定の好みの傾向は未確認。",
|
||
ratings: { love: "大好き", too_much: "やりすぎ!", not_enough: "まだまだ", dislike: "きらい" },
|
||
rateNoteDone: "評価を反映しました",
|
||
rateUpdating: "評価をもとに仮説を考え直し中…",
|
||
fallbackNote: "(APIキー未設定のためサンプル表示中)",
|
||
errorPrefix: "エラー: ",
|
||
networkError: "通信エラーが発生しました。",
|
||
footer: 'モデル: <code>gemma-4-26b-a4b-it</code>(Gemini API経由)。好み調整はモデルの重み更新ではなく、評価結果をもとにGemma自身が仮説を書き直すサーバーサイドのプロンプトベース・パーソナライズです。',
|
||
},
|
||
en: {
|
||
eyebrow: "Gemma 4 — self-tuning nonsense engine",
|
||
sub: "Enter a topic and Gemma generates one absurd Chuck Norris fact. The more you rate, the more it leans toward your taste.",
|
||
placeholder: "e.g. ramen, deadlines, cats",
|
||
genBtn: "Bring it on",
|
||
genBtnLoading: "Generating…",
|
||
resultEmpty: "Nothing generated yet. Type a topic and hit \"Bring it on\".",
|
||
rankLabel: "Personalization Rank",
|
||
beltExplainer: "The belt tracks how many ratings you've given. The more feedback, the more specific Gemma's hypothesis about your taste becomes.",
|
||
hypLabel: "Current Hypothesis",
|
||
bestLabel: "Best Of Collection",
|
||
noTraits: "Not enough data yet. No clear taste detected.",
|
||
ratings: { love: "Love it", too_much: "Too much!", not_enough: "Needs more", dislike: "Not for me" },
|
||
rateNoteDone: "Rating recorded",
|
||
rateUpdating: "Rethinking the hypothesis based on your rating…",
|
||
fallbackNote: "(showing a sample — no API key configured)",
|
||
errorPrefix: "Error: ",
|
||
networkError: "A network error occurred.",
|
||
footer: 'Model: <code>gemma-4-26b-a4b-it</code> (via the Gemini API). Personalization does not update model weights — it\'s server-side, prompt-based: Gemma rewrites its own hypothesis about your taste after each rating.',
|
||
}
|
||
};
|
||
|
||
let currentLang = 'ja';
|
||
let lastFact = "";
|
||
let lastTopic = "";
|
||
let lastFactId = "";
|
||
let isComposing = false;
|
||
|
||
const el = (id) => document.getElementById(id);
|
||
|
||
function applyStaticText(){
|
||
const t = UI[currentLang];
|
||
el('eyebrow').textContent = t.eyebrow;
|
||
el('subText').textContent = t.sub;
|
||
el('topic').placeholder = t.placeholder;
|
||
el('genBtn').textContent = t.genBtn;
|
||
el('resultEmpty').textContent = t.resultEmpty;
|
||
el('rankLabel').textContent = t.rankLabel;
|
||
el('beltExplainer').textContent = t.beltExplainer;
|
||
el('hypLabel').textContent = t.hypLabel;
|
||
el('bestLabel').textContent = t.bestLabel;
|
||
document.querySelectorAll('.rate-btn').forEach(btn => {
|
||
const key = btn.dataset.rating;
|
||
btn.querySelector('.rlabel').textContent = t.ratings[key];
|
||
});
|
||
el('footerText').innerHTML = t.footer;
|
||
}
|
||
|
||
function setLang(lang){
|
||
currentLang = lang;
|
||
el('langJa').classList.toggle('active', lang === 'ja');
|
||
el('langEn').classList.toggle('active', lang === 'en');
|
||
applyStaticText();
|
||
// 結果表示中のネタはそのままにして、静的UIのみ切り替える
|
||
refreshProfile();
|
||
}
|
||
|
||
function renderTraits(traits){
|
||
const ul = el('traitsList');
|
||
ul.innerHTML = '';
|
||
if(!traits || traits.length === 0){
|
||
const li = document.createElement('li');
|
||
li.className = 'placeholder';
|
||
li.textContent = UI[currentLang].noTraits;
|
||
ul.appendChild(li);
|
||
return;
|
||
}
|
||
traits.forEach(t => {
|
||
const li = document.createElement('li');
|
||
li.textContent = t;
|
||
ul.appendChild(li);
|
||
});
|
||
}
|
||
|
||
function showTraitsUpdating(){
|
||
const ul = el('traitsList');
|
||
ul.innerHTML = '';
|
||
const li = document.createElement('li');
|
||
li.className = 'placeholder';
|
||
li.textContent = UI[currentLang].rateUpdating;
|
||
ul.appendChild(li);
|
||
}
|
||
|
||
function renderExamples(examples){
|
||
const ul = el('examplesList');
|
||
ul.innerHTML = '';
|
||
(examples || []).forEach(e => {
|
||
const li = document.createElement('li');
|
||
const factSpan = document.createElement('span');
|
||
factSpan.textContent = e.fact;
|
||
const noteSpan = document.createElement('span');
|
||
noteSpan.className = 'note';
|
||
noteSpan.textContent = e.note;
|
||
li.appendChild(factSpan);
|
||
li.appendChild(noteSpan);
|
||
ul.appendChild(li);
|
||
});
|
||
}
|
||
|
||
function applyBelt(data){
|
||
el('beltName').textContent = data.belt;
|
||
if(data.counts){
|
||
el('cLove').textContent = data.counts.love ?? 0;
|
||
el('cTooMuch').textContent = data.counts.too_much ?? 0;
|
||
el('cNotEnough').textContent = data.counts.not_enough ?? 0;
|
||
el('cDislike').textContent = data.counts.dislike ?? 0;
|
||
}
|
||
const total = data.total ?? 0;
|
||
const pct = Math.min(100, Math.round((total / BELT_MAX) * 100));
|
||
el('beltFill').style.width = pct + '%';
|
||
renderTraits(data.traits);
|
||
renderExamples(data.examples);
|
||
}
|
||
|
||
function applyTier(tier){
|
||
const label = el('tierLabel');
|
||
if(tier === 'gemini'){
|
||
label.textContent = 'MODEL: GEMINI';
|
||
label.classList.add('gemini');
|
||
} else {
|
||
label.textContent = 'MODEL: GEMMA';
|
||
label.classList.remove('gemini');
|
||
}
|
||
}
|
||
|
||
async function refreshProfile(){
|
||
const res = await fetch('/api/profile?lang=' + currentLang);
|
||
const data = await res.json();
|
||
applyBelt(data);
|
||
applyTier(data.tier);
|
||
}
|
||
|
||
function resetRateButtons(){
|
||
document.querySelectorAll('.rate-btn').forEach(btn => {
|
||
btn.disabled = false;
|
||
btn.classList.remove('selected');
|
||
});
|
||
}
|
||
|
||
function openRickModal(){
|
||
el('rickIframe').src = 'https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1';
|
||
el('rickOverlay').classList.add('open');
|
||
}
|
||
|
||
function closeRickModal(){
|
||
el('rickOverlay').classList.remove('open');
|
||
el('rickIframe').src = '';
|
||
}
|
||
|
||
async function generateFact(){
|
||
const topic = el('topic').value.trim();
|
||
const btn = el('genBtn');
|
||
const t = UI[currentLang];
|
||
btn.disabled = true;
|
||
btn.textContent = t.genBtnLoading;
|
||
el('rateGrid').style.display = 'none';
|
||
el('rateNote').textContent = '';
|
||
resetRateButtons();
|
||
|
||
try{
|
||
const res = await fetch('/api/fact', {
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({topic, lang: currentLang})
|
||
});
|
||
const data = await res.json();
|
||
if(data.rickroll){
|
||
openRickModal();
|
||
return;
|
||
}
|
||
if(data.error){
|
||
el('result').innerHTML = `<p class="result-empty">${t.errorPrefix}${escapeHtml(data.error)}</p>`;
|
||
return;
|
||
}
|
||
lastFact = data.fact;
|
||
lastTopic = data.topic || topic;
|
||
lastFactId = data.fact_id || "";
|
||
el('result').innerHTML = `<p class="fact">${escapeHtml(lastFact)}</p>`;
|
||
el('rateGrid').style.display = 'grid';
|
||
applyTier(data.tier);
|
||
if(data.fallback){
|
||
el('rateNote').textContent = t.fallbackNote;
|
||
}
|
||
} catch(e){
|
||
el('result').innerHTML = `<p class="result-empty">${t.networkError}</p>`;
|
||
} finally{
|
||
btn.disabled = false;
|
||
btn.textContent = t.genBtn;
|
||
}
|
||
}
|
||
|
||
async function rate(rating, clickedBtn){
|
||
if(!lastFact || !lastFactId) return;
|
||
const t = UI[currentLang];
|
||
|
||
// 連打防止: 押した瞬間に全ボタンを無効化し、選んだものだけハイライト
|
||
document.querySelectorAll('.rate-btn').forEach(b => b.disabled = true);
|
||
clickedBtn.classList.add('selected');
|
||
|
||
showTraitsUpdating();
|
||
el('rateNote').textContent = '';
|
||
|
||
try{
|
||
const res = await fetch('/api/rate', {
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({fact:lastFact, topic:lastTopic, fact_id:lastFactId, rating, lang: currentLang})
|
||
});
|
||
const data = await res.json();
|
||
if(!data.error){
|
||
applyBelt(data);
|
||
el('rateNote').textContent = t.rateNoteDone;
|
||
}
|
||
} catch(e){
|
||
renderTraits(null);
|
||
}
|
||
// サーバー側でも fact_id 単位で一度きりに制限しているので、
|
||
// ボタンはこの評価が終わるまで(=次に生成するまで)無効のままにする
|
||
}
|
||
|
||
function escapeHtml(str){
|
||
const d = document.createElement('div');
|
||
d.textContent = str;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
el('genBtn').addEventListener('click', generateFact);
|
||
|
||
el('topic').addEventListener('compositionstart', () => { isComposing = true; });
|
||
el('topic').addEventListener('compositionend', () => { isComposing = false; });
|
||
el('topic').addEventListener('keydown', (e) => {
|
||
if(e.key !== 'Enter') return;
|
||
// 日本語入力の変換確定Enterでは送信しない(isComposing / keyCode 229 の二重チェック)
|
||
if(isComposing || e.isComposing || e.keyCode === 229) return;
|
||
generateFact();
|
||
});
|
||
|
||
document.querySelectorAll('.rate-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => rate(btn.dataset.rating, btn));
|
||
});
|
||
|
||
el('langJa').addEventListener('click', () => setLang('ja'));
|
||
el('langEn').addEventListener('click', () => setLang('en'));
|
||
|
||
el('rickClose').addEventListener('click', closeRickModal);
|
||
el('rickOverlay').addEventListener('click', (e) => {
|
||
if(e.target.id === 'rickOverlay') closeRickModal();
|
||
});
|
||
|
||
el('unlockBtn').addEventListener('click', async () => {
|
||
const code = el('unlockCode').value.trim();
|
||
const note = el('unlockNote');
|
||
if(!code) return;
|
||
const btn = el('unlockBtn');
|
||
btn.disabled = true;
|
||
try{
|
||
const res = await fetch('/api/unlock', {
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({code})
|
||
});
|
||
const data = await res.json();
|
||
applyTier(data.tier);
|
||
note.textContent = data.ok
|
||
? (currentLang === 'ja' ? '✨ Geminiモードに切り替わりました' : '✨ Switched to Gemini mode')
|
||
: (currentLang === 'ja' ? 'コードが違うようです' : 'That code doesn\'t look right');
|
||
} catch(e){
|
||
note.textContent = currentLang === 'ja' ? '通信エラーが発生しました。' : 'A network error occurred.';
|
||
} finally{
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
|
||
applyStaticText();
|
||
refreshProfile();
|
||
</script>
|
||
</body>
|
||
</html>
|