chore: rename to Meetingnotes
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class MeetingListViewModel: ObservableObject {
|
||||
@Published var meetings: [Meeting] = []
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var searchText: String = ""
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// Computed property to filter meetings based on search text
|
||||
var filteredMeetings: [Meeting] {
|
||||
guard !searchText.isEmpty else { return meetings }
|
||||
|
||||
return meetings.filter { meeting in
|
||||
// Search in title
|
||||
meeting.title.localizedCaseInsensitiveContains(searchText) ||
|
||||
// Search in user notes
|
||||
meeting.userNotes.localizedCaseInsensitiveContains(searchText) ||
|
||||
// Search in generated notes
|
||||
meeting.generatedNotes.localizedCaseInsensitiveContains(searchText) ||
|
||||
// Search in transcript text
|
||||
meeting.transcriptChunks.contains { chunk in
|
||||
chunk.text.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
loadMeetings()
|
||||
|
||||
// Listen for saved meeting notifications to refresh the list
|
||||
NotificationCenter.default.publisher(for: .meetingSaved)
|
||||
.sink { [weak self] _ in
|
||||
print("🔔 Meeting saved notification received. Reloading meetings list...")
|
||||
self?.loadMeetings()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
NotificationCenter.default.publisher(for: .meetingDeleted)
|
||||
.sink { [weak self] _ in
|
||||
print("🔔 Meeting deleted notification received. Reloading meetings list...")
|
||||
self?.loadMeetings()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func loadMeetings() {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let loadedMeetings = LocalStorageManager.shared.loadMeetings()
|
||||
print("📋 Loaded \(loadedMeetings.count) meetings")
|
||||
self?.meetings = loadedMeetings
|
||||
self?.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
func deleteMeeting(_ meeting: Meeting) {
|
||||
meetings.removeAll { $0.id == meeting.id }
|
||||
_ = LocalStorageManager.shared.deleteMeeting(meeting)
|
||||
}
|
||||
|
||||
func createNewMeeting() -> Meeting {
|
||||
let newMeeting = Meeting()
|
||||
meetings.insert(newMeeting, at: 0)
|
||||
_ = LocalStorageManager.shared.saveMeeting(newMeeting)
|
||||
return newMeeting
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// Add notification name for meeting saved events
|
||||
extension Notification.Name {
|
||||
static let meetingSaved = Notification.Name("MeetingSaved")
|
||||
static let meetingDeleted = Notification.Name("MeetingDeleted")
|
||||
}
|
||||
|
||||
enum MeetingViewTab: String, CaseIterable {
|
||||
case myNotes = "My Notes"
|
||||
case transcript = "Transcript"
|
||||
case enhancedNotes = "Enhanced Notes"
|
||||
}
|
||||
|
||||
enum RecordingState {
|
||||
case idle // Not recording, shows "Transcribe"
|
||||
case recording // Recording, shows "Stop"
|
||||
case paused // Paused, shows "Resume"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class MeetingViewModel: ObservableObject {
|
||||
@Published var meeting: Meeting
|
||||
@Published var isGeneratingNotes = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var isRecording = false
|
||||
@Published var selectedTab: MeetingViewTab = .transcript // Default to transcript tab
|
||||
@Published var recordingState: RecordingState = .idle
|
||||
@Published var isDeleted = false
|
||||
|
||||
private let audioManager = AudioManager()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var isNewMeeting = false
|
||||
|
||||
// Computed property to check if meeting is empty
|
||||
var isEmpty: Bool {
|
||||
return meeting.transcriptChunks.isEmpty &&
|
||||
meeting.userNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
meeting.generatedNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
meeting.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
init(meeting: Meeting = Meeting()) {
|
||||
// Load the latest version of the meeting from storage if it exists
|
||||
if let savedMeeting = LocalStorageManager.shared.loadMeetings().first(where: { $0.id == meeting.id }) {
|
||||
print("🔄 Loading latest version of meeting: \(meeting.id)")
|
||||
self.meeting = savedMeeting
|
||||
} else {
|
||||
print("🆕 Using provided meeting: \(meeting.id)")
|
||||
self.meeting = meeting
|
||||
}
|
||||
|
||||
// Detect if this is a new meeting based on content, not storage existence
|
||||
isNewMeeting = isEmpty
|
||||
|
||||
// Set initial tab based on notes existence
|
||||
if !self.meeting.generatedNotes.isEmpty {
|
||||
selectedTab = .enhancedNotes
|
||||
} else {
|
||||
selectedTab = .transcript
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
.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)
|
||||
|
||||
// Auto-save when meeting properties change
|
||||
$meeting
|
||||
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||
.sink { [weak self] meeting in
|
||||
print("🔄 Auto-saving meeting: \(meeting.id) - title: '\(meeting.title)', notes: '\(meeting.userNotes.prefix(50))...'")
|
||||
self?.saveMeeting()
|
||||
}
|
||||
.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 if recordingState == .recording {
|
||||
recordingState = .paused
|
||||
}
|
||||
}
|
||||
|
||||
var recordingButtonText: String {
|
||||
switch recordingState {
|
||||
case .idle:
|
||||
return "Transcribe"
|
||||
case .recording:
|
||||
return "Stop"
|
||||
case .paused:
|
||||
return "Resume"
|
||||
}
|
||||
}
|
||||
|
||||
func toggleRecording() {
|
||||
switch recordingState {
|
||||
case .idle, .paused:
|
||||
startRecording()
|
||||
case .recording:
|
||||
stopRecording()
|
||||
}
|
||||
}
|
||||
|
||||
func startRecording() {
|
||||
audioManager.startRecording()
|
||||
}
|
||||
|
||||
func stopRecording() {
|
||||
audioManager.stopRecording()
|
||||
saveMeeting()
|
||||
|
||||
// Auto-generate notes if there's a transcript and no existing notes
|
||||
if !meeting.formattedTranscript.isEmpty && meeting.generatedNotes.isEmpty {
|
||||
print("🤖 Auto-generating notes after recording stopped")
|
||||
// Switch to enhanced notes tab immediately when starting generation
|
||||
selectedTab = .enhancedNotes
|
||||
Task {
|
||||
await generateNotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateNotes() async {
|
||||
isGeneratingNotes = true
|
||||
errorMessage = nil
|
||||
|
||||
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(
|
||||
meeting: meeting,
|
||||
userBlurb: userBlurb,
|
||||
systemPrompt: systemPrompt
|
||||
)
|
||||
|
||||
saveMeeting()
|
||||
} catch {
|
||||
errorMessage = "Failed to generate notes: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
isGeneratingNotes = false
|
||||
}
|
||||
|
||||
func saveMeeting() {
|
||||
if isDeleted { return }
|
||||
print("💾 Saving meeting: \(meeting.id)")
|
||||
let success = LocalStorageManager.shared.saveMeeting(meeting)
|
||||
print("💾 Save result: \(success ? "SUCCESS" : "FAILED")")
|
||||
if success {
|
||||
NotificationCenter.default.post(name: .meetingSaved, object: meeting)
|
||||
}
|
||||
}
|
||||
|
||||
func copyTranscript() {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(meeting.formattedTranscript, forType: .string)
|
||||
}
|
||||
|
||||
func copyNotes() {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(meeting.generatedNotes, forType: .string)
|
||||
}
|
||||
|
||||
func copyCurrentTabContent() {
|
||||
NSPasteboard.general.clearContents()
|
||||
|
||||
let content: String
|
||||
switch selectedTab {
|
||||
case .myNotes:
|
||||
content = meeting.userNotes
|
||||
case .transcript:
|
||||
content = meeting.formattedTranscript
|
||||
case .enhancedNotes:
|
||||
content = meeting.generatedNotes
|
||||
}
|
||||
|
||||
NSPasteboard.general.setString(content, forType: .string)
|
||||
}
|
||||
|
||||
func deleteMeeting() {
|
||||
let success = LocalStorageManager.shared.deleteMeeting(meeting)
|
||||
if success {
|
||||
isDeleted = true
|
||||
NotificationCenter.default.post(name: .meetingDeleted, object: meeting)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteIfEmpty() {
|
||||
if isEmpty {
|
||||
print("🗑️ Auto-deleting empty meeting")
|
||||
deleteMeeting()
|
||||
} else {
|
||||
saveMeeting()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class SettingsViewModel: ObservableObject {
|
||||
@Published var settings = Settings()
|
||||
@Published var saveMessage = ""
|
||||
@Published var showingSaveMessage = false
|
||||
|
||||
init() {
|
||||
loadSettings()
|
||||
}
|
||||
|
||||
func loadSettings() {
|
||||
settings.openAIKey = KeychainHelper.shared.get(forKey: "openAIKey") ?? ""
|
||||
settings.userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? ""
|
||||
settings.systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt()
|
||||
}
|
||||
|
||||
func saveSettings() {
|
||||
let openAISaved = KeychainHelper.shared.save(settings.openAIKey, forKey: "openAIKey")
|
||||
let blurbSaved = KeychainHelper.shared.save(settings.userBlurb, forKey: "userBlurb")
|
||||
let promptSaved = KeychainHelper.shared.save(settings.systemPrompt, forKey: "systemPrompt")
|
||||
|
||||
if openAISaved && blurbSaved && promptSaved {
|
||||
saveMessage = "Settings saved successfully!"
|
||||
} else {
|
||||
saveMessage = "Error saving settings"
|
||||
}
|
||||
|
||||
showingSaveMessage = true
|
||||
|
||||
// Hide the message after 3 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
self.showingSaveMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
func resetToDefaults() {
|
||||
settings.systemPrompt = Settings.defaultSystemPrompt()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user