word-conjure / index.html
r3gm's picture
Update index.html
c2083a2 verified
Raw
History Blame Contribute Delete
43.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<title>WordConjure: Multimodal Vocabulary Practice</title>
<!-- Tailwind CSS CDN for unified styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Premium sans-serif typography suitable for high school, university, and adult students -->
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', 'Plus Jakarta Sans', sans-serif;
}
h1, h2, h3, h4, th, .btn-brand {
font-family: 'Plus Jakarta Sans', sans-serif;
}
/* Touch scrollbars styling to avoid blocking touch inputs on mobile wrappers */
.scrollbar-thin::-webkit-scrollbar {
height: 4px;
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 2px;
}
</style>
</head>
<body class="bg-gradient-to-br from-[#f8fafc] via-[#f1f5f9] to-[#e2e8f0] min-h-screen p-4 md:p-8 text-slate-800">
<!-- Mobile-Optimized Language Selection Modal with swipe/scroll bounds checks -->
<div id="welcomeModal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 overflow-y-auto" style="-webkit-overflow-scrolling: touch; touch-action: auto;">
<div class="bg-white p-6 md:p-8 rounded-2xl shadow-2xl max-w-lg w-full border border-slate-200 flex flex-col items-center text-center my-auto max-h-[85vh] overflow-y-auto scrollbar-thin" style="-webkit-overflow-scrolling: touch;">
<div class="w-14 h-14 bg-indigo-50 rounded-full flex items-center justify-center mb-4 text-2xl shadow-inner">
🌍
</div>
<h2 class="text-xl font-bold text-slate-900 mb-1">Welcome</h2>
<p class="text-slate-500 font-medium text-xs mb-5 leading-relaxed">
Select your preferred language to customize the workspace application interface.<br>
Seleccione su idioma de preferencia para configurar la aplicação.
</p>
<div class="w-full grid grid-cols-2 gap-2.5">
<button onclick="selectInitialLang('English')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">English</button>
<button onclick="selectInitialLang('Spanish')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">Español</button>
<button onclick="selectInitialLang('French')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">Français</button>
<button onclick="selectInitialLang('German')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">Deutsch</button>
<button onclick="selectInitialLang('Chinese')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">中文</button>
<button onclick="selectInitialLang('Japanese')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">日本語</button>
<button onclick="selectInitialLang('Korean')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">한국어</button>
<button onclick="selectInitialLang('Russian')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">Русский</button>
<button onclick="selectInitialLang('Portuguese')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">Português</button>
<button onclick="selectInitialLang('Italian')" class="py-2.5 px-3 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 rounded-xl font-semibold text-xs transition-all shadow-xs">Italiano</button>
</div>
</div>
</div>
<!-- Centered Structured Workspace Container -->
<div class="w-full max-w-3xl mx-auto space-y-4">
<!-- Submission Details Accordion: Located in the upper section, short size, expanding vertically -->
<details class="bg-white rounded-2xl shadow-xs p-4 border border-slate-200 cursor-pointer group transition-all duration-300">
<summary class="font-bold text-xs text-slate-500 select-none flex justify-between items-center outline-none">
<span class="flex items-center gap-2">📋 Submission Details & Verification Checklist (Click to Expand)</span>
<span class="text-[10px] text-slate-400 group-open:rotate-180 transition-transform"></span>
</summary>
<div class="mt-3 border-t border-slate-100 pt-3 cursor-default text-xs space-y-3 text-slate-600 font-normal leading-relaxed">
<p>This application is designed for the <strong>Build Small, Play Big Hackathon</strong>. Below is the compliance tracking checklist for validation requirements:</p>
<ul class="space-y-2.5">
<li class="flex items-start gap-2">
<span class="text-emerald-600 font-bold mt-0.5"></span>
<div>
<strong class="text-slate-800">REQ-01: Stay under 32B Parameter Limit</strong>
<p class="text-slate-400">Operates 100% within constraints with a cumulative parameter count of <strong>17.3 Billion parameters</strong>:</p>
<ul class="list-disc pl-4 text-slate-400 mt-0.5 space-y-0.5">
<li>Language Engine: bartowski/Qwen2.5-14B-Instruct-GGUF (14.0B)</li>
<li>Audio TTS Synthesis: Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice (1.7B)</li>
<li>Visual Flashcard Diffusion: Efficient-Large-Model/Sana_Sprint_1.6B (1.6B)</li>
</ul>
</div>
</li>
<li class="flex items-start gap-2">
<span class="text-emerald-600 font-bold mt-0.5"></span>
<div>
<strong class="text-slate-800">REQ-02: Ship a Gradio App</strong>
<p class="text-slate-500">Deploys dynamically via a Gradio mounting container leveraging a FastAPI backend architecture.</p>
</div>
</li>
<li class="flex items-start gap-2">
<span class="text-emerald-600 font-bold mt-0.5"></span>
<div>
<strong class="text-slate-800">REQ-03 & REQ-04: Video Demonstration & Social Media Post</strong>
<p class="text-slate-500">The illustrative video walkthrough is hosted and embedded directly inside the social media announcement post:</p>
<div class="mt-1.5 p-2 bg-slate-50 rounded border border-slate-100 text-slate-500 flex items-center gap-2">
🎥 <span>Announcement & Demo: <a href="https://x.com/_r3gm/status/2066206438712558019" target="_blank" rel="noopener noreferrer" class="text-indigo-600 hover:underline font-semibold">View Announcement & Demo Video on X</a></span>
</div>
</div>
</li>
<li class="flex items-start gap-2">
<span class="text-emerald-600 font-bold mt-0.5"></span>
<div>
<strong class="text-slate-800">REQ-05: Mind the GPU Limit</strong>
<p class="text-slate-500">Optimized for low footprint environments using sequential processing buffers and native FP16 execution maps.</p>
</div>
</li>
</ul>
</div>
</details>
<!-- Main Workspace Card (Premium Minimal Style) -->
<div class="bg-white rounded-2xl shadow-sm p-6 md:p-8 border border-slate-200/80 transition-all duration-300">
<!-- App Language Selection Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-4 border-b border-slate-100 pb-4">
<div>
<h1 id="uiTitle" class="text-xl md:text-2xl font-bold text-slate-900 tracking-tight flex items-center gap-2">
📘 WordConjure: Multimodal Vocabulary Practice
</h1>
<p id="uiSubtitle" class="text-xs text-slate-500 font-medium">An educational system that automatically generates context-rich translations, clean pronunciation guides, and visual cards to optimize foreign language acquisition.</p>
</div>
<div class="text-right w-full sm:w-auto">
<label id="uiAppLangLabel" class="block text-[9px] font-bold text-slate-400 uppercase tracking-wider mb-1">App Language</label>
<select id="guiLangSelect" class="w-full sm:w-auto p-2 border border-slate-200 rounded-xl text-xs font-semibold bg-slate-50 text-slate-700 outline-none cursor-pointer">
<option value="English">English</option>
<option value="Spanish">Español</option>
<option value="French">Français</option>
<option value="German">Deutsch</option>
<option value="Chinese">中文</option>
<option value="Japanese">日本語</option>
<option value="Korean">한국어</option>
<option value="Russian">Русский</option>
<option value="Portuguese">Português</option>
<option value="Italian">Italiano</option>
</select>
</div>
</div>
<!-- Localized 2-Sentence Tutorial Section -->
<div class="mb-6 p-3 bg-slate-50 border border-slate-200/60 rounded-xl flex items-start gap-2.5 text-xs text-slate-600 font-normal leading-relaxed">
<span class="text-sm mt-0.5">💡</span>
<p id="uiTutorial">
<strong>First:</strong> Select your target preferences below and click <strong>"Generate Vocabulary"</strong> to load your custom multimodal study deck.
<strong>Then:</strong> Navigate to the <strong>"Practice Exercises"</strong> tab above to interactively test your memory through structured games.
</p>
</div>
<!-- Dashboard Navigation Tabs -->
<div class="flex gap-3 mb-6">
<button id="tabVocabBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white shadow-xs rounded-xl px-4 py-2 font-bold text-xs transition-all outline-none">📖 Vocabulary Deck</button>
<button id="tabGamesBtn" class="bg-white text-slate-600 hover:bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 font-bold text-xs transition-all outline-none">🎮 Practice Exercises</button>
</div>
<!-- TAB 1: Vocabulary Workspace -->
<div id="tabVocabContent" class="block">
<!-- Advanced Settings Dropdown -->
<details class="mb-5 bg-slate-50 border border-slate-200 rounded-2xl p-4 cursor-pointer group shadow-xs">
<summary id="uiSettingsTitle" class="font-bold text-xs text-slate-700 select-none flex justify-between items-center outline-none">
<span>⚙️ Session Configuration</span>
<span class="text-xs text-slate-400 group-open:rotate-180 transition-transform"></span>
</summary>
<div class="mt-4 border-t border-slate-200 pt-4 cursor-default flex flex-col gap-4">
<!-- Word count selector -->
<div>
<label id="uiWordCountLabel" class="block text-xs font-bold text-slate-600 uppercase tracking-wider mb-2">🔢 Vocabulary size to generate:</label>
<input type="range" id="wordCount" min="1" max="10" value="5" class="w-full accent-indigo-600 h-1 bg-slate-200 rounded-lg cursor-pointer">
<div class="flex justify-between text-[10px] text-slate-500 mt-1 font-semibold">
<span>1</span>
<span id="wordCountVal" class="font-bold text-indigo-600">5 words</span>
<span>10</span>
</div>
</div>
<!-- Explanation language selector -->
<div>
<label id="uiExplLangLabel" class="block text-xs font-bold text-slate-600 uppercase tracking-wider mb-2">🗣️ Explanation Language</label>
<div class="flex flex-col sm:flex-row gap-4 mt-1">
<label class="inline-flex items-center text-xs text-slate-700 font-semibold cursor-pointer">
<input type="radio" name="explLang" value="source" checked class="accent-indigo-600 h-4 w-4 mr-2">
<span id="uiExplSource">App Language</span>
</label>
<label class="inline-flex items-center text-xs text-slate-700 font-semibold cursor-pointer">
<input type="radio" name="explLang" value="target" class="accent-indigo-600 h-4 w-4 mr-2">
<span id="uiExplTarget">Language to Practice</span>
</label>
</div>
</div>
</div>
</details>
<!-- Language Options (Source is Hidden programmatically) -->
<div class="flex flex-col sm:flex-row gap-3 mb-5 items-end">
<div id="srcLangContainer" class="hidden">
<label id="uiSourceLangLabel" class="block text-[10px] font-bold text-slate-500 uppercase tracking-wider mb-1.5">🌍 Source Language (Native)</label>
<select id="srcLang" class="w-full p-2 border border-slate-200 rounded-xl text-xs bg-white outline-none">
<option value="English">English</option>
<option value="Spanish">Spanish</option>
<option value="French">French</option>
<option value="German">German</option>
<option value="Chinese">Chinese</option>
<option value="Japanese">Japanese</option>
<option value="Korean">Korean</option>
<option value="Russian">Russian</option>
<option value="Portuguese">Portuguese</option>
<option value="Italian">Italian</option>
</select>
</div>
<div class="flex-1 w-full">
<label id="uiTargetLangLabel" class="block text-[10px] font-bold text-slate-600 uppercase tracking-wider mb-1.5">🎯 Target Language to Practice</label>
<select id="tgtLang" class="w-full p-3 border border-slate-200 rounded-xl text-xs font-semibold bg-slate-50 text-slate-800 outline-none">
<!-- Filled dynamically based on chosen App Language -->
</select>
</div>
<button id="translateBtn" class="w-full sm:w-auto px-6 py-3 bg-indigo-600 hover:bg-indigo-700 active:translate-y-0.5 text-white font-bold text-xs rounded-xl shadow-xs transition-all">⚡ Generate Vocabulary</button>
</div>
<!-- Vocabulary Table Scroll Container (Touch/Swipe Optimized for Mobile Devices) -->
<div class="overflow-x-auto w-full rounded-xl border border-slate-200 shadow-xs" style="-webkit-overflow-scrolling: touch; touch-action: pan-x pan-y;">
<table class="min-w-full divide-y divide-slate-200 text-xs table-auto">
<thead class="bg-slate-50">
<tr>
<th id="thOriginal" class="px-4 py-3 text-left text-[10px] font-bold text-slate-500 uppercase tracking-wider">Target Word</th>
<th id="thTranslation" class="px-4 py-3 text-left text-[10px] font-bold text-slate-500 uppercase tracking-wider">Translation</th>
<th id="thExplanation" class="px-4 py-3 text-left text-[10px] font-bold text-slate-500 uppercase tracking-wider max-w-xs">Contextual Definition</th>
<th id="thVisual" class="px-4 py-3 text-center text-[10px] font-bold text-slate-500 uppercase tracking-wider w-24">Visual Card</th>
<th id="thAudio" class="px-4 py-3 text-center text-[10px] font-bold text-slate-500 uppercase tracking-wider w-20">Pronunciation</th>
</tr>
</thead>
<tbody id="resultsBody" class="bg-white divide-y divide-slate-100">
<tr>
<td colspan="5" id="tdInitialMsg" class="px-4 py-12 text-center text-xs font-semibold text-slate-400">Configure preferences and generate vocabulary above to populate target dataset.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- TAB 2: Games Hub -->
<div id="tabGamesContent" class="hidden">
<!-- Game Selection Panel -->
<div id="gamesMenu">
<h3 id="uiGameSelectionTitle" class="text-sm font-bold text-slate-800 mb-4">Select Your Practice Exercise</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Flashcard Memory Match Selection Card -->
<div class="border border-slate-200 bg-slate-50/40 rounded-xl p-5 hover:shadow-xs transition-all duration-200 flex flex-col justify-between">
<div>
<div class="text-2xl mb-2">🧩</div>
<h4 id="uiGameMemoryTitle" class="text-xs font-bold text-slate-800 mb-1">Interactive Memory Association</h4>
<p id="uiGameMemoryDesc" class="text-[11px] text-slate-500 font-medium mb-3 leading-relaxed">Match target terms with their translated counterparts and generated visual cards to reinforce semantic retention.</p>
</div>
<button id="playMemoryBtn" class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 active:translate-y-0.5 text-white rounded-xl text-[10px] font-semibold transition-all cursor-pointer shadow-xs">Start Practice</button>
</div>
<!-- Word Monster Selection Card -->
<div class="border border-slate-200 bg-slate-50/40 rounded-xl p-5 hover:shadow-xs transition-all duration-200 flex flex-col justify-between">
<div>
<div class="text-2xl mb-2">👾</div>
<h4 id="uiGameMonsterTitle" class="text-xs font-bold text-slate-800 mb-1">Target Word Association (Word Monster)</h4>
<p id="uiGameMonsterDesc" class="text-[11px] text-slate-500 font-medium mb-3 leading-relaxed">Select and present the appropriate translation card that accurately matches the requested target concept.</p>
</div>
<button id="playMonsterBtn" class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 active:translate-y-0.5 text-white rounded-xl text-[10px] font-semibold transition-all cursor-pointer shadow-xs">Start Practice</button>
</div>
<!-- Audio-Visual Quiz Selection Card -->
<div class="border border-slate-200 bg-slate-50/40 rounded-xl p-5 hover:shadow-xs transition-all duration-200 flex flex-col justify-between">
<div>
<div class="text-2xl mb-2">🏆</div>
<h4 id="uiGameQuizTitle" class="text-xs font-bold text-slate-800 mb-1">Acoustic & Visual Assessment</h4>
<p id="uiGameQuizDesc" class="text-[11px] text-slate-500 font-medium mb-3 leading-relaxed">Examine the audio pronunciation or visual cue and identify the correct matching term from multiple choices.</p>
</div>
<button id="playQuizBtn" class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 active:translate-y-0.5 text-white rounded-xl text-[10px] font-semibold transition-all cursor-pointer shadow-xs">Start Practice</button>
</div>
<!-- Scrambled Word Selection Card -->
<div class="border border-slate-200 bg-slate-50/40 rounded-xl p-5 hover:shadow-xs transition-all duration-200 flex flex-col justify-between">
<div>
<div class="text-2xl mb-2">✏️</div>
<h4 id="uiGameScrambleTitle" class="text-xs font-bold text-slate-800 mb-1">Orthographic Word Reconstruction</h4>
<p id="uiGameScrambleDesc" class="text-[11px] text-slate-500 font-medium mb-3 leading-relaxed">Reassemble scrambled character sequences into the correct spelling of the translated target term.</p>
</div>
<button id="playScrambleBtn" class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 active:translate-y-0.5 text-white rounded-xl text-[10px] font-semibold transition-all cursor-pointer shadow-xs">Start Practice</button>
</div>
<!-- Visual Word Linker Selection Card -->
<div class="border border-slate-200 bg-slate-50/40 rounded-xl p-5 hover:shadow-xs transition-all duration-200 flex flex-col justify-between">
<div>
<div class="text-2xl mb-2">🔗</div>
<h4 id="uiGameLinkerTitle" class="text-xs font-bold text-slate-800 mb-1">Visual Term Correspondence Mapping</h4>
<p id="uiGameLinkerDesc" class="text-[11px] text-slate-500 font-medium mb-3 leading-relaxed">Perform bilateral matching between generated visual cues and target practice words to solidify associations.</p>
</div>
<button id="playLinkerBtn" class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 active:translate-y-0.5 text-white rounded-xl text-[10px] font-semibold transition-all cursor-pointer shadow-xs">Start Practice</button>
</div>
</div>
</div>
<!-- Active Game Stage -->
<div id="gameStage" class="hidden border border-slate-200 rounded-xl p-5 bg-white shadow-xs">
<div class="flex justify-between items-center mb-5 pb-2 border-b border-slate-100">
<button id="backToMenuBtn" class="text-xs font-bold text-slate-400 hover:text-indigo-600 transition-all outline-none">← Back to Menu</button>
<div id="gameScoreDisplay" class="text-xs font-bold text-indigo-600 bg-indigo-50 px-4 py-1.5 rounded-full">Score: 0</div>
</div>
<div id="gameArea"></div>
</div>
</div>
</div>
</div>
<!-- Modal for full size image preview -->
<div id="imageModal" class="hidden fixed inset-0 bg-slate-900/60 flex items-center justify-center p-4 z-50 transition-opacity" onclick="closeModal()">
<div class="bg-white p-6 rounded-2xl shadow-xl max-w-sm w-full border border-slate-200 flex flex-col items-center" onclick="event.stopPropagation()">
<h3 id="modalTitle" class="text-base font-bold text-slate-900 mb-4 capitalize">Word Card</h3>
<img id="modalImage" class="w-64 h-64 border border-slate-100 rounded-xl shadow-inner object-contain bg-slate-50" src="" alt="Full preview">
<button class="mt-5 px-6 py-2 bg-slate-50 hover:bg-slate-100 active:translate-y-0.5 rounded-xl text-xs font-bold text-slate-700 transition-all border border-slate-200" onclick="closeModal()">Close</button>
</div>
</div>
<!-- Gradio Connection logic and interface events -->
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
// Global states to maintain vocabulary database and loaded localizations
window.loadedVocabulary = [];
window.localizations = {};
const translateBtn = document.getElementById("translateBtn");
const resultsBody = document.getElementById("resultsBody");
const guiLangSelect = document.getElementById("guiLangSelect");
const wordCount = document.getElementById("wordCount");
const wordCountVal = document.getElementById("wordCountVal");
// Tab elements
const tabVocabBtn = document.getElementById("tabVocabBtn");
const tabGamesBtn = document.getElementById("tabGamesBtn");
const tabVocabContent = document.getElementById("tabVocabContent");
const tabGamesContent = document.getElementById("tabGamesContent");
// Game Navigation controls
const gamesMenu = document.getElementById("gamesMenu");
const gameStage = document.getElementById("gameStage");
const gameArea = document.getElementById("gameArea");
const backToMenuBtn = document.getElementById("backToMenuBtn");
const playMemoryBtn = document.getElementById("playMemoryBtn");
const playMonsterBtn = document.getElementById("playMonsterBtn");
const playQuizBtn = document.getElementById("playQuizBtn");
const playScrambleBtn = document.getElementById("playScrambleBtn");
const playLinkerBtn = document.getElementById("playLinkerBtn");
let gradioClient = null;
async function getGradioClient() {
if (!gradioClient) {
gradioClient = await Client.connect(window.location.origin);
}
return gradioClient;
}
// Global selection function for Startup Modal
window.selectInitialLang = function(lang) {
guiLangSelect.value = lang;
document.getElementById("welcomeModal").classList.add("hidden");
handleAppLanguageChange(lang);
}
// Handles App Language changes and cascading UI updates
async function handleAppLanguageChange(lang) {
document.getElementById("srcLang").value = lang;
populatePracticeLanguages(lang);
await loadLocalization();
}
// Dynamically populates targets excluding the chosen App Language
function populatePracticeLanguages(appLang) {
const tgtLangSelect = document.getElementById("tgtLang");
tgtLangSelect.innerHTML = "";
const list = [
{ value: "English", text: "English" },
{ value: "Spanish", text: "Spanish" },
{ value: "French", text: "French" },
{ value: "German", text: "German" },
{ value: "Chinese", text: "Chinese" },
{ value: "Japanese", text: "Japanese" },
{ value: "Korean", text: "Korean" },
{ value: "Russian", text: "Russian" },
{ value: "Portuguese", text: "Portuguese" },
{ value: "Italian", text: "Italian" }
];
const filtered = list.filter(item => item.value !== appLang);
filtered.forEach(opt => {
const el = document.createElement("option");
el.value = opt.value;
el.innerText = opt.text;
tgtLangSelect.appendChild(el);
});
}
// 1. Fetch Localization strings based on chosen App Language
async function loadLocalization() {
try {
const client = await getGradioClient();
const response = await client.predict("/get_localization", [guiLangSelect.value]);
window.localizations = response.data[0];
applyLocalization();
} catch (e) {
console.error("Localization loading error:", e);
}
}
// 2. Apply dictionary mappings to DOM nodes
function applyLocalization() {
const loc = window.localizations;
const currentAppLang = guiLangSelect.value;
document.getElementById("uiTitle").innerText = loc.title;
document.getElementById("uiSubtitle").innerText = loc.subtitle;
document.getElementById("uiTutorial").innerHTML = loc.tutorial_text;
document.getElementById("uiSettingsTitle").firstElementChild.innerText = loc.settings;
document.getElementById("uiWordCountLabel").innerText = loc.word_count_label;
document.getElementById("uiExplLangLabel").innerText = loc.expl_lang_label;
document.getElementById("uiSourceLangLabel").innerText = loc.source_lang;
document.getElementById("uiTargetLangLabel").innerText = loc.target_lang;
document.getElementById("translateBtn").innerText = loc.btn_get_words;
tabVocabBtn.innerText = loc.tab_vocab;
tabGamesBtn.innerText = loc.tab_games;
document.getElementById("thOriginal").innerText = loc.original_header;
document.getElementById("thTranslation").innerText = loc.translation_header;
document.getElementById("thExplanation").innerText = loc.explanation_header;
document.getElementById("thVisual").innerText = loc.visual_header;
document.getElementById("thAudio").innerText = loc.audio_header;
document.getElementById("uiGameSelectionTitle").innerText = loc.game_selection_title;
document.getElementById("uiGameMemoryTitle").innerText = loc.game_memory_title;
document.getElementById("uiGameMemoryDesc").innerText = loc.game_memory_desc;
document.getElementById("uiGameMonsterTitle").innerText = loc.game_monster_title;
document.getElementById("uiGameMonsterDesc").innerText = loc.game_monster_desc;
document.getElementById("uiGameQuizTitle").innerText = loc.game_quiz_title;
document.getElementById("uiGameQuizDesc").innerText = loc.game_quiz_desc;
document.getElementById("uiGameScrambleTitle").innerText = loc.game_scramble_title;
document.getElementById("uiGameScrambleDesc").innerText = loc.game_scramble_desc;
document.getElementById("uiGameLinkerTitle").innerText = loc.game_linker_title;
document.getElementById("uiGameLinkerDesc").innerText = loc.game_linker_desc;
document.getElementById("uiAppLangLabel").innerText = loc.app_lang_label || "App Language";
document.getElementById("uiExplSource").innerText = `${loc.app_lang_label || "App Language"} (${currentAppLang})`;
document.getElementById("uiExplTarget").innerText = loc.target_lang;
}
// 3. Tab Switching events with clean layouts
tabVocabBtn.addEventListener("click", () => {
tabVocabContent.classList.replace("hidden", "block");
tabGamesContent.classList.replace("block", "hidden");
tabVocabBtn.className = "bg-indigo-600 text-white shadow-xs rounded-xl px-4 py-2 font-bold text-xs transition-all outline-none";
tabGamesBtn.className = "bg-white text-slate-600 hover:bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 font-bold text-xs transition-all outline-none";
});
tabGamesBtn.addEventListener("click", () => {
tabVocabContent.classList.replace("block", "hidden");
tabGamesContent.classList.replace("hidden", "block");
tabGamesBtn.className = "bg-indigo-600 text-white shadow-xs rounded-xl px-4 py-2 font-bold text-xs transition-all outline-none";
tabVocabBtn.className = "bg-white text-slate-600 hover:bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 font-bold text-xs transition-all outline-none";
});
wordCount.addEventListener("input", (e) => {
wordCountVal.innerText = `${e.target.value} words`;
});
guiLangSelect.addEventListener("change", (e) => {
handleAppLanguageChange(e.target.value);
});
// Fetch translation vocabulary
async function fetchTranslations() {
const src = document.getElementById("srcLang").value;
const tgt = document.getElementById("tgtLang").value;
const count = parseInt(wordCount.value, 10);
const explLang = document.querySelector('input[name="explLang"]:checked').value;
resultsBody.innerHTML = `
<tr>
<td colspan="5" class="px-4 py-12 text-center text-xs text-indigo-600 font-semibold">
<span class="inline-block animate-pulse text-xs mb-1">⚡ Processing Multimodal Vocab Generation...</span>
<div class="text-[10px] text-slate-400 font-medium">Generating explanations, visual context, and native audio tracks sequentially...</div>
</td>
</tr>
`;
try {
const client = await getGradioClient();
const response = await client.predict("/get_translations", [src, tgt, count, explLang]);
window.loadedVocabulary = response.data[0];
resultsBody.innerHTML = "";
window.loadedVocabulary.forEach(row => {
const [original, translated, explanation, visualB64, audioB64] = row;
const tr = document.createElement("tr");
tr.className = "hover:bg-slate-50 transition-colors";
tr.innerHTML = `
<td class="px-4 py-4 text-slate-800 font-semibold text-xs">${original}</td>
<td class="px-4 py-4 text-indigo-600 font-bold text-sm">${translated}</td>
<td class="px-4 py-4 text-slate-500 text-[11px] font-medium leading-relaxed max-w-xs break-words">${explanation}</td>
<td class="px-4 py-4 text-center">
${visualB64 ? `
<div class="relative inline-block group">
<img src="data:image/png;base64,${visualB64}"
class="w-10 h-10 mx-auto rounded-lg border border-slate-200 shadow-xs hover:scale-105 active:scale-95 cursor-pointer transition-transform duration-150 object-cover"
alt="${translated}"
data-src="data:image/png;base64,${visualB64}"
data-word="${translated}">
</div>
` : '-'}
</td>
<td class="px-4 py-4 text-center">
<button class="play-preload-btn w-8 h-8 bg-slate-50 hover:bg-indigo-50 hover:text-indigo-600 border border-slate-200 active:translate-y-0.5 rounded-full flex items-center justify-center text-xs transition-transform duration-100"
data-audio="data:audio/mp3;base64,${audioB64}">
🔊
</button>
</td>
`;
resultsBody.appendChild(tr);
});
// Bind image modal inspection clicks
resultsBody.querySelectorAll("img").forEach(img => {
img.addEventListener("click", () => {
modalImage.src = img.getAttribute("data-src");
modalTitle.innerText = img.getAttribute("data-word");
imageModal.classList.remove("hidden");
});
});
// Bind audio playback
resultsBody.querySelectorAll(".play-preload-btn").forEach(btn => {
btn.addEventListener("click", () => {
const audioSrc = btn.getAttribute("data-audio");
if (audioSrc) {
const audio = new Audio(audioSrc);
audio.play().catch(e => console.error("Playback error:", e));
}
});
});
} catch (error) {
resultsBody.innerHTML = `
<tr>
<td colspan="5" class="px-4 py-8 text-center text-xs text-red-500 font-medium">
Error generating vocabulary: ${error.message}
</td>
</tr>
`;
}
}
// Dynamically load external game script and initialize the game (Flashcard Memory Match)
playMemoryBtn.addEventListener("click", () => {
gamesMenu.classList.add("hidden");
gameStage.classList.remove("hidden");
if (!document.getElementById("memoryGameScript")) {
const script = document.createElement("script");
script.id = "memoryGameScript";
script.src = "/games/game_memory.js";
script.onload = () => {
window.initMemoryGame("gameArea", window.loadedVocabulary, window.localizations);
};
document.head.appendChild(script);
} else {
window.initMemoryGame("gameArea", window.loadedVocabulary, window.localizations);
}
});
// Dynamically load external game script and initialize the game (Word Monster)
playMonsterBtn.addEventListener("click", () => {
gamesMenu.classList.add("hidden");
gameStage.classList.remove("hidden");
if (!document.getElementById("monsterGameScript")) {
const script = document.createElement("script");
script.id = "monsterGameScript";
script.src = "/games/game_monster.js";
script.onload = () => {
window.initMonsterGame("gameArea", window.loadedVocabulary, window.localizations);
};
document.head.appendChild(script);
} else {
window.initMonsterGame("gameArea", window.loadedVocabulary, window.localizations);
}
});
// Dynamically load external game script and initialize the game (Audio-Visual Quiz)
playQuizBtn.addEventListener("click", () => {
gamesMenu.classList.add("hidden");
gameStage.classList.remove("hidden");
if (!document.getElementById("quizGameScript")) {
const script = document.createElement("script");
script.id = "quizGameScript";
script.src = "/games/game_quiz.js";
script.onload = () => {
window.initQuizGame("gameArea", window.loadedVocabulary, window.localizations);
};
document.head.appendChild(script);
} else {
window.initQuizGame("gameArea", window.loadedVocabulary, window.localizations);
}
});
// Dynamically load external game script and initialize the game (Scrambled Word Builder)
playScrambleBtn.addEventListener("click", () => {
gamesMenu.classList.add("hidden");
gameStage.classList.remove("hidden");
if (!document.getElementById("scrambleGameScript")) {
const script = document.createElement("script");
script.id = "scrambleGameScript";
script.src = "/games/game_scramble.js";
script.onload = () => {
window.initScrambleGame("gameArea", window.loadedVocabulary, window.localizations);
};
document.head.appendChild(script);
} else {
window.initScrambleGame("gameArea", window.loadedVocabulary, window.localizations);
}
});
// Dynamically load external game script and initialize the game (Visual Word Linker)
playLinkerBtn.addEventListener("click", () => {
gamesMenu.classList.add("hidden");
gameStage.classList.remove("hidden");
if (!document.getElementById("linkerGameScript")) {
const script = document.createElement("script");
script.id = "linkerGameScript";
script.src = "/games/game_linker.js";
script.onload = () => {
window.initLinkerGame("gameArea", window.loadedVocabulary, window.localizations);
};
document.head.appendChild(script);
} else {
window.initLinkerGame("gameArea", window.loadedVocabulary, window.localizations);
}
});
// Safe transition back to the games selection menu
backToMenuBtn.addEventListener("click", () => {
gameStage.classList.add("hidden");
gamesMenu.classList.remove("hidden");
gameArea.innerHTML = ""; // Clear active game DOM elements
});
window.closeModal = function() {
imageModal.classList.add("hidden");
}
translateBtn.addEventListener("click", fetchTranslations);
</script>
</body>
</html>