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>
298 lines
12 KiB
Markdown
298 lines
12 KiB
Markdown
# 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
|