feat: note templates

This commit is contained in:
Owen Gretzinger
2025-07-13 00:04:14 -04:00
parent 05196ebe71
commit 8b1416d7e6
16 changed files with 856 additions and 153 deletions
+43 -19
View File
@@ -29,6 +29,8 @@ class MeetingViewModel: ObservableObject {
@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 var cancellables = Set<AnyCancellable>()
@@ -62,6 +64,20 @@ class MeetingViewModel: ObservableObject {
selectedTab = .transcript
}
// Load templates and selected template
loadTemplates()
// Observe template selection: save to meeting and regenerate notes on changes (skip initial)
$selectedTemplateId
.dropFirst()
.sink { [weak self] newTemplateId in
guard let self = self else { return }
self.meeting.templateId = newTemplateId
Task {
await self.generateNotes()
}
}
.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
@@ -144,6 +160,17 @@ class MeetingViewModel: ObservableObject {
}
}
func loadTemplates() {
templates = LocalStorageManager.shared.loadTemplates()
// Load per-meeting template or default to Standard Meeting
if let meetingTemplateId = meeting.templateId {
selectedTemplateId = meetingTemplateId
} else if let defaultTemplate = templates.first(where: { $0.title == "Standard Meeting" }) {
selectedTemplateId = defaultTemplate.id
}
}
func generateNotes() async {
isGeneratingNotes = true
errorMessage = nil
@@ -151,27 +178,24 @@ class MeetingViewModel: ObservableObject {
// 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()
// 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)"
// Load settings for generation
let userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? ""
let systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt()
// Use streaming generation
let stream = NotesGenerator.shared.generateNotesStream(
meeting: meeting,
userBlurb: userBlurb,
systemPrompt: systemPrompt,
templateId: selectedTemplateId
)
for await chunk in stream {
meeting.generatedNotes += chunk
}
saveMeeting()
isGeneratingNotes = false
}
+61 -12
View File
@@ -5,33 +5,82 @@ class SettingsViewModel: ObservableObject {
@Published var settings = Settings()
@Published var saveMessage = ""
@Published var showingSaveMessage = false
@Published var templates: [NoteTemplate] = []
init() {
loadSettings()
loadTemplates()
}
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()
// Load selected template ID
if let templateIdString = KeychainHelper.shared.get(forKey: "selectedTemplateId"),
let templateId = UUID(uuidString: templateIdString) {
settings.selectedTemplateId = templateId
}
}
func saveSettings() {
func loadTemplates() {
templates = LocalStorageManager.shared.loadTemplates()
// Validate that the selected template still exists
if let selectedId = settings.selectedTemplateId {
if !templates.contains(where: { $0.id == selectedId }) {
// Selected template was deleted, clear the selection
settings.selectedTemplateId = nil
}
}
// If no template is selected, select the first default template
if settings.selectedTemplateId == nil {
if let defaultTemplate = templates.first(where: { $0.title == "Standard Meeting" }) {
settings.selectedTemplateId = defaultTemplate.id
} else if let firstTemplate = templates.first {
// Fallback to first available template
settings.selectedTemplateId = firstTemplate.id
}
}
}
func saveSettings(showMessage: Bool = true) {
// Validate that systemPrompt contains all required template placeholders
let requiredKeys = ["meeting_title", "meeting_date", "transcript", "user_blurb", "user_notes", "template_content"]
let missing = requiredKeys.filter { !settings.systemPrompt.contains("{{\($0)}}") }
if !missing.isEmpty {
if showMessage {
saveMessage = "Cannot save settings: missing placeholders \(missing.map { "{{\($0)}}" }.joined(separator: ", ")) in system prompt"
showingSaveMessage = true
}
return
}
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"
// Save selected template ID
var templateIdSaved = true
if let templateId = settings.selectedTemplateId {
templateIdSaved = KeychainHelper.shared.save(templateId.uuidString, forKey: "selectedTemplateId")
}
showingSaveMessage = true
// Hide the message after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showingSaveMessage = false
if showMessage {
if openAISaved && blurbSaved && promptSaved && templateIdSaved {
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
}
}
}
@@ -0,0 +1,45 @@
import Foundation
import SwiftUI
@MainActor
class TemplatesViewModel: ObservableObject {
@Published var templates: [NoteTemplate] = []
@Published var isLoading = false
@Published var errorMessage: String?
init() {
loadTemplates()
}
func loadTemplates() {
isLoading = true
templates = LocalStorageManager.shared.loadTemplates()
isLoading = false
}
func saveTemplate(_ template: NoteTemplate) {
if LocalStorageManager.shared.saveTemplate(template) {
loadTemplates()
} else {
errorMessage = "Failed to save template"
}
}
func deleteTemplate(_ template: NoteTemplate) {
if LocalStorageManager.shared.deleteTemplate(template) {
loadTemplates()
} else {
errorMessage = "Cannot delete default templates"
}
}
func createNewTemplate() -> NoteTemplate {
return NoteTemplate(
title: "New Template",
context: "",
sections: [
TemplateSection(title: "Section 1", description: "Description of section 1")
]
)
}
}