| <!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> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <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; |
| } |
| |
| .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"> |
|
|
| |
| <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> |
|
|
| |
| <div class="w-full max-w-3xl mx-auto space-y-4"> |
| |
| |
| <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> |
|
|
| |
| <div class="bg-white rounded-2xl shadow-sm p-6 md:p-8 border border-slate-200/80 transition-all duration-300"> |
| |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div id="tabVocabContent" class="block"> |
| |
| <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"> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| </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> |
|
|
| |
| <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> |
|
|
| |
| <div id="tabGamesContent" class="hidden"> |
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <script type="module"> |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| |
| |
| 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"); |
| |
| |
| const tabVocabBtn = document.getElementById("tabVocabBtn"); |
| const tabGamesBtn = document.getElementById("tabGamesBtn"); |
| const tabVocabContent = document.getElementById("tabVocabContent"); |
| const tabGamesContent = document.getElementById("tabGamesContent"); |
| |
| |
| 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; |
| } |
| |
| |
| window.selectInitialLang = function(lang) { |
| guiLangSelect.value = lang; |
| document.getElementById("welcomeModal").classList.add("hidden"); |
| handleAppLanguageChange(lang); |
| } |
| |
| |
| async function handleAppLanguageChange(lang) { |
| document.getElementById("srcLang").value = lang; |
| populatePracticeLanguages(lang); |
| await loadLocalization(); |
| } |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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); |
| }); |
| |
| |
| 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); |
| }); |
| |
| |
| resultsBody.querySelectorAll("img").forEach(img => { |
| img.addEventListener("click", () => { |
| modalImage.src = img.getAttribute("data-src"); |
| modalTitle.innerText = img.getAttribute("data-word"); |
| imageModal.classList.remove("hidden"); |
| }); |
| }); |
| |
| |
| 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> |
| `; |
| } |
| } |
| |
| |
| 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); |
| } |
| }); |
| |
| |
| 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); |
| } |
| }); |
| |
| |
| 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); |
| } |
| }); |
| |
| |
| 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); |
| } |
| }); |
| |
| |
| 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); |
| } |
| }); |
| |
| |
| backToMenuBtn.addEventListener("click", () => { |
| gameStage.classList.add("hidden"); |
| gamesMenu.classList.remove("hidden"); |
| gameArea.innerHTML = ""; |
| }); |
| |
| window.closeModal = function() { |
| imageModal.classList.add("hidden"); |
| } |
| |
| translateBtn.addEventListener("click", fetchTranslations); |
| </script> |
| </body> |
| </html> |