Intermediate

Upload & Review UI

In this step, you will create a drag-and-drop upload interface with extraction result display, inline editing, and correction capabilities.

What We Are Building

A single-page web interface that lets users drag and drop documents, see real-time processing progress, review extraction results, and correct any errors before exporting the data.

Step 1: The Upload Interface

<!-- frontend/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document Intelligence</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
           background: #f5f5f5; color: #333; }
    .container { max-width: 900px; margin: 0 auto; padding: 20px; }
    h1 { text-align: center; margin: 20px 0; color: #6366f1; }

    /* Drop zone */
    .drop-zone { border: 3px dashed #ccc; border-radius: 12px; padding: 60px 20px;
      text-align: center; cursor: pointer; transition: all 0.3s;
      background: white; margin-bottom: 20px; }
    .drop-zone.drag-over { border-color: #6366f1; background: #eef2ff; }
    .drop-zone p { font-size: 18px; color: #666; }
    .drop-zone .icon { font-size: 48px; margin-bottom: 10px; }

    /* Job list */
    .job-list { list-style: none; }
    .job-item { background: white; border-radius: 8px; padding: 16px;
      margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .job-header { display: flex; justify-content: space-between; align-items: center; }
    .job-name { font-weight: 600; }
    .job-status { padding: 4px 12px; border-radius: 20px; font-size: 13px; }
    .status-pending { background: #fef3c7; color: #92400e; }
    .status-processing { background: #dbeafe; color: #1e40af; }
    .status-completed { background: #d1fae5; color: #065f46; }
    .status-failed { background: #fee2e2; color: #991b1b; }
    .progress-bar { height: 6px; background: #e5e7eb; border-radius: 3px; margin-top: 8px; }
    .progress-fill { height: 100%; background: #6366f1; border-radius: 3px; transition: width 0.5s; }

    /* Results */
    .results-panel { background: white; border-radius: 8px; padding: 20px;
      margin-top: 12px; border: 1px solid #e5e7eb; }
    .field-row { display: flex; gap: 12px; margin-bottom: 8px; align-items: center; }
    .field-label { font-weight: 600; min-width: 140px; color: #6b7280; }
    .field-value { flex: 1; }
    .field-value input { width: 100%; padding: 6px 10px; border: 1px solid #d1d5db;
      border-radius: 6px; font-size: 14px; }
    .btn { padding: 8px 20px; border: none; border-radius: 6px; cursor: pointer;
      font-size: 14px; font-weight: 500; }
    .btn-primary { background: #6366f1; color: white; }
    .btn-export { background: #10b981; color: white; }
  </style>
</head>
<body>
  <div class="container">
    <h1>📄 Document Intelligence</h1>

    <div class="drop-zone" id="dropZone">
      <div class="icon">📤</div>
      <p>Drag & drop documents here<br>or click to browse</p>
      <input type="file" id="fileInput" multiple accept=".pdf,.png,.jpg,.jpeg,.tiff,.bmp"
        style="display:none">
    </div>

    <ul class="job-list" id="jobList"></ul>
  </div>

  <script>
    const dropZone = document.getElementById("dropZone");
    const fileInput = document.getElementById("fileInput");
    const jobList = document.getElementById("jobList");
    const jobs = {};

    dropZone.addEventListener("click", () => fileInput.click());
    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");
      handleFiles(e.dataTransfer.files);
    });
    fileInput.addEventListener("change", () => handleFiles(fileInput.files));

    async function handleFiles(files) {
      for (const file of files) {
        const form = new FormData();
        form.append("file", file);
        const res = await fetch("/api/upload", { method: "POST", body: form });
        const data = await res.json();

        // Start processing
        const procRes = await fetch(`/api/process?filename=${encodeURIComponent(file.name)}`,
          { method: "POST" });
        const procData = await procRes.json();

        addJobToUI(procData.job_id, file.name);
        pollJob(procData.job_id);
      }
    }

    function addJobToUI(jobId, filename) {
      const li = document.createElement("li");
      li.className = "job-item";
      li.id = `job-${jobId}`;
      li.innerHTML = `
        <div class="job-header">
          <span class="job-name">${filename}</span>
          <span class="job-status status-pending">Pending</span>
        </div>
        <div class="progress-bar"><div class="progress-fill" style="width:0%"></div></div>
      `;
      jobList.prepend(li);
    }

    async function pollJob(jobId) {
      const interval = setInterval(async () => {
        const res = await fetch(`/api/jobs/${jobId}`);
        const data = await res.json();
        updateJobUI(jobId, data);
        if (data.status === "completed" || data.status === "failed") {
          clearInterval(interval);
          if (data.status === "completed") loadResults(jobId);
        }
      }, 1000);
    }

    function updateJobUI(jobId, data) {
      const el = document.getElementById(`job-${jobId}`);
      if (!el) return;
      const statusEl = el.querySelector(".job-status");
      statusEl.textContent = data.status;
      statusEl.className = `job-status status-${data.status}`;
      el.querySelector(".progress-fill").style.width = `${data.progress * 100}%`;
    }

    async function loadResults(jobId) {
      const res = await fetch(`/api/results/${jobId}`);
      const data = await res.json();
      const el = document.getElementById(`job-${jobId}`);
      const panel = document.createElement("div");
      panel.className = "results-panel";

      let html = "<h3>Extracted Data</h3>";
      const fields = data.structured_data || {};
      for (const [key, value] of Object.entries(fields)) {
        if (typeof value !== "object") {
          html += `<div class="field-row">
            <span class="field-label">${key}</span>
            <span class="field-value"><input value="${value || ""}"></span>
          </div>`;
        }
      }
      html += `<div style="margin-top:12px">
        <button class="btn btn-export" onclick="exportJSON('${jobId}')">Export JSON</button>
      </div>`;
      panel.innerHTML = html;
      el.appendChild(panel);
    }

    function exportJSON(jobId) {
      window.open(`/api/results/${jobId}/download`, "_blank");
    }
  </script>
</body>
</html>

Step 2: Results API Endpoints

# Add to app/main.py

@app.get("/api/results/{job_id}")
async def get_results(job_id: str):
    """Get extraction results for a completed job."""
    results_path = Path(settings.results_dir) / f"{job_id}.json"
    if not results_path.exists():
        raise HTTPException(status_code=404, detail="Results not found")
    with open(results_path) as f:
        return json.load(f)

@app.get("/api/results/{job_id}/download")
async def download_results(job_id: str):
    """Download results as a JSON file."""
    results_path = Path(settings.results_dir) / f"{job_id}.json"
    if not results_path.exists():
        raise HTTPException(status_code=404, detail="Results not found")
    return FileResponse(
        str(results_path),
        media_type="application/json",
        filename=f"extraction-{job_id}.json"
    )
💡
UX improvement: The inline editing fields let users correct extraction errors before exporting. Track correction rates to identify which fields need better extraction prompts.

Key Takeaways

  • Drag-and-drop upload with file type validation provides a smooth user experience.
  • Real-time polling updates progress bars and status badges as documents process.
  • Inline editable fields let users correct extraction errors before exporting.
  • JSON export provides clean structured data for downstream systems.