# 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
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 (`<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: 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
