diff --git a/src/lib/stores/recording.svelte.ts b/src/lib/stores/recording.svelte.ts index 3e25070..414f670 100644 --- a/src/lib/stores/recording.svelte.ts +++ b/src/lib/stores/recording.svelte.ts @@ -3,6 +3,7 @@ import { api, events, type TranscriptSegment, type MeetingId } from "../api"; import { settings } from "./settings.svelte"; +import { SvelteMap } from "svelte/reactivity"; class RecordingStore { meetingId = $state(null); @@ -16,6 +17,11 @@ class RecordingStore { levelPeak = $state(0); /** Set while a capture-device reconnect is in progress; cleared on recovery (FR-CAP-6). */ deviceNotice = $state(null); + /** Live notes redesign: freeform text typed in the Notes pane while recording. */ + notesText = $state(""); + /** anchor_ms (a segment's start_ms) -> note text, for moments annotated this recording. */ + segmentNotes = new SvelteMap(); + private notesSaveTimer: ReturnType | null = null; async init() { await events.onRecordingState((p) => { @@ -53,6 +59,8 @@ class RecordingStore { this.segments = []; this.retention = record; this.deviceNotice = null; + this.notesText = ""; + this.segmentNotes.clear(); // T8.7/FR-TRX-4: whatever language is currently configured in Settings // becomes this meeting's requested language, persisted on its record. const language = settings.settings.whisper_language ?? undefined; @@ -61,6 +69,9 @@ class RecordingStore { } async stop() { + // The debounced save below can lag up to 500ms behind typing — flush + // whatever's pending first so stop_recording's merge sees the latest text. + await this.flushNotes(); if (this.meetingId) await api.stopRecording(this.meetingId); this.state = "idle"; this.levelRms = 0; @@ -70,6 +81,10 @@ class RecordingStore { /** Abandon an accidental recording: stop capture and delete it entirely. */ async cancel() { + if (this.notesSaveTimer) { + clearTimeout(this.notesSaveTimer); + this.notesSaveTimer = null; + } if (this.meetingId) await api.cancelRecording(this.meetingId); this.meetingId = null; this.segments = []; @@ -77,6 +92,8 @@ class RecordingStore { this.levelRms = 0; this.levelPeak = 0; this.deviceNotice = null; + this.notesText = ""; + this.segmentNotes.clear(); } /** Toggle audio retention mid-meeting (FR-REC-1); caller must have gated consent already. */ @@ -85,6 +102,41 @@ class RecordingStore { await api.setRecordingRetention(this.meetingId, record); this.retention = record; } + + /** + * Live notes redesign: freeform text typed in the Notes pane while + * recording. Debounced 500ms (same cadence as the post-finalize editor's + * `scheduleSave`) so every keystroke doesn't round-trip to the backend. + */ + setNotesText(text: string) { + this.notesText = text; + if (!this.meetingId) return; + const meetingId = this.meetingId; + if (this.notesSaveTimer) clearTimeout(this.notesSaveTimer); + this.notesSaveTimer = setTimeout(() => { + this.notesSaveTimer = null; + api.updateLiveNotes(meetingId, text).catch(() => {}); + }, 500); + } + + private async flushNotes() { + if (this.notesSaveTimer) { + clearTimeout(this.notesSaveTimer); + this.notesSaveTimer = null; + } + if (this.meetingId) { + await api.updateLiveNotes(this.meetingId, this.notesText).catch(() => {}); + } + } + + /** Attach (or clear, with `text: ""`) a note to a clicked transcript + * segment's moment (the "click a transcript line, add a note" feature). */ + async setSegmentNote(anchorMs: number, text: string) { + if (!this.meetingId) return; + if (text.trim() === "") this.segmentNotes.delete(anchorMs); + else this.segmentNotes.set(anchorMs, text); + await api.setSegmentNote(this.meetingId, anchorMs, text); + } } export const recording = new RecordingStore();