diff --git a/meetingnotes/Services/NotesGenerator.swift b/meetingnotes/Services/NotesGenerator.swift index d482cf7..715d0c4 100644 --- a/meetingnotes/Services/NotesGenerator.swift +++ b/meetingnotes/Services/NotesGenerator.swift @@ -10,51 +10,62 @@ class NotesGenerator { private init() {} - /// Generates meeting notes from meeting data using template-based system prompt + /// Generates meeting notes from meeting data using template-based system prompt with streaming /// - Parameters: /// - meeting: The meeting object containing all necessary data /// - userBlurb: Information about the user for context /// - systemPrompt: The system prompt template with placeholders - /// - Returns: Generated meeting notes - func generateNotes(meeting: Meeting, - userBlurb: String, - systemPrompt: String) async throws -> String { - - guard let apiKey = KeychainHelper.shared.get(forKey: "openAIKey"), !apiKey.isEmpty else { - throw NSError(domain: "NotesGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "OpenAI API key not configured"]) - } - - let openAI = OpenAI(apiToken: apiKey) - - // Create date formatter for meeting date - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .full - dateFormatter.timeStyle = .short - - // Prepare template variables - let templateVariables: [String: String] = [ - "meeting_title": meeting.title.isEmpty ? "Untitled Meeting" : meeting.title, - "meeting_date": dateFormatter.string(from: meeting.date), - "transcript": meeting.formattedTranscript, - "user_blurb": userBlurb, - "user_notes": meeting.userNotes - ] - - // Process the system prompt template - let systemContent = Settings.processTemplate(systemPrompt, with: templateVariables) - let systemMessage = ChatQuery.ChatCompletionMessageParam(role: .system, content: systemContent)! + /// - Returns: AsyncStream of partial generated notes + func generateNotesStream(meeting: Meeting, + userBlurb: String, + systemPrompt: String) -> AsyncStream { + return AsyncStream { continuation in + Task { + do { + guard let apiKey = KeychainHelper.shared.get(forKey: "openAIKey"), !apiKey.isEmpty else { + continuation.finish() + return + } + + let openAI = OpenAI(apiToken: apiKey) + + // Create date formatter for meeting date + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .full + dateFormatter.timeStyle = .short + + // Prepare template variables + let templateVariables: [String: String] = [ + "meeting_title": meeting.title.isEmpty ? "Untitled Meeting" : meeting.title, + "meeting_date": dateFormatter.string(from: meeting.date), + "transcript": meeting.formattedTranscript, + "user_blurb": userBlurb, + "user_notes": meeting.userNotes + ] + + // Process the system prompt template + let systemContent = Settings.processTemplate(systemPrompt, with: templateVariables) + let systemMessage = ChatQuery.ChatCompletionMessageParam(role: .system, content: systemContent)! - print(systemContent) - - let query = ChatQuery(messages: [systemMessage], model: .gpt4_1) - - let result = try await openAI.chats(query: query) - - guard let generatedNotes = result.choices.first?.message.content else { - throw NSError(domain: "NotesGenerator", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to generate notes"]) + print(systemContent) + + let query = ChatQuery(messages: [systemMessage], model: .gpt4_1) + + let stream: AsyncThrowingStream = openAI.chatsStream(query: query) + + for try await result in stream { + if let content = result.choices.first?.delta.content { + continuation.yield(content) + } + } + + continuation.finish() + } catch { + print("Error in streaming generation: \(error)") + continuation.finish() + } + } } - - return generatedNotes } /// Validates if OpenAI API key is configured diff --git a/meetingnotes/ViewModels/MeetingViewModel.swift b/meetingnotes/ViewModels/MeetingViewModel.swift index b8a4ee6..bdf9cc5 100644 --- a/meetingnotes/ViewModels/MeetingViewModel.swift +++ b/meetingnotes/ViewModels/MeetingViewModel.swift @@ -148,17 +148,25 @@ class MeetingViewModel: ObservableObject { isGeneratingNotes = true errorMessage = nil + // Clear existing notes for streaming + meeting.generatedNotes = "" + do { // Load settings for generation let userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? "" let systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt() - meeting.generatedNotes = try await NotesGenerator.shared.generateNotes( + // Use streaming generation + let stream = NotesGenerator.shared.generateNotesStream( meeting: meeting, userBlurb: userBlurb, systemPrompt: systemPrompt ) + for await chunk in stream { + meeting.generatedNotes += chunk + } + saveMeeting() } catch { errorMessage = "Failed to generate notes: \(error.localizedDescription)"