From 7babafc65fd713db5b3e223383ab4caaff8d1b30 Mon Sep 17 00:00:00 2001 From: vegard Date: Mon, 16 Mar 2026 18:10:31 +0100 Subject: [PATCH] =?UTF-8?q?Storyboard-spec,=20canvas-primitiv=20og=20unive?= =?UTF-8?q?rsell=20overf=C3=B8ring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 2 + docs/features/canvas_primitiv.md | 213 +++++++++++++++++ docs/features/universell_overfoering.md | 202 ++++++++++++++++ docs/proposals/README.md | 12 +- docs/proposals/card_chaining.md | 26 +++ docs/proposals/card_heat_map.md | 25 ++ docs/proposals/collaborative_cursors.md | 29 +++ docs/proposals/emotion_tags.md | 26 +++ docs/proposals/flow_meter.md | 24 ++ docs/proposals/ghost_cards.md | 27 +++ docs/proposals/pinboard_mode.md | 26 +++ docs/proposals/storyboard.md | 298 ++++++++++++++++++++++++ 12 files changed, 909 insertions(+), 1 deletion(-) create mode 100644 docs/features/canvas_primitiv.md create mode 100644 docs/features/universell_overfoering.md create mode 100644 docs/proposals/card_chaining.md create mode 100644 docs/proposals/card_heat_map.md create mode 100644 docs/proposals/collaborative_cursors.md create mode 100644 docs/proposals/emotion_tags.md create mode 100644 docs/proposals/flow_meter.md create mode 100644 docs/proposals/ghost_cards.md create mode 100644 docs/proposals/pinboard_mode.md create mode 100644 docs/proposals/storyboard.md diff --git a/CLAUDE.md b/CLAUDE.md index 565a5c5..faad128 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,8 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: - `kanban.md` — Drag-and-drop planlegging - `kalender.md` — Redaksjonell kalender med abonnementsmodell og ICS-eksport - `notater.md` — Scratchpad/notatblokk med auto-save og debounce + - `canvas_primitiv.md` — Felles fritt-canvas underlag (pan, zoom, drag — brukes av whiteboard og storyboard) + - `universell_overfoering.md` — Flytt objekter mellom blokker (drag-and-drop + "Send til...") - `whiteboard.md` — Sanntids frihåndstavle (møterom, chat, solo) - `live_transkripsjon.md` — Whisper-pipeline (felles motor for studio/møter/fabrikk) - `live_ai.md` — Live AI: faktoid-oppslag (studio) + referent (møter) diff --git a/docs/features/canvas_primitiv.md b/docs/features/canvas_primitiv.md new file mode 100644 index 0000000..317e306 --- /dev/null +++ b/docs/features/canvas_primitiv.md @@ -0,0 +1,213 @@ +# Feature: Canvas-primitiv — felles fritt-canvas underlag +**Filsti:** `docs/features/canvas_primitiv.md` + +## 1. Konsept + +Canvas-primitivet er den felles underliggende komponenten for alle friform-views i Sidelinja: whiteboard (tegning), storyboard (kort-canvas), og fremtidige canvas-baserte visninger. Det håndterer kamera (pan, zoom), viewport-styring, objekt-plassering og interaksjon — men vet ingenting om *hva* som rendres. + +### 1.1 Hvorfor et felles primitiv? + +Whiteboard og storyboard har identisk infrastruktur-behov: +- Uendelig canvas med pan og zoom +- Objekter med `(x, y)`-posisjon +- Drag-and-drop av objekter +- Viewport culling (ikke render det som er utenfor synsfeltet) +- Touch-støtte (pinch-zoom, to-finger-pan) +- Responsivt design (fungerer på mobil, tablet, desktop) + +Forskjellen er *innholdet*: whiteboard rendrer streker/figurer, storyboard rendrer meldingsboks-kort. Primitivet abstraherer det felles, slik at begge views gjenbruker 100 % av canvas-logikken. + +### 1.2 Arkitekturprinsipp + +``` +Canvas-primitiv (felles) +├── Kamera: pan, zoom, transform matrix +├── Viewport: culling, synlige objekter +├── Interaksjon: pointer events, touch, drag +├── Grid: valgfri snap, hjelpelinje +└── Render-delegering: slot/callback for innhold + +Whiteboard (consumer) +├── Tegneverktøy: penn, linje, rektangel, tekst +├── Strøk-modell: SVG paths / canvas paths +└── SpacetimeDB: strøk-synkronisering + +Storyboard (consumer) +├── Kort-rendering: i kompakt modus +├── Status-modell: Klar / Tatt opp / Droppet / Arkivert +├── Portal-soner: overføringsmekanikk til andre blokker +└── SpacetimeDB: kort-posisjon + status-synkronisering +``` + +## 2. Kamera-modell + +### 2.1 Transform + +Kameraet representeres som en 2D affin transformasjon: + +```typescript +interface Camera { + x: number; // pan offset X (world coords) + y: number; // pan offset Y (world coords) + zoom: number; // scale factor (1.0 = 100%) +} +``` + +Rendring via CSS `transform` på en wrapper-div: + +```css +.canvas-world { + transform: translate(calc(var(--cam-x) * 1px), calc(var(--cam-y) * 1px)) + scale(var(--cam-zoom)); + transform-origin: 0 0; +} +``` + +### 2.2 Zoom-begrensning + +- Min zoom: `0.1` (10 % — fugleperspektiv, brukes av Pinboard Mode) +- Max zoom: `3.0` (300 % — detalj) +- Default: `1.0` +- Zoom pivoterer rundt musepeker/finger-midtpunkt + +### 2.3 Pan + +- **Desktop:** Hold mellomknapp eller mellomrom + dra. Alternativt: to-finger-drag på trackpad. +- **Touch:** To-finger-pan (én finger = dra objekter, to fingre = pan). +- **Edge-pan:** Når man drar et objekt nær kanten av viewport, scroller canvaset automatisk i den retningen. + +## 3. Viewport Culling + +Bare objekter som overlapper med det synlige viewport-rektangelet rendres i DOM. For storyboard med 50–200 kort er dette en optimalisering som holder DOM-et lett. + +```typescript +function visibleObjects(objects: CanvasObject[], camera: Camera, viewportSize: { w: number, h: number }): CanvasObject[] { + const worldRect = screenToWorld(camera, viewportSize); + return objects.filter(obj => intersects(obj.bounds, worldRect)); +} +``` + +En margin (f.eks. 200px i world-space) legges til for å unngå pop-in ved pan. + +## 4. Objektmodell + +Canvas-primitivet opererer på generiske objekter: + +```typescript +interface CanvasObject { + id: string; + x: number; + y: number; + width: number; + height: number; + // Consumer-spesifikk data håndteres via generics/props +} +``` + +Consumer (whiteboard, storyboard) bestemmer *hva* som rendres for hvert objekt via en render-callback eller Svelte snippet: + +```svelte + + + + +``` + +## 5. Interaksjon + +### 5.1 Pointer events + +All interaksjon håndteres via pointer events (unified mouse + touch): + +| Gest | Desktop | Touch | Handling | +|------|---------|-------|----------| +| Pan | Mellomknapp-drag / Space+drag | To-finger-drag | Flytt kamera | +| Zoom | Scroll wheel | Pinch | Zoom inn/ut | +| Velg | Klikk | Tap | Velg objekt | +| Flytt | Venstreklikk-drag på objekt | Én-finger-drag på objekt | Flytt objekt | +| Multi-select | Shift+klikk / lasso | Lang-trykk + drag | Velg flere | + +### 5.2 Snap-to-grid (valgfri) + +Når aktivert, snapper objekter til et rutenett ved drag-slipp: + +```typescript +function snap(value: number, gridSize: number): number { + return Math.round(value / gridSize) * gridSize; +} +``` + +Default: av. Kan toggles via hurtigtast eller toolbar. + +### 5.3 Seleksjon + +- Klikk på tom flate: deselect alle +- Klikk på objekt: velg det (deselect andre) +- Shift+klikk: toggle seleksjon +- Lasso: dra på tom flate uten Space = tegn seleksjonsboks + +## 6. Responsivt design + +Canvas-primitivet skal fungere på alle skjermstørrelser: + +| Skjerm | Tilpasning | +|--------|-----------| +| Desktop (>1024px) | Full interaksjon, alle hurtigtaster | +| Tablet (768–1024px) | Touch-gester, toolbar i bunn | +| Mobil (<768px) | Forenklet toolbar, større treffområder for objekter, ingen lasso | + +Touch-treffområder skal være minimum 44x44px (WCAG 2.5.5). + +## 7. Fullskjerm-modus (BlockShell-feature) + +Enhver blokk i `BlockShell` kan gå i fullskjerm. Dette er en generell feature, ikke spesifikk for canvas: + +- **Toggle:** Dobbeltklikk på blokk-headeren, eller knapp i header +- **Implementering:** Blokken settes til `position: fixed; inset: 0; z-index: 50` +- **Escape:** Trykk Esc eller klikk "minimer"-knapp for å gå tilbake +- **URL-state:** Fullskjerm-tilstand lagres ikke i URL — det er en visuell modus, ikke en side + +## 8. SpacetimeDB-integrasjon + +Canvas-primitivet selv har ingen SpacetimeDB-kobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til SpacetimeDB: + +```typescript +interface CanvasEvents { + onObjectMove: (id: string, x: number, y: number) => void; + onObjectResize: (id: string, w: number, h: number) => void; + onCameraChange: (camera: Camera) => void; + onSelectionChange: (ids: string[]) => void; +} +``` + +Storyboard-consumeren bruker `onObjectMove` til å kalle en SpacetimeDB-reducer for å synkronisere posisjon til andre klienter. + +## 9. Bygger på + +- **SvelteKit:** Svelte 5 `$state`/`$derived` for reaktiv kamera- og objekt-state +- **CSS transforms:** Ingen Canvas2D eller WebGL — DOM-basert rendering for å beholde Svelte-komponent-rendering inne i objektene +- **Pointer Events API:** Unified input for mus og touch + +## 10. Implementeringsstrategi + +### Fase 1: Kjerne-primitiv +- `` Svelte-komponent med kamera (pan/zoom), viewport culling, og objekt-drag +- Touch-støtte (pinch-zoom, to-finger-pan) +- BlockShell fullskjerm-toggle + +### Fase 2: Storyboard som første consumer +- `` rendrer meldingsboks-kort på canvaset +- SpacetimeDB-synk for posisjon og status +- Portal-soner for overføring + +### Fase 3: Whiteboard-migrering +- Migrere eksisterende whiteboard-spec til å bruke canvas-primitivet +- Tegneverktøy som overlay oppå primitivet + +## 11. Instruks for Claude Code +- Canvas-primitivet er en ren Svelte-komponent uten backend-avhengigheter +- Bruk CSS transforms, ikke Canvas2D — innholdet inne i objekter er vanlige Svelte-komponenter +- All state styres via Svelte 5 `$state` og `$derived` — ingen external state management +- Pointer events, ikke mouse events — unified input +- Test med touch-emulering i DevTools for responsivitet +- Viewport culling er påkrevd fra dag 1 — ikke optimaliser bort diff --git a/docs/features/universell_overfoering.md b/docs/features/universell_overfoering.md new file mode 100644 index 0000000..14a5cbb --- /dev/null +++ b/docs/features/universell_overfoering.md @@ -0,0 +1,202 @@ +# Feature: Universell overføring — flytt objekter mellom blokker +**Filsti:** `docs/features/universell_overfoering.md` + +## 1. Konsept + +Universell overføring er mekanikken som lar brukere flytte meldingsboks-objekter mellom vilkårlige blokker: fra storyboard til chat, fra kanban til kalender, fra chat til storyboard. Enhver blokk kan *sende* og *motta* objekter. Meldingsboksen er alltid det underliggende objektet — overføringen endrer kun *view-config*, ikke selve meldingen. + +### 1.1 Grunnprinsipp + +En meldingsboks (node i kunnskapsgrafen) kan ha flere samtidige roller via view-configs (se `meldingsboks.md` §4). Universell overføring gjør dette til en førsteklasses brukerinteraksjon: + +``` +Bruker drar kort fra Storyboard → slipper på Chat-blokken + → Meldingen får en ny plassering i chatten (placement-record) + → Meldingen beholder sin posisjon på storyboardet + → Diskusjonstråden er synlig begge steder (samme objekt) +``` + +Alternativt kan brukeren *flytte* (fjerne fra kilde, legge til i mål) i stedet for å *kopiere* (beholde begge). Kontekstmeny gir valget. + +## 2. Plasseringsrelasjon (Placement) + +Når en melding vises i en kontekst (chat, kanban, storyboard, kalender), trenger vi metadata om *hvordan* den vises der. Dette er **plasseringsrelasjonen** — en edge i grafen mellom meldingen og konteksten, med metadata. + +### 2.1 Datamodell + +```sql +CREATE TABLE message_placements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + context_type TEXT NOT NULL, -- 'chat', 'kanban', 'storyboard', 'calendar', 'notes' + context_id UUID NOT NULL, -- channel_id, board_id, episode_id, calendar_id, note_id + entered_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- når objektet ankom denne konteksten + position JSONB, -- kontekst-spesifikk posisjon (se §2.2) + UNIQUE (message_id, context_type, context_id) +); + +CREATE INDEX idx_placements_context ON message_placements(context_type, context_id, entered_at); +CREATE INDEX idx_placements_message ON message_placements(message_id); +``` + +### 2.2 Posisjonsdata per kontekst + +`position`-feltet er JSONB og inneholder kontekst-spesifikk plassering: + +| Kontekst | position-innhold | Eksempel | +|----------|-----------------|----------| +| Chat | `null` (sorteres etter `entered_at`) | `null` | +| Kanban | `{ "column_id": "...", "position": 1.5 }` | Kolonne + rekkefølge | +| Storyboard | `{ "x": 340, "y": 120 }` | Fritt canvas-posisjon | +| Kalender | `{ "date": "2026-03-20", "all_day": true }` | Dato + tidspunkt | +| Notes | `{ "position": 3 }` | Rekkefølge i notatet | + +### 2.3 Forhold til eksisterende view-configs + +`message_placements` erstatter *ikke* de eksisterende view-config-tabellene (`kanban_card_view`, `calendar_event_view`) umiddelbart. Strategien er: + +1. **Fase 1:** `message_placements` brukes for nye kontekster (storyboard, notes) og for overføringsmekanikken +2. **Fase 2:** Eksisterende view-configs migreres gradvis til `message_placements` (kanban-posisjon, kalender-dato) +3. **Fase 3:** `kanban_card_view` og `calendar_event_view` kan fjernes når all logikk bruker placements + +### 2.4 `entered_at` vs `created_at` + +- `messages.created_at` = når meldingen ble skrevet +- `message_placements.entered_at` = når meldingen ankom *denne konteksten* + +En melding opprettet mandag i chatten som dras til storyboardet onsdag har `created_at = mandag` og storyboard-plassering med `entered_at = onsdag`. I chatten sorteres den etter sin chat-plasserings `entered_at` — som er mandag (opprinnelig plassering). I storyboardet vises den på canvas-posisjonen, uavhengig av tid. + +## 3. Sende-mekanikk (source) + +### 3.1 Drag-and-drop + +Brukeren drar et objekt ut av en blokk. Når objektet forlater blokk-grensen: + +1. Objektet blir en "ghost" (semi-transparent drag-representasjon) +2. Andre blokker som kan motta objektet highlighter sin mottakssone +3. Slipp på en mottakssone → overføring + +### 3.2 Kontekstmeny: "Send til..." + +For situasjoner der drag-and-drop er upraktisk (mobil, lang avstand): + +``` +Høyreklikk kort → "Send til..." → + ├── 📋 Kanban: Episodeplanlegging + ├── 💬 Chat: #studio-diskusjon + ├── 📅 Kalender: Redaksjonskalender + └── 🎬 Storyboard: Episode 47 +``` + +Listen viser alle blokker i det aktive workspacet som kan motta meldinger. + +### 3.3 Flytt vs. kopier + +- **Kopier** (default): Meldingen får en ny plassering i mål-konteksten, beholder plasseringen i kilde-konteksten +- **Flytt** (hold Shift ved drag, eller velg i kontekstmeny): Plasseringen i kilde-konteksten fjernes, ny plassering opprettes i mål + +## 4. Mottak-mekanikk (target) + +Hver blokk-type definerer en **mottaker** som bestemmer hva som skjer når et objekt ankommer: + +### 4.1 Mottaker per blokk-type + +| Blokk | Default-plassering | Visuell feedback | +|-------|-------------------|-----------------| +| **Chat** | Ny melding i bunnen, `entered_at = now()` | Kort blinker inn i chatflyten | +| **Kanban** | Første kolonne (eller "Innboks"), posisjon øverst | Kort glir inn i kolonnen | +| **Storyboard** | Senter av viewport (eller slipp-posisjon) | Kort fader inn | +| **Kalender** | Dagens dato, heldagshendelse | Dato highlighter | +| **Notes** | Ny blokk i bunnen av notatet | Tekst fades inn | + +### 4.2 Mottakssone-rendering + +Hver blokk rendrer en visuell drop-target når en drag er aktiv: + +- **Hele blokken** lyser opp med en subtil border/glow +- **Spesifikke soner** (f.eks. en kolonne i kanban, en dato i kalender) highlighter ved hover +- **Avviste drops** (feil type objekt) viser en dempet tilstand + +### 4.3 Mottaker-interface + +```typescript +interface BlockReceiver { + /** Kan denne blokken motta dette objektet? */ + canReceive(message: Message): boolean; + + /** Opprett plassering for mottatt objekt */ + receive(message: Message, dropPosition?: { x: number, y: number }): Placement; + + /** Visuell feedback for aktiv drag */ + renderDropZone(): void; +} +``` + +Alle blokk-typer implementerer dette interfacet. + +## 5. SpacetimeDB-integrasjon + +Plasseringsdata for sanntidskontekster (storyboard, kanban) eies av SpacetimeDB: + +```rust +#[table(name = message_placement, public)] +pub struct MessagePlacement { + #[primary_key] + pub id: String, + pub message_id: String, + pub context_type: String, + pub context_id: String, + pub entered_at: Timestamp, + pub position_json: String, // JSON-serialisert posisjon + pub workspace_id: String, +} + +#[reducer] +pub fn place_message(ctx: &ReducerContext, placement: MessagePlacement) { ... } + +#[reducer] +pub fn remove_placement(ctx: &ReducerContext, message_id: String, context_type: String, context_id: String) { ... } + +#[reducer] +pub fn move_on_canvas(ctx: &ReducerContext, placement_id: String, new_position_json: String) { ... } +``` + +Sync-workeren persisterer til PG `message_placements`-tabellen. + +## 6. Responsivt design + +- **Desktop:** Drag-and-drop mellom blokker fungerer naturlig +- **Tablet:** Drag-and-drop fungerer med touch, men "Send til..."-meny er primær +- **Mobil:** Kun "Send til..."-meny (blokker er stacked i én kolonne, drag mellom dem er upraktisk) + +## 7. Bygger på + +- **Meldingsboks** (`meldingsboks.md`): Alle overførte objekter er meldingsbokser +- **Kunnskapsgraf** (`kunnskapsgraf_og_relasjoner.md`): Plasseringer er relasjoner i grafen +- **BlockShell** / PageGrid: Blokk-rammen som rendrer mottakssoner +- **SpacetimeDB** (`synkronisering.md`): Sanntidssynk av plasseringer + +## 8. Konsekvenser for eksisterende kode + +### 8.1 BlockShell utvidelse + +`BlockShell` trenger: +- `onDragEnter`/`onDragLeave`/`onDrop` handlers for visuell feedback +- Prop for `receiver: BlockReceiver` fra innholdsblokken +- Fullskjerm-toggle (knapp i header + Esc for å lukke) + +### 8.2 Ny plasserings-tabell + +`message_placements` er ny. Eksisterende `kanban_card_view` og `calendar_event_view` lever parallelt inntil migrering. + +### 8.3 SpacetimeDB-modul + +Ny tabell `message_placement` med reducers for place/remove/move. + +## 9. Instruks for Claude Code +- Overføring oppretter aldri en kopi av meldingen — kun en ny plassering (view-config) +- `entered_at` er alltid `now()` ved overføring, aldri kopiert fra kilden +- En melding uten noen plasseringer er "løs" — den eksisterer i grafen men vises ikke noe sted. UI skal advare om dette ved siste fjerning +- Drag-and-drop bruker HTML5 Drag and Drop API for blokk-til-blokk, og pointer events for intra-canvas (storyboard/whiteboard) +- Hold overføringslogikken i en sentral `transferService` — ikke spread ut i hver blokk-type +- Mottaker-interfacet er obligatorisk for alle blokk-typer diff --git a/docs/proposals/README.md b/docs/proposals/README.md index 51972fd..fb25f6f 100644 --- a/docs/proposals/README.md +++ b/docs/proposals/README.md @@ -39,10 +39,20 @@ Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/fe | [Podcasting 2.0](podcasting_2_0.md) | Lav | Høy | Podcastfabrikken, kunnskapsgraf, RSS | | [Web Clipper](web_clipper.md) | Lav–Middels | Høy | Jobbkø, AI Gateway, meldingsboks, kunnskapsgraf | | [Visuelle Waveforms](waveforms.md) | Lav–Middels | Høy | Podcastfabrikken, jobbkø, editor | +| **Innspilling & Storyboard** | | | | +| [Storyboard](storyboard.md) | Middels–Stor | Høy | Canvas-primitiv, meldingsboks, universell overføring, Studioet, Podcastfabrikken | +| [Card Chaining](card_chaining.md) | Lav | Middels | Kunnskapsgraf, Storyboard, AI Gateway | +| [Ghost Cards](ghost_cards.md) | Lav–Middels | Høy | Storyboard, meldingsboks, kunnskapsgraf | +| [Pinboard Mode](pinboard_mode.md) | Lav | Høy | Storyboard, kanban | +| [Flow Meter](flow_meter.md) | Lav | Middels | Storyboard | +| [Emotion Tags](emotion_tags.md) | Lav | Middels | Meldingsboks, kanban, storyboard | +| **Samarbeid** | | | | +| [Collaborative Cursors](collaborative_cursors.md) | Lav | Middels | SpacetimeDB, Svelte | +| [Card Heat Map](card_heat_map.md) | Lav | Middels | Meldingsboks, kanban/storyboard | **Forfremmet til feature:** [Meldingsboks](../features/meldingsboks.md) — universell diskusjonsprimitiv (erstatter separate modeller for chat, kanban-kort, kalenderhendelser, faktoider, notater). -**Lavthengende frukter** (lav innsats, høy wow): Serendipity Roulette, Podcast Time Machine, Meme Generator, Audience Voice Memo. +**Lavthengende frukter** (lav innsats, høy wow): Serendipity Roulette, Podcast Time Machine, Meme Generator, Audience Voice Memo, Pinboard Mode, Ghost Cards. ## Format Forslagsfiler er lette — ingen streng mal. Minimum: diff --git a/docs/proposals/card_chaining.md b/docs/proposals/card_chaining.md new file mode 100644 index 0000000..d48e481 --- /dev/null +++ b/docs/proposals/card_chaining.md @@ -0,0 +1,26 @@ +# Card Chaining — Automatisk kobling av relaterte kort + +## Idé + +Når to kort plasseres ved siden av hverandre (i kanban, storyboard eller kalender), opprettes automatisk en graf-edge mellom dem. Systemet kan også foreslå overganger: "Og apropos drømmer..." + +## Hvorfor interessant? + +Podcast-segmenter henger ofte sammen tematisk, men koblingen er implisitt. Card chaining gjør den eksplisitt — uten manuelt arbeid. Gir bedre flyt under innspilling og bedre metadata for kunnskapsgrafen. + +## Fungerer slik +1. Dra kort A ved siden av kort B i storyboard +2. System oppretter `graph_edge` med `relation_type: 'sequence'` og `origin: 'proximity'` +3. Valgfritt: AI foreslår overgangssetning basert på begge kortenes innhold +4. Ved eksport/arkivering: sekvensen bevares som episode-struktur + +## Bygger på +- Kunnskapsgraf (graph_edges) +- Storyboard (proximity detection) +- AI Gateway (overgangsforslag) + +## Innsats +Lav — graph_edges finnes, bare UI for proximity + auto-edge. + +## Wow-faktor +Middels — subtilt, men forbedrer metadata-kvaliteten dramatisk over tid. diff --git a/docs/proposals/card_heat_map.md b/docs/proposals/card_heat_map.md new file mode 100644 index 0000000..794b6c1 --- /dev/null +++ b/docs/proposals/card_heat_map.md @@ -0,0 +1,25 @@ +# Card Heat Map — Visuell indikator for engasjement + +## Idé + +Kort på storyboard/kanban gløder basert på hvor mye oppmerksomhet de har fått: hover-tid, antall redigeringer, diskusjonstråd-lengde, og tid brukt i innspilling. + +## Hvorfor interessant? + +Hjelper med å se hva teamet faktisk er engasjert i — uten å lese alt. Under innspilling: "det kortet gløder mest, kanskje vi bør ta det først." + +## Fungerer slik +1. Klient tracker hover-tid per kort (lokal state) +2. Server aggregerer: antall edits, tråd-lengde, reaksjoner +3. Kombinert score → CSS-variabel (`--heat: 0.0–1.0`) → glow-effekt +4. Valgfritt: "Hot topics"-filter som sorterer kort etter heat + +## Bygger på +- Meldingsboks (reaksjoner, tråd-lengde) +- Kanban/Storyboard (visuell rendering) + +## Innsats +Lav — ren frontend-logikk med enkel server-aggregering. + +## Wow-faktor +Middels — subtilt men nyttig for redaksjonell prioritering. diff --git a/docs/proposals/collaborative_cursors.md b/docs/proposals/collaborative_cursors.md new file mode 100644 index 0000000..1ab4b2c --- /dev/null +++ b/docs/proposals/collaborative_cursors.md @@ -0,0 +1,29 @@ +# Collaborative Cursors — Sanntids-pekere for flerbrukermiljø + +## Idé + +Alle brukere som er på samme side ser hverandres musepekere som fargede prikker med navn. Fungerer på storyboard, kanban, whiteboard og kalender. + +## Hvorfor interessant? + +Gir "jamming together"-følelse under innspilling og planlegging. Produsent og host ser hverandre jobbe i sanntid uten å snakke om det. + +## Fungerer slik +1. Klient sender `{ user_id, x, y, page }` til SpacetimeDB ved musebevegelse (throttlet til ~10 Hz) +2. Andre klienter renderer fargede SVG-sirkler med brukernavn +3. Prikken fader ut etter 5 sekunder uten bevegelse +4. Valgfritt: kort "trail" som viser bevegelsesretning + +## Bygger på +- SpacetimeDB (pub/sub for posisjoner) +- Svelte ($state store for cursor-map) + +## Innsats +Lav — under 50 linjer Svelte + en SpacetimeDB-reducer. + +## Wow-faktor +Middels — visuelt tiltalende, men ikke kritisk funksjonalitet. + +## Åpne spørsmål +- Bør pekere vises i chat-visning også, eller bare canvas-baserte views? +- Throttling-strategi: SpacetimeDB-reducer eller klient-side debounce? diff --git a/docs/proposals/emotion_tags.md b/docs/proposals/emotion_tags.md new file mode 100644 index 0000000..a571f23 --- /dev/null +++ b/docs/proposals/emotion_tags.md @@ -0,0 +1,26 @@ +# Emotion Tags — Hurtigkategorisering av segmenter + +## Idé + +Kort i storyboard/kanban kan tagges med stemnings-ikoner — morsom, seriøs, kontroversiell, personlig. Taggen farger kortets ramme og fungerer som filter i etterarbeid. + +## Hvorfor interessant? + +Under redigering er det nyttig å filtrere på stemning: "vis alle kontroversielle segmenter" eller "vi trenger en morsom bit mellom to tunge tema." Raskere enn å lese alle kortene. + +## Fungerer slik +1. Predefinerte tags med ikoner og farger (konfigurerbart per workspace) +2. Klikk-basert tagging — ett klikk per tag, toggle on/off +3. Visuelt: farget border + ikon-badge på kortet +4. Filter: "vis kun 🔥-kort" i storyboard og kanban +5. Lagres som `message.metadata.emotion_tags: string[]` + +## Passer inn i eksisterende +- **Meldingsboks**: tags i metadata-feltet, ingen ny tabell +- **Reaksjoner**: kan gjenbruke reaksjons-mekanismen (emoji = emotion tag) + +## Innsats +Lav — ren frontend + metadata-felt. + +## Wow-faktor +Middels — nyttig for store episoder med mange segmenter. diff --git a/docs/proposals/flow_meter.md b/docs/proposals/flow_meter.md new file mode 100644 index 0000000..ca953aa --- /dev/null +++ b/docs/proposals/flow_meter.md @@ -0,0 +1,24 @@ +# Flow Meter — Visuell episodeprogresjon + +## Idé + +En tynn progresjonslinje langs toppen av storyboardet som fylles etter hvert som kort dras til "Tatt opp". Grønn = solid episode, gul = trenger mer, rød = for kort. Basert på antall segmenter og total opptakstid. + +## Hvorfor interessant? + +Under innspilling er det lett å miste oversikten over om man har nok materiale. Flow meter gir et intuitivt "mage-sjekk" uten å telle manuelt. + +## Fungerer slik +1. Konfigurerbar målvarighet per workspace (f.eks. 45 min) +2. Summerer varighet for alle "Tatt opp"-kort +3. Fargeovergang: rød (0–30%) → gul (30–70%) → grønn (70–100%) +4. Valgfritt: pulserer sakte når man nærmer seg mål + +## Bygger på +- Storyboard (episode-sekvens med tidsstempler) + +## Innsats +Lav — én beregning + CSS gradient. + +## Wow-faktor +Middels — liten ting, men fjerner mental overhead. diff --git a/docs/proposals/ghost_cards.md b/docs/proposals/ghost_cards.md new file mode 100644 index 0000000..b2babce --- /dev/null +++ b/docs/proposals/ghost_cards.md @@ -0,0 +1,27 @@ +# Ghost Cards — Visuelle spor fra tidligere episoder + +## Idé + +Når en episode er ferdig og arkivert, etterlater kort som ble "Tatt opp" svake, semi-transparente spøkelseskort på storyboardet. De fungerer som påminnelser: "vi snakket om dette forrige uke — har vi oppfølging?" + +## Hvorfor interessant? + +Podcast-temaer henger sammen over tid. Ghost cards gir visuell kontinuitet mellom episoder uten manuell sporing. Perfekt for serier og løpende historier. + +## Fungerer slik +1. Ved arkivering: kort som var "Tatt opp" får `ghost_episode_id` i metadata +2. Ved neste episode: ghost cards vises med `opacity: 0.3` og episode-nummer +3. Klikk på ghost → se original diskusjonstråd og tidsstempel +4. Dra ghost → promoter til nytt aktivt kort for oppfølging (beholder graf-edge til originalen) +5. Konfigurerbart: vis ghosts fra siste N episoder (default: 3) + +## Bygger på +- Storyboard (visuell rendering) +- Meldingsboks (message med view-config) +- Kunnskapsgraf (edge mellom original og oppfølger) + +## Innsats +Lav–Middels — mest metadata-design og UI for ghost-rendering. + +## Wow-faktor +Høy — gir podcasten "hukommelse" som oppleves magisk for brukeren. diff --git a/docs/proposals/pinboard_mode.md b/docs/proposals/pinboard_mode.md new file mode 100644 index 0000000..0640a73 --- /dev/null +++ b/docs/proposals/pinboard_mode.md @@ -0,0 +1,26 @@ +# Pinboard Mode — Fugleperspektiv over episode-arc + +## Idé + +Hurtigtast (f.eks. `Ctrl+0`) zoomer ut storyboardet til fugleperspektiv. Kort krymper, titler blir store. Du ser hele episodens arc som en visuell flyt: intro → morsom bit → dypt spørsmål → outro. Dra kort som Lego-klosser for å endre rekkefølge. + +## Hvorfor interessant? + +Under innspilling er man "for nær" — man ser enkelt-kort, ikke helheten. Pinboard mode gir 3-sekunders overblikk: "vi har tre morsomme segmenter på rad, vi trenger noe tungt i midten." + +## Fungerer slik +1. Toggle via hurtigtast eller knapp +2. CSS transform: `scale(0.4)` + økt font-size på titler +3. Kort viser kun: tittel, status-farge, varighet (hvis tatt opp) +4. Drag-and-drop endrer rekkefølge i episode-sekvensen +5. Klikk på kort = zoom tilbake til normalvisning på det kortet + +## Bygger på +- Storyboard (episode-sekvens) +- Kanban (drag-and-drop) + +## Innsats +Lav — ren CSS/UI-jobb. + +## Wow-faktor +Høy — visuelt imponerende og genuint nyttig under innspilling. diff --git a/docs/proposals/storyboard.md b/docs/proposals/storyboard.md new file mode 100644 index 0000000..d2efbce --- /dev/null +++ b/docs/proposals/storyboard.md @@ -0,0 +1,298 @@ +# 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