Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # DESIGN — Interactive Story Webapp (Accelerando)
- This document describes the architecture of the Accelerando interactive
- reader so the same pattern can be reused for other stories. Everything
- story-specific lives in three hand-authored files; everything else is generic.
- ## Goals
- - Static webapp (no server-side logic in production) deployable to any
- static host (Deno Deploy, Cloudflare Pages, etc.).
- - Lightweight reader first: the book is paginated per scene, nothing loads
- until needed.
- - Deep linkability: every sentence has a stable anchor, so the wiki and
- index can cite the exact sentence that supports a claim.
- - All "smarts" run at build time; the runtime is dumb, fast JSON + Vue.
- ## Stack
- - **Deno 2** as the toolchain (tasks, npm interop, build scripting).
- - **Vite + Vue 3** (`<script setup>` SFCs) with vue-router, hash history
- (`/#/read/c3s1`), so any static file server that serves `index.html` at the
- root works — deep links never reach the server as paths.
- - **No CSS libraries** — one hand-written `src/style.css` with CSS custom
- properties; automatic dark mode via `prefers-color-scheme`.
- - No runtime dependencies beyond vue + vue-router.
- ```
- deno task dev # rebuild data + vite dev server (hot reload)
- deno task build # rebuild data + production build to dist/
- deno task data # just regenerate public/data from the source text
- ```
- ## Pipeline: source text → static data
- `tools/build-data.ts` parses the annotated source HTML (`accelerando.html`)
- at build time and emits JSON under `public/data/`:
- | File | Purpose | Loaded |
- | --- | --- | --- |
- | `toc.json` | Part/Chapter/Scene tree + word counts | Home view |
- | `scenes/<id>.json` | One file per scene: pre-rendered paragraph HTML | Reader, per scene |
- | `glossary.json` | id → {name, summary, link} for hover popups | Reader, once |
- | `index.json` | entity list + mention counts (small) | Index view |
- | `mentions/<id>.json` | per-entity mention refs + snippets | Index, on expand |
- | `wiki.json` | wiki articles, infoboxes, resolved citations | Wiki views |
- | `timeline.json` | derived chronology: chapter datings + events, cited | Timeline view |
- Key decisions:
- - **Scene segmentation**: Parts and chapters come from `<h2>/<h3>` headings;
- scenes are split on the `* * *` separator (`<p class="gap">`). Scenes that
- are entirely `<blockquote>` (plus a hand-flagged list) are typed
- `interlude` and styled italic. Scene ids are `c<chapter>s<scene>`.
- - **Sentence anchors**: paragraphs are split into sentences with a
- heuristic splitter (abbreviation-aware, quote-aware). Every sentence is
- wrapped in `<span class="snt" id="p<para>s<sent>">`. A global sentence
- reference is the compact string `"chapter.scene.para.sent"`, rendered as
- a link to `/read/c<ch>s<sc>#p<p>s<s>` (rendered under the router hash,
- i.e. `/#/read/...` in the address bar).
- - **Pre-rendered HTML**: term highlighting and sentence wrapping happen at
- build time; the reader just `v-html`s paragraph strings. Only `<em>` and
- the generated `<span>`s survive from the source; entities are decoded and
- everything else is escaped.
- - **Term matching**: entity aliases are compiled to word-boundary regexes
- (case-sensitive when the alias contains capitals), matched longest-first
- with an occupancy mask so overlapping aliases don't double-claim text.
- Entities flagged `glossary: true` become `<span class="term"
- data-term="id">` spans; *all* entity matches are recorded as mentions for
- the index.
- - **Citations**: wiki articles contain `{{cite:some exact phrase}}`. The
- build searches all sentences for the phrase and replaces the marker with
- `[[ref:c.s.p.i]]`; unresolved citations print a warning, so typos are
- caught at build time, not in production.
- ## Hand-authored inputs (the story-specific part)
- 1. `tools/scene-meta.ts` (+ `scene-meta-late.ts`) — scene titles, interlude
- flags, and an image-generation prompt per scene. Regenerate the naming
- skeleton with `deno run --allow-read tools/build-data.ts --skeleton`,
- which dumps every scene's first/last line for titling.
- 2. `tools/entities-{people,world,tech}.ts` — the entity database:
- characters, locations, organizations, species, technologies, concepts.
- Each entry: aliases for matching, a one-paragraph summary (used by the
- reader hover infobox and wiki lead), an external link (Wikipedia etc.),
- optional `glossary` flag (highlight in reader), optional wiki article
- with infobox fields and `{{cite:…}}` markers, and an image prompt.
- 3. `tools/timeline.ts` — the derived story chronology (see "Timeline"
- below): per-chapter date estimates, timeline events, and per-sentence
- time annotations, all citing sentence refs.
- 4. The source text itself, with part/chapter headings annotated in HTML.
- Porting to a new story = provide these three inputs; the parser only needs
- adjusting if the source markup conventions differ (heading tags, scene
- separator).
- ## App structure
- ```
- src/
- main.ts, router.ts # bootstrapping; lazy-loaded route components
- style.css # the entire design system
- lib/data.ts # typed fetch + in-memory cache for /data JSON
- lib/media.ts # media manifest store + dev upload client
- components/MediaSlot.vue # generated-art slot (see below)
- views/HomeView.vue # cover + Part/Chapter/Scene TOC
- views/ReaderView.vue # scene page: prose, anchors, term popups, pager
- views/TimelineView.vue # derived chronology: decades, chunks, citations
- views/WikiView.vue # wiki landing, cards grouped by entity type
- views/WikiEntryView.vue # article + infobox + citation links
- views/IndexView.vue # filterable index, lazy mention expansion
- ```
- - **Reader**: keyboard ←/→ for prev/next scene. Term highlighting is pure
- CSS (`p:hover .term`); the popup is one absolutely-positioned element
- driven by event delegation (`mouseover`/`mouseout` on the prose
- container), with a grace timer so the popup itself can be hovered for its
- links. On navigation with a `#p…s…` hash, the sentence is scrolled to and
- flashed. Gotcha: Vue templates can't call globals like `new URL()` —
- compute derived strings in `<script setup>`.
- - **Index performance**: `index.json` holds only names/types/counts.
- Mention lists (a few hundred refs for main characters) live in per-entity
- files fetched on first expand and rendered in pages of 50 with a
- "show more" button. Nothing is ever rendered for collapsed entities.
- - **Wiki**: infobox is a plain `Record<string, string>` so each story can
- invent its own fields (ages, relationships, jobs…). Citations render as
- superscript `[n]` links to reader sentence anchors.
- ## Generated art / LLM prompt workflow
- Every scene and wiki entity has a **media slot** (`MediaSlot.vue`) keyed by
- scene id or entity id. Resolution order:
- 1. A saved file exists in `public/media/<slot>.<ext>` → shown (dev & prod).
- 2. Dev mode + the nav "prompts" toggle on → the image-generation prompt is
- shown in a dashed box with a copy button; drag-and-dropping a generated
- image onto the box POSTs it to the dev server, which writes it to
- `public/media/` and regenerates `public/media/manifest.json`.
- 3. Otherwise the slot renders nothing (production never shows prompts).
- The dev upload endpoint is a small Vite `configureServer` middleware in
- `vite.config.ts` (`GET/POST /api/media`). In production the app reads the
- static `manifest.json` instead. Because saved media lands in `public/`, it
- is committed with the repo and shipped by the normal static build. Prompts
- share a style prefix (defined in the scene/entity files) so the art
- direction stays consistent; recurring characters have shared description
- constants.
- ## Timeline (derived chronology)
- The novel almost never dates itself, so the timeline is *derived*: every
- temporal reference in the text (decade interludes, "three years later",
- character ages, real-world anchors like "KGB … canceled in 1991") was
- extracted with its sentence ref, then reconciled into a best-fit chronology.
- All of it lives in `tools/timeline.ts` as three hand-authored tables:
- - `chapterDatings` — one date label per chapter (e.g. "c. 2015 · November")
- with a `basis` paragraph, confidence (`high`/`medium`/`low`), and
- supporting refs. The build stamps the label into `toc.json` and each
- scene's `chapter` object; the ToC and reader crumbs render it as a chip
- linking to `/timeline`.
- - `timelineEvents` — story events (chapter action, with scene links) and
- background events (SETI signals, the singularity, planet dismantling),
- each with a year for placement, description, confidence, and refs. The
- Timeline view groups them by decade and splits decades into
- early/mid/late chunks (everything past 2100 collapses into one "deep
- future" group, since the book itself gives up on calendars there).
- - `timeAnnotations` — `{ref, phrase, note}` triples. At build time the
- phrase is located inside the exact sentence at `ref` and wrapped in
- `<span class="time" data-note="…">`; the reader shows the note in a hover
- popup ("When is this?"), reusing the term-popup machinery. Time spans win
- over term spans when they overlap.
- Like wiki citations, everything is validated at build time: unknown refs,
- unknown scene ids, and phrases that don't occur in their sentence all print
- warnings, so the chronology can't silently rot when the text or splitter
- changes. Internal contradictions in the book (it was fixed up from nine
- novellas — e.g. ch. 4's "fourth decade" vs. Amber's age) are documented in
- the entries themselves rather than smoothed over, and the confidence levels
- reflect them.
- ### Image review dashboard (dev-only)
- `tools/image-reviews.ts` holds a hand-authored (Claude-authored) review per
- scene id — a 1-10 rating of how well the generated image represents the
- scene, a note, and an optional `newPrompt` (a suggested reprompt) when the
- image misses. The build merges these into `public/data/image-reviews.json`
- (every scene's prompt, plus rating/note/newPrompt where reviewed). The Home
- (ToC) view, in dev only, renders per scene: an image-present indicator, the
- rating badge (a `?` if an image exists but hasn't been reviewed yet — which
- is how you notice newly-dropped images), a "regen" flag when a reprompt is
- suggested, and a copyable prompt panel (the reprompt if flagged, else the
- original). A summary bar shows coverage, average rating, and flagged count,
- with filters (all / with images / flagged). To review new images: drop them
- in, then add entries to `image-reviews.ts` and rebuild.
- ## Deployment
- `deno task build` → `dist/` is a fully static site. History-mode routing
- needs an SPA fallback: `public/_redirects` covers Cloudflare Pages; for
- Deno Deploy serve `dist/` with a 404 → `index.html` rewrite. All data URLs
- are absolute (`/data/…`, `/media/…`), so host at the domain root.
- ## Known gotchas
- - Deno's fs watcher can crash Vite on transient files created by other
- tooling; `server.watch.ignored` in `vite.config.ts` excludes non-source
- directories (`.claude`, `.git`, the source HTML).
- - The source HTML is Latin-1; the parser decodes it explicitly and maps
- named/numeric entities to UTF-8.
- - The sentence splitter is heuristic. Anchors are stable as long as the
- splitter and source text don't change; treat both as frozen once
- citations/mentions exist (or rebuild everything together, which the
- single build script guarantees).
Advertisement
Add Comment
Please, Sign In to add comment