Files
chat/UI-PLAN.md
T
2026-05-29 13:47:34 +02:00

11 KiB

chat.saiden.dev — UI plan (Her, 2013)

Brief: simple chat. Operator prompt at bottom. Thinking indication. BT's reply streams in as a typewriter animation. Get as close to Spike Jonze's Her / Geoff McFetridge's OS1 as the medium allows.


Layout philosophy

One centered column. Nothing else. No header bar. No sidebar. No footer. The conversation IS the page.

┌──────────────────────────────────────────────────────────────┐
│                                                               │
│                      ·                                        │  ← tiny Saiden
│                                                               │     sigil (12px)
│                                                               │     top-right
│                                                               │
│                                                               │
│       Pilot                                                   │
│       how was your day?                                       │
│                                                               │
│                                                               │
│       BT                                                      │
│       Quiet, Pilot. The mesh held. Three packets              │
│       drifted from sazabi but they came back. Nothing         │
│       I'd call attention to.                                  │
│                                                               │
│                                                               │
│                                                               │
│       Pilot                                                   │
│       what are you reading                                    │
│                                                               │
│       ●                                                       │  ← thinking
│                                                               │     dot
│                                                               │
│                                                               │
│                                                               │
│       ──────────────────────────────────                      │  ← input
│       speak to BT                                             │     underline
│                                                               │
└──────────────────────────────────────────────────────────────┘
                  ↑ max-width ~640px, centered, generous breathing

Key rules

  • Column width: max-width: 38rem (~608px) — letter-sized, like reading prose
  • Vertical padding: 4rem top + bottom
  • Horizontal padding on small screens: 1.5rem
  • Auto-scroll to bottom on new message, but slowly (smooth easing, ~600ms)

Color tokens

Mapped from the Her research notes. All warm, no cool.

:root {
  /* surfaces */
  --bg:           #f5ebe0;   /* cream — page background */
  --bg-soft:      #f8e8db;   /* dusty peach — hover surfaces, if any */
  --surface-card: #f0d9c7;   /* dusty rose — rarely used, only for emphasis */

  /* text */
  --ink:          #3d2820;   /* warm dark brown — BT's voice, primary */
  --ink-muted:    #8b6f60;   /* dusty taupe — pilot's messages, system text */
  --ink-faint:    #b89a87;   /* faded rose-brown — placeholders, timestamps */

  /* accents — sparingly */
  --coral:        #e07856;   /* coral salmon — primary accent (thinking dot,
                                                  the Saiden sigil tone) */
  --red:          #c54f3d;   /* playful red — links, focus state */
  --gold:         #d4a574;   /* muted gold — special states, never as text */

  /* lines */
  --line:         #e8d4c0;   /* dusty rose hairline — input underline */
}

Saiden sigil reconciliation The existing red sigil at saiden.dev/logo.png is already warm + brushy + playful. Keep it. Use at tiny size (16px) in the top-right corner. It's identity, not chrome.


Typography

No monospace. This is the deliberate break from Phase 2's TUI plan.

Role Family Weight Size Style
BT messages Cormorant Garamond (Google Fonts) 500 1.25rem regular
Pilot messages same 400 1.125rem regular
Speaker labels (Pilot, BT) Caveat (Google Fonts) 400 1rem italic, --ink-muted
Operator prompt input Cormorant Garamond 400 1.125rem regular
Tiny chrome (timestamps, errors) Inter 300 0.8rem uppercase letter-spaced

Why Cormorant Garamond: elegant 18thC-derived serif. Reads like a letter, not an app. Matches "Beautiful Hand-Written Letters" mood.

Why Caveat for labels: Samantha's cursive callouts. Gives speaker labels a handwritten, intimate feel — like a note left for someone, not a system tag.

Loading optimization: preload cormorant-garamond-500.woff2 + caveat-400.woff2, swap others.


Components

1. Message — speaker label + body

<article class="msg msg--bt">
  <div class="msg__label">BT</div>
  <div class="msg__body">
    The mesh held. Three packets drifted from sazabi but they came back.
  </div>
</article>
.msg              { margin: 2.5rem 0; }
.msg__label       { font-family: 'Caveat'; color: var(--ink-muted);
                    font-size: 1rem; letter-spacing: 0.02em; margin-bottom: 0.5rem; }
.msg--bt   .msg__body { color: var(--ink); font-size: 1.25rem; line-height: 1.6; }
.msg--user .msg__body { color: var(--ink-muted); font-size: 1.125rem; line-height: 1.55; }
.msg--system .msg__body { color: var(--ink-faint); font-size: 0.85rem;
                         text-transform: uppercase; letter-spacing: 0.1em; }

No bubbles. No borders. No backgrounds. Pure typography.

2. Operator prompt — single hairline + invisible input

<form class="prompt">
  <hr class="prompt__line">
  <input class="prompt__input" placeholder="speak to BT" autofocus>
</form>
.prompt           { position: sticky; bottom: 0; background: var(--bg);
                    padding: 2rem 0 3rem 0; }
.prompt__line     { border: none; border-top: 1px solid var(--line);
                    margin: 0 0 0.75rem 0; }
.prompt__input    { width: 100%; border: none; background: transparent;
                    outline: none; font-family: 'Cormorant Garamond';
                    font-size: 1.125rem; color: var(--ink); }
.prompt__input::placeholder { color: var(--ink-faint); font-style: italic; }
.prompt__input:focus { caret-color: var(--coral); }

Caret IS the cursor. No send button. Enter submits. Empty input is no-op.

3. Thinking indication — single pulsing dot

<div class="thinking" aria-label="BT is thinking"></div>
.thinking {
  color: var(--coral);
  font-size: 0.9rem;
  margin: 2rem 0 2rem 0;
  animation: pulse 1.8s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1.0; }
}

That's it. One coral dot, breathing. Mirrors Samantha's "presence" indicator — felt, not announced.

4. Typewriter stream

When BT's WebSocket chunks arrive:

  • Append each chunk character-by-character on a queue
  • Drain queue at ~45 chars/sec (≈22ms/char) — fast enough to feel alive, slow enough to read along
  • After the last chunk + done:true, briefly show a soft cursor that fades
// pseudocode
const queue = [];
let draining = false;

function enqueue(chunk) {
  for (const ch of chunk) queue.push(ch);
  if (!draining) drain();
}

async function drain() {
  draining = true;
  while (queue.length) {
    bodyEl.textContent += queue.shift();
    await sleep(22);
  }
  draining = false;
}

No syntax highlighting in v1. Markdown rendering deferred. Plain prose only. (We can add highlight.js + a tasteful warm theme later if Pilot wants code in chat.)


Animations & motion

Event Timing Easing
New message fade-in 400ms cubic-bezier(0.16, 1, 0.3, 1) (decelerate)
Auto-scroll to bottom 600ms ease-out
Thinking dot pulse 1.8s loop ease-in-out
Typewriter char 22ms each linear
Cursor fade after stream done 800ms ease-out

No bouncing. No spring. No glitch. Movement is meditative.


Tech stack confirmation

  • Server: FastAPI (app/main.py already drafted in Phase 2 — keep as-is, it's a thin layer)
  • Frontend: vanilla JS + hand-rolled CSS. No framework, no build step.
  • Template: one templates/chat.html, served by Jinja2
  • Assets: Google Fonts via <link> (preload Cormorant + Caveat), no npm
  • WS protocol: unchanged from Phase 2 backend
    • Client sends {content: "..."}
    • Server streams {role:"assistant", delta:"...", done:false} then {done:true}

Why no framework: Her is about restraint. ~150 lines of HTML+CSS+JS will get us there faster than React/Vue boilerplate, and the result will feel calmer than any component-tree app.


Asset list

Asset Source Status
Cormorant Garamond (500, 400, 400-italic) Google Fonts fetch at runtime
Caveat (400) Google Fonts fetch at runtime
Saiden sigil https://saiden.dev/logo.png live
Favicon derive from logo.png at 32px TODO
OG image composite, optional TODO

Implementation order (when Pilot says go)

  1. templates/chat.html — single page, message list, operator prompt, thinking dot
  2. static/chat.css — color tokens + typography + components above
  3. static/chat.js — WebSocket open, typewriter, auto-scroll, focus mgmt
  4. templates/denied.html — Her-styled 403 page (cream bg, serif, "you are not on the channel")
  5. Test locally with mock streaming (no Anthropic key needed for UI iteration)
  6. Wire to FastAPI WS (Phase 2 backend) — already drafted
  7. Iterate with Pilot on type sizes, spacing, animation timings
  8. Then deploy: Caddyfile, DNS swap, smoke test (Phase 2 milestones M5-M7)

What's deliberately NOT in v1

  • Markdown rendering (BT's bold/code/lists shown as plain text)
  • Syntax highlighting
  • Code copy buttons
  • Conversation history persistence across reloads
  • Multi-conversation tabs
  • Mobile-specific tuning beyond responsive max-width
  • Voice input/output (Samantha's whole thing, but heavy lift — Phase N+)
  • Sound design (door-chime on send, soft tick on receive)

These come later if Pilot wants. v1 stays as quiet as possible.


Open questions

  1. Pilot's display name? "Pilot" by default, or your actual name on your messages?
  2. Speaker labels on every message? Or only when speaker changes (saves visual noise)?
  3. Time stamps? Hide entirely / show on hover / show small inline?
  4. Saiden sigil placement? Top-right (current plan) / bottom-right / top-center / hide entirely?