Spaces:
Running
Running
| <html lang="cs"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🔥 České Contexto</title> | |
| <style> | |
| body { font-family: system-ui, -apple-system, sans-serif; max-width: 500px; margin: 30px auto; padding: 20px; background: #f8f9fa; color: #212529; } | |
| .card { background: white; padding: 24px; border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); margin-bottom: 20px; } | |
| h1 { text-align: center; color: #d97706; margin-top: 0; } | |
| input, button { width: 100%; padding: 12px; margin-top: 8px; border-radius: 8px; border: 1px solid #ced4da; box-sizing: border-box; font-size: 16px; } | |
| button { background: #d97706; color: white; border: none; font-weight: bold; cursor: pointer; transition: background 0.2s; } | |
| button:hover { background: #b45309; } | |
| button:disabled { background: #adb5bd; cursor: not-allowed; } | |
| .result-item { margin: 8px 0; padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; font-weight: bold; } | |
| .hot { background: #fee2e2; color: #dc2626; } | |
| .warm { background: #fef3c7; color: #d97706; } | |
| .cold { background: #e0f2fe; color: #0284c7; } | |
| #status { text-align: center; font-size: 0.9em; color: #6c757d; margin-bottom: 15px; } | |
| details { margin-bottom: 15px; background: #f1f3f5; padding: 10px; border-radius: 8px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>🔥 České Contexto</h1> | |
| <div id="status">⏳ Načítám AI model (stahuje se jen při prvním načtení)...</div> | |
| <details> | |
| <summary>⚙️ Nastavení tajného slova (pro správce)</summary> | |
| <input type="password" id="secretWordInput" value="pivo" placeholder="Zadej tajné slovo"> | |
| </details> | |
| <form id="guessForm"> | |
| <input type="text" id="guessInput" placeholder="Zadej české slovo..." disabled required autocomplete="off"> | |
| <button type="submit" id="submitBtn" disabled>Hádat</button> | |
| </form> | |
| </div> | |
| <div class="card"> | |
| <h3 style="margin-top:0;">Tvoje pokusy:</h3> | |
| <div id="results"></div> | |
| </div> | |
| <script type="module"> | |
| import { pipeline, cos_sim } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.14.0'; | |
| let extractor = null; | |
| let secretEmbedding = null; | |
| const guesses = []; | |
| const statusEl = document.getElementById('status'); | |
| const guessInput = document.getElementById('guessInput'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const secretInput = document.getElementById('secretWordInput'); | |
| const resultsEl = document.getElementById('results'); | |
| // Vyvážená kalibrace teploty (každá setina podobnosti nad 0.72 přidá ~3 °C) | |
| function calculateTemperature(sim) { | |
| const minSim = 0.72; // Hranice pro zcela nesouvisející slova | |
| if (sim <= minSim) { | |
| // Zcela mimo slova zůstanou v modrém pásmu (0 až 15 °C) | |
| return Math.max(0, Math.round((sim / minSim) * 15 * 10) / 10); | |
| } | |
| // Plynulé lineární roztažení od 15 °C do 100 °C | |
| const norm = (sim - minSim) / (1.0 - minSim); | |
| const score = 15 + (norm * 85); | |
| return Math.min(100, Math.round(score * 10) / 10); | |
| } | |
| async function init() { | |
| try { | |
| extractor = await pipeline('feature-extraction', 'Xenova/multilingual-e5-small'); | |
| statusEl.innerText = "✅ Kalibrovaný AI model načten! Můžeš hádat."; | |
| guessInput.disabled = false; | |
| submitBtn.disabled = false; | |
| await updateSecret(); | |
| } catch (e) { | |
| statusEl.innerText = "❌ Chyba při načítání modelu: " + e.message; | |
| } | |
| } | |
| async function getEmbedding(text) { | |
| // Symetrický prefix "query: " výrazně zpřesňuje porovnávání jednotlivých slov | |
| const output = await extractor('query: ' + text.trim().toLowerCase(), { pooling: 'mean', normalize: true }); | |
| return output.data; | |
| } | |
| async function updateSecret() { | |
| if (!extractor) return; | |
| const word = secretInput.value.trim().toLowerCase(); | |
| if (word) { | |
| secretEmbedding = await getEmbedding(word); | |
| } | |
| } | |
| secretInput.addEventListener('change', updateSecret); | |
| document.getElementById('guessForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const word = guessInput.value.trim().toLowerCase(); | |
| if (!word || !secretEmbedding) return; | |
| guessInput.value = ''; | |
| const guessEmb = await getEmbedding(word); | |
| const rawSim = cos_sim(secretEmbedding, guessEmb); | |
| // Aplikujeme kalibrační vzorec | |
| const score = calculateTemperature(rawSim); | |
| guesses.push({ word, score }); | |
| guesses.sort((a, b) => b.score - a.score); | |
| renderResults(); | |
| }); | |
| function renderResults() { | |
| resultsEl.innerHTML = ''; | |
| guesses.forEach(g => { | |
| const div = document.createElement('div'); | |
| let cls = 'cold'; | |
| let icon = '🔵'; | |
| if (g.score >= 75) { cls = 'hot'; icon = '🔴'; } | |
| else if (g.score >= 45) { cls = 'warm'; icon = '🟠'; } | |
| div.className = `result-item ${cls}`; | |
| div.innerHTML = `<span>${icon} ${g.word}</span> <span>${g.score} °C</span>`; | |
| resultsEl.appendChild(div); | |
| }); | |
| } | |
| init(); | |
| </script> |