feat: ability to navigate around while still recording (#43)

This commit is contained in:
Owen Gretzinger
2025-07-25 16:06:01 -04:00
committed by GitHub
parent 35bf122dec
commit 6afbed0fad
6 changed files with 213 additions and 69 deletions
@@ -11,6 +11,7 @@ class MeetingListViewModel: ObservableObject {
@Published var searchText: String = ""
private var cancellables = Set<AnyCancellable>()
private let recordingSessionManager = RecordingSessionManager.shared
// Computed property to filter meetings based on search text
var filteredMeetings: [Meeting] {
+50 -53
View File
@@ -15,24 +15,26 @@ enum MeetingViewTab: String, CaseIterable {
case enhancedNotes = "Enhanced Notes"
}
enum RecordingState {
case idle // Not recording, shows "Transcribe" or "Resume" based on transcript content
case recording // Recording, shows "Stop"
}
@MainActor
class MeetingViewModel: ObservableObject {
@Published var meeting: Meeting
@Published var isGeneratingNotes = false
@Published var errorMessage: String?
@Published var isRecording = false
@Published private var recordingStateChanged = false // Trigger SwiftUI updates
// Computed property that always uses the direct RecordingSessionManager check
var isRecording: Bool {
return recordingSessionManager.isRecordingMeeting(meeting.id)
}
@Published var selectedTab: MeetingViewTab = .transcript // Default to transcript tab
@Published var recordingState: RecordingState = .idle
@Published var isDeleted = false
@Published var templates: [NoteTemplate] = []
@Published var selectedTemplateId: UUID?
private let audioManager = AudioManager()
private let recordingSessionManager = RecordingSessionManager.shared
private var cancellables = Set<AnyCancellable>()
private var isNewMeeting = false
@@ -54,6 +56,8 @@ class MeetingViewModel: ObservableObject {
self.meeting = meeting
}
// Detect if this is a new meeting based on content, not storage existence
isNewMeeting = isEmpty
@@ -78,31 +82,38 @@ class MeetingViewModel: ObservableObject {
}
.store(in: &cancellables)
// NEW: Seed the audio manager with any existing transcript chunks so the initial
// published value doesn't overwrite the saved transcript with an empty array.
audioManager.transcriptChunks = self.meeting.transcriptChunks
// Update meeting transcript chunks when audio manager transcript chunks change
audioManager.$transcriptChunks
.sink { [weak self] newChunks in
self?.meeting.transcriptChunks = newChunks
// Trigger SwiftUI updates when recording state changes
Publishers.CombineLatest(recordingSessionManager.$isRecording, recordingSessionManager.$activeMeetingId)
.sink { [weak self] (isRecording, activeMeetingId) in
guard let self = self else { return }
// Toggle the dummy property to trigger SwiftUI re-render
self.recordingStateChanged.toggle()
}
.store(in: &cancellables)
// Update isRecording when audio manager recording state changes
audioManager.$isRecording
.sink { [weak self] isRecording in
self?.isRecording = isRecording
self?.updateRecordingState()
}
.store(in: &cancellables)
// Update error message when audio manager encounters errors
audioManager.$errorMessage
// Update error message when recording session manager encounters errors
recordingSessionManager.$errorMessage
.compactMap { $0 }
.sink { [weak self] errorMessage in
self?.errorMessage = errorMessage
print("🚨 Audio Manager Error: \(errorMessage)")
print("🚨 Recording Session Manager Error: \(errorMessage)")
}
.store(in: &cancellables)
// If currently recording this meeting, load live transcript chunks
if recordingSessionManager.isRecordingMeeting(meeting.id) {
self.meeting.transcriptChunks = recordingSessionManager.getTranscriptChunks(for: meeting.id)
}
// Listen to real-time transcript updates for this meeting if it's being recorded
recordingSessionManager.$activeRecordingTranscriptChunksUpdated
.dropFirst()
.sink { [weak self] updatedChunks in
guard let self = self else { return }
// Only update if this meeting is the active recording
if recordingSessionManager.isRecordingMeeting(self.meeting.id) {
self.meeting.transcriptChunks = updatedChunks
}
}
.store(in: &cancellables)
@@ -117,37 +128,26 @@ class MeetingViewModel: ObservableObject {
}
.store(in: &cancellables)
// Auto-start recording for empty meetings
if isNewMeeting {
print("🚀 Auto-starting recording for empty meeting")
startRecording()
}
}
private func updateRecordingState() {
if isRecording {
recordingState = .recording
} else {
recordingState = .idle
}
}
var recordingButtonText: String {
switch recordingState {
case .idle:
// Use the same computed isRecording property for perfect consistency
if isRecording {
return "Stop"
} else {
// Check if there's existing transcript content
return meeting.transcriptChunks.isEmpty ? "Transcribe" : "Resume"
case .recording:
return "Stop"
}
}
func toggleRecording() {
switch recordingState {
case .idle:
startRecording()
case .recording:
// Use the same computed isRecording property for perfect consistency
if isRecording {
stopRecording()
} else {
startRecording()
}
}
@@ -159,7 +159,7 @@ class MeetingViewModel: ObservableObject {
switch validationResult {
case .success():
// Key is valid, proceed with recording
audioManager.startRecording()
recordingSessionManager.startRecording(for: meeting.id)
case .failure(let error):
// Show error message
errorMessage = error.localizedDescription
@@ -169,7 +169,7 @@ class MeetingViewModel: ObservableObject {
}
func stopRecording() {
audioManager.stopRecording()
recordingSessionManager.stopRecording()
saveMeeting()
// Auto-generate notes if there's a transcript and no existing notes
@@ -198,9 +198,6 @@ class MeetingViewModel: ObservableObject {
isGeneratingNotes = true
errorMessage = nil
// Clear any audio manager errors as well
audioManager.errorMessage = nil
// Clear existing notes for streaming
meeting.generatedNotes = ""
@@ -287,7 +284,7 @@ class MeetingViewModel: ObservableObject {
}
func deleteIfEmpty() {
if isEmpty {
if isEmpty && !isRecording {
print("🗑️ Auto-deleting empty meeting")
deleteMeeting()
} else {