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 |
| 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) | src/cinema-os/CinemaOSNavCluster.tsx |
| Apps: command-center / search / topic / question / questions / curated / reading / reading-plan / prayer-room / discipleship / summary / profile / author | src/cinema-os/apps/*.tsx |
| AuthorCinemaApp (julkinen tekijΓ€profiili in-cinema; author intent, user+guest; reuses useAuthorContributions) | src/cinema-os/apps/AuthorCinemaApp.tsx |
| PrayerRoomApp (multiplayer Rukoushuone in-shell; ownsChrome β OS chrome hidden) | src/cinema-os/apps/PrayerRoomApp.tsx |
| DiscipleshipCinemaApp (full TÀnÀÀn Discipleship-tila in-shell; dual-mode CinemaReaderScreen + ownsChrome) | src/cinema-os/apps/DiscipleshipCinemaApp.tsx |
| SummaryCinemaApp (Kooste played as verse cinema; dual-mode CinemaReaderScreen mode="summaryItems" + ownsChrome) | src/cinema-os/apps/SummaryCinemaApp.tsx |
| Launch entry (TÀnÀÀn top) | src/pages/tanaan-page/CinemaOSEntryCard.tsx |
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 (βonShowAllVerses, only if set) Β· βΆnext Β· β Β· βπβΉοΈ) via getCinemaPortalContainer, only while isActive. Header is ONLY the jae row (book-ikoni Β· "jakeet" Β· versesShown/referenceCount); the topic name + card N/M live in TopicFlow's single global "AIHE: <nimi> N/M" title, not repeated here) | src/components/cinema/topic/TopicVersesCinemaView.tsx |
| VerseSnapshotStrip (horizontal snapshot filmstrip: ref + 2-line preview per verse, start/end Flag bookends, click-to-jump) | src/components/cinema/topic/VerseSnapshotStrip.tsx |
| TopicStepHeader (shared emphasized step header: icon + heading + N/M) | src/components/cinema/topic/TopicStepHeader.tsx |
| TopicVersesView (OLD grid model β now the list option; versePresentation toggle) | src/components/cinema/topic/TopicVersesView.tsx |
| useTopicVersePresentation (localStorage cinema/list, default cinema; toggle in settings sheet) | src/hooks/topic/useTopicVersePresentation.ts |
| CinemaVersePreviewPopup (Haku>Jakeet result β verse-in-chapter popup, per-verse jae-cinema) | src/components/cinema/CinemaVersePreviewPopup.tsx |
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, default 30s),
β 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()(fromcomponents/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'snav.back()(and ESC, shell X, fullscreen-exit) runs interceptors LIFO first, then pops the OS stack. Generalizes TopicCinema's oldpopOrClose. Question/Curated have NO internal layers β no interceptor. - Only the top frame is mounted (Phase 1).
nav.replacePRESERVES 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-osso the/cinemalanding (CinemaLandingPage) is untouched.CinemaShellalready portals todocument.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 "Rajaus
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 ("Rajaus kirjoittain" = books from results; "Rajaus
taivutuksin" = extractWordForms, now EXPORTED from VerseFilterBar) are inline
(NOT Radix Select β stay visible in fullscreen). Pagination 16/page; below it the
"Aiheeseen liittyvΓ€t jakeet" panel (topicalVerses deduped vs shown ids, VIA-topic
tag) β both click β CinemaVersePreviewPopup. Cards = wrapping dark boxes (flex flex-wrap); the snippet uses a cinema-local cinemaSnippet (match-LEADING, small
lead) NOT the wide-page snippetAroundMatch (preContext 60) β in the narrow w-[300px]
line-clamp-2 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 offsetTop); click/ENTER opens the preview (found verse in chapter
context, search term highlighted via the query prop + buildHighlightRegex,
per-verse Film button β TopicVerseCinema). "Aja jae-cinema" runs the full
carousel startβfinish. Internal layers (list β preview β verseCinema, plus runAll) +
useCinemaBackHandler β same model as ReadingCinemaApp, 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):
// Provides:
// 1. Fullscreen modal shell (useCinemaFullscreen)
// 2. Animated background visuals (CinemaBackground + GSAP Ken Burns)
// 3. Background music (useCinemaAudio, music only)
// 4. User preferences (useCinemaPreferences)
// 5. Visual/music pickers + keyboard shortcuts (B/N/V/M)
// 6. Render props: CinemaShellContext (audioState, selectedTrack, preferences, toggleMusic, etc.)
// 7. dimControls prop to fade controls when overlays active
CinemaReaderScreen
Content layer at src/features/cinema/CinemaReaderScreen.tsx (~1033 lines):
// Wraps content in CinemaShell, adds:
// 1. Map BundleVerse β CinemaVerse for cinema-reader package
// 2. Bible audio playback (separate from CinemaShell's music)
// 3. Handle verse navigation (index state)
// 4. Auto-advance (WPM-based or audio cue-based)
// 5. Audio sync (manual nav β immediate audio-seek to that verse, no prompt)
// 6. Discipleship overlay rendering (delegates logic to useDiscipleshipOrchestration)
// 7. Compact mode for phones β passes compact + sheet callbacks to CinemaReader
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ÀÀniCinemaVersionVoiceSection, 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):
// Extracted from CinemaReaderScreen, manages:
// 1. Task queue state (queue, index, completedIds, overlays)
// 2. Auto-open logic (initialTask, autoStart, task selector)
// 3. Reading plan verse loading (loadReadingPlanReferences)
// 4. Kooste references loading (loadKoosteReferences)
// 5. Task completion persistence (persistTaskCompletion helper)
// 6. Quiz insertion after reading plan completion
// 7. Task routing, jumping, skipping
// 8. Mini-task choice routing
// 9. Prayer message handling
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") useTodayTasksnormalizes practices + plans + prayers βUnifiedTodayTask[]useDiscipleshipTasksconvenience 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:
DiscipleshipTransitionOverlaywith progress bar + free navigation DiscipleshipVerseBaron 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}, noonDiscipleshipToggle) - Skips reading plan memory quizzes (
!props.autoStartguard on quiz insertion) - Auto-skips transition overlays between tasks (useEffect at line ~1285-1290)
- When all tasks done: calls
onAllTasksCompletedand closes
Reading Plan Completion
handleComplete calls mark_reading_day_complete RPC when a reading plan finishes. Key details:
- Completion-type plans: RPC advances
current_dayimmediately (e.g. 5β6) - Dashboard fix:
get_today_dashboardcheckscompleted_at::date = CURRENT_DATEfor completion-type plans (notcurrent_daywhich already advanced) - Client-side fix:
useTodayTaskscheckscompleted_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+ cardbg-black/60 backdrop-blur-md max-w-md - Uses
<MiniTaskView>(SVG circular countdown timer, pause viaDiscipleshipPomodoroButton) 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 againsttask.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 atratio > 0.4to prevent flicker- Per-step auto-advance:
duration_seconds(default 30s) βscrollToStep(i+1)unless user is scrolling - User-scroll detection: any
scrollevent flipsuserScrollingRef = truewith 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; // lucide icon via getPracticeIcon
duration_seconds: number; // timer (CuratedPlanFlow) OR auto-scroll delay (ScrollingPlanFlow)
choices?: MiniTaskChoice[]; // if present β hides JATKA in MiniTaskView (choice-to-advance)
verse_ref?: string; // resolved via fetchVerseTexts
closing_text?: string;
image_url?: string;
show_break_button?: boolean; // default true
}
Plan Completion
- Last task / LOPETA β flow calls
onAllTasksCompleted()thenonClose()synchronously CuratedPlanCinema.handleAllTasksCompleted:- Sets
completedRef.current = trueBEFORE the async RPC (close fires synchronously after) - Calls
supabase.rpc('complete_grand_plan', { p_grand_plan_id: planId }) - Invalidates
["grand-plans"]
- Sets
handleCloseβonClose(), thenif completedRef && completionRedirectUrlβnavigate(url)after 300ms
Critical Gotchas
continuousandlinearare identical in code. Only"scrolling"branches β the other two both fall through toCuratedPlanFlow. Branch INSIDE the flow if you need them to differ, don't split the router.- ScrollingPlanFlow reimplements step UI. Inline
StepContentis a simpler cousin ofMiniTaskView(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 byInfoFlow. ScrollingPlanFlow still carries its own duplicate inlineIntersectionObserver+ timer. A snap-scroll fix in one does not reach the other β prefer migrating ScrollingPlanFlow onto the hook over editing both. Tuning constants (0.4threshold,1500msuser-scroll debounce) are deliberately matched; don't "clean up" either copy. - Completion order is load-bearing.
completedRef.current = truemust run BEFORE theawait complete_grand_planRPC β flow callsonAllTasksCompletedthenonClosesynchronously, andhandleClosereads the ref immediately. - Two RPCs on mount.
useCuratedPlanCinemafiresget_grand_plan_itemsANDget_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_planshas no match β silentcontinuousdefault. Worth checking first when a plan "won't scroll". - ScrollingPlanFlow doesn't invoke
complete_grand_plan. That'sCuratedPlanCinema's job. A new flow must callonAllTasksCompleted()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(theFilmCTA, 3 variants) βInfoCinema(wrapper:CinemaShell hideControls+ music/settings sheets, mirrorsCuratedPlanCinema) βInfoFlow(the snap-scroll) βInfoView variant="scroll"(one block card).- Mechanics come from the shared
useSnapScrollStepshook β InfoFlow does NOT reimplement the IntersectionObserver. Per-card auto-advance from each block'sduration_seconds(0 = none); user-scroll pauses it; last card never auto-advances. finalSliderender-prop: optional extra snap-step after the blocks (indexblocks.length), e.g.CinemaQuestionWrapUp(feedback bar + related questions). Gets{ closeCinema, isActive }. Left-rail marker is aHelpCircle(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.contentKeyremount pattern:InfoCinemapassescontentKey(thequestionId) 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-plankey={cinema-day-N}.InfoViewis dual-variant:variant="scroll"(cinema) vs the pageInfoTextCard. Same block, two renderers β edit the one the task asks for (seequestions_answersmanifest domain).- Renders nothing with 0 published+non-hidden blocks (CTA hidden,
InfoCinemareturns 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). PassinginitialConfigopens straight into"room"view (skips setup) β this is how every "launch" entry point works. - Entry helpers:
useTodayPrayerRoomopens stored room β fallback most recent β fresh "PΓ€ivΓ€n rukoushuone" - Prayer-calendar Cinema launch (
CalendarDetailView,/rukouskalenterit?cal=<id>): aFilm"Cinema" button navigatesnavigate("/rukoushuone", { state: { config } })with the full today-room sources on (useMyCalendar/useSubscribedCalendars: true) pluscalendarId(guarantees this calendar's prayers are present even if unsubscribed + enables per-day nav) and the transientstartCalendarIdfield.PrayerRoomScreenruns a one-shot effect that jumpscurrentPrayerIndexto the first prayer whosecalendarId === startCalendarIdonce the aggregated list has loaded it β so the room opens on that calendar but the rest stays arrow-navigable.startCalendarIdis UI-only β NOT persisted (theprayer_roomsmutations 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>)useSubscribedCalendarsminusmutedCalendarIds(sub-<id>)config.calendarIdextra browsing calendar (cal-<id>)
- Ad-hoc
- Legacy rooms normalize
useMyCalendar/useSubscribedCalendarstotrue; new rooms default toggles tofalsein setup β asymmetric by design - Host keyboard: ArrowLeft/Right (prayer nav), ArrowUp/Down (calendar day, only if
config.calendarId) - Left weekday rail (
PrayerRoomCalendarRail+useCalendarRailModel): for prayer-calendar cards (currentPrayer.source === "calendar") a vertical left rail shows the calendar's own scheduled weekdays (MonβSun, viagetScheduledWeekdays), with the currently-browsed day as the single active marker β green, not amber, and no past/future colouring (unlike InfoFlow/ScrollingPlanFlow). Driven bycalendarDayOffsets[currentPrayer.calendarId](works for subscribed calendars too, not justconfig.calendarId). Moves with ArrowUp/Down + header chevrons; host can click a marker to jump (offset delta βshiftCurrentCalendarDay+setCurrentPrayerIndex(0)).useCalendarRailModelis a pureuseMemoplaced after the realtime hooks (no hook-order risk). - Verses: OSIS is canonical removal key; adds and removes auto-persist when
config.idexists
Realtime Multiplayer
Channel: prayer-room:<roomId> via Supabase Realtime.
- Presence (keyed by user.id):
displayName,avatarUrlfetched fromprofilesbefore 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 orautoGrantNexton release/leave; events:raise_hand,grant_talk,release_talk - WebRTC audio mesh: STUN-only (
stun:stun.l.google.com:19302), 2β5 users, oneRTCPeerConnection+<audio>per peer, signaling over the same channel;micOpen = activeSpeakerId === userId && pttPressedtoggles track enable - Channel ownership:
usePrayerRoomSyncowns subscribe/unsubscribe.usePushToTalkanduseWebRTCAudioattach 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 shadcnAlertDialogorDialogβ both auto-portal via Radix todocument.bodyand become invisible inside the fullscreen element.asDialog/inlineprops on child dialogs exist for this. Seereferences/learnings.mdβ "Radix Dialog/AlertDialog Auto-Portals" for the full pattern. - Z-index stack: CinemaShell base < Header/BottomBar
z-[10002]< inline modalsz-[10003]. Don't invent new values. - Title pill is rendered by PrayerRoomHeader, not CinemaShell. Pass NO
titleprop 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. calendarNamealready includes "Rukouskalenteri" prefix. It comes from the DBnamecolumn (e.g."Rukouskalenteri Suomi"). Don't prependprayerRoom.header.calendarPrefixagain β that produced the duplicate "Rukouskalenteri Rukouskalenteri Suomi" bug. TheCalendaricon next to the name carries the semantics.- Prayer typography matches
.cinema-verse-text. PrayerRoomContent's prayer body usesclamp(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 havemax-w-2xl mx-autoor any narrower cap that would clip this lane. src/hooks/usePrayerRoom.tsis unused. PrayerRoomScreen manages state inline; do not "refactor to use the hook" without migrating sync/PTT/WebRTC plumbing too.prayer_rooms/prayer_room_invitationsnot 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 β useelevenlabs:{voiceId}format
Debug Audio Sync
Add interval logging in useCinemaAudio to trace findCurrentCue() output.
References
references/audio-cue-format.md- Detailed cue timing specificationreferences/elevenlabs-api.md- ElevenLabs API referencereferences/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 modereferences/topic-cinema.md- Topic Cinema (aihe-cinema) full detail: internal view stack (flow β chapter β verse), portal/ESC-pino gotchas,CinemaVerseActions,TopicChapterReaderpicker gotchas, two presentation models + fixed bottom cluster +suspended, verse-range/verse-order,get_chapter_bundlenormalization,TopicVerseCinemaauto-advance/start-verse, single global "AIHE: nimi N/M" title, info-palanen card + jae-snapshot popup + author link,authorcinema-app, wrap-up layout/continuation pathsreferences/info-cinema.md- Info Cinema (Q&A / topic info-palaset): one-page snap-scroll card flow, shareduseSnapScrollSteps,finalSlidewrap-up,contentKeyremount, dual-variant InfoViewreferences/prayer-room.md- Prayer Room (Rukoushuone): CinemaShell consumer with realtime sync, PTT, WebRTC audio mesh, invitationsreferences/curated-plans.md- Curated grand plans: progression modes (continuous / linear / scrolling), flow routing, completion RPC, MiniTask data modelreferences/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, gotchasreferences/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-cinemareferences/learnings.md- Bug patterns and fixesDocs/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.