<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AccessPDF — PDF Accessibility Tool</title>
<style>
  :root {
    --bg: #f8f9fa; --fg: #212529; --accent: #0d6efd; --accent-hover: #0a58ca;
    --success: #198754; --warning: #ffc107; --danger: #dc3545;
    --card: #fff; --border: #dee2e6; --muted: #6c757d;
    --radius: 8px; --shadow: 0 1px 3px rgba(0,0,0,.1);
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: var(--bg); color: var(--fg); line-height: 1.5; }

  .container { max-width: 960px; margin: 0 auto; padding: 1.5rem; }

  header { text-align: center; padding: 2rem 0 1rem; }
  header h1 { font-size: 1.75rem; font-weight: 700; }
  header p { color: var(--muted); margin-top: .25rem; }

  /* Drop zone */
  #drop-zone {
    border: 2px dashed var(--border); border-radius: var(--radius);
    padding: 3rem 2rem; text-align: center; cursor: pointer;
    transition: border-color .2s, background .2s;
    background: var(--card);
  }
  #drop-zone.drag-over { border-color: var(--accent); background: #e7f1ff; }
  #drop-zone p { font-size: 1.1rem; color: var(--muted); }
  #drop-zone .icon { font-size: 2.5rem; margin-bottom: .5rem; }
  #file-input { display: none; }

  /* Progress */
  #progress { display: none; padding: 2rem; }
  .progress-container {
    background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
    padding: 1.5rem; max-width: 500px; margin: 0 auto;
  }
  .progress-title {
    font-weight: 600; font-size: 1rem; text-align: center; margin-bottom: 1.25rem;
  }
  .progress-bar-track {
    width: 100%; height: 8px; background: #e9ecef; border-radius: 4px;
    overflow: hidden; margin-bottom: 1.25rem;
  }
  .progress-bar-fill {
    height: 100%; background: var(--accent); border-radius: 4px;
    transition: width .4s ease; width: 0%;
  }
  .progress-steps { display: flex; flex-direction: column; gap: .5rem; }
  .progress-step {
    display: flex; align-items: center; gap: .6rem; font-size: .875rem;
    color: var(--muted);
  }
  .progress-step.active { color: var(--accent); font-weight: 500; }
  .progress-step.done { color: var(--success); }
  .step-icon {
    width: 20px; height: 20px; border-radius: 50%; border: 2px solid var(--border);
    display: flex; align-items: center; justify-content: center;
    font-size: .7rem; flex-shrink: 0;
  }
  .progress-step.active .step-icon {
    border-color: var(--accent); background: var(--accent); color: #fff;
    animation: pulse 1s ease-in-out infinite;
  }
  .progress-step.done .step-icon {
    border-color: var(--success); background: var(--success); color: #fff;
  }
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .6; } }

  /* Error */
  .error-msg { background: #f8d7da; color: #842029; padding: .75rem 1rem;
    border-radius: var(--radius); margin: 1rem 0; }

  /* Results section */
  #results { display: none; }
  .summary-bar {
    display: flex; gap: 1rem; flex-wrap: wrap; align-items: center;
    background: var(--card); border-radius: var(--radius); padding: 1rem;
    box-shadow: var(--shadow); margin-bottom: 1rem;
  }
  .summary-bar .filename { font-weight: 600; flex: 1; }
  .summary-bar .stat { font-size: .875rem; color: var(--muted); }
  .summary-bar .stat b { color: var(--fg); }

  /* AI settings panel */
  .ai-panel {
    background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
    padding: 1rem 1.25rem; margin-bottom: 1rem;
  }
  .ai-panel-header {
    display: flex; align-items: center; gap: .75rem; margin-bottom: .5rem;
  }
  .ai-panel-header h3 { font-size: .95rem; font-weight: 600; flex: 1; }
  .toggle-switch { position: relative; width: 44px; height: 24px; flex-shrink: 0; }
  .toggle-switch input { opacity: 0; width: 0; height: 0; }
  .toggle-slider {
    position: absolute; inset: 0; background: var(--border); border-radius: 24px;
    cursor: pointer; transition: background .2s;
  }
  .toggle-slider::before {
    content: ""; position: absolute; width: 18px; height: 18px; left: 3px; top: 3px;
    background: #fff; border-radius: 50%; transition: transform .2s;
  }
  .toggle-switch input:checked + .toggle-slider { background: var(--accent); }
  .toggle-switch input:checked + .toggle-slider::before { transform: translateX(20px); }

  .ai-controls { display: none; gap: .75rem; flex-wrap: wrap; align-items: end; margin-top: .75rem; }
  .ai-controls.visible { display: flex; }

  .ai-controls .field { display: flex; flex-direction: column; gap: .25rem; }
  .ai-controls .field label { font-size: .8rem; color: var(--muted); font-weight: 500; }
  .ai-controls select, .ai-controls input[type="text"], .ai-controls input[type="password"] {
    padding: .4rem .6rem; border: 1px solid var(--border); border-radius: 4px;
    font-size: .875rem; font-family: inherit;
  }
  .ai-controls select:focus, .ai-controls input:focus {
    outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(13,110,253,.25);
  }
  #api-key-field { display: none; }
  #api-key-field.visible { display: flex; }
  #ollama-model-field { display: flex; }

  .actions-bar { display: flex; gap: .75rem; margin-bottom: 1.5rem; flex-wrap: wrap; }

  .bottom-download {
    background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
    padding: 1.25rem; margin-top: 1.5rem; text-align: center;
  }
  .bottom-download p {
    color: var(--success); font-weight: 500; margin-bottom: .75rem; font-size: .95rem;
  }
  .bottom-download .btn { margin: 0 .375rem; }

  /* Image cards */
  .image-card {
    background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
    padding: 1.25rem; margin-bottom: 1.25rem;
  }
  .image-card .card-header {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: .75rem;
  }
  .image-card .card-header h3 { font-size: .95rem; font-weight: 600; }
  .image-card .badge {
    font-size: .75rem; padding: .2rem .5rem; border-radius: 4px;
    font-weight: 600; text-transform: uppercase;
  }
  .badge-needs_review { background: var(--warning); color: #000; }
  .badge-approved { background: var(--success); color: #fff; }
  .badge-decorative { background: var(--muted); color: #fff; }

  .image-card .image-preview {
    max-width: 100%; max-height: 300px; border-radius: 4px;
    background: #eee; display: block; margin-bottom: .75rem;
  }

  /* AI draft suggestion */
  .ai-draft-box {
    display: none; background: #e8f4f8; border-left: 3px solid var(--accent);
    padding: .6rem .75rem; border-radius: 0 4px 4px 0; margin-bottom: .5rem;
    font-size: .875rem;
  }
  .ai-draft-box.visible { display: block; }
  .ai-draft-box .draft-label { font-size: .75rem; color: var(--accent); font-weight: 600;
    margin-bottom: .25rem; }
  .ai-draft-box .draft-text { color: var(--fg); }
  .ai-draft-box .btn-use-draft {
    margin-top: .4rem; font-size: .8rem; padding: .25rem .6rem;
  }

  .image-card textarea {
    width: 100%; min-height: 80px; padding: .5rem; border: 1px solid var(--border);
    border-radius: 4px; font-family: inherit; font-size: .9rem; resize: vertical;
  }
  .image-card textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(13,110,253,.25); }

  .card-actions { display: flex; gap: .5rem; margin-top: .75rem; flex-wrap: wrap; }

  /* Buttons */
  .btn {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .45rem .9rem; border: none; border-radius: 4px;
    font-size: .875rem; font-weight: 500; cursor: pointer; transition: background .15s;
  }
  .btn-primary { background: var(--accent); color: #fff; }
  .btn-primary:hover { background: var(--accent-hover); }
  .btn-success { background: var(--success); color: #fff; }
  .btn-success:hover { background: #157347; }
  .btn-secondary { background: #6c757d; color: #fff; }
  .btn-secondary:hover { background: #565e64; }
  .btn-outline { background: transparent; color: var(--fg); border: 1px solid var(--border); }
  .btn-outline:hover { background: #e9ecef; }
  .btn:disabled { opacity: .5; cursor: not-allowed; }

  .no-images { text-align: center; padding: 2rem; color: var(--muted); }

  /* Accessibility report panel */
  .report-panel {
    background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
    padding: 1.25rem; margin-bottom: 1rem;
  }
  .report-panel h3 { font-size: 1rem; font-weight: 600; margin-bottom: .75rem; }
  .report-grid {
    display: grid; grid-template-columns: 1fr 1fr; gap: .4rem .75rem;
    font-size: .875rem; margin-bottom: .75rem;
  }
  .report-grid .label { color: var(--muted); }
  .report-grid .value { font-weight: 500; }
  .report-grid .pass { color: var(--success); }
  .report-grid .fail { color: var(--danger); }
  .report-issues { margin-top: .75rem; font-size: .85rem; }
  .report-issues .issue {
    padding: .35rem .6rem; margin-bottom: .35rem; border-radius: 4px;
    display: flex; gap: .5rem; align-items: baseline;
  }
  .issue-error { background: #f8d7da; color: #842029; }
  .issue-warning { background: #fff3cd; color: #664d03; }
  .issue-info { background: #cff4fc; color: #055160; }
  .issue .severity { font-weight: 700; font-size: .75rem; text-transform: uppercase; flex-shrink: 0; }
  .report-pass-banner {
    background: #d1e7dd; color: #0f5132; padding: .6rem .75rem; border-radius: 4px;
    font-weight: 500; text-align: center;
  }
  .report-fixed {
    margin-top: .75rem; padding: .75rem; background: #d1e7dd; border-radius: 4px;
    font-size: .85rem; color: #0f5132;
  }
  .report-fixed h4 { font-size: .85rem; font-weight: 600; margin-bottom: .4rem; }
  .report-fixed ul { margin: 0; padding-left: 1.25rem; }
  .report-fixed li { margin-bottom: .2rem; }
  .report-remaining {
    margin-top: .5rem; padding: .6rem .75rem; background: #fff3cd; border-radius: 4px;
    font-size: .85rem; color: #664d03;
  }
  .scanned-warning {
    background: #fff3cd; color: #664d03; padding: .75rem 1rem; border-radius: var(--radius);
    margin-bottom: 1rem; border-left: 4px solid var(--warning); font-size: .9rem;
  }
  .scanned-warning strong { display: block; margin-bottom: .25rem; }
  .batch-file-item .scanned-label {
    font-size: .75rem; color: #664d03; background: #fff3cd; padding: .1rem .4rem;
    border-radius: 3px; margin-left: .25rem;
  }

  /* Mode selector */
  .mode-selector {
    display: flex; justify-content: center; gap: 1.5rem; margin-bottom: 1rem;
  }
  .mode-selector label {
    display: flex; align-items: center; gap: .4rem; font-size: .9rem; cursor: pointer;
    color: var(--muted);
  }
  .mode-selector input:checked + span { color: var(--fg); font-weight: 500; }

  /* Batch file list in progress */
  .batch-file-list { margin-top: .75rem; font-size: .85rem; }
  .batch-file-item {
    display: flex; align-items: center; gap: .5rem; padding: .3rem 0;
  }
  .batch-file-item .file-status {
    width: 18px; height: 18px; border-radius: 50%; border: 2px solid var(--border);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: .65rem; flex-shrink: 0;
  }
  .batch-file-item.done .file-status { border-color: var(--success); background: var(--success); color: #fff; }
  .batch-file-item.fixing .file-status { border-color: var(--accent); background: var(--accent); color: #fff; animation: pulse 1s ease-in-out infinite; }
  .batch-file-item.error .file-status { border-color: var(--danger); background: var(--danger); color: #fff; }
  .batch-file-item .file-error { color: var(--danger); font-size: .8rem; margin-left: .25rem; }

  footer { text-align: center; color: var(--muted); font-size: .8rem; padding: 2rem 0 1rem; }
</style>
</head>
<body>
<div class="container">
  <header>
    <h1>AccessPDF</h1>
    <p>Upload a PDF to fix accessibility issues and add image descriptions</p>
  </header>

  <!-- Mode selector -->
  <div class="mode-selector" id="mode-selector">
    <label><input type="radio" name="mode" value="single" checked> <span>Fix one PDF</span></label>
    <label><input type="radio" name="mode" value="batch"> <span>Batch fix (structural only)</span></label>
  </div>

  <!-- Upload -->
  <div id="upload-section">
    <div id="drop-zone" tabindex="0" role="button" aria-label="Upload PDF file">
      <div class="icon" aria-hidden="true">&#128196;</div>
      <p id="drop-zone-text">Drag &amp; drop a PDF here, or click to browse</p>
    </div>
    <input type="file" id="file-input" accept=".pdf">
    <input type="file" id="batch-file-input" accept=".pdf" multiple style="display:none">
  </div>

  <!-- Processing -->
  <div id="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
    <div class="progress-container">
      <div class="progress-title" id="progress-title">Processing your PDF...</div>
      <div class="progress-bar-track">
        <div class="progress-bar-fill" id="progress-fill"></div>
      </div>
      <div class="progress-steps">
        <div class="progress-step" id="step-uploading" data-stage="uploading">
          <span class="step-icon"></span> Uploading PDF
        </div>
        <div class="progress-step" id="step-analyzing" data-stage="analyzing">
          <span class="step-icon"></span> Analyzing accessibility
        </div>
        <div class="progress-step" id="step-fixing" data-stage="fixing">
          <span class="step-icon"></span> Fixing structure
        </div>
        <div class="progress-step" id="step-loading" data-stage="loading">
          <span class="step-icon"></span> Extracting images
        </div>
      </div>
    </div>
  </div>

  <!-- Batch processing -->
  <div id="batch-progress" style="display:none">
    <div class="progress-container">
      <div class="progress-title" id="batch-progress-title">Fixing PDFs...</div>
      <div class="progress-bar-track">
        <div class="progress-bar-fill" id="batch-progress-fill"></div>
      </div>
      <div class="batch-file-list" id="batch-file-list"></div>
    </div>
  </div>

  <!-- Batch results -->
  <div id="batch-results" style="display:none">
    <div class="summary-bar">
      <span class="filename">Batch results</span>
      <span class="stat">Total: <b id="batch-stat-total">0</b></span>
      <span class="stat" style="color:var(--success)">Done: <b id="batch-stat-done">0</b></span>
      <span class="stat" style="color:var(--danger)">Failed: <b id="batch-stat-failed">0</b></span>
    </div>
    <div class="batch-file-list" id="batch-results-list" style="padding:.5rem 0"></div>
    <div class="actions-bar">
      <button class="btn btn-success" id="btn-batch-download">Download all as ZIP</button>
      <button class="btn btn-outline" id="btn-batch-new">Upload more</button>
    </div>
  </div>

  <!-- Results -->
  <div id="results">
    <div class="summary-bar">
      <span class="filename" id="result-filename"></span>
      <span class="stat">Images: <b id="stat-total">0</b></span>
      <span class="stat">Approved: <b id="stat-approved">0</b></span>
      <span class="stat">Needs review: <b id="stat-needs">0</b></span>
    </div>

    <!-- Scanned PDF warning -->
    <div class="scanned-warning" id="scanned-warning" style="display:none">
      <strong>Scanned PDF detected</strong>
      This PDF appears to be scanned (image-only pages with no text layer).
      The fixes applied may not produce meaningful accessibility improvements.
      Run OCR software (e.g. Adobe Acrobat, <a href="https://github.com/ocrmypdf/OCRmyPDF" target="_blank" rel="noopener">ocrmypdf</a>) first to add a text layer.
    </div>

    <!-- Accessibility report -->
    <div class="report-panel" id="report-panel" style="display:none">
      <h3>Accessibility Report</h3>
      <div class="report-grid" id="report-grid"></div>
      <div class="report-issues" id="report-issues"></div>
      <div id="report-fixed"></div>
    </div>

    <!-- AI settings panel -->
    <div class="ai-panel" id="ai-panel">
      <div class="ai-panel-header">
        <h3>Alt text mode</h3>
        <span style="font-size:.85rem;color:var(--text);font-weight:500" id="mode-label">Manual</span>
        <label class="toggle-switch">
          <input type="checkbox" id="ai-toggle">
          <span class="toggle-slider"></span>
        </label>
        <span style="font-size:.85rem;color:var(--muted);font-weight:500" id="ai-label-right">AI</span>
      </div>
      <div class="ai-controls" id="ai-controls">
        <div class="field">
          <label for="provider-select">Provider</label>
          <select id="provider-select">
            <option value="ollama" selected>Ollama (recommended)</option>
            <option value="gemini">Google Gemini (free tier)</option>
            <option value="anthropic">Anthropic (Claude)</option>
            <option value="openai">OpenAI (GPT-4)</option>
          </select>
          <div id="ollama-hint" style="font-size:.75rem;color:var(--muted);margin-top:2px;line-height:1.4">
            Free, local, no API key needed.
            <a href="https://ollama.com" target="_blank" rel="noopener" style="color:var(--accent)">Install Ollama</a>,
            then pull a vision model below.
          </div>
        </div>
        <div class="field" id="ollama-model-field">
          <label for="ollama-model-select">Model</label>
          <select id="ollama-model-select">
            <option value="llava">llava (best quality)</option>
            <option value="moondream">moondream (faster)</option>
          </select>
        </div>
        <div class="field" id="api-key-field">
          <label for="api-key-input">API key</label>
          <input type="password" id="api-key-input" placeholder="Paste your API key here" style="width:260px">
          <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener"
             style="font-size:.75rem;color:var(--accent);margin-top:2px">Get a free Gemini key</a>
        </div>
        <div class="field">
          <label for="doc-context-input">Document context (optional)</label>
          <input type="text" id="doc-context-input"
                 placeholder="e.g., This is a chemistry textbook about organic reactions"
                 style="width:100%">
          <div style="font-size:.75rem;color:var(--muted);margin-top:2px">
            Helps the AI understand what your document is about, improving alt text accuracy.
          </div>
        </div>
        <button class="btn btn-primary" id="btn-generate">Generate all</button>
      </div>
    </div>

    <div class="actions-bar">
      <button class="btn btn-primary" id="btn-save" disabled>Save alt text</button>
      <button class="btn btn-success" id="btn-download" disabled>Download fixed PDF</button>
      <button class="btn btn-outline" id="btn-new">Upload another</button>
    </div>

    <div id="image-list"></div>

    <div id="bottom-download" class="bottom-download" style="display:none;">
      <p>Your alt text will be included in the download.</p>
      <button class="btn btn-primary" id="btn-save-bottom" disabled>Save alt text</button>
      <button class="btn btn-success" id="btn-download-bottom" disabled>Download fixed PDF (with alt text)</button>
    </div>
  </div>

  <footer>AccessPDF &mdash; open-source PDF accessibility tool</footer>
</div>

<script>
(function() {
  const dropZone = document.getElementById("drop-zone");
  const fileInput = document.getElementById("file-input");
  const uploadSection = document.getElementById("upload-section");
  const progressDiv = document.getElementById("progress");
  const progressTitle = document.getElementById("progress-title");
  const progressFill = document.getElementById("progress-fill");
  const resultsDiv = document.getElementById("results");
  const imageList = document.getElementById("image-list");
  const btnSave = document.getElementById("btn-save");
  const btnDownload = document.getElementById("btn-download");
  const btnNew = document.getElementById("btn-new");
  const aiToggle = document.getElementById("ai-toggle");
  const aiControls = document.getElementById("ai-controls");
  const modeLabel = document.getElementById("mode-label");
  const providerSelect = document.getElementById("provider-select");
  const apiKeyField = document.getElementById("api-key-field");
  const apiKeyInput = document.getElementById("api-key-input");
  const btnGenerate = document.getElementById("btn-generate");
  const aiPanel = document.getElementById("ai-panel");
  const bottomDownload = document.getElementById("bottom-download");
  const btnSaveBottom = document.getElementById("btn-save-bottom");
  const btnDownloadBottom = document.getElementById("btn-download-bottom");

  const batchFileInput = document.getElementById("batch-file-input");
  const dropZoneText = document.getElementById("drop-zone-text");
  const modeSelector = document.getElementById("mode-selector");
  const batchProgressDiv = document.getElementById("batch-progress");
  const batchProgressTitle = document.getElementById("batch-progress-title");
  const batchProgressFill = document.getElementById("batch-progress-fill");
  const batchFileList = document.getElementById("batch-file-list");
  const batchResultsDiv = document.getElementById("batch-results");
  const batchResultsList = document.getElementById("batch-results-list");
  const btnBatchDownload = document.getElementById("btn-batch-download");
  const btnBatchNew = document.getElementById("btn-batch-new");

  let currentJobId = null;
  let currentBatchId = null;
  let currentMode = "single";
  let imageData = [];

  // --- AI toggle ---
  aiToggle.addEventListener("change", () => {
    const on = aiToggle.checked;
    aiControls.classList.toggle("visible", on);
    modeLabel.style.color = on ? "var(--muted)" : "var(--text)";
    document.getElementById("ai-label-right").style.color = on ? "var(--accent)" : "var(--muted)";
  });

  const docContextInput = document.getElementById("doc-context-input");

  const ollamaHint = document.getElementById("ollama-hint");
  const ollamaModelField = document.getElementById("ollama-model-field");
  const ollamaModelSelect = document.getElementById("ollama-model-select");

  providerSelect.addEventListener("change", () => {
    const isOllama = providerSelect.value === "ollama";
    apiKeyField.classList.toggle("visible", !isOllama);
    ollamaHint.style.display = isOllama ? "" : "none";
    ollamaModelField.style.display = isOllama ? "" : "none";
    // Update the "get key" link based on provider
    const keyLink = apiKeyField.querySelector("a");
    if (keyLink) {
      const links = {
        gemini: { href: "https://aistudio.google.com/apikey", text: "Get a free Gemini key" },
        anthropic: { href: "https://console.anthropic.com/settings/keys", text: "Get an Anthropic key" },
        openai: { href: "https://platform.openai.com/api-keys", text: "Get an OpenAI key" },
      };
      const info = links[providerSelect.value];
      if (info) { keyLink.href = info.href; keyLink.textContent = info.text; keyLink.style.display = ""; }
      else { keyLink.style.display = "none"; }
    }
  });

  // --- Mode switching ---
  modeSelector.querySelectorAll("input[name='mode']").forEach(radio => {
    radio.addEventListener("change", () => {
      currentMode = radio.value;
      if (currentMode === "batch") {
        dropZoneText.textContent = "Drag & drop PDFs here, or click to browse (multiple files)";
        dropZone.setAttribute("aria-label", "Upload multiple PDF files");
      } else {
        dropZoneText.textContent = "Drag & drop a PDF here, or click to browse";
        dropZone.setAttribute("aria-label", "Upload PDF file");
      }
    });
  });

  // --- Upload handling ---
  dropZone.addEventListener("click", () => {
    if (currentMode === "batch") batchFileInput.click();
    else fileInput.click();
  });
  dropZone.addEventListener("keydown", (e) => {
    if (e.key === "Enter" || e.key === " ") {
      if (currentMode === "batch") batchFileInput.click();
      else fileInput.click();
    }
  });
  fileInput.addEventListener("change", () => { if (fileInput.files.length) upload(fileInput.files[0]); });
  batchFileInput.addEventListener("change", () => {
    if (batchFileInput.files.length) uploadBatch(batchFileInput.files);
  });

  dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("drag-over"); });
  dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag-over"));
  dropZone.addEventListener("drop", (e) => {
    e.preventDefault();
    dropZone.classList.remove("drag-over");
    if (!e.dataTransfer.files.length) return;
    if (currentMode === "batch") uploadBatch(e.dataTransfer.files);
    else upload(e.dataTransfer.files[0]);
  });

  // Progress stage config: order, labels, percentages
  const STAGES = [
    { key: "uploading", label: "Uploading PDF", pct: 10 },
    { key: "analyzing", label: "Analyzing accessibility", pct: 35 },
    { key: "fixing", label: "Fixing structure", pct: 65 },
    { key: "loading", label: "Extracting images", pct: 90 },
  ];

  function updateProgressUI(stage) {
    const stageIdx = STAGES.findIndex(s => s.key === stage);
    const pct = stageIdx >= 0 ? STAGES[stageIdx].pct : 0;
    const label = stageIdx >= 0 ? STAGES[stageIdx].label + "..." : "Processing...";

    progressFill.style.width = pct + "%";
    progressTitle.textContent = label;
    progressDiv.setAttribute("aria-valuenow", pct);

    STAGES.forEach((s, i) => {
      const el = document.getElementById("step-" + s.key);
      if (!el) return;
      el.classList.remove("active", "done");
      if (i < stageIdx) el.classList.add("done");
      else if (i === stageIdx) el.classList.add("active");
    });
  }

  async function upload(file) {
    if (!file.name.toLowerCase().endsWith(".pdf")) {
      alert("Please upload a PDF file.");
      return;
    }

    uploadSection.style.display = "none";
    modeSelector.style.display = "none";
    progressDiv.style.display = "block";
    resultsDiv.style.display = "none";

    // Reset progress bar
    progressFill.style.width = "0%";
    STAGES.forEach(s => {
      const el = document.getElementById("step-" + s.key);
      if (el) el.classList.remove("active", "done");
    });
    updateProgressUI("uploading");

    const form = new FormData();
    form.append("file", file);

    try {
      // Upload and get job_id back immediately
      const resp = await fetch("/api/upload", { method: "POST", body: form });
      const data = await resp.json();

      if (!resp.ok) {
        showError(data.detail || "Upload failed.");
        return;
      }

      currentJobId = data.job_id;

      // Poll for status updates
      await pollForCompletion(data.job_id);
    } catch (err) {
      showError("Network error: " + err.message);
    }
  }

  async function pollForCompletion(jobId) {
    const POLL_INTERVAL = 500; // ms

    while (true) {
      await new Promise(r => setTimeout(r, POLL_INTERVAL));

      try {
        const resp = await fetch("/api/job/" + jobId);
        const data = await resp.json();

        if (!resp.ok) {
          showError("Job check failed.");
          return;
        }

        // Update progress bar
        updateProgressUI(data.stage);

        if (data.stage === "error") {
          showError("Processing error: " + (data.error || "Unknown error"));
          return;
        }

        if (data.stage === "done") {
          // Finish the progress bar at 100%
          progressFill.style.width = "100%";
          progressTitle.textContent = "Done!";
          STAGES.forEach(s => {
            const el = document.getElementById("step-" + s.key);
            if (el) { el.classList.remove("active"); el.classList.add("done"); }
          });

          // Brief pause to show 100% before switching to results
          await new Promise(r => setTimeout(r, 400));

          imageData = data.images || [];
          showResults(data);
          return;
        }
      } catch (err) {
        // Network hiccup -- keep polling
      }
    }
  }

  function showError(msg) {
    progressDiv.style.display = "none";
    uploadSection.style.display = "block";
    const el = document.createElement("div");
    el.className = "error-msg";
    el.textContent = msg;
    uploadSection.insertBefore(el, uploadSection.firstChild);
    setTimeout(() => el.remove(), 8000);
  }

  // --- Results display ---
  function showResults(data) {
    progressDiv.style.display = "none";
    resultsDiv.style.display = "block";

    document.getElementById("result-filename").textContent = data.filename || "PDF";
    updateStats(data.stats);

    // Show scanned PDF warning if applicable
    const scannedWarning = document.getElementById("scanned-warning");
    const isScanned = data.analysis && data.analysis.is_scanned;
    scannedWarning.style.display = isScanned ? "block" : "none";

    // Render accessibility report
    renderReport(data.analysis);

    // Show/hide AI panel based on whether there are images
    aiPanel.style.display = imageData.length > 0 ? "block" : "none";

    renderImageCards();

    btnSave.disabled = imageData.length === 0;
    btnDownload.disabled = false;

    // Show bottom download bar if there are images
    if (imageData.length > 0) {
      bottomDownload.style.display = "block";
      btnSaveBottom.disabled = false;
      btnDownloadBottom.disabled = false;
    } else {
      bottomDownload.style.display = "none";
    }
  }

  function renderReport(analysis) {
    const panel = document.getElementById("report-panel");
    const grid = document.getElementById("report-grid");
    const issuesDiv = document.getElementById("report-issues");

    if (!analysis) { panel.style.display = "none"; return; }
    panel.style.display = "block";

    // Build the metrics grid
    const yesNo = (val) => val
      ? '<span class="value pass">Yes</span>'
      : '<span class="value fail">No</span>';

    grid.innerHTML = `
      <span class="label">Pages</span><span class="value">${analysis.pages}</span>
      <span class="label">Tagged</span>${yesNo(analysis.is_tagged)}
      <span class="label">Language set</span>${yesNo(analysis.has_lang)}
      <span class="label">Title</span><span class="value">${analysis.title ? escapeHtml(analysis.title) : '<span class="fail">Not set</span>'}</span>
      <span class="label">Images</span><span class="value">${analysis.images}</span>
      <span class="label">Errors</span><span class="value${analysis.errors > 0 ? ' fail' : ' pass'}">${analysis.errors}</span>
      <span class="label">Warnings</span><span class="value${analysis.warnings > 0 ? '' : ' pass'}">${analysis.warnings}</span>
    `;

    // Build issues list
    if (analysis.issues && analysis.issues.length > 0) {
      const icons = { error: "X", warning: "!", info: "i" };
      issuesDiv.innerHTML = analysis.issues.map(issue => {
        const cls = "issue-" + issue.severity;
        const icon = icons[issue.severity] || "?";
        return `<div class="issue ${cls}">
          <span class="severity">${icon}</span>
          <span>${escapeHtml(issue.message)}</span>
        </div>`;
      }).join("");
    } else {
      issuesDiv.innerHTML = '<div class="report-pass-banner">No accessibility issues found!</div>';
    }

    // Show what was fixed and what still needs work
    const fixedDiv = document.getElementById("report-fixed");
    const autoFixed = [];
    const needsHuman = [];

    // Determine what the pipeline auto-fixes
    if (!analysis.is_tagged) autoFixed.push("Added document structure tags");
    if (!analysis.has_lang) autoFixed.push("Set document language");
    if (!analysis.title) autoFixed.push("Set document title from content");
    // These are always done by the pipeline
    autoFixed.push("Reading order optimized");
    autoFixed.push("Headings detected and tagged");
    autoFixed.push("Bookmarks created from headings");

    // Check for things that need human input
    const hasAltTextIssue = analysis.issues && analysis.issues.some(i => i.rule === "image-alt-text");
    if (hasAltTextIssue || analysis.images > 0) {
      needsHuman.push("Image descriptions (alt text) -- review and approve below");
    }

    let fixedHtml = "";
    if (autoFixed.length > 0) {
      fixedHtml += `<div class="report-fixed">
        <h4>Fixed automatically in the download:</h4>
        <ul>${autoFixed.map(f => `<li>${f}</li>`).join("")}</ul>
      </div>`;
    }
    if (needsHuman.length > 0) {
      fixedHtml += `<div class="report-remaining">
        Still needs your input: ${needsHuman.join("; ")}
      </div>`;
    }
    fixedDiv.innerHTML = fixedHtml;
  }

  function renderImageCards() {
    imageList.innerHTML = "";
    if (imageData.length === 0) {
      imageList.innerHTML = '<div class="no-images">No images found in this PDF. Structural fixes have been applied.</div>';
      return;
    }
    imageData.forEach((img, idx) => {
      imageList.appendChild(createImageCard(img, idx));
    });
  }

  function updateStats(stats) {
    if (!stats) return;
    document.getElementById("stat-total").textContent = stats.total || 0;
    document.getElementById("stat-approved").textContent = stats.approved || 0;
    document.getElementById("stat-needs").textContent = stats.needs_review || 0;
  }

  function createImageCard(img, idx) {
    const card = document.createElement("div");
    card.className = "image-card";
    card.dataset.hash = img.image_hash;

    const draftVisible = img.ai_draft ? "visible" : "";
    const textValue = img.alt_text || img.ai_draft || "";

    card.innerHTML = `
      <div class="card-header">
        <h3>${img.id} (page ${img.page})</h3>
        <span class="badge badge-${img.status}">${img.status.replace("_", " ")}</span>
      </div>
      <img class="image-preview" src="/api/job/${currentJobId}/image/${img.image_hash}"
           alt="Image from page ${img.page}" loading="lazy">
      <div class="context-hint-field" style="padding:0 1rem">
        <label for="ctx-${idx}" style="font-size:.75rem;color:var(--muted)">Context hint (optional)</label>
        <input type="text" id="ctx-${idx}" class="context-hint"
               placeholder="e.g., Diagram of a benzene ring"
               value="${escapeHtml(img.context || "")}"
               style="width:100%;font-size:.85rem;padding:4px 8px;border:1px solid var(--border);border-radius:4px">
      </div>
      <div class="ai-draft-box ${draftVisible}" data-idx="${idx}">
        <div class="draft-label">AI suggestion</div>
        <div class="draft-text">${escapeHtml(img.ai_draft || "")}</div>
        <button class="btn btn-outline btn-use-draft" data-idx="${idx}">Use this</button>
      </div>
      <label for="alt-${idx}">Alt text:</label>
      <textarea id="alt-${idx}" placeholder="Describe this image for screen readers&hellip;"
        >${escapeHtml(textValue)}</textarea>
      <div class="card-actions">
        <button class="btn btn-success btn-approve" data-idx="${idx}">Approve</button>
        <button class="btn btn-secondary btn-decorative" data-idx="${idx}">Mark decorative</button>
      </div>
    `;
    return card;
  }

  function escapeHtml(str) {
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML;
  }

  // --- AI Generate ---
  btnGenerate.addEventListener("click", async () => {
    if (!currentJobId || imageData.length === 0) return;

    btnGenerate.disabled = true;
    btnGenerate.textContent = "Starting...";

    const payload = { provider: providerSelect.value };
    const key = apiKeyInput.value.trim();
    if (key) payload.api_key = key;
    if (providerSelect.value === "ollama") payload.model = ollamaModelSelect.value;
    const docCtx = docContextInput.value.trim();
    if (docCtx) payload.document_context = docCtx;

    try {
      const resp = await fetch(`/api/job/${currentJobId}/generate`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      const data = await resp.json();

      if (!resp.ok) {
        alert(data.detail || "Generation failed.");
        btnGenerate.textContent = "Generate all";
        btnGenerate.disabled = false;
        return;
      }

      // Poll for generation progress
      await pollForGeneration();
    } catch (err) {
      alert("Generation error: " + err.message);
      btnGenerate.textContent = "Generate all";
      btnGenerate.disabled = false;
    }
  });

  function _updateDraftsLive(freshImages) {
    // Update cards in-place as AI drafts arrive (without re-rendering everything)
    freshImages.forEach((img, idx) => {
      if (!img.ai_draft) return;
      // Skip if we already showed this draft
      if (imageData[idx] && imageData[idx].ai_draft === img.ai_draft) return;

      // Update our local data
      if (imageData[idx]) imageData[idx].ai_draft = img.ai_draft;

      // Find the card and inject the draft
      const card = imageList.children[idx];
      if (!card) return;

      const draftBox = card.querySelector(".ai-draft-box");
      if (draftBox) {
        draftBox.classList.add("visible");
        const draftText = draftBox.querySelector(".draft-text");
        if (draftText) draftText.textContent = img.ai_draft;
      }

      // Auto-fill the textarea if it's still empty
      const textarea = card.querySelector("textarea");
      if (textarea && !textarea.value.trim()) {
        textarea.value = img.ai_draft;
        imageData[idx].alt_text = img.ai_draft;
      }
    });
  }

  async function pollForGeneration() {
    const POLL_INTERVAL = 1000; // ms

    while (true) {
      await new Promise(r => setTimeout(r, POLL_INTERVAL));

      try {
        const resp = await fetch("/api/job/" + currentJobId);
        const data = await resp.json();
        if (!resp.ok) break;

        const gs = data.gen_stage;
        const cur = data.gen_current || 0;
        const tot = data.gen_total || 0;

        if (gs === "generating") {
          btnGenerate.textContent = tot > 0
            ? `Generating ${cur}/${tot}...`
            : "Generating...";
          // Live-update cards as drafts arrive
          _updateDraftsLive(data.images || []);
          continue;
        }

        if (gs === "done" || gs === "error") {
          // Refresh image data and cards
          imageData = data.images || [];
          renderImageCards();
          updateStats(data.stats);

          const errs = data.gen_errors || [];
          if (errs.length > 0) {
            const generated = tot - errs.length;
            alert(`Generated ${generated} draft(s), but ${errs.length} failed.\n${errs[0].error}`);
          }

          const generated = imageData.filter(i => i.ai_draft).length;
          btnGenerate.textContent = generated > 0 ? `Generated ${generated}!` : "Generate all";
          setTimeout(() => { btnGenerate.textContent = "Generate all"; btnGenerate.disabled = false; }, 2500);
          return;
        }
      } catch (err) {
        // Network hiccup -- keep polling
      }
    }

    btnGenerate.textContent = "Generate all";
    btnGenerate.disabled = false;
  }

  // --- Card actions ---
  imageList.addEventListener("click", (e) => {
    const btn = e.target.closest("button");
    if (!btn) return;
    const idx = parseInt(btn.dataset.idx);
    const card = btn.closest(".image-card");

    // "Use this" button copies AI draft to textarea
    if (btn.classList.contains("btn-use-draft")) {
      const textarea = card.querySelector("textarea");
      const draftText = card.querySelector(".draft-text");
      if (textarea && draftText) {
        textarea.value = draftText.textContent;
        imageData[idx].alt_text = draftText.textContent;
      }
      return;
    }

    const badge = card.querySelector(".badge");
    const textarea = card.querySelector("textarea");

    if (btn.classList.contains("btn-approve")) {
      imageData[idx].status = "approved";
      imageData[idx].alt_text = textarea.value;
      badge.className = "badge badge-approved";
      badge.textContent = "approved";
    } else if (btn.classList.contains("btn-decorative")) {
      imageData[idx].status = "decorative";
      imageData[idx].alt_text = "";
      textarea.value = "";
      badge.className = "badge badge-decorative";
      badge.textContent = "decorative";
    }
  });

  btnSave.addEventListener("click", async () => {
    btnSave.disabled = true;
    btnSave.textContent = "Saving\u2026";

    const updates = imageData.map(img => ({
      image_hash: img.image_hash,
      alt_text: img.alt_text,
      status: img.status,
    }));

    // Sync textarea values and context hints
    document.querySelectorAll(".image-card").forEach((card, idx) => {
      const ta = card.querySelector("textarea");
      if (ta) updates[idx].alt_text = ta.value;
      const ctx = card.querySelector(".context-hint");
      if (ctx) updates[idx].context = ctx.value;
    });

    try {
      await fetch(`/api/job/${currentJobId}/alt-text`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(updates),
      });

      const resp = await fetch(`/api/job/${currentJobId}`);
      const data = await resp.json();
      updateStats(data.stats);

      btnSave.textContent = "Saved!";
      btnSaveBottom.textContent = "Saved!";
      setTimeout(() => {
        btnSave.textContent = "Save alt text"; btnSave.disabled = false;
        btnSaveBottom.textContent = "Save alt text"; btnSaveBottom.disabled = false;
      }, 1500);
    } catch (err) {
      btnSave.textContent = "Save alt text";
      btnSave.disabled = false;
      btnSaveBottom.textContent = "Save alt text";
      btnSaveBottom.disabled = false;
      alert("Save failed: " + err.message);
    }
  });

  btnDownload.addEventListener("click", async () => {
    btnDownload.disabled = true;
    btnDownload.textContent = "Preparing\u2026";
    btnDownloadBottom.disabled = true;
    btnDownloadBottom.textContent = "Preparing\u2026";

    try {
      await fetch(`/api/job/${currentJobId}/inject`, { method: "POST" });

      const a = document.createElement("a");
      a.href = `/api/job/${currentJobId}/download`;
      a.download = "";
      document.body.appendChild(a);
      a.click();
      a.remove();

      btnDownload.textContent = "Download fixed PDF";
      btnDownload.disabled = false;
      btnDownloadBottom.textContent = "Download fixed PDF (with alt text)";
      btnDownloadBottom.disabled = false;
    } catch (err) {
      btnDownload.textContent = "Download fixed PDF";
      btnDownload.disabled = false;
      btnDownloadBottom.textContent = "Download fixed PDF (with alt text)";
      btnDownloadBottom.disabled = false;
      alert("Download failed: " + err.message);
    }
  });

  // Bottom bar buttons mirror the top ones
  btnSaveBottom.addEventListener("click", () => btnSave.click());
  btnDownloadBottom.addEventListener("click", () => btnDownload.click());

  // --- Batch upload & progress ---
  async function uploadBatch(files) {
    // Validate all are PDFs
    const pdfs = [];
    for (let i = 0; i < files.length; i++) {
      if (!files[i].name.toLowerCase().endsWith(".pdf")) {
        alert("Only PDF files are accepted. '" + files[i].name + "' is not a PDF.");
        return;
      }
      pdfs.push(files[i]);
    }
    if (pdfs.length === 0) return;

    uploadSection.style.display = "none";
    modeSelector.style.display = "none";
    batchProgressDiv.style.display = "block";
    batchResultsDiv.style.display = "none";

    // Reset progress
    batchProgressFill.style.width = "0%";
    batchProgressTitle.textContent = "Uploading " + pdfs.length + " file(s)...";
    batchFileList.innerHTML = pdfs.map(f =>
      '<div class="batch-file-item pending"><span class="file-status"></span> ' + escapeHtml(f.name) + '</div>'
    ).join("");

    const form = new FormData();
    pdfs.forEach(f => form.append("files", f));

    try {
      const resp = await fetch("/api/batch/upload", { method: "POST", body: form });
      const data = await resp.json();

      if (!resp.ok) {
        showBatchError(data.detail || "Upload failed.");
        return;
      }

      currentBatchId = data.batch_id;
      batchProgressTitle.textContent = "Fixing " + data.total_files + " PDF(s)...";
      await pollBatchProgress(data.batch_id);
    } catch (err) {
      showBatchError("Network error: " + err.message);
    }
  }

  async function pollBatchProgress(batchId) {
    const POLL_INTERVAL = 500;

    while (true) {
      await new Promise(r => setTimeout(r, POLL_INTERVAL));

      try {
        const resp = await fetch("/api/batch/" + batchId);
        const data = await resp.json();
        if (!resp.ok) { showBatchError("Batch check failed."); return; }

        // Update progress bar
        const total = data.total_files || 1;
        const done = data.done_count + data.failed_count;
        const pct = Math.round((done / total) * 100);
        batchProgressFill.style.width = pct + "%";
        batchProgressTitle.textContent = "Fixing PDFs... (" + done + "/" + total + ")";

        // Update per-file list
        batchFileList.innerHTML = data.files.map(f => {
          const cls = f.status === "done" ? "done"
            : f.status === "fixing" ? "fixing"
            : f.status === "error" ? "error" : "pending";
          const icon = f.status === "done" ? "ok"
            : f.status === "error" ? "X"
            : f.status === "fixing" ? "..." : "";
          const errHtml = f.error ? ' <span class="file-error">(' + escapeHtml(f.error) + ')</span>' : "";
          return '<div class="batch-file-item ' + cls + '">'
            + '<span class="file-status">' + icon + '</span> '
            + escapeHtml(f.name) + errHtml + '</div>';
        }).join("");

        if (data.stage === "done" || data.stage === "error") {
          batchProgressFill.style.width = "100%";
          batchProgressTitle.textContent = "Done!";
          await new Promise(r => setTimeout(r, 400));
          showBatchResults(data);
          return;
        }
      } catch (err) {
        // Network hiccup -- keep polling
      }
    }
  }

  function showBatchError(msg) {
    batchProgressDiv.style.display = "none";
    modeSelector.style.display = "flex";
    uploadSection.style.display = "block";
    const el = document.createElement("div");
    el.className = "error-msg";
    el.textContent = msg;
    uploadSection.insertBefore(el, uploadSection.firstChild);
    setTimeout(() => el.remove(), 8000);
  }

  function showBatchResults(data) {
    batchProgressDiv.style.display = "none";
    batchResultsDiv.style.display = "block";

    document.getElementById("batch-stat-total").textContent = data.total_files;
    document.getElementById("batch-stat-done").textContent = data.done_count;
    document.getElementById("batch-stat-failed").textContent = data.failed_count;

    batchResultsList.innerHTML = data.files.map(f => {
      const cls = f.status === "done" ? "done" : f.status === "error" ? "error" : "pending";
      const icon = f.status === "done" ? "ok" : f.status === "error" ? "X" : "";
      const errHtml = f.error ? ' <span class="file-error">(' + escapeHtml(f.error) + ')</span>' : "";
      const scannedHtml = f.is_scanned ? ' <span class="scanned-label">scanned</span>' : "";
      return '<div class="batch-file-item ' + cls + '">'
        + '<span class="file-status">' + icon + '</span> '
        + escapeHtml(f.name) + scannedHtml + errHtml + '</div>';
    }).join("");

    btnBatchDownload.disabled = data.done_count === 0;
  }

  btnBatchDownload.addEventListener("click", () => {
    if (!currentBatchId) return;
    const a = document.createElement("a");
    a.href = "/api/batch/" + currentBatchId + "/download";
    a.download = "";
    document.body.appendChild(a);
    a.click();
    a.remove();
  });

  btnBatchNew.addEventListener("click", () => {
    currentBatchId = null;
    batchResultsDiv.style.display = "none";
    batchProgressDiv.style.display = "none";
    modeSelector.style.display = "flex";
    uploadSection.style.display = "block";
    batchFileInput.value = "";
  });

  btnNew.addEventListener("click", () => {
    currentJobId = null;
    imageData = [];
    resultsDiv.style.display = "none";
    uploadSection.style.display = "block";
    modeSelector.style.display = "flex";
    fileInput.value = "";
    aiToggle.checked = false;
    aiControls.classList.remove("visible");
    modeLabel.style.color = "var(--text)";
    document.getElementById("ai-label-right").style.color = "var(--muted)";
  });
})();
</script>
</body>
</html>
