server/docs/proposals/storyboard.md
vegard 7babafc65f Storyboard-spec, canvas-primitiv og universell overføring
Tre nye/omskrevne dokumenter som definerer fritt-canvas arkitekturen:
- Canvas-primitiv: felles underlag for whiteboard og storyboard (pan, zoom, drag, viewport culling)
- Universell overføring: message_placements-tabell og blokk-til-blokk drag-and-drop
- Storyboard: full spec med episode-sekvens, LiveKit-kobling, inter-board overføring

Inkluderer også storyboard-relaterte mini-proposals (ghost cards, pinboard mode,
flow meter, emotion tags, card chaining, collaborative cursors, card heat map).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:10:31 +01:00

298 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Storyboard — Fritt canvas for innspillingsplanlegging og live-produksjon
## 1. Konsept
Storyboardet er et fritt canvas der meldingsboks-kort plasseres, flyttes og grupperes visuelt. Det brukes **før, under og etter innspilling** — fra idémyldring til ferdig episodestruktur. Ikke et erstatningsverktøy for kanban (som er for langsiktig planlegging), men et taktilt arbeidsflate for å *se* en episode ta form.
Storyboardet er en consumer av Canvas-primitivet (`docs/features/canvas_primitiv.md`) og deltar i universell overføring (`docs/features/universell_overfoering.md`).
## 2. Kjernemodell
### 2.1 Kort = Meldingsboks med storyboard-plassering
Alle kort på storyboardet er vanlige `messages`-noder. Plasseringen på canvaset styres av `message_placements`-tabellen:
```
message_placements:
message_id → messages.id
context_type = 'storyboard'
context_id → episode_id (eller standalone storyboard-id)
entered_at = når kortet ble lagt på boardet
position = { "x": 340, "y": 120, "status": "klar" }
```
Kortet kan samtidig leve i en chat, på et kanban-brett, eller i en kalender. Endringer i chatten (nye svar, redigeringer) propagerer til storyboard-visningen fordi det er *samme melding*.
### 2.2 Episode-objekt
Et storyboard er knyttet til en episode (node av type `episode`) eller eksisterer som frittstående canvas (for idémyldring uten episode-tilknytning).
Episode-noden eier:
- **Sekvensliste:** En ordnet liste over kort som er "Tatt opp", med tidsstempel for når de ble markert. Lagres som `episode_sequence` i SpacetimeDB.
- **Målvarighet:** Konfigurerbar per workspace (f.eks. 45 min) — brukes av Flow Meter.
### 2.3 Statuser
Kort har status (lagret i `position`-JSONB på plasseringen):
| Status | Visuelt | Betydning |
|--------|---------|-----------|
| **Klar** | Solid, hvit/lys border | Planlagt, venter på tur |
| **Tatt opp** | Grønn border, tidsstempel-badge | Snakket om under innspilling |
| **Droppet** | Dimmet (opacity 0.4), rød stripe | Ikke brukt denne gang |
| **Arkivert** | Skjult (filter) | Ferdig behandlet |
Status-endring skjer via:
- Drag til en status-sone (valgfri — konfigurer per workspace)
- Hurtigtast: `R` = Tatt opp, `D` = Droppet (når kort er valgt)
- Kontekstmeny
- Flytende toolbar ved seleksjon
### 2.4 Flere storyboards
Et workspace kan ha mange storyboards — ett per episode, pluss frittstående for idémyldring. Hver er en `StoryboardBlock` med `props.episodeId` (eller `props.boardId` for frittstående).
Tre episoder under planlegging = tre storyboard-blokker, enten:
- På samme side (splittet grid-layout)
- På ulike sider i workspace-navigasjonen
## 3. Fritt canvas
Storyboardet bruker Canvas-primitivet for all canvas-interaksjon:
- **Pan/zoom:** Se `canvas_primitiv.md` §2
- **Objekt-plassering:** Kort har `(x, y)` i world-space, ingen kolonner eller rader
- **Snap-to-grid:** Av som default, toggle med `G`
- **Ingen akse-begrensning:** Brukeren plasserer kort fritt. Ingen antatt retning eller tidslinje
### 3.1 Kort-rendering
Hvert kort rendres som en `<StoryboardCard>` inne i canvas-primitivets objekt-slot:
```
┌─────────────────────────┐
│ 🔵 Tittel │ ← Status-farge som border
│ │
│ Kort sammendrag av body │ ← Avkortet tekst
│ │
│ 💬 3 ⏱ 04:32 │ ← Svar-count + varighet (hvis tatt opp)
└─────────────────────────┘
```
Kort-størrelse er fast bredde (variabel ved zoom), høyde tilpasser seg innholdet opp til en max.
### 3.2 Kort-interaksjon
- **Klikk:** Velg kort, vis flytende toolbar
- **Dobbeltklikk:** Åpne meldingsboksen i utvidet modus (full diskusjonstråd)
- **Drag:** Flytt kort på canvaset
- **Høyreklikk:** Kontekstmeny (status, send til, fjern, slett)
## 4. Overføring mellom blokker
Storyboardet deltar fullt i universell overføring (`universell_overfoering.md`):
### 4.1 Som sender
Dra et kort ut av storyboard-blokken → ghost følger musepekeren → slipp på annen blokk:
- **→ Chat:** Melding ankommer som ny chatmelding med `entered_at = now()`
- **→ Kanban:** Melding blir kort i første kolonne
- **→ Kalender:** Melding blir hendelse på dagens dato
- **→ Annet storyboard:** Melding får ny canvas-posisjon i mål-boardet
### 4.2 Som mottaker
Storyboardet kan motta fra alle blokk-typer:
- **Default-plassering:** Senter av viewport
- **Drag-plassering:** Der brukeren slipper objektet på canvaset
- **Status:** Nye kort ankommer som "Klar"
### 4.3 Inter-storyboard overføring
For å flytte kort mellom episoder (f.eks. "dette passer bedre i episode 48"):
- Dra kortet til en annen storyboard-blokk
- Eller bruk "Send til..." → velg annet storyboard
- Plasseringen i kilde-boardet fjernes, ny plassering opprettes i mål-boardet
## 5. Kort som oppstår under innspilling
Under live innspilling dukker nye idéer opp. Flere veier inn:
| Metode | Flyt |
|--------|------|
| **Hurtigtast (`N`)** | Popup for tittel + body → kort plasseres nær senter |
| **Fra chat** | Skriv melding i studio-chat → "Send til Storyboard" |
| **AI-forslag** | Live AI foreslår kort basert på transkripsjon (se §8) |
Alle veier oppretter en meldingsboks + plassering med status "Klar".
## 6. Kobling til LiveKit / Studioet
### 6.1 Tidsstempel ved "Tatt opp"
Når et kort settes til "Tatt opp" under en aktiv innspilling:
1. **Klient spør LiveKit** om nåværende innspillings-tidspunkt (offset fra oppstart)
2. Tidspunktet lagres i plasserings-metadata: `position.recorded_at_offset = 1823` (sekunder)
3. Episode-sekvensen oppdateres med kortet i riktig posisjon
Etter innspilling, når Whisper har prosessert lyden, kan `recorded_at_offset` matches mot Whisper-segmenter for å koble kort til eksakte transkripsjonsavsnitt.
### 6.2 Episode-sekvens
```rust
#[table(name = episode_sequence_entry, public)]
pub struct EpisodeSequenceEntry {
#[primary_key]
pub id: String,
pub episode_id: String,
pub message_id: String,
pub sequence_position: f32, // REAL for midpoint-innsetting
pub recorded_at_offset: Option<i64>, // sekunder fra innspillingsstart
pub workspace_id: String,
}
```
Sekvensen er den ordnede listen over "Tatt opp"-kort og blir grunnlaget for episode-strukturen i Podcastfabrikken.
## 7. Etter innspilling
### 7.1 Episode-oppsummering
Automatisk generert visning etter innspilling:
```
Episode 47 — Oppsummering
━━━━━━━━━━━━━━━━━━━━━━━━
1. [00:00] Intro — Kommunevalg 2027 💬 2 svar
2. [04:32] Listekandidatene i Oslo 💬 5 svar
3. [12:15] Valgomaten — første resultater 💬 1 svar
4. [18:40] Debatten om bompenger 💬 3 svar
━━━━━━━━━━━━━━━━━━━━━━━━
Totalt: 23:10 av 45:00 mål (51%)
Droppet: 3 kort (bevart for neste episode)
```
### 7.2 Arkivering
Ett-klikks arkivering:
1. Alle "Tatt opp"-kort settes til "Arkivert"
2. "Droppet"-kort beholder status (synlige for neste episode som Ghost Cards)
3. "Klar"-kort beholder status (ubrukte idéer)
4. Episode-sekvensen fryses (immutable etter arkivering)
## 8. AI-integrasjon (fremtidig)
Under innspilling kan Live AI foreslå nye kort:
1. Whisper transkriberer i sanntid
2. AI analyserer transkripsjon: "det ble nevnt et nytt tema som ikke er på boardet"
3. AI oppretter et foreslått kort (status: "Foreslått", visuelt distinkt — stiplet border)
4. Bruker aksepterer → status endres til "Klar"
Dette bygger på Live AI (`docs/features/live_ai.md`) og er ikke del av MVP.
## 9. SpacetimeDB-modell
### 9.1 Tabeller
```rust
// Kort-posisjon og status (via universell overføring)
// Bruker message_placement-tabellen — se universell_overfoering.md
// Episode-sekvens (ordnet liste over "Tatt opp"-kort)
#[table(name = episode_sequence_entry, public)]
pub struct EpisodeSequenceEntry {
#[primary_key]
pub id: String,
pub episode_id: String,
pub message_id: String,
pub sequence_position: f32,
pub recorded_at_offset: Option<i64>,
pub workspace_id: String,
}
```
### 9.2 Reducers
```rust
#[reducer]
pub fn set_card_status(ctx: &ReducerContext, placement_id: String, status: String) { ... }
#[reducer]
pub fn record_card(ctx: &ReducerContext, episode_id: String, message_id: String, offset: Option<i64>) {
// Sett status til "Tatt opp" + legg til i episode-sekvens
}
#[reducer]
pub fn reorder_sequence(ctx: &ReducerContext, episode_id: String, entries: Vec<(String, f32)>) { ... }
#[reducer]
pub fn archive_episode(ctx: &ReducerContext, episode_id: String) { ... }
```
## 10. Responsivt design
| Skjerm | Tilpasning |
|--------|-----------|
| Desktop | Full canvas med zoom/pan, hurtigtaster, drag-and-drop |
| Tablet | Touch-gester, flytende toolbar i bunn, "Send til"-meny |
| Mobil | Listevisning av kort (gruppert etter status), "Send til"-meny, ingen canvas |
Mobil får en **alternativ listevisning** fordi fritt canvas på liten skjerm er upraktisk. Listen grupperer kort etter status og lar brukeren endre status via swipe.
## 11. Bygger på
| Feature / Infra | Rolle |
|-----------------|-------|
| Canvas-primitiv | Fritt canvas med zoom/pan/drag |
| Meldingsboks | Kort = meldinger med view-config |
| Universell overføring | Portal-mekanikk mellom blokker |
| Kunnskapsgraf | Kort er noder, relasjoner mellom kort |
| SpacetimeDB | Sanntidssynk av posisjon og status |
| Studioet / LiveKit | Tidsstempel ved "Tatt opp" under innspilling |
| Podcastfabrikken | Episode-sekvens → redigeringsgrunnlag |
## 12. Innsats
MiddelsStor — canvas-primitivet er det tyngste løftet, men gjenbrukes av whiteboard. Selve storyboard-logikken er middels (status, sekvens, arkivering).
## 13. Wow-faktor
Høy — dette er "limen" mellom redaksjonelt arbeid og faktisk innspilling. Visuelt imponerende og genuint nyttig.
## 14. Implementeringsfaser
### Fase 1: Fundament
1. Canvas-primitiv (`<Canvas>`) med pan, zoom, drag, viewport culling
2. BlockShell fullskjerm-toggle
3. `message_placements`-tabell (PG + SpacetimeDB)
4. `StoryboardBlock` registrert i block registry
### Fase 2: Kjerne-storyboard
5. `<StoryboardCard>` med status-visning og hurtigtaster
6. Episode-sekvens med SpacetimeDB-synk
7. Universell overføring: "Send til..." kontekstmeny
8. Drag-and-drop mellom blokker
### Fase 3: Innspillingsintegrasjon
9. LiveKit-kobling for tidsstempler ved "Tatt opp"
10. Episode-oppsummering og arkivering
11. Kort opprettet under innspilling (hurtigtast + fra chat)
### Fase 4: Polish og utvidelser
12. Ghost Cards (forrige episodes kort)
13. Pinboard Mode (zoom-ut til fugleperspektiv)
14. Flow Meter (visuell progresjon)
15. Mobil listevisning
16. AI-foreslåtte kort under innspilling
## 15. Åpne spørsmål (parkert)
- Skal frittstående storyboards (uten episode) ha en egen node-type, eller er de bare episoder uten publiseringsdato?
- Bør canvaset ha et bakgrunnsmønster (dots/grid) for orientering, eller blank?
- Hvor mange kort tåler canvaset før ytelsen lider? Sannsynlig grense: 200500 DOM-noder med viewport culling.
## 16. Instruks for Claude Code
- Storyboard er en blokk-type (`StoryboardBlock.svelte`) i block registry
- Kort-posisjon eies av SpacetimeDB via `message_placements` — aldri direkte PG fra frontend
- Episode-sekvens er en egen SpacetimeDB-tabell, ikke metadata på episode-noden
- Bruk Canvas-primitivet for all canvas-logikk — ikke reimplementer pan/zoom
- Mobil-fallback (listevisning) er en egen komponent, ikke en "responsiv" versjon av canvaset
- Status-endring skal alltid gå via SpacetimeDB reducer, aldri direkte state-mutasjon