This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Couples Timer is a web app for structured partner conversations ("Zwiegespräch") based on the Moeller method. It acts as a neutral "third instance" managing speaking time, order, and pauses so partners can focus on content rather than timing.
The project is being extended into a two-part system:
- CoupleTimer (frontend, this repo): session structuring, phase timeline, speakers A/B, timer/audio events, recording controls, review UI for transcript/summary. Today the app is a self-contained client (LocalStorage persistence). With the extension, it additionally captures per-session audio (one continuous file + phase-aligned metadata) and uploads it.
- VibeMind (backend service, separate): session API + audio storage, speaker diarization (e.g. pyannote.audio), batch transcription (Vibe/Sona, Whisper/whisper.cpp, optional faster-whisper), transcript ↔ diarization ↔ phase merge, semantic summary via OpenRouter / OpenAI-compatible LLM, Markdown / TXT export (PDF later).
Core design decision — hybrid speaker attribution: the timer's phase plan (slotA → speaker A, slotB → speaker B, etc.) is treated as a strong prior, not ground truth. The backend reconciles it against acoustic diarization so interjections, overlaps, and dialog inside monologic slots get attributed to the speaker who actually spoke. Final assignment = diarized speaker mapped onto timer A/B, weighted by phase-window overlap, corrected on clear acoustic divergence.
MVP scope:
- IN: session-level audio recording, phase/timeline export, backend diarization, timer↔diarization speaker mapping, batch transcription, speaker-attributed transcript, semantic summary, Markdown + TXT export, OpenRouter / OpenAI-compatible LLM config, review UI for manual speaker correction.
- OUT (deferred): live transcription, live diarization, mandatory local-only LLM, polished PDF export, automated psychological interpretation.
The summary output is structured (Kurzüberblick → Hauptthemen → was Person A/B sagte → Gesprächsdynamik → Vereinbarungen → offene Fragen → nächste Schritte → vollständiges Transkript) and must phrase dynamics as observable communication structure, not psychological diagnosis. Detailed MVP definition, data model (AudioSegment, DiarizationTurn, SpeakerMapping, TranscriptTurn), pipeline, and effort estimate (~35–65 PT with diarization) live in projekt-vibeTimer.md.
The CoupleTimer frontend is implemented and shipping (timer engine, presets Maintain/Commitment/Listening, custom-mode builder, DE/EN i18n, audio cues, persistence). The VibeMind extension is specified but not yet implemented — no recording, upload, diarization, transcription, summary, or export code exists in this repo yet. Next step: populate the Specification phase and start scaffolding MediaRecorder capture + session/phase timeline export from the existing SessionEngine.
- React 19 + TypeScript 5.9, bundled with Vite 7 (
base: './'for GitHub Pages / custom-domain hosting) - Tailwind CSS 3.4, Framer Motion 12
- i18next + react-i18next (DE/EN),
@dnd-kitfor the custom-mode sequence builder - Vitest 4 (jsdom) + Testing Library for unit/component tests; Playwright for E2E
- Note:
react-router-domis in dependencies but the app currently has no router —App.tsxmounts<SessionProvider><SessionView/></SessionProvider>andSessionViewswitches between setup / active / completed views from context state.
npm run dev # Vite dev server on http://localhost:5173
npm run build # Production build to dist/
npm run preview # Serve the built bundle
npm run test # Vitest in watch mode
npm run test:run # Vitest single run (CI-style)
npm run test -- src/domain/__tests__/SessionEngine.test.ts # single test file
npm run test:e2e # Playwright (auto-starts `npm run dev`, see playwright.config.ts)
npm run lint # ESLint
npm run format # Prettier write on src/**/*.{ts,tsx}
npm run deploy # build + gh-pages -d dist (publishes to gh-pages branch)Vitest coverage (include) is intentionally scoped to src/domain/** and src/services/** — UI/components are not part of the coverage target. Test setup file: src/test/setup.ts. E2E specs live in e2e/.
src/
├── domain/ Pure TS: SessionEngine, SessionMode (+ presets), SessionState,
│ PhaseType/PhaseConfig, AudioEvent, GuidanceLevel, ParticipantConfig
├── services/ Side-effecting adapters with Protocol interfaces:
│ AudioService (Web Audio API), TimerService (performance.now drift correction),
│ GuidanceService, PersistenceService, ParticipantPersistenceService,
│ BackgroundPersistenceService
├── viewmodels/ SessionViewModel.ts (selectors over SessionState),
│ useSessionSetup.ts (custom-mode builder hook)
├── contexts/ SessionContext.tsx — wires SessionEngine + services + persisted state
│ into a single React context; primary integration point for UI
├── hooks/ useFullscreen, useTipRotation
├── components/ UI (no business logic): SessionView (root switcher), SessionSetup,
│ ModeSelector, CustomModeEditor, TimerDisplay, PhaseIndicator,
│ ProgressBar, GuidancePanel, TipDisplay, ControlButtons,
│ MainLayout, Settings/EnhancedSettings, FocusModeToggle,
│ BackgroundSettings, LanguageSwitcher, onboarding/, ...
└── i18n/ index.ts + locales/{de,en}/translation.json
The dependency direction is one-way: components → viewmodels → domain, with services injected into SessionEngine via constructor (and into SessionContext via optional props for tests). Domain has zero React/DOM imports.
src/domain/SessionEngine.ts is the single source of truth for session state. It owns a SessionState (status: idle/running/paused/finished, phase index, elapsed) and orchestrates three injected services:
AudioServiceProtocol— fires bowl tones on phase transitions (see Audio Events below)TimerServiceProtocol—performance.now()-based ticker; engine recomputes remaining time each tick to self-correct drift (target accuracy: ±1s per 30 min)GuidanceServiceProtocol— returns tips appropriate to the current phase + GuidanceLevel
State changes flow through a single subscribe(callback) channel; SessionContext re-renders React on each tick. getState() returns an immutable copy.
PhaseType: prep | slotA | slotB | transition | closingA | closingB | cooldown. Helpers in PhaseType.ts (isSpeakingPhase, isPartnerAPhase/B, getPhaseColor, getPhaseI18nKey) are the canonical way to derive UI properties — don't hardcode phase strings.
SessionMode is { id, name, phases: PhaseConfig[], guidanceLevel, ... }. Presets are defined in SessionMode.presets.ts (Maintain ~90 min, Commitment ~60 min, Listening ~45 min — exact sequences are in _5_konkrete_preset_definitionen_verbindlich_.md and are verbindlich / non-negotiable). Custom modes are validated via isSessionModeValid (must contain ≥1 slotA and ≥1 slotB; per-phase duration bounds enforced).
Six bowl events are generated at runtime via Web Audio API (sinus + harmonics with decay) — there are no WAV files in public/. Triggers:
| Event | Trigger |
|---|---|
sessionStart |
bowl_deep_single — session starts |
slotEnd |
bowl_rising — speaking slot ends |
transitionEnd |
bowl_clear — transition ends |
closingStart |
bowl_double — closing phase begins |
cooldownStart |
bowl_fade — cooldown begins |
cooldownEnd |
bowl_triple — session complete |
Browser policy: audio only plays after the user's first interaction. The engine respects this — AudioService is unlocked from a user gesture in SessionContext/setup.
- No skip / shorten on individual phases. Only
pause,resume,stop(whole session). The "no skip buttons" rule is core to the Moeller method — don't add UI that violates it, even on Custom modes. - Strict phase order.
SessionEngineadvances deterministically throughmode.phases; never mutate the phase array mid-session. - Custom-mode validation (
SessionMode.ts → isSessionModeValid): ≥1 slotA, ≥1 slotB, durations within per-type bounds (prep 30–600s, slotA/B 300–1800s, transition 30–300s, closingA/B 60–600s, cooldown 300–1800s). - GuidanceLevel gates tips (see
GuidanceService):minimal= cooldown only;moderate= + transition;high= + prep. - Persistence keys are owned by the dedicated
*PersistenceServicemodules — go through them, don't read/write LocalStorage directly from components. - i18n: every user-facing string lives in
src/i18n/locales/{de,en}/translation.json. Phase labels usegetPhaseI18nKey(phase)→phases.<phaseType>. Adding a string in one locale without the other will break the language switch.
npm run deploy builds and pushes dist/ to the gh-pages branch (custom domain in CNAME). vite.config.ts uses base: './' so assets resolve under both subpath and root hosting. deploy-ftp.sh and deploy-hostinger.sh are alternative deployment scripts; see DEPLOYMENT.md / HOSTINGER-DEPLOY.md if asked to deploy elsewhere.