| name | cinema-voice-architect |
| description | Architect for Cinema Mode / Cinema OS in Raamattu Nyt: CinemaShell, verse audio-sync & cues,
auto-advance, dual-track audio (Bible+music, ElevenLabs TTS), Ken Burns, Discipleship Cinema
(task orchestration, Nyt Kooste, verse memory quizzes, TanaanPage), curated/scrolling grand
plans, Info Cinema (Q&A / topic info-palaset, snap-scroll), Topic Cinema (aihe-cinema), and
realtime multiplayer Prayer Rooms (rukoushuone, PTT, WebRTC). Use for any cinema / voice /
audio-sync, discipleship-tila, info-palaset, or prayer-room work.
|
Cinema Voice Architect
Expert skill for Cinema Mode and audio/voice implementation.
Quick Reference
Base Layer (CinemaShell)
| Component | Location |
|---|
| CinemaShell | src/components/cinema/CinemaShell.tsx |
| Cinema Background | src/components/cinema/CinemaBackground.tsx |
| Background Music Picker | src/components/cinema/BackgroundMusicPicker.tsx |
| Background Visual Picker | src/components/cinema/BackgroundVisualPicker.tsx |
| Cinema Preferences Hook | src/hooks/useCinemaPreferences.ts |
| Cinema Fullscreen Hook | src/hooks/useCinemaFullscreen.ts |
| Cinema Audio Hook | src/hooks/useCinemaAudio.ts |
| CinemaShellContext (shell ctx as React context for shell-less apps) | src/components/cinema/CinemaShellContext.ts |
Cinema OS / Command Center (orchestration layer)
| Component / Module | Location |
|---|
| Design doc (read first) | Docs/cinema/CINEMA-OS.md |
| Types (Intent / CinemaApp / CinemaNav) | src/cinema-os/types.ts |
| Navigation reducer (stack) | src/cinema-os/navigation.ts |
| Registry + deep-link translation | src/cinema-os/registry.ts |
Web-reitti → intent (webRouteToIntent; sisällön route-linkit pysyvät cinemassa) | src/cinema-os/routeIntents.ts |
| App registration (all apps) | src/cinema-os/cinemaApps.ts |
| CinemaOSProvider (stack + history + 2-level back) | src/cinema-os/CinemaOSProvider.tsx |
| CinemaHost (the ONE persistent shell) | src/cinema-os/CinemaHost.tsx |
Contexts + useCinemaNav / useCinemaBackHandler | src/cinema-os/context.ts |
useCinemaShell (re-export) | src/cinema-os/useCinemaShell.ts |
| Session history (localStorage) | src/cinema-os/history.ts |
Route element (/cinema-os/*) | src/cinema-os/CinemaOSRoot.tsx |
| Shared settings/music sheets | src/cinema-os/CinemaOSSheets.tsx |
Shared settings-sheet wiring (single source for CinemaOSSheets AND CinemaReaderScreen). ⚙ sheet = bg+textsize+completion+speed; Raamatun versio + lukuääni (CinemaVersionVoiceSection) are split to the 🔊 music popover (audio side) | src/components/cinema/useCinemaSettingsProps.ts |
| Cinema OS help overlay (i-icon → "what is Cinema OS") | src/cinema-os/CinemaOSHelpOverlay.tsx |
| CinemaOSNavCluster (top-left Back + Command Center pill; shared by CinemaOSChrome + ownsChrome apps like the prayer room) |
Content Layer (CinemaReaderScreen)
| Component | Location |
|---|
| CinemaReaderScreen | src/features/cinema/CinemaReaderScreen.tsx |
| Bible Audio Hook | src/hooks/useBibleAudio.ts |
| Auto-Advance Hook | src/hooks/useAutoAdvance.ts |
| Chapter Bundle Hook | src/hooks/useChapterBundle.tsx |
| Audio Sync | src/lib/cinemaAudioSync.ts |
| Audio Estimation | src/lib/audioEstimation.ts |
| Cinema Types | src/types/cinema.ts |
Discipleship Layer
| Component | Location |
|---|
| Discipleship Orchestration | src/hooks/useDiscipleshipOrchestration.ts |
| Discipleship Utils | src/components/cinema/discipleshipUtils.tsx |
| Discipleship TaskSelector | src/components/cinema/DiscipleshipTaskSelector.tsx |
| Discipleship InlineTask | src/components/cinema/DiscipleshipInlineTask.tsx |
| Discipleship VerseBar | src/components/cinema/DiscipleshipVerseBar.tsx |
| Discipleship Pomodoro | src/components/cinema/DiscipleshipPomodoroButton.tsx |
| Discipleship Transition | src/components/cinema/DiscipleshipTransitionOverlay.tsx |
| Verse Memory Quiz | src/components/practice/VerseMemoryQuiz.tsx |
| Discipleship Tasks Hook | src/hooks/useDiscipleshipTasks.ts |
| Nyt Summary Hook | src/hooks/useNytSummary.ts |
| Reading Plan Quiz Hook | src/hooks/useReadingPlanQuiz.ts |
| Smart Verse Selection | src/hooks/useSmartVerseSelection.ts |
Program Mode (Curated Plans)
| Component | Location |
|---|
| CuratedPlanCinema (wrapper + flow router) | src/components/cinema/CuratedPlanCinema.tsx |
CuratedPlanFlow (continuous / linear) | src/components/cinema/CuratedPlanFlow.tsx |
ScrollingPlanFlow (scrolling) | src/components/cinema/ScrollingPlanFlow.tsx |
| MiniTaskView (inside CuratedPlanFlow) | src/components/cinema/MiniTaskView.tsx |
| Curated Plan Cinema Hook | packages/shared-practices/src/hooks/useCuratedPlanCinema.ts |
Shared types (GrandPlanProgressionType, MiniTaskContentConfig, MiniTaskRoute) | packages/shared-practices/src/types.ts |
| Shared snap-scroll mechanics (InfoFlow + ScrollingPlanFlow) | src/hooks/useSnapScrollSteps.ts |
Info Cinema (Q&A / Topic Info-palaset)
| Component / Hook | Location |
|---|
| InfoCinema (wrapper: CinemaShell + sheets) | src/components/cinema/InfoCinema.tsx |
| InfoFlow (one-page snap-scroll, card per block) | src/components/cinema/InfoFlow.tsx |
InfoView (variant="scroll" = one block card) | src/components/cinema/InfoView.tsx |
CinemaQuestionWrapUp (Q&A finalSlide) | src/components/cinema/CinemaQuestionWrapUp.tsx |
QuestionAnswersCinema (the Film CTA) | src/pages/question-detail/QuestionAnswersCinema.tsx |
| CinemaWrapUpRelatedQuestions (shared "Liittyvät kysymykset" list) | src/components/cinema/CinemaWrapUpRelatedQuestions.tsx |
Topic Cinema (aihe-cinema)
| Component / Hook | Location |
|---|
| TopicCinema (wrapper: CinemaShell + sheets, mirrors InfoCinema) | src/components/cinema/TopicCinema.tsx |
TopicFlow (snap-scroll over heterogeneous TopicCinemaStep[]) | src/components/cinema/topic/TopicFlow.tsx |
TopicCinemaWrapUp (topic finalSlide: feedback → related questions → LaunchTile row Aiheet/Lukusuunnitelma/Rukoushuone) | src/components/cinema/topic/TopicCinemaWrapUp.tsx |
| Card views (explanation / strongs / verses / highlights) | src/components/cinema/topic/Topic*View.tsx |
| Step composer hook | src/hooks/cinema/useTopicCinemaSteps.ts |
TopicCinemaButton (the Film CTA on /aihe/<slug> + haku/aihekortti + preview) | src/pages/topic-page/TopicCinemaButton.tsx |
| CinemaVerseActions (per-verse: pohdittavaksi/koosteeseen/muistiinpano + open) | src/components/cinema/CinemaVerseActions.tsx |
| TopicChapterReader (kevyt luku-cinema: 6-verse scroll, prev/next chapter, X) | src/components/cinema/topic/TopicChapterReader.tsx |
TopicVerseCinema (1-verse carousel via @raamattu-nyt/cinema-reader embedded) | src/components/cinema/topic/TopicVerseCinema.tsx |
TopicVersesCinemaView (default verses step: full-width single-verse carousel, useVerseCues+useAutoAdvance, ←/→=verse, ↑/↓ bubble to flow=card. Renders a FIXED portaled bottom cluster (snapshot strip + segmented progress + consolidated control bar, one rounded pill: ◀prev · 📖Lue (openChapter) · 🎬Cinema (Film icon → 1-verse jae-cinema, only if onOpenVerseCinema) · 🎓Discipleship popover (amber GraduationCap → CinemaVerseActions menu: Pohdittavaksi/Lisää koosteeseen/Muistiinpano/Harjoittele jae/Tutki jaetta) · ▶/⏸ (amber play/pause) · ☰Kaikki (→, only if set) ) via , only while . Header is ONLY the jae row (book-ikoni · "jakeet" · versesShown/referenceCount); the topic name + card N/M live in TopicFlow's single global "AIHE: N/M" title, not repeated here) |
Topic cinema is a 4th CinemaShell consumer (its own TopicFlow over a discriminated TopicCinemaStep[], NOT InfoFlow), reusing useSnapScrollSteps, CinemaShell, and InfoView variant="scroll" for info-block cards; background music only, no verse-TTS.
For full details — internal view stack (flow → chapter → verse) + portal/ESC-pino gotchas, CinemaVerseActions, TopicChapterReader picker gotchas, the TWO presentation models (cinema/list) + fixed bottom cluster + TopicFlow.suspended, verse-range/verse-order rules, get_chapter_bundle book normalization, TopicVerseCinema auto-advance/start-verse, the single global "AIHE: <nimi> N/M" title, the info-palanen card + jae-snapshot popup + author link, the author cinema-app, and the wrap-up layout/continuation paths: See references/topic-cinema.md.
Prayer Room (Rukoushuone)
| Component / Hook | Location |
|---|
| PrayerRoomScreen | src/features/prayer-room/PrayerRoomScreen.tsx |
| PrayerRoomPage (route) | src/pages/PrayerRoomPage.tsx |
| PrayerRoomSetup | src/features/prayer-room/PrayerRoomSetup.tsx |
| PrayerRoomContent | src/features/prayer-room/PrayerRoomContent.tsx |
| PrayerRoomHeader | src/features/prayer-room/PrayerRoomHeader.tsx |
| PrayerRoomBottomBar | src/features/prayer-room/PrayerRoomBottomBar.tsx |
| PrayerRoomCalendarRail (left weekday rail) | src/features/prayer-room/PrayerRoomCalendarRail.tsx |
| useCalendarRailModel | src/features/prayer-room/hooks/useCalendarRailModel.ts |
| PrayerRoomInviteDialog | src/features/prayer-room/PrayerRoomInviteDialog.tsx |
| CalendarPrayerBrowser | src/features/prayer-room/CalendarPrayerBrowser.tsx |
| Prayer Room Types | src/features/prayer-room/types.ts |
| useTodayPrayerRoom | src/features/prayer-room/useTodayPrayerRoom.ts |
| usePrayerRooms | src/hooks/usePrayerRooms.ts |
| usePrayerRoomSync (Realtime) | src/hooks/usePrayerRoomSync.ts |
| usePrayerRoomInvitations | src/hooks/usePrayerRoomInvitations.ts |
| usePushToTalk | src/hooks/usePushToTalk.ts |
| useWebRTCAudio | src/hooks/useWebRTCAudio.ts |
Audio Pipeline
| Component | Location |
|---|
| Audio Service | src/lib/audioService.ts |
| ElevenLabs Voices | src/lib/elevenLabsVoices.ts |
| Audio Generation | supabase/functions/generate-audio/index.ts |
Pages
| Component | Location |
|---|
| Tanaan Page | src/pages/TanaanPage.tsx |
| Discipleship Landing | src/pages/DiscipleshipLandingPage.tsx |
| Spiritual Path Faith | src/pages/SpiritualPathFaithPage.tsx |
| Daily Reading View | src/components/reading-plans/DailyReadingView.tsx |
Architecture Overview
CinemaShell (base layer ~438 lines)
│ Fullscreen, background visuals (Ken Burns), background music,
│ preferences, visual/music pickers, keyboard shortcuts (B/N/V/M)
│ Props: isOpen, onClose, title?, hideControls?, dimControls?
│ Exports: CinemaShellContext (render props for children)
│
├── CinemaReaderScreen (content layer ~1033 lines)
│ │ Verse fetching/mapping, Bible audio, auto-advance, audio sync,
│ │ completion overlay. Uses CinemaShell as wrapper via render props.
│ │ Modes: "chapter" | "verseList" | "summaryItems"
│ │
│ └── useDiscipleshipOrchestration (task queue ~792 lines)
│ │ Task queue, completion persistence, quiz insertion,
│ │ transition overlays, verse bar, kooste, prayer messages
│ │
│ └── Overlay Components
│ ├── DiscipleshipTaskSelector (pick tasks)
│ ├── DiscipleshipInlineTask (prayer/practice/quiz)
│ ├── DiscipleshipTransitionOverlay (between tasks)
│ └── DiscipleshipVerseBar (note/share on pause)
│
├── CuratedPlanCinema (program mode ~66 lines)
│ │ Wrapper: loads grand plan mini_tasks + progression_type via useCuratedPlanCinema.
│ │ CinemaShell with hideControls. Routes to flow based on progression_type:
│ │ progressionType === "scrolling" ? ScrollingPlanFlow : CuratedPlanFlow
│ │ On all-tasks-complete: calls complete_grand_plan RPC + invalidates ["grand-plans"].
│ │
│ ├── CuratedPlanFlow (continuous / linear ~133 lines)
│ │ │ One mini-task card at a time, AnimatePresence crossfade.
│ │ │ Scrim bg-black/40 + card bg-black/60 backdrop-blur-md.
│ │ │ completedIds Set; backward jumps un-complete the target.
│ │ │ Choice routing: next | jump_to_sort_order.
│ │ │
│ │ └── MiniTaskView (step UI ~223 lines)
│ │ SVG circular countdown timer, pause via DiscipleshipPomodoroButton,
│ │ JATKA hidden when choices exist (choice-to-advance).
│ │
│ └── ScrollingPlanFlow (scrolling ~336 lines)
│ Snap-scroll all steps on one page, numbered progress pills (click-to-jump),
│ IntersectionObserver (threshold 0.4) tracks active step,
│ per-step auto-advance timer (duration_seconds; 0/missing = off),
│ user-scroll detection pauses auto-advance 1.5s.
│ Inline StepContent (NOT MiniTaskView) — no countdown ring, no completedIds.
│ See references/curated-plans.md for full details.
│
├── InfoCinema (Q&A / topic info-palaset ~121 lines)
│ │ CinemaShell with hideControls; renders InfoFlow from parent-loaded
│ │ InfoBlocks. Mirrors CuratedPlanCinema. See references/info-cinema.md.
│ │
│ └── InfoFlow (one-page snap-scroll, card per block ~350 lines)
│ Uses useSnapScrollSteps (SHARED mechanics). Left progress rail +
│ numbered pills, per-card auto-advance, prayer-room-style bottom bar.
│ Optional finalSlide (CinemaQuestionWrapUp). key={contentKey} remount.
│
├── PrayerRoomScreen (prayer room consumer ~499 lines)
│ │ CinemaShell with hideControls; owns prayer/verse state, realtime sync,
│ │ PTT, WebRTC audio mesh, invitations. Route: /rukoushuone.
│ │ See references/prayer-room.md for full details.
│ │
│ └── Parallel runtime systems (scoped to config.id)
│ ├── usePrayerRoomSync (presence + host state broadcast, debounced 100ms)
│ ├── usePushToTalk (single-speaker floor + FIFO hand queue)
│ └── useWebRTCAudio (STUN-only audio mesh, 2–5 users)
│
└── Future: Meditation, etc.
└── CinemaShell + custom content
Audio Pipeline:
generate-audio Edge Function → ElevenLabs API (with timestamps)
→ audio_assets table (hash-cached) → audio_cues table (verse timing)
Audio Split
CinemaShell and CinemaReaderScreen both use useCinemaAudio but for different purposes:
- CinemaShell:
bibleAudioUrl: null (music-only)
- CinemaReaderScreen:
backgroundMusicUrl: null (Bible audio-only, music from CinemaShell context)
Cinema OS / Command Center
Orchestration layer that wraps the individual cinemas in one persistent
CinemaShell so the user moves Topic → Search → Question → Curated as a single
continuous session (music/background never reset — "älä poistu Cinemasta").
Route /cinema-os (entry card atop the Tänään page). Read
Docs/cinema/CINEMA-OS.md first.
/cinema-os → CinemaOSProvider (stack reducer + history + localStorage)
└─ CinemaHost (mounted ONCE)
└─ CinemaPreferencesProvider (hoisted, shared once)
└─ <CinemaShell> (persistent: music / bg / fullscreen)
└─ CinemaShellContext.Provider
└─ ActiveCinemaApp = registry[topIntent.type].Component
command-center · search · topic · question · curated
Three separated concerns: Intent (serializable {type,payload}) →
Registry (intent.type → CinemaApp, declarative, no host branches) →
Navigation (stack reducer + history). Apps are shell-less content that
consume useCinemaShell(); they never render a CinemaShell.
Search → Topic chaining: SearchCinemaApp picks a topic →
nav.launch({type:"topic", payload:{slug}}) → OS pushes Topic Cinema; back
returns to Search. QuestionCinemaApp's info-block CTA →
nav.launch({type:"curated", ...}). Apps special-case nothing — they emit intents.
Invariants (load-bearing — don't break these)
- Apps are shell-less. Consume
useCinemaShell() (from
components/cinema/CinemaShellContext). The standalone wrappers
(TopicCinema, InfoCinema, CuratedPlanCinema) are LEFT UNTOUCHED for their
existing launch buttons; the OS apps (src/cinema-os/apps/*) are parallel
shell-less mirrors. Don't merge them yet.
Intent.payload = plain serializable data only (slug/id/query) — never
functions/objects. One rule → history + localStorage + deep-link + (Phase 2)
DB are the same representation. Derive auth/version/i18n INSIDE the app, not
in the payload.
- Two-level back: apps with internal layers (Topic's flow→chapter→verse)
register
useCinemaBackHandler(fn); the host's nav.back() (and ESC, shell X,
fullscreen-exit) runs interceptors LIFO first, then pops the OS stack.
Generalizes TopicCinema's old popOrClose. Question/Curated have NO internal
layers → no interceptor.
- Only the top frame is mounted (Phase 1).
nav.replace PRESERVES the frame
key (in-place payload update, no remount) — Search uses it to persist
query/tab so the topic round-trip restores them. Provider URL-sync is
path-based; replace does NOT record history. Keep-alive is a Phase 2 opt-in
(CinemaApp.keepAlive, default false, never Reader/PrayerRoom).
- Only "destinations" are intents. Topic's chapter/verse stay internal
layers (transient, not URL-addressable). Only top-level intents get URLs.
- Phase 1 base is
/cinema-os so the /cinema landing (CinemaLandingPage) is
untouched. CinemaShell already portals to document.body (universal overlay
root) — apps must NOT add their own portal.
Phase 2 (deferred)
DB history (cinema_sessions/cinema_events), opt-in keep-alive,
browser-back↔OS-back, verses⇄topics swipe remain deferred.
Discipleship-tila is now an in-shell app (discipleship, done — the
previously-deferred CinemaReaderScreen migration). CinemaReaderScreen is
dual-mode: the embedded flag makes its outer wrapper skip its own
CinemaPreferencesProvider AND its CinemaShell, and CinemaReaderScreenContent
consumes useCinemaShell() instead of the render-prop ctx (the music-ref-sync just
reads the same CinemaShellContext). DiscipleshipCinemaApp feeds it
useDiscipleshipTasks() with no autoStart → full useDiscipleshipOrchestration
flow (selector → reading + prayer + practice + memory quiz + Nyt Kösete). ownsChrome: true (CinemaReader ControlBar + overlays replace OS pills; exit/ESC → nav.back,
completion → nav.home); heavy engine/audio mount-bound (never keep-alive). Standalone
/tanaan launch (TanaanModals) unchanged — it omits embedded. Same dual-mode +
ownsChrome template as the prayer room.
Kooste (summary) is an in-shell app too (summary, done). SummaryCinemaApp
resolves {summaryId?} (else active via useActiveSummary) → fetchSummaryDetail
(pages/summary/summaryDetailLoader) → buildCinemaItems (pages/summary/SummaryCinemaMode,
exported pure fns) → dual-mode CinemaReaderScreen embedded mode="summaryItems".
ownsChrome:true. Launched from the SearchCinemaApp "Muut" tab (Koosteet result)
and a Command Center "Kooste" tile. SearchCinemaApp now has 4 tabs — Jakeet, Aiheet,
Kysymykset (usePublishedQuestionSearch → question intent), Muut
(search_user_content RPC; only Koosteet links → summary, rest read-only). Strongs
is intentionally absent (no cinema target → per the "no link for non-cinema" rule).
The Jakeet tab is NOT an auto-carousel by default — it mirrors the base /search page.
Data comes from useUnifiedSearch (NOT the old useVersesSearch) → verses
(match_rank/is_word_form) + topicalVerses. Hits are ordered literal-first then
inflected (splitVersesByRank drops rank-5 fuzzy; compareByRankThenLocation within
each group). Classification = isInflectedHit(v, query): single-word query → inflected
iff the verse text lacks the query as an exact token (Finnish FTS returns inflections as
rank-1 WITHOUT is_word_form, so the rank/flag alone misses them); multi-word → falls back
to is_word_form || match_rank>=2. Inflected hits carry a TAIVUTUS badge. The
"Taivutuksin" filter uses exact token match (NOT startsWith — the base form would
otherwise match all its inflections and filter nothing).
This orderedVerses drives the whole cinema verse search (list + preview + "Aja"
carousel). Two filter dropdowns ("Kirjoittain" = books from results; "Taivutuksin" =
extractWordForms, EXPORTED from VerseFilterBar) are inline (NOT Radix Select → stay
visible in fullscreen), and live inside a framed RAJAUKSET block (corner tag,
verse-card width + border classes, amber when a filter is on, to clear all;
). Choosing an option closes the dropdown and the chosen value shows
in the collapsed trigger — the web mirrors this interaction. : it creates a stacking context that traps the dropdown's
panel inside it, so the verse cards cover the open menu (raising z-index does
not help). ; below it the
panel ( deduped vs shown ids, VIA-
tag) — both click → . Cards = wrapping dark boxes (); the snippet uses a cinema-local (match-LEADING, small
lead) NOT the wide-page (preContext 60) — in the narrow
card the wide pre-context pushed the highlight past the 2 visible lines;
leading "…" dropped at a clean word boundary. arrow keys rove the current page
(column count from DOM ); click/ENTER opens the preview (found verse in chapter
context, via the prop + ,
per-verse Film button → ). runs the full
carousel start→finish. Internal layers (list → preview → verseCinema, plus ) +
— same model as , NO new app.
Prayer Room is now an in-shell app (prayer-room, done — was previously a
separate-destination exclusion). PrayerRoomScreen is dual-mode: an
embedded flag switches between mounting its own CinemaShell (standalone
/rukoushuone) and consuming useCinemaShell() (under the host). It owns its
chrome (ownsChrome: true on the registry entry → CinemaOSChrome returns null
for it; PrayerRoomHeader/BottomBar replace the OS pills, header X = nav.back,
ESC closes an open invite/setup modal first via useCinemaBackHandler). Payload
{roomId?/startCalendarId?/create?} is resolved by PrayerRoomApp
(usePrayerRooms + resolveTodayPrayerRoomConfig/normalizeLoadedRoom); setup is
an internal layer. Realtime/PTT/WebRTC are mount-bound (never keep-alive) —
leaving the app leaves the room (channel removeChannel). The pattern (dual-mode
ownsChrome) is the template for migrating any other own-shell feature
(e.g. the deferred Reader).
Cinema Mode Components
CinemaShell
Reusable base layer at src/components/cinema/CinemaShell.tsx (~438 lines):
CinemaReaderScreen
Content layer at src/features/cinema/CinemaReaderScreen.tsx (~1033 lines):
Mobile UI (Compact Mode)
On phones (useBreakpoint().isPhone || isCozy, <768px) Cinema Mode collapses the desktop two-row control panel + top-left HUD pills into a single row + two icon-triggered bottom sheets, freeing ~90px for the verse text.
🔊 icon → CinemaMusicPopover (Raamatun versio + lukuääni CinemaVersionVoiceSection, track, favorite, volume, favorites-only, change track)
⚙ icon → CinemaSettingsSheet (background, completion mode, speed, voice volume + on/off)
- Adaptive verse typography via CSS
clamp() + vw (3 tiers; small-phone ≤380px gets 24-30px to fit Acts 1:14 on iPhone SE)
- Sheets render inside native fullscreen via
getCinemaPortalContainer() (Radix portal targeting fix)
Critical: @media (max-width: 479px) matches iPhone 14 Pro Max (430px). Use max-width: 380px for genuine "small phone" rules.
For full details (component contracts, control distribution table, font tier table, portal pattern, gotchas): See references/mobile-ui.md
useDiscipleshipOrchestration
Task queue hook at src/hooks/useDiscipleshipOrchestration.ts (~792 lines):
Animation Modes
Five animation modes in src/types/cinema.ts: slide, zoom, stack, loopH, loopV
Ken Burns Effect
CinemaBackground.tsx: Random transform over 25s (scale 1.0-1.15, translate ±5%)
Audio System
ElevenLabs Integration
Voices in src/lib/elevenLabsVoices.ts: Venla (female, T5qAFgaL2uYxoUtojUzQ), Urho (male, 1WVCONUwYGulVaKg4oTr)
Audio Generation Flow
Client → Edge Function → check hash cache → ElevenLabs API (with timestamps)
→ parse timestamps → verse cues → store MP3 → save metadata + cues → return
For detailed API reference, see references/elevenlabs-api.md
Audio Cue Format
See references/audio-cue-format.md for full specification.
Auto-Advance (Without Audio)
useAutoAdvance: WPM-based timing (default 150, adjustable 50-400). Min 1.5s per verse.
Priority: audio cues > auto-advance timer.
Dual-Track Audio
useCinemaAudio: Bible + background music tracks with independent volume (0-1).
Database Schema
See existing tables: audio_assets, audio_cues, cinema_preferences, background_tracks, background_visuals in bible_schema.
Discipleship Cinema Mode
Immersive full-screen task flow launched from Tanaan page. Guides users through daily reading plans, prayers, practices, and memory quizzes sequentially.
For full details: See references/discipleship-cinema.md
Key Concepts
- Launched via "Discipleship-tila" button on
/tanaan → CinemaReaderScreen(mode="verseList")
useTodayTasks normalizes practices + plans + prayers → UnifiedTodayTask[]
useDiscipleshipTasks convenience hook composes all data sources
- Task routing:
reading_plan → verse reader, prayer/practice → DiscipleshipInlineTask
- Quiz content types:
verse_memory, read_memory, reading_plan_memory → VerseMemoryQuiz
- Between tasks:
DiscipleshipTransitionOverlay with progress bar + free navigation
DiscipleshipVerseBar on pause: Add to Nyt Kooste, Note, Share, Pomodoro Break
- Nyt Kooste: Auto-created daily verse collection, appended as final task
- Reading Plan Memory Quiz: Auto-inserted after each reading plan day completion (discipleship mode only, NOT autoStart)
- initialTask prop: Start specific task immediately, bypass selector
- autoStart prop: Skip task selector, auto-queue all uncompleted tasks, auto-skip transitions
- Break duration from
app_config.discipleship_break_minutes (default 5 min)
Two Cinema Launch Modes from Tanaan
| Mode | Button | Props | Behavior |
|---|
| Reading Plan Cinema | Blue Play button | discipleshipTasks={readingPlanCinemaTasks} autoStart | Auto-plays reading plans only. No task selector, no discipleship UI toggle, no memory quizzes, no transition overlays. |
| Discipleship Cinema | Amber Clapperboard button | discipleshipTasks={allTasks} | Full discipleship flow: task selector → reading + prayer + practice → quizzes → kooste. |
autoStart behavior:
- Queues all uncompleted tasks, starts first immediately (line ~1071-1092)
- Hides discipleship toggle/next button in CinemaReader (
discipleshipMode={false}, no onDiscipleshipToggle)
- Skips reading plan memory quizzes (
!props.autoStart guard on quiz insertion)
- Auto-skips transition overlays between tasks (useEffect at line ~1285-1290)
- When all tasks done: calls
onAllTasksCompleted and closes
Reading Plan Completion
handleComplete calls mark_reading_day_complete RPC when a reading plan finishes. Key details:
- Completion-type plans: RPC advances
current_day immediately (e.g. 5→6)
- Dashboard fix:
get_today_dashboard checks completed_at::date = CURRENT_DATE for completion-type plans (not current_day which already advanced)
- Client-side fix:
useTodayTasks checks completed_days.includes(current_day - 1) for completion-type plans as fallback
- Query invalidation: Must invalidate
["today-dashboard"], ["reading-plan-streaks"], AND ["user-reading-plans"]
Reading Plan Memory Quiz
useReadingPlanQuiz generates a 3-verse quiz after reading plan completion
(2 plan verses + 1 decoy). Selection priority: user-marked refs
(marked_refs in quiz task metadata, from nytKoosteRefsRef) → preferred
books (NT, Psalms, Proverbs) → any plan verse. Full algorithm:
references/discipleship-cinema.md.
Tanaan Page & DiscipleshipLandingPage
/tanaan sections (TodayTaskList, TomorrowTasksSection,
PermanentTasksSection, RecurringTasksSection, PracticeActivationCard)
and the /opetuslapseus marketing landing page are documented in
references/discipleship-cinema.md. Locations: Quick Reference → Discipleship
Layer / Pages.
Curated Plans (Grand Plans) & Progression Modes
Curated grand plans are a second CinemaShell consumer. The wrapper CuratedPlanCinema routes to one of two flow components based on the plan's progression_type, then renders inside CinemaShell hideControls.
For full details: See references/curated-plans.md
Progression Types
type GrandPlanProgressionType = 'continuous' | 'linear' | 'scrolling';
| Value | Flow Component | UX |
|---|
continuous (default) | CuratedPlanFlow | One mini-task card at a time + countdown timer |
linear | CuratedPlanFlow | Same code path as continuous — semantic distinction only |
scrolling | ScrollingPlanFlow | All steps on one snap-scrollable page with auto-advance |
Flow router (CuratedPlanCinema.tsx line ~55):
const FlowComponent = progressionType === "scrolling" ? ScrollingPlanFlow : CuratedPlanFlow;
Both flows share the same props contract: { tasks, onAllTasksCompleted, onClose }.
CuratedPlanFlow (continuous / linear)
- One mini-task card at a time via
AnimatePresence mode="wait" crossfade
- Scrim
bg-black/40 + card bg-black/60 backdrop-blur-md max-w-md
- Uses
<MiniTaskView> (SVG circular countdown timer, pause via DiscipleshipPomodoroButton)
completedIds: Set<string> tracks done tasks; advance skips already-done
- Backward jump via choice → un-completes the target (allows replay)
- X button = skip WITHOUT marking complete
- Choice routing:
{type:'next'} | {type:'jump_to_sort_order', sort_order} — sort_order matched against task.metadata.grand_plan_sort_order
ScrollingPlanFlow (scrolling)
- All steps rendered on a single snap-scrollable page (
snap-y snap-mandatory)
- Numbered progress pills at top (click-to-jump), amber = active, emerald = past, faint = future
IntersectionObserver (threshold: [0, 0.25, 0.5, 0.75, 1], root = container) tracks active step; gates state update at ratio > 0.4 to prevent flicker
- Per-step auto-advance:
duration_seconds — 0/missing = off (same as InfoFlow), positive → scrollToStep(i+1) unless user is scrolling
- User-scroll detection: any
scroll event flips userScrollingRef = true with 1500ms debounce — pauses auto-advance
- Scrim
bg-black/50 (stronger than CuratedPlanFlow)
- Uses its own inline
StepContent (NOT MiniTaskView) — no countdown ring, no completedIds
JATKA (non-last) = scroll, LOPETA (last) = finish + close
MiniTaskContentConfig (per-step data)
interface MiniTaskContentConfig {
title: string;
body: string;
icon?: string;
duration_seconds: number;
choices?: MiniTaskChoice[];
verse_ref?: string;
closing_text?: string;
image_url?: string;
show_break_button?: boolean;
}
Plan Completion
- Last task / LOPETA → flow calls
onAllTasksCompleted() then onClose() synchronously
CuratedPlanCinema.handleAllTasksCompleted:
- Sets
completedRef.current = true BEFORE the async RPC (close fires synchronously after)
- Calls
supabase.rpc('complete_grand_plan', { p_grand_plan_id: planId })
- Invalidates
["grand-plans"]
handleClose → onClose(), then if completedRef && completionRedirectUrl → navigate(url) after 300ms
Critical Gotchas
continuous and linear are identical in code. Only "scrolling" branches — the other two both fall through to CuratedPlanFlow. Branch INSIDE the flow if you need them to differ, don't split the router.
- ScrollingPlanFlow reimplements step UI. Inline
StepContent is a simpler cousin of MiniTaskView (no timer, no pause). Behavioral changes to MiniTaskView do NOT propagate — fix both or refactor to share.
- Snap-scroll mechanics are now extracted to
useSnapScrollSteps, but ScrollingPlanFlow has NOT adopted it. src/hooks/useSnapScrollSteps.ts (active-index tracking, jump, user-scroll detection, per-step auto-advance) was extracted from ScrollingPlanFlow's proven behaviour and is used by InfoFlow. ScrollingPlanFlow still carries its own duplicate inline IntersectionObserver + timer. A snap-scroll fix in one does not reach the other — prefer migrating ScrollingPlanFlow onto the hook over editing both. Tuning constants (0.4 threshold, 1500ms user-scroll debounce) are deliberately matched; don't "clean up" either copy.
- Completion order is load-bearing.
completedRef.current = true must run BEFORE the await complete_grand_plan RPC — flow calls onAllTasksCompleted then onClose synchronously, and handleClose reads the ref immediately.
- Two RPCs on mount.
useCuratedPlanCinema fires get_grand_plan_items AND get_user_grand_plans (the latter only for progression_type). Consider folding progression_type into items if you refactor.
- Default progression falls back to
continuous. When the user isn't a member yet, get_user_grand_plans has no match → silent continuous default. Worth checking first when a plan "won't scroll".
- ScrollingPlanFlow doesn't invoke
complete_grand_plan. That's CuratedPlanCinema's job. A new flow must call onAllTasksCompleted() at the end, or the plan never marks complete.
- Intersection threshold 0.4 and user-scroll debounce 1500ms are tuned. Lowering the threshold flickers active-index; shortening the debounce fires auto-advance mid-flick. Don't "clean up" these constants.
Info Cinema (Q&A / Topic Info-palaset)
Third CinemaShell consumer. Renders a question's/topic's published Info-palaset as a one-page vertical snap-scroll — one card at a time, top → bottom — the same UX as the curated ScrollingPlanFlow but for read-only answer content. This is the "yksi sivu jota mennään kortti kerrallaan alaspäin" flow.
For full details: See references/info-cinema.md
Key Concepts
QuestionAnswersCinema (the Film CTA, 3 variants) → InfoCinema (wrapper: CinemaShell hideControls + music/settings sheets, mirrors CuratedPlanCinema) → InfoFlow (the snap-scroll) → InfoView variant="scroll" (one block card).
- Mechanics come from the shared
useSnapScrollSteps hook — InfoFlow does NOT reimplement the IntersectionObserver. Per-card auto-advance from each block's duration_seconds (0 = none); user-scroll pauses it; last card never auto-advances.
finalSlide render-prop: optional extra snap-step after the blocks (index blocks.length), e.g. CinemaQuestionWrapUp (feedback bar + related questions). Gets { closeCinema, isActive }. Left-rail marker is a HelpCircle (not a number), auto-advance disabled. onFinalSlideEnter = its Enter-key default action (e.g. "Näytä seuraava kysymys"); Enter is reserved for this — never add Enter-to-advance on content cards.
contentKey remount pattern: InfoCinema passes contentKey (the questionId) to <InfoFlow key={contentKey}> → CinemaShell stays mounted (bg/music continue) while InfoFlow remounts (scroll resets) → seamless "next question" transition without leaving cinema. Same family as reading-plan key={cinema-day-N}.
InfoView is dual-variant: variant="scroll" (cinema) vs the page InfoTextCard. Same block, two renderers — edit the one the task asks for (see questions_answers manifest domain).
- Renders nothing with 0 published+non-hidden blocks (CTA hidden,
InfoCinema returns null). A "Cinema button missing" report usually means the publish gate, not cinema code.
Prayer Room (Rukoushuone)
Realtime multiplayer full-screen prayer experience built on CinemaShell (not CinemaReader). Canonical example of a CinemaShell-only consumer: reuses fullscreen + Ken Burns + background music + preferences, adds its own content, host-driven realtime sync, push-to-talk, and WebRTC audio mesh on top.
For full details: See references/prayer-room.md
Key Concepts
- Route:
/rukoushuone → PrayerRoomPage → PrayerRoomScreen(initialConfig?, onClose). Passing initialConfig opens straight into "room" view (skips setup) — this is how every "launch" entry point works.
- Entry helpers:
useTodayPrayerRoom opens stored room → fallback most recent → fresh "Päivän rukoushuone"
- Prayer-calendar Cinema launch (
CalendarDetailView, /rukouskalenterit?cal=<id>): a Film "Cinema" button navigates navigate("/rukoushuone", { state: { config } }) with the full today-room sources on (useMyCalendar/useSubscribedCalendars: true) plus calendarId (guarantees this calendar's prayers are present even if unsubscribed + enables per-day nav) and the transient startCalendarId field. PrayerRoomScreen runs a one-shot effect that jumps currentPrayerIndex to the first prayer whose calendarId === startCalendarId once the aggregated list has loaded it — so the room opens on that calendar but the rest stays arrow-navigable. startCalendarId is UI-only — NOT persisted (the prayer_rooms mutations whitelist columns), so saved/reopened rooms never re-trigger the jump.
- Two views:
"setup" (form) and "room" (CinemaShell hideControls + render props)
isHost = !config.id || !user ? true : config.hostUserId === user.id — solo sessions and anonymous users are always host
- Content sources (merged + deduped by
baseId = id.replace(/^(cal-|mine-|sub-)/, "")):
- Ad-hoc
config.prayers (uuid ids)
useMyCalendar → user's own + followed active prayers (mine-<id>)
useSubscribedCalendars minus mutedCalendarIds (sub-<id>)
config.calendarId extra browsing calendar (cal-<id>)
- Legacy rooms normalize
useMyCalendar/useSubscribedCalendars to true; rooms default toggles to in setup — asymmetric by design
Realtime Multiplayer
Channel: prayer-room:<roomId> via Supabase Realtime.
- Presence (keyed by user.id):
displayName, avatarUrl fetched from profiles before tracking
- Broadcast "sync": Host sends
{ currentPrayerIndex, currentVerseIndex, calendarDayOffset } debounced 100ms; participants apply in useEffect gated on !isHost && syncState
- Push-to-talk (PTT): Single speaker at a time; FIFO
handQueue; host grants via avatar click or autoGrantNext on release/leave; events: raise_hand, grant_talk, release_talk
- WebRTC audio mesh: STUN-only (
stun:stun.l.google.com:19302), 2–5 users, one RTCPeerConnection + <audio> per peer, signaling over the same channel; micOpen = activeSpeakerId === userId && pttPressed toggles track enable
- Channel ownership:
usePrayerRoomSync owns subscribe/unsubscribe. usePushToTalk and useWebRTCAudio attach listeners but do NOT unsubscribe.
Invitations
Table public.prayer_room_invitations with invitee_id OR invitee_email, status pending|accepted|declined. Invite flow auto-persists the room first via createRoom.mutateAsync when opening the invite dialog from an unsaved config.
Critical Gotchas
- Modals inline, not portaled. Setup, invite, verse-full popup, and remove-verse confirmation render as
absolute inset-0 z-[10003] children of the CinemaShell subtree. Do NOT use shadcn AlertDialog or Dialog — both auto-portal via Radix to document.body and become invisible inside the fullscreen element. asDialog / inline props on child dialogs exist for this. See references/learnings.md → "Radix Dialog/AlertDialog Auto-Portals" for the full pattern.
- Z-index stack: CinemaShell base < Header/BottomBar
z-[10002] < inline modals z-[10003]. Don't invent new values.
- Title pill is rendered by PrayerRoomHeader, not CinemaShell. Pass NO
title prop to <CinemaShell> from PrayerRoomScreen — the header renders the pill on its own row 1 alongside the prayer-count nav. Adding the title back to CinemaShell yields a stacked duplicate.
calendarName already includes "Rukouskalenteri" prefix. It comes from the DB name column (e.g. "Rukouskalenteri Suomi"). Don't prepend prayerRoom.header.calendarPrefix again — that produced the duplicate "Rukouskalenteri Rukouskalenteri Suomi" bug. The Calendar icon next to the name carries the semantics.
- Prayer typography matches
.cinema-verse-text. PrayerRoomContent's prayer body uses clamp(36px, 5vw, 56px) + min(1100px, 88vw) lane — exactly the same as cinema-reader's verse text (packages/cinema-reader/src/styles/cinema.css). The middle container must NOT have max-w-2xl mx-auto or any narrower cap that would clip this lane.
src/hooks/usePrayerRoom.ts is unused. PrayerRoomScreen manages state inline; do not "refactor to use the hook" without migrating sync/PTT/WebRTC plumbing too.
prayer_rooms / prayer_room_invitations not in types.ts — hooks still use (supabase as any). When types regenerate, drop the casts (see /supabase-typing-architect).
- STUN-only mesh fails on symmetric NAT / strict firewalls. Add TURN + SFU if scaling beyond 5 users.
Common Tasks
Add New Voice
- Get voice ID from ElevenLabs → add to
elevenLabsVoices.ts → add to admin UI → use elevenlabs:{voiceId} format
Debug Audio Sync
Add interval logging in useCinemaAudio to trace findCurrentCue() output.
References
references/audio-cue-format.md - Detailed cue timing specification
references/elevenlabs-api.md - ElevenLabs API reference
references/discipleship-cinema.md - Full discipleship cinema mode documentation (components, hooks, flows, gotchas)
references/reading-plan-transition.md - State flow for "Next Day" in reading plan cinema mode
references/topic-cinema.md - Topic Cinema (aihe-cinema) full detail: internal view stack (flow → chapter → verse), portal/ESC-pino gotchas, CinemaVerseActions, TopicChapterReader picker gotchas, two presentation models + fixed bottom cluster + suspended, verse-range/verse-order, get_chapter_bundle normalization, TopicVerseCinema auto-advance/start-verse, single global "AIHE: nimi N/M" title, info-palanen card + jae-snapshot popup + author link, author cinema-app, wrap-up layout/continuation paths
references/info-cinema.md - Info Cinema (Q&A / topic info-palaset): one-page snap-scroll card flow, shared useSnapScrollSteps, finalSlide wrap-up, contentKey remount, dual-variant InfoView
references/prayer-room.md - Prayer Room (Rukoushuone): CinemaShell consumer with realtime sync, PTT, WebRTC audio mesh, invitations
references/curated-plans.md - Curated grand plans: progression modes (continuous / linear / scrolling), flow routing, completion RPC, MiniTask data model
references/mobile-ui.md - Cinema Mode mobile UI: compact controls (🔊 + ⚙ sheets), adaptive verse typography (clamp+vw, ≤380px tier), text-weight (Lihava/Kevyt) toggle, native fullscreen portal pattern, gotchas
references/topic-verse-bottom-cluster.md - Aihe-jae-cinema bottom cluster (filmstrip + segmented progress + consolidated pill bar): component contracts, portal/single-bar mechanics, generic-vs-aihe-specific porting analysis for adopting into jae-cinema
references/learnings.md - Bug patterns and fixes
Docs/cinema/CINEMA-OS.md - Cinema OS / Command Center: persistent shell + Intent/Registry/Navigation, two-level back, deep links, history, KeepAlive/Frame State, Search Cinema, phasing (NOT under references/ — it's a shared system doc)
Cross-cutting learnings: See .claude/LEARNINGS.md → "CSS/Layout" section for framer-motion patterns and animation gotchas.