291 lines
11 KiB
Markdown
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?
|