AIHistoryModels.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import Foundation
  2. enum AIHistoryToolKind: String, Codable, CaseIterable, Identifiable {
  3. case postGenerator
  4. case titleOptimizer
  5. case commentWriter
  6. var id: String { rawValue }
  7. var title: String {
  8. switch self {
  9. case .postGenerator: "Post Generator"
  10. case .titleOptimizer: "Title Optimizer"
  11. case .commentWriter: "Comment Writer"
  12. }
  13. }
  14. var iconKind: SidebarIconKind {
  15. switch self {
  16. case .postGenerator: .postGenerator
  17. case .titleOptimizer: .titleOptimizer
  18. case .commentWriter: .commentWriter
  19. }
  20. }
  21. }
  22. enum AIHistoryFilter: String, CaseIterable, Identifiable {
  23. case all
  24. case postGenerator
  25. case titleOptimizer
  26. case commentWriter
  27. var id: String { rawValue }
  28. var title: String {
  29. switch self {
  30. case .all: "All"
  31. case .postGenerator: "Posts"
  32. case .titleOptimizer: "Titles"
  33. case .commentWriter: "Comments"
  34. }
  35. }
  36. var toolKind: AIHistoryToolKind? {
  37. switch self {
  38. case .all: nil
  39. case .postGenerator: .postGenerator
  40. case .titleOptimizer: .titleOptimizer
  41. case .commentWriter: .commentWriter
  42. }
  43. }
  44. }
  45. struct StoredPollOption: Codable, Equatable {
  46. let id: UUID
  47. var text: String
  48. init(id: UUID = UUID(), text: String = "") {
  49. self.id = id
  50. self.text = text
  51. }
  52. init(from option: PollOption) {
  53. id = option.id
  54. text = option.text
  55. }
  56. var pollOption: PollOption {
  57. PollOption(id: id, text: text)
  58. }
  59. }
  60. struct StoredPostDraft: Codable, Equatable {
  61. var postType: RedditPostType
  62. var subreddit: String
  63. var topic: String
  64. var tone: PostTone
  65. var title: String
  66. var body: String
  67. var linkURL: String
  68. var videoURL: String
  69. var pollOptions: [StoredPollOption]
  70. var pollDuration: PollDuration
  71. var isNSFW: Bool
  72. var isSpoiler: Bool
  73. var isOC: Bool
  74. var flair: String
  75. init(from draft: PostDraft) {
  76. postType = draft.postType
  77. subreddit = draft.subreddit
  78. topic = draft.topic
  79. tone = draft.tone
  80. title = draft.title
  81. body = draft.body
  82. linkURL = draft.linkURL
  83. videoURL = draft.videoURL
  84. pollOptions = draft.pollOptions.map(StoredPollOption.init)
  85. pollDuration = draft.pollDuration
  86. isNSFW = draft.isNSFW
  87. isSpoiler = draft.isSpoiler
  88. isOC = draft.isOC
  89. flair = draft.flair
  90. }
  91. var postDraft: PostDraft {
  92. PostDraft(
  93. postType: postType,
  94. subreddit: subreddit,
  95. topic: topic,
  96. tone: tone,
  97. title: title,
  98. body: body,
  99. linkURL: linkURL,
  100. videoURL: videoURL,
  101. imageFileURL: nil,
  102. pollOptions: pollOptions.map(\.pollOption),
  103. pollDuration: pollDuration,
  104. isNSFW: isNSFW,
  105. isSpoiler: isSpoiler,
  106. isOC: isOC,
  107. flair: flair
  108. )
  109. }
  110. }
  111. struct StoredTitleDraft: Codable, Equatable {
  112. var subreddit: String
  113. var topic: String
  114. var tone: PostTone
  115. var titleGoal: TitleGoal
  116. var originalTitle: String
  117. var postType: RedditPostType
  118. var isNSFW: Bool
  119. var isSpoiler: Bool
  120. var isOC: Bool
  121. var flair: String
  122. var variantCount: TitleVariantCount
  123. init(from draft: TitleDraft) {
  124. subreddit = draft.subreddit
  125. topic = draft.topic
  126. tone = draft.tone
  127. titleGoal = draft.titleGoal
  128. originalTitle = draft.originalTitle
  129. postType = draft.postType
  130. isNSFW = draft.isNSFW
  131. isSpoiler = draft.isSpoiler
  132. isOC = draft.isOC
  133. flair = draft.flair
  134. variantCount = draft.variantCount
  135. }
  136. var titleDraft: TitleDraft {
  137. TitleDraft(
  138. subreddit: subreddit,
  139. topic: topic,
  140. tone: tone,
  141. titleGoal: titleGoal,
  142. originalTitle: originalTitle,
  143. postType: postType,
  144. isNSFW: isNSFW,
  145. isSpoiler: isSpoiler,
  146. isOC: isOC,
  147. flair: flair,
  148. variantCount: variantCount
  149. )
  150. }
  151. }
  152. struct StoredTitleVariant: Codable, Equatable {
  153. let id: UUID
  154. var title: String
  155. var score: Int
  156. var reasoning: String
  157. init(from variant: TitleVariant) {
  158. id = variant.id
  159. title = variant.title
  160. score = variant.score
  161. reasoning = variant.reasoning
  162. }
  163. var titleVariant: TitleVariant {
  164. TitleVariant(id: id, title: title, score: score, reasoning: reasoning)
  165. }
  166. }
  167. struct StoredTitleAnalysis: Codable, Equatable {
  168. var overallScore: Int
  169. var lengthScore: Int
  170. var engagementScore: Int
  171. var clarityScore: Int
  172. var suggestions: [String]
  173. init(from analysis: TitleAnalysis) {
  174. overallScore = analysis.overallScore
  175. lengthScore = analysis.lengthScore
  176. engagementScore = analysis.engagementScore
  177. clarityScore = analysis.clarityScore
  178. suggestions = analysis.suggestions
  179. }
  180. var titleAnalysis: TitleAnalysis {
  181. TitleAnalysis(
  182. overallScore: overallScore,
  183. lengthScore: lengthScore,
  184. engagementScore: engagementScore,
  185. clarityScore: clarityScore,
  186. suggestions: suggestions
  187. )
  188. }
  189. }
  190. struct StoredCommentDraft: Codable, Equatable {
  191. var commentType: CommentType
  192. var subreddit: String
  193. var postTitle: String
  194. var postBody: String
  195. var postURL: String
  196. var parentComment: String
  197. var topic: String
  198. var tone: PostTone
  199. var intent: CommentIntent
  200. var length: CommentLength
  201. var body: String
  202. var includeQuote: Bool
  203. var useMarkdown: Bool
  204. var variantCount: CommentVariantCount
  205. init(from draft: CommentDraft) {
  206. commentType = draft.commentType
  207. subreddit = draft.subreddit
  208. postTitle = draft.postTitle
  209. postBody = draft.postBody
  210. postURL = draft.postURL
  211. parentComment = draft.parentComment
  212. topic = draft.topic
  213. tone = draft.tone
  214. intent = draft.intent
  215. length = draft.length
  216. body = draft.body
  217. includeQuote = draft.includeQuote
  218. useMarkdown = draft.useMarkdown
  219. variantCount = draft.variantCount
  220. }
  221. var commentDraft: CommentDraft {
  222. CommentDraft(
  223. commentType: commentType,
  224. subreddit: subreddit,
  225. postTitle: postTitle,
  226. postBody: postBody,
  227. postURL: postURL,
  228. parentComment: parentComment,
  229. topic: topic,
  230. tone: tone,
  231. intent: intent,
  232. length: length,
  233. body: body,
  234. includeQuote: includeQuote,
  235. useMarkdown: useMarkdown,
  236. variantCount: variantCount
  237. )
  238. }
  239. }
  240. struct StoredCommentVariant: Codable, Equatable {
  241. let id: UUID
  242. var body: String
  243. var score: Int
  244. var reasoning: String
  245. init(from variant: CommentVariant) {
  246. id = variant.id
  247. body = variant.body
  248. score = variant.score
  249. reasoning = variant.reasoning
  250. }
  251. var commentVariant: CommentVariant {
  252. CommentVariant(id: id, body: body, score: score, reasoning: reasoning)
  253. }
  254. }
  255. enum AIHistoryPayload: Codable, Equatable {
  256. case postGenerator(StoredPostDraft)
  257. case titleOptimizer(
  258. draft: StoredTitleDraft,
  259. analysis: StoredTitleAnalysis?,
  260. variants: [StoredTitleVariant],
  261. selectedVariantID: UUID?
  262. )
  263. case commentWriter(
  264. draft: StoredCommentDraft,
  265. variants: [StoredCommentVariant],
  266. selectedVariantID: UUID?
  267. )
  268. }
  269. struct AIHistoryEntry: Identifiable, Codable, Equatable {
  270. let id: UUID
  271. let toolKind: AIHistoryToolKind
  272. let createdAt: Date
  273. let displayTitle: String
  274. let displaySubtitle: String
  275. let payload: AIHistoryPayload
  276. init(
  277. id: UUID = UUID(),
  278. toolKind: AIHistoryToolKind,
  279. createdAt: Date = .now,
  280. displayTitle: String,
  281. displaySubtitle: String,
  282. payload: AIHistoryPayload
  283. ) {
  284. self.id = id
  285. self.toolKind = toolKind
  286. self.createdAt = createdAt
  287. self.displayTitle = displayTitle
  288. self.displaySubtitle = displaySubtitle
  289. self.payload = payload
  290. }
  291. }
  292. extension AIHistoryEntry {
  293. static func fromPostGenerator(draft: PostDraft) -> AIHistoryEntry {
  294. let title = draft.title.trimmingCharacters(in: .whitespaces)
  295. let displayTitle = title.isEmpty ? "Untitled post" : title
  296. let subreddit = formattedSubredditName(draft.subreddit)
  297. return AIHistoryEntry(
  298. toolKind: .postGenerator,
  299. displayTitle: displayTitle,
  300. displaySubtitle: subreddit,
  301. payload: .postGenerator(StoredPostDraft(from: draft))
  302. )
  303. }
  304. static func fromTitleOptimizer(
  305. draft: TitleDraft,
  306. analysis: TitleAnalysis?,
  307. variants: [TitleVariant],
  308. selectedVariantID: UUID?
  309. ) -> AIHistoryEntry {
  310. let activeTitle: String
  311. if let selectedVariantID,
  312. let variant = variants.first(where: { $0.id == selectedVariantID }) {
  313. activeTitle = variant.title
  314. } else {
  315. activeTitle = draft.originalTitle
  316. }
  317. let title = activeTitle.trimmingCharacters(in: .whitespaces)
  318. let displayTitle = title.isEmpty ? "Untitled optimization" : title
  319. let subreddit = formattedSubredditName(draft.subreddit)
  320. return AIHistoryEntry(
  321. toolKind: .titleOptimizer,
  322. displayTitle: displayTitle,
  323. displaySubtitle: subreddit,
  324. payload: .titleOptimizer(
  325. draft: StoredTitleDraft(from: draft),
  326. analysis: analysis.map(StoredTitleAnalysis.init),
  327. variants: variants.map(StoredTitleVariant.init),
  328. selectedVariantID: selectedVariantID
  329. )
  330. )
  331. }
  332. static func fromCommentWriter(
  333. draft: CommentDraft,
  334. variants: [CommentVariant],
  335. selectedVariantID: UUID?
  336. ) -> AIHistoryEntry {
  337. let activeBody: String
  338. if let selectedVariantID,
  339. let variant = variants.first(where: { $0.id == selectedVariantID }) {
  340. activeBody = variant.body
  341. } else {
  342. activeBody = draft.body
  343. }
  344. let trimmed = activeBody.trimmingCharacters(in: .whitespacesAndNewlines)
  345. let displayTitle: String
  346. if trimmed.isEmpty {
  347. displayTitle = "Untitled comment"
  348. } else {
  349. let firstLine = trimmed.split(separator: "\n", maxSplits: 1).first.map(String.init) ?? trimmed
  350. displayTitle = firstLine.count > 80 ? String(firstLine.prefix(77)) + "…" : firstLine
  351. }
  352. let subreddit = formattedSubredditName(draft.subreddit)
  353. return AIHistoryEntry(
  354. toolKind: .commentWriter,
  355. displayTitle: displayTitle,
  356. displaySubtitle: subreddit,
  357. payload: .commentWriter(
  358. draft: StoredCommentDraft(from: draft),
  359. variants: variants.map(StoredCommentVariant.init),
  360. selectedVariantID: selectedVariantID
  361. )
  362. )
  363. }
  364. private static func formattedSubredditName(_ subreddit: String) -> String {
  365. let name = PostDraftValidator.normalizedSubreddit(subreddit)
  366. guard !name.isEmpty else { return "r/subreddit" }
  367. return "r/\(name)"
  368. }
  369. }