E3: UI — Epic Design Doc
Status: 🔄 In Refinement (Step 0) Authors: Dan Hannah & Clay Created: 2026-04-18 Parent: QuoteAI Project Design Doc
Overview
Goals & Non-Goals
Goals:
- Ship a structured quote-request form (the "wall" per John) with all 15+ required fields, validated
- Ship a draft-view page that renders the CC-generated quote in 4M-template shape
- Wire the form → CC → draft-view flow end-to-end
- Dark-mode-first design (per user preference) — clean, "nice-looking," professional enough to show John
- Feedback buttons on the draft view (👍/👎 per line item)
Non-Goals:
- No authentication / login
- No approval workflow UI (Full MVP)
- No quote log / dashboard UI (Full MVP)
- No PDF export (copy-paste from the rendered draft is fine for demo)
- No responsive mobile polish (desktop demo first)
- No admin UI for managing ingested data (CLI only, per E1)
Problem Statement
The UI is the face of the demo. Two things have to work: (1) the form must feel like a natural upgrade to the Brehob spreadsheet salespeople fill today, and (2) the output must look like a Brehob quote, not a "generic AI draft." Both are needed for John to react with "this is how every quote should look."
Per John's "wall" constraint, the form is the only way salespeople interact with the system. No chat, no freeform AI prompt. The form is the contract.
What Is This Epic?
A Next.js (App Router) UI with two primary pages:
/quotes/new— the structured quote request form/quotes/draft/[id]— the CC-assembled draft view in Brehob template format
Plus the glue that connects them: a Server Action or API route that receives the form submission, writes a pending request to a temp table, triggers CC to assemble the draft, and displays the result.
Context
Dependents
- Demo experience — E3 is what John actually sees
- Future Full MVP E4 (Approval Workflow) — adds a review queue on top of
/quotes/draft/*
Dependencies
- E0 (Foundation) — Next.js shell, Tailwind, shadcn/ui primitives exist
- E2 (MCP Servers) — CC needs them running to assemble drafts
- E1 (Data) — via E2
- Claude Code — the demo's generator; triggered from the app
Current State
Next.js shell exists from E0 with a /health page. No forms, no draft view, no generate flow.
Affected Systems
| System / Layer | How It's Affected |
|---|---|
app/ (Next.js) | New routes: /quotes/new, /quotes/draft/[id]; shared form components |
| Postgres | New temp table quote_drafts (demo-only) to persist form submissions + generated drafts |
| Claude Code | Triggered from a server-side flow with the form data + template reference |
Design
Page: /quotes/new
Form built with React Hook Form + Zod validation. Sections:
- Customer Info — Company, contact, address, ship-to, email, phone
- Quote Metadata — Salesperson name, TM number, date required, (auto-filled) quote number preview
- Line Items (repeatable, 1-N) — Each with: description hint, qty, CFM, PSI, HP, voltage, cooling (air/water), unit price (entered by salesperson)
- Context — Free-text field for "special requirements" (e.g., "food-grade environment, oilless required")
Submit button: "Generate Draft"
Validation (per design doc "The Wall"):
- CFM: 1–5,000
- PSI: 10–500
- HP: 1–500
- Voltage: must match patterns like
230/1/60,460/3/60 - TM Number: required (salesperson's territory number)
- All customer fields required
- Date required: must be a future date
- At least one line item with a description
Page: /quotes/draft/[id]
Renders the assembled draft in Brehob template shape. Layout:
- Header bar — Quote #, status badge, copy-to-clipboard button
- Document preview — Rendered markdown (from CC's output) styled to mimic the 4M letterhead/template
- Per-line-item feedback — Each line item has 👍/👎 with optional comment
- Action bar — "Regenerate" button (useful during iteration), "Copy Quote Text" (for paste-into-email-to-customer)
Important: This is a display layer for CC's output, not a WYSIWYG editor. For demo, salesperson edits happen by re-submitting the form. Full MVP can add inline editing.
Generate Flow (form → CC → draft)
Demo architecture uses Claude Code as the generation engine. This is the only real "magic" in the demo — the flow needs to be explicit about it.
1. Form submits to POST /api/quotes/generate (Next.js Route Handler)
2. Handler validates form data against Zod schema
3. Handler inserts a row into `quote_drafts` with status='pending', form_json
4. Handler invokes Claude Code via a local subprocess (or a "/quoteai-generate" skill)
passing: form_json + path to templates/brehob-quote.md + MCP server config
5. CC calls search_line_items, search_equipment, search_past_quotes as needed
6. CC returns assembled draft text
7. Handler updates quote_drafts row: status='ready', generated_text
8. Handler redirects to /quotes/draft/[id]
9. Page renders generated_text in Brehob template style
CC invocation detail: Easiest path is a local CLI spawn: claude -p "$PROMPT" --mcp-config ./cc-mcp-config.json. The prompt assembles form data + reference to the template file. CC handles MCP tool calls internally.
If the subprocess path is awkward, alternative: CC runs as a persistent background agent and the app talks to it via a local HTTP endpoint. TBD in S0 spike.
Styling / Design System
- shadcn/ui components (Form, Input, Card, Button, Badge, Tabs)
- Tailwind with dark-mode class on
<html>by default - Typography — Inter or similar for UI; serif for the document preview (mimics Brehob's Word-doc feel)
- Color palette — neutral dark with a single accent (maybe Brehob blue?) — keep it calm, professional
- Print styles — document preview should print/PDF cleanly even though explicit PDF export is post-MVP
Data Model Changes
One new table for demo flow persistence:
CREATE TABLE quote_drafts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
form_json JSONB NOT NULL,
generated_text TEXT,
status TEXT DEFAULT 'pending', -- pending, ready, failed
generated_at TIMESTAMPTZ,
error TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
This is demo-scoped. When Full MVP E5 lands, quote_drafts rows get promoted into quote_log + quote_log_items. Or the table gets renamed. Decide during Full MVP scoping.
API / Interface Changes
Route Handlers (Next.js)
| Route | Method | Purpose |
|---|---|---|
/api/quotes/generate | POST | Receive form, trigger CC, return draft ID |
/api/quotes/[id] | GET | Fetch draft by ID (used by the draft page) |
/api/quotes/[id]/feedback | POST | Log 👍/👎 on a line item |
Client Components
<QuoteRequestForm />— the form<LineItemEditor />— repeatable line-item sub-form<DraftView />— renders generated_text<FeedbackButton />— per-line-item rating
Edge Cases & Gotchas
| Scenario | Expected Behavior | Why It's Tricky |
|---|---|---|
| CC takes >30s to generate | Show loading state with progress message | Generation isn't instant; UX needs to feel "working" not "broken" |
| CC fails / errors | Update quote_drafts.status='failed', show retry button on draft page | Don't silently lose the form data |
| User submits same form twice (double-click) | Idempotency key or disable button on submit | Classic form gotcha |
| Multi-option quote (~50% of real quotes) | Support adding an "Option 2" tab with its own line items | Design doc calls this out as open; E3 must answer it |
| Very long descriptions in draft | Auto-wrap, don't overflow; print-safe | Some installation blocks are 500+ words |
| Dark mode clashes with document preview (which mimics a white-paper feel) | Preview area uses a light-on-dark-background surface even in dark mode | Document-preview-in-dark-mode is a UX tension |
| Voltage regex too strict | Offer suggestions, not just reject | Real data shows variations John might enter differently |
Testing Strategy
Test Layers
| Layer | Applies? | Notes |
|---|---|---|
| Unit tests | Yes | Zod schemas; form validation; voltage pattern matcher |
| Integration tests | Yes | Form submit → DB row created; draft page fetches by ID |
| Visual regression (Crucible) | Yes | Form page + draft page have baselines. Dark mode is the canonical baseline per user preference. |
| E2E (Playwright or manual) | Yes | Full flow: open /quotes/new, fill valid form, submit, wait, see draft |
| Accessibility | Yes | Basic a11y check: form fields have labels, errors are announced |
Required Fixtures
| Fixture Name | What It Tests | Priority |
|---|---|---|
fixtures/e3-form-submission.json | Full valid form payload for CC to assemble against | 🔴 High |
fixtures/e3-golden-draft.md | Expected Brehob-format output for the golden scenario | 🔴 High |
fixtures/benchmark-quotes/4m-industries.md | End-to-end benchmark: given 4M's original form inputs, the generated draft should match this finished Brehob quote for description fidelity, formatting, and completeness | 🔴 High |
fixtures/benchmark-quotes/bowen-engineering.md | End-to-end benchmark: standard-complexity multi-line system quote | 🟡 Medium |
fixtures/benchmark-quotes/munson-hospital.md | End-to-end benchmark: complex quote with installation / custom scope | 🟡 Medium |
baselines/form-dark.png | Form page dark-mode baseline | 🔴 High |
baselines/draft-dark.png | Draft page dark-mode baseline (with seeded content) | 🔴 High |
Note on benchmark quotes (new per 2026-04-18 decision): Distinct from the golden retrieval test (E1). The golden retrieval test asks "did we find the right line items?" The benchmark quotes ask "does the assembled draft match a known-good Brehob quote?" Both run during E3's E2E verification.
Verification Rules
- Golden scenario passes end-to-end before demo. Open form → fill it in → draft appears matching
golden-draft.mdwithin acceptable drift. - Benchmark quotes pass end-to-end. Given the original form inputs for 4M / Bowen / Munson, the generated drafts match the benchmark fixtures for description fidelity, formatting, and completeness. This is the assembled-output quality gate (complementary to the golden retrieval test in E1).
- Dark mode is the primary baseline. Per user preference.
- Form validation errors are clear and in-place (not a toast at the top).
- All user preferences from memory files respected (dark mode especially).
Stories
| Story | Summary | Status | Commit |
|---|---|---|---|
| S0 | Agent SDK round-trip spike + reconciled handoff from Claude Design | ✅ Shipped | 72595c7 |
| S1 | quote_drafts staging table + atomic quote_no allocation | ✅ Shipped | f233420 |
| S2 | /quotes/new — Form A with React Hook Form + Zod validation | ✅ Shipped | 7deda9d |
| S3 | Brehob Letterhead component + verified data const | ✅ Shipped | de4f27f |
| S4 | Claude Agent SDK wiring + SSE route handler | ✅ Shipped | 4625f6a |
| S5 | markdown → RenderMeta adapter + SSE wire-up | ✅ Shipped | 30defbd |
| S6 | Price injection — {{LINE_N_TOTAL}} / {{TOTAL}} substitution on render | ✅ Shipped | 7a4bc5e |
| S7 | /quotes/[id] — streaming "Generating" screen with SSE consumer | ✅ Shipped | 2212101 |
| S8 | /quotes/draft/[id] — Draft F three-rail view | ✅ Shipped | 17d8370 |
| S9 | Regenerate-with-guidance — dialog + action + chip composer | ✅ Shipped | 9d7c1c1 |
| S10 | Content-failure banner for Draft F | ✅ Shipped | 58e291f |
| S11 | Backend smoke script + demo SMOKE.md checklist | ✅ Shipped | c97d787, f114b72 |
Post-S11 demo polish (2026-04-21 → 2026-04-22)
Not numbered stories — small scoped changes landing ahead of John's review.
| Work | Summary | Commit |
|---|---|---|
| App shell | Header + landing + recent drafts + demo scenarios | 7231547 |
| Thinking fix | Disable Sonnet 4.6 extended thinking (437s → 110s) — see E3-D4 | ecb250d |
| Router fix | Relocate quote pages under app/app/ so Next.js picks them up | 0aefb1e |
| Hi-fi design port | Warm-greyscale palette + Source Serif 4 / Inter / JetBrains Mono ported to form + landing + header — see E3-D7 | 11eac9d |
| Streaming UX | Four-stage display-driven checklist + client-side pacer + window-scroll auto-follow — see E3-D5, E3-D6 | 2d75fa8 |
| Preamble trim | Server-side md_delta buffer until first fence — see E3-D8 | 4975380 |
| Replay + sticky | /quotes/[id]/replay route + draft-rail top-offset fix | 6f9db34 |
Note on story drift. The pre-implementation Stories list (2026-04-18) had different summaries for S3-S10 — field validation, line-item repeater, multi-option tabs, feedback buttons, loading states, E2E walkthrough, dark-mode polish. Implementation diverged as shape emerged. Most notably S5 (multi-option quotes) was dropped per E3-D1, and post-generation UX (streaming, three-rail draft, regenerate flow) became the bulk of the work. The table above reflects what shipped, not the original aspiration.
Known Issues / Tech Debt
| Issue | Severity | Notes |
|---|---|---|
quote_drafts overlaps with future quote_log | 🟡 Medium | Intentional demo-scoped; merge in Full MVP |
| No inline editing of the draft | 🟡 Medium | Demo accepts "regenerate with different form input"; Full MVP adds edit |
| No print-optimized CSS | 🟢 Low | Good enough; PDF export is post-MVP anyway |
| CC subprocess has no timeout / cancellation | 🟡 Medium | Add a 60s timeout; failed generation shows retry |
No user identity in quote_drafts | 🟢 Low | Demo is single-tenant Brehob; Full MVP adds salesperson identity |
Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| CC subprocess path is fragile | Medium | 🔴 High | S0 spike explicitly derisks this; fall back to persistent-agent + local HTTP if needed |
| Draft rendering doesn't look "Brehob enough" | Medium | 🔴 High | Build golden-draft.md as a ground truth; iterate styling until rendered output matches visually |
| Multi-option quotes are messy in the UI | Medium | 🟡 Medium | Scope S5 explicitly; prototype before committing to tabs vs stacked sections |
| Form feels clunky or slow | Low | Medium | React Hook Form is fast; use optimistic UI where possible |
| Dark mode + Brehob-letterhead-feel clashes | Medium | 🟢 Low | Preview area can be a light surface within the dark chrome — common pattern in admin tools |
Decisions Log
| Date | Decision | Rationale | Alternatives Considered |
|---|---|---|---|
| 2026-04-18 | Next.js App Router | E0 already chose this | Pages Router (rejected: legacy) |
| 2026-04-18 | React Hook Form + Zod | Matches shadcn/ui's patterns; type-safe validation | Formik (rejected: older), raw state (rejected: tedious for 15+ fields) |
| 2026-04-18 | Dark mode as default (not opt-in) | Per user memory — Dan uses Foundry exclusively in dark mode | Light default with toggle (rejected: contradicts preference) |
| 2026-04-18 | CC as generator for demo | Per project design doc Rev 7 | Anthropic API in-app (rejected: Full MVP) |
| 2026-04-18 | Display-only draft view (no inline edit) | Simpler; "regenerate" loop is fine for demo iteration | WYSIWYG editor (rejected: scope creep) |
| 2026-04-18 | quote_drafts is a demo-scoped staging table | Avoids mixing with quote_log which has approval workflow semantics | Promote directly to quote_log (rejected: quote_log schema assumes approval state) |
| 2026-04-18 | Feedback is per-line-item, not per-quote | Matches retrieval granularity; better signal | Per-quote (rejected: too coarse) |
| 2026-04-18 | Visual baselines mandatory for form + draft pages | Per user preference and QA methodology | Skip for demo (rejected: risks visual regression during iteration) |
| 2026-04-18 | End-to-end benchmark quotes for assembled-output validation | Pick 2-3 finished Brehob quotes (4M Industries, Bowen Engineering, Munson Hospital) as system-output ground truth. Given the same form inputs, generated drafts are compared for description fidelity, formatting, and completeness. Complementary to E1's golden retrieval test. | Golden retrieval test only (rejected: doesn't validate assembled output quality) |
| 2026-04-21 | Markdown output from model, not structured JSON (E3-D2) | Agent SDK emits tokens incrementally; composing a valid JSON object piece-by-piece during a 60-90s stream is fragile (one unescaped quote = parse failure). Model drafts a markdown doc in natural shape; server-side adapter (app/lib/cc/adapter.ts) parses markdown to typed RenderMeta for Draft F. Streams cleanly, survives malformed output, and lets the draft be human-readable even mid-generation. | Structured JSON output (rejected: fragile streaming, model needs to escape every quote, ~1/3 slower in spike); freeform prose with post-hoc parsing (rejected: unbounded format drift) |
| 2026-04-22 | Port hi-fi design tokens + rename --accent to --brand (E3-D7) | design/e4-hifi.html defined the warm-greyscale palette + Source Serif 4 + Inter + JetBrains Mono direction. Ported to app/app/globals.css wholesale. Hi-fi's --accent is warm orange (brand color); shadcn's --accent is a subtle hover/focus surface token that Tailwind maps to bg-accent. Using the same name would have made every shadcn bg-accent render warm orange. Renamed hi-fi → --brand; shadcn's --accent now aliases --surface-3. If you ever port more hi-fi CSS snippets, mentally substitute --accent → --brand. | Keep shadcn cold-grey defaults (rejected: visual identity matters for John's review); override shadcn's --accent to the warm orange (rejected: would break every shadcn hover/focus state) |
Active decisions being validated during John's demo review live in quoteai/decisions.md — E3-D1 (no multi-option), E3-D4 (thinking disabled), E3-D5 (four-stage checklist), E3-D6 (display pacer), E3-D8 (preamble trim). They graduate here once the demo validates them.
E3 is the demo. Everything previous exists to make this moment — John opens the page, fills the form, and sees the draft — land right.