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

12 KiB
Raw Blame History

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

#[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

// 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

#[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

  1. <StoryboardCard> med status-visning og hurtigtaster
  2. Episode-sekvens med SpacetimeDB-synk
  3. Universell overføring: "Send til..." kontekstmeny
  4. Drag-and-drop mellom blokker

Fase 3: Innspillingsintegrasjon

  1. LiveKit-kobling for tidsstempler ved "Tatt opp"
  2. Episode-oppsummering og arkivering
  3. Kort opprettet under innspilling (hurtigtast + fra chat)

Fase 4: Polish og utvidelser

  1. Ghost Cards (forrige episodes kort)
  2. Pinboard Mode (zoom-ut til fugleperspektiv)
  3. Flow Meter (visuell progresjon)
  4. Mobil listevisning
  5. 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