# 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 `` 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, // 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, 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) { // 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 Middels–Stor — 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 (``) 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. `` 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: 200–500 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