Files
2026-05-29 13:47:34 +02:00

291 lines
11 KiB
Markdown

# 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.
```css
: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
```html
<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>
```
```css
.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
```html
<form class="prompt">
<hr class="prompt__line">
<input class="prompt__input" placeholder="speak to BT" autofocus>
</form>
```
```css
.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
```html
<div class="thinking" aria-label="BT is thinking"></div>
```
```css
.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
```javascript
// 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?