feat: use Coder for meeting processing

This commit is contained in:
2026-07-14 22:22:31 +02:00
parent e0b576ad95
commit 6d676de99e
19 changed files with 743 additions and 980 deletions
+33 -13
View File
@@ -28,20 +28,24 @@ class MeetingViewModel: ObservableObject {
// Computed property to determine if Generate button should animate
var shouldAnimateGenerateButton: Bool {
let generateButtonEnabled = !meeting.transcript.isEmpty && !isGeneratingNotes && !isRecording && !isStartingRecording
let generateButtonEnabled = !meeting.transcript.isEmpty && !isGeneratingNotes && !isRecording && !isProcessing && !isStartingRecording
let noEnhancedNotesYet = meeting.generatedNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
return generateButtonEnabled && noEnhancedNotesYet
}
// Computed property to determine if Transcribe button should animate
// Computed property to determine if the record button should animate
var shouldAnimateTranscribeButton: Bool {
return !isRecording && meeting.transcriptChunks.isEmpty && !isStartingRecording
return !isRecording && !isProcessing && meeting.transcriptChunks.isEmpty && !isStartingRecording
}
// Computed property that always uses the direct RecordingSessionManager check
var isRecording: Bool {
return recordingSessionManager.isRecordingMeeting(meeting.id)
}
var isProcessing: Bool {
return recordingSessionManager.isProcessing && recordingSessionManager.activeMeetingId == meeting.id
}
@Published var selectedTab: MeetingViewTab = .transcript // Default to transcript tab
@Published var isDeleted = false
@@ -110,6 +114,12 @@ class MeetingViewModel: ObservableObject {
}
.store(in: &cancellables)
recordingSessionManager.$isProcessing
.sink { [weak self] _ in
self?.recordingStateChanged.toggle()
}
.store(in: &cancellables)
// Update error message when recording session manager encounters errors
recordingSessionManager.$errorMessage
.compactMap { $0 }
@@ -130,7 +140,7 @@ class MeetingViewModel: ObservableObject {
self.meeting.transcriptChunks = recordingSessionManager.getTranscriptChunks(for: meeting.id)
}
// Listen to real-time transcript updates for this meeting if it's being recorded
// Listen for final transcript updates for this meeting.
recordingSessionManager.$activeRecordingTranscriptChunksUpdated
.dropFirst()
.sink { [weak self] updatedChunks in
@@ -158,18 +168,20 @@ class MeetingViewModel: ObservableObject {
var recordingButtonText: String {
// Use the same computed isRecording property for perfect consistency
if isProcessing {
return "Processing"
}
if isRecording {
return "Stop"
} else {
// Check if there's existing transcript content
return meeting.transcriptChunks.isEmpty ? "Transcribe" : "Resume"
return meeting.transcriptChunks.isEmpty ? "Record" : "Resume"
}
}
func toggleRecording() {
// Prevent duplicate actions while validating API key or starting recording
if isValidatingKey || isStartingRecording { return }
if isValidatingKey || isStartingRecording || isProcessing { return }
// Use the same computed isRecording property for perfect consistency
if isRecording {
stopRecording()
@@ -183,7 +195,7 @@ class MeetingViewModel: ObservableObject {
isValidatingKey = true
isStartingRecording = true
Task {
let validationResult = await APIKeyValidator.shared.validateCurrentAPIKey()
let validationResult = await CoderAPIValidator.shared.validateCurrentAPIKey()
defer { isValidatingKey = false }
switch validationResult {
@@ -201,8 +213,16 @@ class MeetingViewModel: ObservableObject {
}
func stopRecording() {
recordingSessionManager.stopRecording()
saveMeeting()
isStartingRecording = true
Task {
let chunks = await recordingSessionManager.stopRecording()
meeting.transcriptChunks = chunks
saveMeeting()
if !meeting.formattedTranscript.isEmpty {
await generateNotes()
}
isStartingRecording = false
}
}
func loadTemplates() {
@@ -301,7 +321,7 @@ class MeetingViewModel: ObservableObject {
// If this meeting is currently being recorded, stop the recording first
if recordingSessionManager.isRecordingMeeting(meeting.id) {
print("🛑 Stopping recording for meeting being deleted: \(meeting.id)")
recordingSessionManager.stopRecording()
recordingSessionManager.cancelRecording()
}
let success = LocalStorageManager.shared.deleteMeeting(meeting)
@@ -312,11 +332,11 @@ class MeetingViewModel: ObservableObject {
}
func deleteIfEmpty() {
if isEmpty && !isRecording {
if isEmpty && !isRecording && !isProcessing {
print("🗑️ Auto-deleting empty meeting")
deleteMeeting()
} else {
saveMeeting()
}
}
}
}
@@ -7,15 +7,43 @@ class SettingsViewModel: ObservableObject {
@Published var saveMessage = ""
@Published var showingSaveMessage = false
@Published var templates: [NoteTemplate] = []
@Published var coderModels: [CoderModel] = []
@Published var isLoadingModels = false
@Published var connectionMessage = ""
init() {
loadTemplates()
}
/// Loads the API key from keychain (only called when actually needed)
/// Loads the Coder service token from Keychain.
func loadAPIKey() {
if settings.openAIKey.isEmpty {
settings.openAIKey = KeychainHelper.shared.getAPIKey() ?? ""
if settings.coderAPIKey.isEmpty {
settings.coderAPIKey = KeychainHelper.shared.getCoderAPIKey() ?? ""
}
}
@MainActor
func refreshModels() async {
isLoadingModels = true
connectionMessage = ""
defer { isLoadingModels = false }
do {
coderModels = try await CoderAPIClient.shared.models(
baseURL: settings.coderBaseURL,
apiKey: settings.coderAPIKey
)
let chatModels = coderModels.filter(\.supportsChat)
let transcriptionModels = coderModels.filter(\.supportsTranscription)
if !chatModels.contains(where: { $0.id == settings.notesModel }), let first = chatModels.first {
settings.notesModel = first.id
}
if !transcriptionModels.contains(where: { $0.id == settings.transcriptionModel }), let first = transcriptionModels.first {
settings.transcriptionModel = first.id
}
connectionMessage = "Connected to Coder"
} catch {
coderModels = []
connectionMessage = error.localizedDescription
}
}
@@ -55,10 +83,10 @@ class SettingsViewModel: ObservableObject {
// Only save API key to keychain - other values are automatically saved to UserDefaults
// via computed properties when they're modified
let openAISaved = KeychainHelper.shared.saveAPIKey(settings.openAIKey)
let coderSaved = KeychainHelper.shared.saveCoderAPIKey(settings.coderAPIKey)
if showMessage {
if openAISaved {
if coderSaved {
saveMessage = "Settings saved successfully!"
} else {
saveMessage = "Error saving settings"