<!DOCTYPE html> <html lang="en"> <head> <meta...

🧩 Syntax:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vocab Drill</title>

<style>
  :root {
    --bg: #ffffff;
    --text: #111111;
    --accent: #2e7d32;
    --error: #c62828;
    --border: #eeeeee;
    --subtle: #888888;
    --highlight: #fff5f5;
  }

  body {
    margin: 0;
    font-family: "Gentium Book Plus", serif;
    background: var(--bg);
    color: var(--text);
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
  }

  #app {
    width: 600px;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 40px 20px;
  }

  #progress-container {
    width: 100%;
    height: 4px;
    background: var(--border);
    position: fixed;
    top: 0;
    left: 0;
    display: none;
  }
  #progress-bar {
    height: 100%;
    background: var(--text);
    width: 0%;
    transition: width 0.3s ease;
  }

  #setup-view, #drill-view, #summary-view { width: 100%; text-align: center; }
  .hidden { display: none !important; }

  #front-display {
    font-size: 42px;
    margin-top: 20px;
    line-height: 1.2;
  }

  #note-display {
    font-size: 16px;
    color: var(--subtle);
    margin-top: 10px;
    margin-bottom: 40px;
    min-height: 20px;
    font-style: italic;
  }

  #title-text { font-size: 42px; margin-bottom: 40px; }

  .slot-bar {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-bottom: 15px;
  }

  .slot-btn {
    padding: 6px 20px;
    font-size: 13px;
    background: transparent;
    border: 1px solid var(--border);
    color: var(--subtle);
    cursor: pointer;
    border-radius: 4px;
  }

  .slot-btn.active {
    border-color: var(--text);
    color: var(--text);
    font-weight: bold;
    background: #f9f9f9;
  }

  textarea {
    width: 100%;
    height: 200px;
    font-family: "Fira Code", monospace;
    font-size: 13px;
    padding: 15px;
    margin-bottom: 25px;
    border: 1px solid var(--border);
    border-radius: 4px;
    box-sizing: border-box;
    outline: none;
  }

  input[type="text"] {
    font-family: inherit;
    font-size: 28px;
    width: 100%;
    border: none;
    border-bottom: 2px solid var(--border);
    padding: 10px;
    outline: none;
    text-align: center;
    background: transparent;
  }

  button.main-btn {
    padding: 12px 40px;
    font-family: inherit;
    font-size: 16px;
    cursor: pointer;
    background: var(--text);
    color: var(--bg);
    border: none;
    border-radius: 2px;
  }

  #feedback {
    margin-top: 25px;
    height: 30px;
    font-size: 18px;
  }

  .correct { color: var(--accent); }
  .wrong { color: var(--error); font-weight: bold; }
  .reveal { color: var(--subtle); font-style: italic; }

  #bottom {
    margin-top: 80px;
    width: 100%;
    display: flex;
    justify-content: space-between;
    font-size: 12px;
    color: var(--subtle);
    border-top: 1px solid var(--border);
    padding-top: 20px;
    letter-spacing: 0.1em;
  }

  .ledger-container {
    margin-top: 30px;
    text-align: left;
    width: 100%;
    max-height: 400px;
    overflow-y: auto;
    border-top: 1px solid var(--border);
  }

  .ledger-row {
    display: flex;
    padding: 12px 10px;
    border-bottom: 1px solid var(--border);
    font-size: 16px;
    align-items: baseline;
  }

  .ledger-row.missed { background-color: var(--highlight); }
  .ledger-status { width: 25px; font-size: 12px; }
  .ledger-row.missed .ledger-status { color: var(--error); }
  .ledger-front { flex: 1; font-weight: bold; }
  
  .logeion-link {
    text-decoration: none;
    color: inherit;
    border-bottom: 1px dotted var(--border);
    transition: border-color 0.2s;
  }

  .logeion-link:hover {
    border-bottom-color: var(--text);
    cursor: help;
  }

  .ledger-back { flex: 1.5; color: var(--subtle); }
</style>
</head>
<body>

<div id="progress-container"><div id="progress-bar"></div></div>

<div id="app">
  <div id="setup-view">
    <div id="title-text">Vocab Drill</div>
    <div class="slot-bar">
      <button id="btn-A" class="slot-btn active" onclick="switchSlot('A')">Slot A</button>
      <button id="btn-B" class="slot-btn" onclick="switchSlot('B')">Slot B</button>
    </div>
    <textarea id="jsonPaste" placeholder='[{"front": "ὁ ἄνθρωπος", "back": "man"}]' oninput="autoSave()"></textarea>
    <div style="margin-bottom: 25px; font-size: 12px; color: var(--subtle);">
      <input type="file" id="fileInput" style="display:none">
      <label for="fileInput" style="cursor:pointer; text-decoration: underline;">Upload JSON</label>
    </div>
    <button class="main-btn" onclick="handleStart()">Begin Session</button>
  </div>

  <div id="drill-view" class="hidden">
    <div id="front-display"></div>
    <div id="note-display"></div>
    <input id="input" type="text" autocomplete="off" placeholder="Type answer...">
    <div id="feedback"></div>
    <div id="bottom">
      <div id="stats-line"></div>
      <div id="instruction-text">TAB reveal • ESC skip</div>
    </div>
  </div>

  <div id="summary-view" class="hidden">
    <h2 style="font-weight: normal; color: var(--subtle); margin-bottom: 10px;">Session Complete</h2>
    <div id="stat-accuracy" style="font-size: 14px; color: var(--subtle); margin-bottom: 30px;"></div>
    <div id="ledger" class="ledger-container"></div>
    <button class="main-btn" onclick="showSetup()" style="margin-top: 40px;">New Session</button>
  </div>
</div>

<script>
let deck = [];
let queue = [];
let current = null;
let initialCount = 0;
let stats = { finished: 0, wrongAnswers: {} };
let currentSlot = 'A';
let isPausedForError = false;

window.onload = () => loadSlot('A');

function switchSlot(slot) {
  currentSlot = slot;
  document.getElementById('btn-A').classList.toggle('active', slot === 'A');
  document.getElementById('btn-B').classList.toggle('active', slot === 'B');
  loadSlot(slot);
}

function loadSlot(slot) {
  const saved = localStorage.getItem(`vdrill_deck_${slot}`);
  document.getElementById('jsonPaste').value = saved || "";
}

function autoSave() {
  localStorage.setItem(`vdrill_deck_${currentSlot}`, document.getElementById('jsonPaste').value);
}

document.getElementById('fileInput').addEventListener('change', e => {
  const reader = new FileReader();
  reader.onload = evt => {
    document.getElementById('jsonPaste').value = evt.target.result;
    autoSave();
  };
  reader.readAsText(e.target.files[0]);
});

function handleStart() {
  try {
    deck = JSON.parse(document.getElementById('jsonPaste').value.trim());
    if (!Array.isArray(deck) || deck.length === 0) throw "Err";
    startSession();
  } catch (e) {
    alert("Invalid JSON format.");
  }
}

function startSession() {
  queue = shuffle([...deck]);
  initialCount = queue.length;
  stats = { finished: 0, wrongAnswers: {} };
  isPausedForError = false;
  document.getElementById('setup-view').classList.add('hidden');
  document.getElementById('summary-view').classList.add('hidden');
  document.getElementById('drill-view').classList.remove('hidden');
  document.getElementById('progress-container').style.display = 'block';
  nextCard();
}

function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

function nextCard() {
  if (queue.length === 0) return showSummary();
  isPausedForError = false;
  current = queue.shift();
  document.getElementById('front-display').innerText = current.front;
  document.getElementById('note-display').innerText = current.note || "";
  document.getElementById('input').value = "";
  document.getElementById('input').disabled = false;
  document.getElementById('feedback').innerText = "";
  document.getElementById('instruction-text').innerText = "TAB reveal • ESC skip";
  document.getElementById('input').focus();
  updateStats();
}

function submit() {
  const inputVal = document.getElementById('input').value;
  const answers = Array.isArray(current.back) ? current.back : [current.back];
  const correct = isCorrect(inputVal, answers);
  const feedback = document.getElementById('feedback');

  if (correct) {
    stats.finished++;
    feedback.innerText = "✓ " + answers.join(", ");
    feedback.className = "correct";
    setTimeout(nextCard, 600);
  } else {
    feedback.innerText = "✗ " + answers.join(", ");
    feedback.className = "wrong";
    stats.wrongAnswers[current.front] = (stats.wrongAnswers[current.front] || 0) + 1;
    isPausedForError = true;
    document.getElementById('input').disabled = true;
    document.getElementById('instruction-text').innerText = "ENTER to continue";
    queue.splice(Math.min(2, queue.length), 0, current); 
    queue.splice(Math.min(7, queue.length), 0, current);
  }
  updateStats();
}

function isCorrect(input, answers) {
  const norm = s => s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
  const nInput = norm(input);
  return answers.some(ans => {
    const nAns = norm(ans);
    if (nInput === nAns) return true;
    const d = (function(a, b){
      const tmp = Array.from({length:a.length+1},()=>[]);
      for(let i=0;i<=a.length;i++)tmp[i][0]=i;
      for(let j=0;j<=b.length;j++)tmp[0][j]=j;
      for(let i=1;i<=a.length;i++)for(let j=1;j<=b.length;j++)
        tmp[i][j]=Math.min(tmp[i-1][j]+1,tmp[i][j-1]+1,tmp[i-1][j-1]+(a[i-1]===b[j-1]?0:1));
      return tmp[a.length][b.length];
    })(nInput, nAns);
    return d <= Math.max(1, Math.floor(nAns.length / 6));
  });
}

function updateStats() {
  const pct = Math.max(0, Math.min(100, (stats.finished / initialCount) * 100));
  document.getElementById('progress-bar').style.width = `${pct}%`;
  document.getElementById('stats-line').innerText = `${stats.finished} done • ${queue.length} queue`;
}

function showSummary() {
  document.getElementById('drill-view').classList.add('hidden');
  document.getElementById('summary-view').classList.remove('hidden');
  document.getElementById('progress-bar').style.width = '100%';
  
  const totalMisses = Object.keys(stats.wrongAnswers).length;
  const accuracy = Math.round(((initialCount - totalMisses) / initialCount) * 100);
  const wordLabel = totalMisses === 1 ? "word" : "words";
  document.getElementById('stat-accuracy').innerText = `${accuracy}% accuracy • ${totalMisses} ${wordLabel} flagged`;

  const ledger = document.getElementById('ledger');
  ledger.innerHTML = deck.map(card => {
    const isMissed = stats.wrongAnswers[card.front];
    const backText = Array.isArray(card.back) ? card.back.join(", ") : card.back;
    
    // Logic: Clean the front for Logeion (strip articles, take first word)
    const cleanForUrl = (text) => {
      const articles = ['ὁ', 'ἡ', 'τὸ', 'ο', 'η', 'το'];
      // Replace commas with spaces, trim, then split into array of words
      const words = text.replace(/,/g, ' ').trim().split(/\s+/);
      // If first word is an article, take the second one. Otherwise take the first.
      if (articles.includes(words[0]) && words.length > 1) return words[1];
      return words[0];
    };

    const logeionUrl = `https://logeion.uchicago.edu/${encodeURIComponent(cleanForUrl(card.front))}`;
    
    return `
      <div class="ledger-row ${isMissed ? 'missed' : ''}">
        <div class="ledger-status">${isMissed ? '●' : ''}</div>
        <div class="ledger-front">
          <a href="${logeionUrl}" target="_blank" class="logeion-link">${card.front}</a>
        </div>
        <div class="ledger-back">${backText}</div>
      </div>
    `;
  }).join('');
}

function showSetup() {
  document.getElementById('setup-view').classList.remove('hidden');
  document.getElementById('drill-view').classList.add('hidden');
  document.getElementById('summary-view').classList.add('hidden');
  document.getElementById('progress-container').style.display = 'none';
}

document.addEventListener('keydown', e => {
  if (document.getElementById('drill-view').classList.contains('hidden')) return;
  if (e.key === 'Enter') {
    if (isPausedForError) nextCard();
    else if (document.getElementById('input').value.trim() !== "") submit();
  } else if (e.key === 'Tab') {
    e.preventDefault();
    reveal();
  } else if (e.key === 'Escape') {
    nextCard();
  }
});

function reveal() {
  const answers = Array.isArray(current.back) ? current.back : [current.back];
  const feedback = document.getElementById('feedback');
  feedback.innerText = "→ " + answers.join(", ");
  feedback.className = "reveal";
}
</script>
</body>
</html>