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:
4remtop + 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.pyalready 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}
- Client sends
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)
templates/chat.html— single page, message list, operator prompt, thinking dotstatic/chat.css— color tokens + typography + components abovestatic/chat.js— WebSocket open, typewriter, auto-scroll, focus mgmttemplates/denied.html— Her-styled 403 page (cream bg, serif, "you are not on the channel")- Test locally with mock streaming (no Anthropic key needed for UI iteration)
- Wire to FastAPI WS (Phase 2 backend) — already drafted
- Iterate with Pilot on type sizes, spacing, animation timings
- 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
- Pilot's display name? "Pilot" by default, or your actual name on your messages?
- Speaker labels on every message? Or only when speaker changes (saves visual noise)?
- Time stamps? Hide entirely / show on hover / show small inline?
- Saiden sigil placement? Top-right (current plan) / bottom-right / top-center / hide entirely?