diff --git a/.env.development b/.env.development index 94f4bbc46..989e7f5e9 100644 --- a/.env.development +++ b/.env.development @@ -282,6 +282,8 @@ CALENDAR_DATABASE_URL=postgresql://mana:devpassword@localhost:5432/mana_platform # Production: https://stt-api.mana.how # Local dev: http://localhost:3020 STT_URL=https://stt-api.mana.how +# API key for mana-stt (set in your local .env, never commit a real key) +MANA_STT_API_KEY= # ============================================ # CONTEXT PROJECT diff --git a/apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte b/apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte index bb6ec0371..0ba47be89 100644 --- a/apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte @@ -11,6 +11,7 @@ useAllDreams, } from './queries'; import { dreamsStore } from './stores/dreams.svelte'; + import { dreamRecorder, formatElapsed } from './recorder.svelte'; import { MOOD_COLORS, MOOD_LABELS, type Dream, type DreamMood, type SleepQuality } from './types'; import type { ViewProps } from '$lib/app-registry'; import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui'; @@ -59,6 +60,18 @@ symbolFilter ? filteredByMode.filter((d) => d.symbols?.includes(symbolFilter!)) : filteredByMode ); let filtered = $derived(searchDreams(filteredBySymbol, searchQuery)); + + // While the inline editor is open, the `dreams` array updates whenever the + // transcript lands. If the user hasn't typed anything yet, fold the fresh + // content into the edit buffer so they see the transcription appear inline. + $effect(() => { + if (!editingId) return; + const live = dreams.find((d) => d.id === editingId); + if (!live) return; + if (!editContent.trim() && live.content.trim()) { + editContent = live.content; + } + }); let grouped = $derived(groupByMonth(filtered)); let insights = $derived(computeInsights(dreams)); @@ -166,6 +179,38 @@ ); const MOODS: DreamMood[] = ['angenehm', 'neutral', 'unangenehm', 'albtraum']; + + // ── Voice capture ───────────────────────────────────────── + let recError = $state(null); + + async function handleMicClick() { + recError = null; + if (dreamRecorder.status === 'recording') { + try { + const result = await dreamRecorder.stop(); + if (result.durationMs < 500) { + recError = 'Aufnahme war zu kurz.'; + return; + } + const dream = await dreamsStore.createFromVoice(result.blob, result.durationMs, 'de'); + // Open the dream so the user sees the transcript appear inline + viewMode = 'list'; + startEdit(dream); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + if (msg !== 'cancelled') recError = msg; + } + } else if (dreamRecorder.status === 'idle') { + await dreamRecorder.start(); + if (dreamRecorder.error) { + recError = dreamRecorder.error; + } + } + } + + function cancelRecording() { + dreamRecorder.cancel(); + }
@@ -191,6 +236,38 @@ }} /> {:else} + +
+ + {#if dreamRecorder.status === 'recording'} + + {/if} +
+ {#if recError} +

{recError}

+ {/if} +
e.preventDefault()} class="quick-add"> 🌙 @@ -289,6 +366,18 @@ placeholder="Titel (optional)..." autofocus /> + {#if dream.processingStatus === 'transcribing'} +
+ ●●● + Transkribiert deine Aufnahme… +
+ {:else if dream.processingStatus === 'failed'} +
+ Transkription fehlgeschlagen{dream.processingError + ? `: ${dream.processingError}` + : ''} +
+ {/if}