14.3 UI option B: small web app

Overview and links for this section of the guide.

Goal: a small, safe web UI

The web app option gives you a nicer user experience without adopting a heavy framework. The goal is:

  • a single page with a textarea,
  • a “Summarize” button,
  • rendered structured bullets,
  • clear failure states (timeout, rate limit, blocked, invalid output).
Keep the UI thin

The web UI is a wrapper around your existing pipeline. Your core value lives in the schema, prompt, validation, and error handling—not in the frontend.

Security first: why you need a backend

Do not call the model API directly from the browser. That would expose credentials.

A safe architecture is:

  • frontend: static HTML/JS that sends the article text to your backend
  • backend: holds credentials and calls the model via your LLM wrapper
  • response: validated JSON summary returned to the browser
Never ship credentials to the client

If your browser can see the key, attackers can too. Even “temporary” keys leak.

UX requirements (the minimum that feels good)

A small app can still feel professional with a few basics:

  • Input validation: disable submit if empty; enforce max length.
  • Progress state: show “Summarizing…” while request is in flight.
  • Cancel/retry: allow retry with guidance (don’t encourage spam).
  • Error states: show user-friendly messages per error category.
  • Privacy baseline: don’t store user input by default.

Architecture (thin UI, thin API, strong boundary)

Keep the backend architecture aligned with Section 13:

  • HTTP handler does input validation + response formatting
  • domain function does “summarize article” orchestration
  • LLM wrapper does the model call + validation + retries/timeouts

This prevents a common mistake: putting prompts and model calls inside your HTTP handler.

Your HTTP handler should be boring

If your handler is long and full of prompt strings, you’re building a fragile system. Move model work behind the wrapper boundary.

Endpoints and contracts

Keep your API minimal. A practical v1:

  • GET /: serve the HTML page
  • POST /api/summarize: accepts JSON with article_text and optional title

Request contract

{
  "article_text": "string",
  "title": "string | null"
}

Response contract (recommended)

Return an outcome envelope instead of raw summary only:

{
  "status": "ok" | "blocked" | "timeout" | "rate_limit" | "invalid_output" | "validation_error" | "unknown",
  "result": { ...schema... } | null,
  "message": "string | null",
  "request_id": "string"
}

This makes frontend handling straightforward and prevents “mysterious failures.”

Repo structure (recommended)

summarizer-web/
  README.md
  .gitignore
  .env.example
  src/
    web/
      server.py (or server.ts)
      static/
        index.html
        main.js
        styles.css
    app/
      summarize.py
    llm/
      client.py
      prompts/
      schemas/
  tests/
    test_api_contract.py
    test_app_logic.py

Testing strategy (unit + integration)

Tests should be deterministic:

  • Unit tests: validate schema enforcement and app logic with a fake LLM client.
  • Integration test: call /api/summarize with a stubbed backend (or dependency injection) to verify request/response contract.

Avoid tests that call the real model API.

Prompt sequence to build it

Use the same incremental approach as the CLI path:

  • scaffold files + minimal server,
  • build the API endpoint returning placeholder data,
  • wire in the LLM wrapper,
  • add validation + error handling,
  • add UX states on the frontend.
Don’t start with frontend polish

Get the data flow working first. Then polish the UI once outputs are validated and error handling is correct.

Ship points for the web path

  • SP1: web server runs; page loads; submit returns placeholder JSON.
  • SP2: API wired to LLM wrapper; returns validated schema output.
  • SP3: error categories implemented end-to-end (backend + UI states).
  • SP4: timeouts/retries/fallbacks; prompt versions logged.

Where to go next