CommentWriterViewModel.swift 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import AppKit
  2. import SwiftUI
  3. @MainActor
  4. @Observable
  5. final class CommentWriterViewModel {
  6. var draft = CommentDraft()
  7. var selectedTab: CommentWriterTab = .compose
  8. var isGenerating = false
  9. var errorMessage: String?
  10. var successMessage: String?
  11. var variants: [CommentVariant] = []
  12. var selectedVariantID: UUID?
  13. var showOverwriteConfirmation = false
  14. private var hasPremiumAccess = false
  15. private let injectedGenerationService: (any CommentGenerationServiceProtocol)?
  16. private let emptyDraft = CommentDraft()
  17. init(
  18. hasPremiumAccess: Bool = false,
  19. generationService: (any CommentGenerationServiceProtocol)? = nil
  20. ) {
  21. self.hasPremiumAccess = hasPremiumAccess
  22. self.injectedGenerationService = generationService
  23. }
  24. func setPremiumAccess(_ hasPremiumAccess: Bool) {
  25. self.hasPremiumAccess = hasPremiumAccess
  26. }
  27. private var generationService: any CommentGenerationServiceProtocol {
  28. injectedGenerationService ?? CommentGenerationServiceFactory.make(hasPremiumAccess: hasPremiumAccess)
  29. }
  30. var formattedSubreddit: String {
  31. let name = PostDraftValidator.normalizedSubreddit(draft.subreddit)
  32. guard !name.isEmpty else { return "r/subreddit" }
  33. return "r/\(name)"
  34. }
  35. var usesLiveAI: Bool {
  36. AIConfiguration.usesLiveAI(hasPremiumAccess: hasPremiumAccess)
  37. }
  38. var hasDraftChanges: Bool {
  39. draft != emptyDraft
  40. }
  41. var canGenerate: Bool {
  42. CommentDraftValidator.canGenerate(from: draft, isGenerating: isGenerating)
  43. }
  44. var canExport: Bool {
  45. let body = activeCommentBody.trimmingCharacters(in: .whitespaces)
  46. guard !body.isEmpty, body.count <= CommentDraftValidator.maxCommentLength else { return false }
  47. return (try? CommentDraftValidator.validateForGeneration(from: draft)) != nil
  48. }
  49. var activeCommentBody: String {
  50. if let selectedID = selectedVariantID,
  51. let variant = variants.first(where: { $0.id == selectedID }) {
  52. return variant.body
  53. }
  54. return draft.body
  55. }
  56. var bestVariant: CommentVariant? {
  57. variants.max(by: { $0.score < $1.score })
  58. }
  59. var needsOverwriteConfirmation: Bool {
  60. !draft.body.trimmingCharacters(in: .whitespaces).isEmpty
  61. }
  62. func selectCommentType(_ type: CommentType) {
  63. draft.commentType = type
  64. if type == .topLevel {
  65. draft.parentComment = ""
  66. }
  67. clearMessages()
  68. }
  69. func selectVariant(_ variant: CommentVariant) {
  70. selectedVariantID = variant.id
  71. clearMessages()
  72. }
  73. func applyVariant(_ variant: CommentVariant) {
  74. draft.body = variant.body
  75. selectedVariantID = variant.id
  76. successMessage = "Variant applied."
  77. errorMessage = nil
  78. }
  79. func generateComment() async {
  80. guard canGenerate else { return }
  81. if needsOverwriteConfirmation {
  82. showOverwriteConfirmation = true
  83. return
  84. }
  85. await performGenerate(replaceExisting: true)
  86. }
  87. func confirmOverwriteAndGenerate() async {
  88. showOverwriteConfirmation = false
  89. await performGenerate(replaceExisting: true)
  90. }
  91. func cancelOverwriteConfirmation() {
  92. showOverwriteConfirmation = false
  93. }
  94. func copyToClipboard() {
  95. let content = activeCommentBody.trimmingCharacters(in: .whitespaces)
  96. guard !content.isEmpty else {
  97. errorMessage = CommentDraftValidationError.emptyComment.localizedDescription
  98. successMessage = nil
  99. return
  100. }
  101. guard content.count <= CommentDraftValidator.maxCommentLength else {
  102. errorMessage = CommentDraftValidationError.commentTooLong.localizedDescription
  103. successMessage = nil
  104. return
  105. }
  106. NSPasteboard.general.clearContents()
  107. NSPasteboard.general.setString(content, forType: .string)
  108. successMessage = "Comment copied to clipboard."
  109. errorMessage = nil
  110. }
  111. func resetDraft() {
  112. draft = CommentDraft()
  113. selectedTab = .compose
  114. variants = []
  115. selectedVariantID = nil
  116. clearMessages()
  117. }
  118. func clearMessages() {
  119. errorMessage = nil
  120. successMessage = nil
  121. }
  122. func notifyDraftEdited() {
  123. clearMessages()
  124. }
  125. private func performGenerate(replaceExisting: Bool) async {
  126. isGenerating = true
  127. errorMessage = nil
  128. successMessage = nil
  129. do {
  130. let result = try await generationService.generateComment(from: draft)
  131. if replaceExisting || draft.body.trimmingCharacters(in: .whitespaces).isEmpty {
  132. draft.body = String(result.body.prefix(CommentDraftValidator.maxCommentLength))
  133. }
  134. variants = result.variants
  135. selectedVariantID = result.variants.first?.id
  136. selectedTab = .preview
  137. let engine = usesLiveAI ? "AI" : "local AI templates"
  138. successMessage = "Generated comment with \(result.variants.count) variants using \(engine)."
  139. } catch {
  140. errorMessage = UserFacingError.message(for: error)
  141. }
  142. isGenerating = false
  143. }
  144. }