EmailWriterViewModel.swift 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import AppKit
  2. import Combine
  3. import Foundation
  4. @MainActor
  5. final class EmailWriterViewModel: ObservableObject {
  6. static let maxWords = 5_000
  7. @Published var subject: String = ""
  8. @Published var prompt: String = ""
  9. @Published var generatedEmail: String = ""
  10. @Published var emailType: EmailType = .followUp
  11. @Published var tone: EmailTone = .professional
  12. @Published private(set) var isGenerating = false
  13. @Published private(set) var errorMessage: String?
  14. private let emailWriterService: any EmailWriterServicing
  15. private let historyStorage: HistoryStoring
  16. init(
  17. emailWriterService: any EmailWriterServicing = EmailWriterService(),
  18. historyStorage: HistoryStoring = HistoryStorageService.shared
  19. ) {
  20. self.emailWriterService = emailWriterService
  21. self.historyStorage = historyStorage
  22. }
  23. var wordCountLimitLabel: String {
  24. "\(prompt.wordCount)/\(Self.maxWords)"
  25. }
  26. var canGenerate: Bool {
  27. !prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isGenerating
  28. }
  29. var canCopy: Bool {
  30. !generatedEmail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
  31. }
  32. func pastePrompt() {
  33. guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
  34. prompt = clipboard.truncated(toMaxWords: Self.maxWords)
  35. generatedEmail = ""
  36. errorMessage = nil
  37. }
  38. func clearAll() {
  39. subject = ""
  40. prompt = ""
  41. generatedEmail = ""
  42. errorMessage = nil
  43. }
  44. func copyGeneratedEmail() {
  45. guard canCopy else { return }
  46. NSPasteboard.general.clearContents()
  47. NSPasteboard.general.setString(generatedEmail, forType: .string)
  48. }
  49. func generateEmail() {
  50. let trimmedPrompt = prompt.trimmingCharacters(in: .whitespacesAndNewlines)
  51. guard !trimmedPrompt.isEmpty else { return }
  52. isGenerating = true
  53. generatedEmail = ""
  54. errorMessage = nil
  55. let subjectLine = subject.trimmingCharacters(in: .whitespacesAndNewlines)
  56. Task {
  57. do {
  58. let email = try await emailWriterService.generateEmail(
  59. prompt: trimmedPrompt,
  60. subject: subjectLine,
  61. emailType: emailType,
  62. tone: tone
  63. )
  64. generatedEmail = email
  65. saveToHistory(
  66. subject: subjectLine,
  67. prompt: trimmedPrompt,
  68. generatedEmail: email
  69. )
  70. } catch {
  71. errorMessage = error.localizedDescription
  72. }
  73. isGenerating = false
  74. }
  75. }
  76. func restore(from entry: HistoryEntry) {
  77. guard case .emailWriter(let data) = entry.payload else { return }
  78. subject = data.subject
  79. prompt = data.prompt
  80. generatedEmail = data.generatedEmail
  81. emailType = data.emailType
  82. tone = data.tone
  83. errorMessage = nil
  84. }
  85. private func saveToHistory(subject: String, prompt: String, generatedEmail: String) {
  86. let payload = HistoryPayload.emailWriter(
  87. EmailWriterHistoryData(
  88. subject: subject,
  89. prompt: prompt,
  90. generatedEmail: generatedEmail,
  91. emailType: emailType,
  92. tone: tone
  93. )
  94. )
  95. let titleSource = subject.isEmpty ? prompt : subject
  96. let entry = HistoryEntry(
  97. tool: .aiEmailWriter,
  98. title: HistoryHelpers.title(from: titleSource, fallback: "Generated Email"),
  99. preview: HistoryHelpers.preview(from: generatedEmail),
  100. payload: payload
  101. )
  102. historyStorage.save(entry)
  103. }
  104. }