TitleOptimizerViewModel.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import AppKit
  2. import SwiftUI
  3. @MainActor
  4. @Observable
  5. final class TitleOptimizerViewModel {
  6. var draft = TitleDraft()
  7. var selectedTab: TitleOptimizerTab = .optimize
  8. var isOptimizing = false
  9. var errorMessage: String?
  10. var successMessage: String?
  11. var analysis: TitleAnalysis?
  12. var variants: [TitleVariant] = []
  13. var selectedVariantID: UUID?
  14. private let optimizationService: any TitleOptimizationServiceProtocol
  15. init(optimizationService: any TitleOptimizationServiceProtocol = MockTitleOptimizationService()) {
  16. self.optimizationService = optimizationService
  17. }
  18. var formattedSubreddit: String {
  19. let trimmed = draft.subreddit.trimmingCharacters(in: .whitespaces)
  20. guard !trimmed.isEmpty else { return "r/subreddit" }
  21. return trimmed.hasPrefix("r/") ? trimmed : "r/\(trimmed)"
  22. }
  23. var canOptimize: Bool {
  24. !draft.topic.trimmingCharacters(in: .whitespaces).isEmpty
  25. && !draft.subreddit.trimmingCharacters(in: .whitespaces).isEmpty
  26. && !draft.originalTitle.trimmingCharacters(in: .whitespaces).isEmpty
  27. && !isOptimizing
  28. }
  29. var activeTitle: String {
  30. if let selectedID = selectedVariantID,
  31. let variant = variants.first(where: { $0.id == selectedID }) {
  32. return variant.title
  33. }
  34. return draft.originalTitle
  35. }
  36. var bestVariant: TitleVariant? {
  37. variants.max(by: { $0.score < $1.score })
  38. }
  39. var previewDraft: PostDraft {
  40. PostDraft(
  41. postType: draft.postType,
  42. subreddit: draft.subreddit,
  43. topic: draft.topic,
  44. tone: draft.tone,
  45. title: activeTitle,
  46. isNSFW: draft.isNSFW,
  47. isSpoiler: draft.isSpoiler,
  48. isOC: draft.isOC,
  49. flair: draft.flair
  50. )
  51. }
  52. func selectPostType(_ type: RedditPostType) {
  53. draft.postType = type
  54. clearMessages()
  55. }
  56. func selectVariant(_ variant: TitleVariant) {
  57. selectedVariantID = variant.id
  58. clearMessages()
  59. }
  60. func applyVariant(_ variant: TitleVariant) {
  61. draft.originalTitle = variant.title
  62. selectedVariantID = variant.id
  63. successMessage = "Title applied."
  64. errorMessage = nil
  65. }
  66. func optimizeTitles() async {
  67. guard canOptimize else { return }
  68. isOptimizing = true
  69. errorMessage = nil
  70. successMessage = nil
  71. do {
  72. let result = try await optimizationService.optimizeTitles(from: draft)
  73. analysis = result.analysis
  74. variants = result.variants
  75. selectedVariantID = result.variants.first?.id
  76. successMessage = "Generated \(result.variants.count) title suggestions."
  77. } catch {
  78. errorMessage = error.localizedDescription
  79. }
  80. isOptimizing = false
  81. }
  82. func copyToClipboard() {
  83. let title = activeTitle.trimmingCharacters(in: .whitespaces)
  84. guard !title.isEmpty else { return }
  85. NSPasteboard.general.clearContents()
  86. NSPasteboard.general.setString(title, forType: .string)
  87. successMessage = "Copied to clipboard."
  88. errorMessage = nil
  89. }
  90. func resetDraft() {
  91. draft = TitleDraft()
  92. selectedTab = .optimize
  93. analysis = nil
  94. variants = []
  95. selectedVariantID = nil
  96. clearMessages()
  97. }
  98. func exportText() -> String {
  99. var lines: [String] = []
  100. lines.append("Subreddit: \(formattedSubreddit)")
  101. lines.append("Topic: \(draft.topic)")
  102. lines.append("Goal: \(draft.titleGoal.title)")
  103. lines.append("Tone: \(draft.tone.title)")
  104. lines.append("")
  105. lines.append("Original: \(draft.originalTitle)")
  106. if let analysis {
  107. lines.append("")
  108. lines.append("Analysis Score: \(analysis.overallScore)/100")
  109. lines.append(" Length: \(analysis.lengthScore)")
  110. lines.append(" Engagement: \(analysis.engagementScore)")
  111. lines.append(" Clarity: \(analysis.clarityScore)")
  112. }
  113. if !variants.isEmpty {
  114. lines.append("")
  115. lines.append("Suggestions:")
  116. for (index, variant) in variants.enumerated() {
  117. lines.append("\(index + 1). [\(variant.score)] \(variant.title)")
  118. }
  119. }
  120. return lines.joined(separator: "\n")
  121. }
  122. private func clearMessages() {
  123. errorMessage = nil
  124. successMessage = nil
  125. }
  126. }