think
16px
820px

Batch Upload — Design

Date: 2026-07-04
Status: Approved (brainstorm with user)
Modules: CORE feature; ai + semantic decorate it (analysis, suggestions) and it degrades cleanly without them.

Goal

Upload up to 100 files at once through a full-page workbench that preserves everything
the single-file Smart Upload Assistant does per file — AI summary/classification/tags,
content-aware folder suggestion, Doc-ID numbering, preview — with per-file auto-filing
("drop a mixed stack, AI sorts it into folders, user reviews and confirms") and without a
cluttered UI. Nothing is created until "Create all"; leaving the page abandons cleanly.

Decisions (made during brainstorm)

  1. Scale: medium — up to ~50-100 files. Full-page manager, queued analysis with a
    concurrency cap, robust per-row progress states. (Migration-scale hundreds+ is out of
    scope — that would be a background-job system.)
  2. Filing: per-file auto-filing. Each file gets its own content-aware folder
    suggestion; the top suggestion is pre-selected as that file's destination; the batch
    may span many folders. User overrides per file or accepts all.
  3. Layout: master-detail workbench. File list left (status + destination per row),
    the full single-file editor right for the selected file, preview as a popup modal,
    bulk actions in a top bar.
  4. Commit semantics (carried from the single-file philosophy): staged client-side →
    "Create all" runs a queue → per-row created/failed status → failures retry per row,
    successes never roll back → cancel/abandon leaves zero rows.
  5. Batch operations on the queue (user addition): shift/ctrl multi-select with a
    selection action bar (remove, set classification/format/folder, add tag, accept
    suggestions, retry).

Entry points

  • The documents toolbar gains Batch upload next to Upload/Write → route
    /documents/batch-upload (opened with the current folder as context).
  • The existing Upload Document modal's dropzone becomes multiple; dropping 2+ files
    forwards them (as in-memory File objects via router state) to the batch page. One
    file keeps today's modal exactly as-is.
  • Files never leave browser memory until creation. A beforeunload guard warns while
    rows are pending. Hard cap 100 files; the same extension allowlist as AddVersion,
    enforced at add-time with a per-file reject message.

The workbench

Top bar

Add files · Accept all suggestions · Defaults ▾ (apply classification / Doc-ID
format / add-tag to ALL rows) · live tally (12 files · 9 ready · 2 analyzing · 1 needs input) · primary Create all (N) where N = the creatable rows only — blocked rows
(warning badge) are skipped, stay pending, and never hold the rest of the batch hostage.
The button is disabled only when N = 0.

Left: file list

One compact row per file:
- Status dot: queued → analyzing → ready | no-text | failed | created ✓ |
create-failed ✗.
- Title (from filename, editable inline on click-into-text and in the editor).
- Destination line: → /path 88% (accepted suggestion) or the chosen folder; empty +
warning badge when no destination.
- Warning badge when the row is blocked from creation: missing {custom:…} values,
no folder selected, disallowed extension.
- Row click = select (opens in editor). Checkbox column mirrors selection.

Selection model & batch operations (file-manager standard)

  • Click = single select · Ctrl/Cmd-click = toggle · Shift-click = range from anchor ·
    Ctrl/Cmd-A (list focused) = select all · Esc = clear.
  • Selection action bar (Carbon batch-actions pattern) slides in at ≥2 selected with
    "N selected": Remove from queue · Set classification ▾ · Set Doc-ID format ▾
    · Add tag… · Set folder… (one browse, applied to all selected) · Accept AI
    suggestions
    · Retry failed.
  • Defaults ▾ in the top bar always targets ALL rows; the selection bar targets the
    selection. Same operations, unambiguous scope.

Right: detail editor

The single-file form, bound to the selected row: Title · Classification (AI-preset with
badge, registry-validated) · AI suggestions panel (editable summary, tag chips — same
component behavior as the upload modal) · Doc-ID format + per-file {custom:…} inputs ·
FolderSuggestBox (that file's own top-3 content-aware suggestions + current folder +
browse) · Preview (popup modal, blob URL, same viewer as the upload modal) · Remove.
With multi-select ≥2, the editor swaps to a compact "N files selected" panel exposing
the selection-bar actions — no half-bound form fields.

AI pipeline per file (existing endpoints only)

A client-side queue, concurrency 3, runs each file through:
1. POST /ai/analyze-upload (multipart; admin page-cap + lang locale apply) →
summary/classification/tags + analysis_id (+truncated).
2. POST /semantic/suggest-folder with {title, filename, text: frozen AI summary}
top-3 stored on the row, top-1 auto-selected as destination.

Behavior rules:
- The admin upload_suggest_mode is honored per file: immediate → suggestion runs
name-only at add-time and step 2's content pass is skipped; after_analysis (default)
→ suggestion waits for analysis; both → name-only at add, refreshed after analysis.
- Analysis disabled / ai unlicensed → rows skip step 1; suggestions are name-only.
semantic unlicensed → no suggestions at all; destination defaults to the page's
origin folder; FolderSuggestBox degrades to current+browse (existing behavior).
- Budget 429 mid-batch: the AI queue stops for remaining rows, a banner explains
("token budget exhausted — remaining files continue without AI suggestions"), those
rows stay fully usable manually. Creation is unaffected.
- Any per-row analysis failure (no_text, provider error) degrades that row to manual —
never blocks the batch.

Create all

Sequential creation queue, concurrency 2, per row:
POST /documents (confirmed classification, format_id, custom_vars, chosen folder_id) →
POST /documents/{id}/versions with analysis_id + edited summary (seeds content_text
exactly like the single-file path; truncated analyses fall back to async extraction as
already built) → PUT /documents/{id}/tags (selected tags).

  • Per-row terminal state: created ✓ (row becomes a link to the document) or
    create-failed ✗ (row keeps its data + Retry; the tally shows failures).
  • No rollback of successes; re-running Create all only processes non-created rows.
  • Doc-IDs come from the existing gapless counters at creation. Rows whose chosen format
    has {custom:…} tokens are blocked (warning badge + tally "needs input") until
    values are filled — creation never burns a counter on an invalid row.
  • After the queue drains: summary line (10 created · 2 failed) + the page stays for
    retries/links. Created rows are removed from the pending tally.

Backend deltas (deliberately tiny)

  • analysisCacheMax 64 → 256 (go/internal/httpapi/analysis_cache.go): a 100-file
    batch must not evict its own entries before Create redeems them. No other backend
    change — analyze, suggest, create, version+redeem, and tags all exist.

Error handling

Situation Behavior
File type not allowed / over 100 cap rejected at add-time with a toast naming the file(s)
Analysis fails / no text / budget 429 row degrades to manual (name-only suggestion); batch continues
No destination folder on a row warning badge; row excluded from Create all (N) count
Missing {custom:…} values warning badge "needs input"; row blocked until filled
Create/upload/tags call fails row create-failed ✗ + Retry; successes untouched
Navigating away with pending rows beforeunload confirm; nothing persisted
Token/session expiry mid-batch failed rows accumulate with Retry (calls 401 → row failure, not page crash)

Testing (repo discipline: never go test on the live DB)

  • Build/vet + tsc/vite build.
  • Curl-scripted e2e of the creation path: 10-file mixed batch (PDF/text/docx) → assert
    per-file document_enrichment rows, seeded content_text, tags, and distinct
    destination folders
    ; a truncated row falls back to async extract; a foreign/expired
    analysis_id row still creates (silent normal path); budget-exhaustion mid-batch
    degrades remaining rows; cleanup purges all e2e docs.
  • UI click-through: multi-drop handoff from the modal, shift/ctrl selection, selection
    bar ops, preview popup, per-row retry, core-only rendering (no AI/semantic panels).

Out of scope (deliberate)

  • Migration-scale imports (hundreds+; background jobs, resumability) — separate project.
  • Server-side batch orchestration/staging tables — the client queue over existing
    endpoints is sufficient at this scale and keeps cancel-leaves-nothing for free.
  • Duplicate detection, ZIP expansion, folder-structure import.
  • Editing files after created ✓ (that's the document detail page's job).