14.3 UI option B: small web app
Overview and links for this section of the guide.
On this page
- Goal: a small, safe web UI
- Security first: why you need a backend
- UX requirements (the minimum that feels good)
- Architecture (thin UI, thin API, strong boundary)
- Endpoints and contracts
- Repo structure (recommended)
- Testing strategy (unit + integration)
- Prompt sequence to build it
- Ship points for the web path
- Where to go next
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).
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
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.
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_textand optionaltitle
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/summarizewith 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.
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.