import Foundation enum AIHistoryToolKind: String, Codable, CaseIterable, Identifiable { case postGenerator case titleOptimizer case commentWriter var id: String { rawValue } var title: String { switch self { case .postGenerator: "Post Generator" case .titleOptimizer: "Title Optimizer" case .commentWriter: "Comment Writer" } } var iconKind: SidebarIconKind { switch self { case .postGenerator: .postGenerator case .titleOptimizer: .titleOptimizer case .commentWriter: .commentWriter } } } enum AIHistoryFilter: String, CaseIterable, Identifiable { case all case postGenerator case titleOptimizer case commentWriter var id: String { rawValue } var title: String { switch self { case .all: "All" case .postGenerator: "Posts" case .titleOptimizer: "Titles" case .commentWriter: "Comments" } } var toolKind: AIHistoryToolKind? { switch self { case .all: nil case .postGenerator: .postGenerator case .titleOptimizer: .titleOptimizer case .commentWriter: .commentWriter } } } struct StoredPollOption: Codable, Equatable { let id: UUID var text: String init(id: UUID = UUID(), text: String = "") { self.id = id self.text = text } init(from option: PollOption) { id = option.id text = option.text } var pollOption: PollOption { PollOption(id: id, text: text) } } struct StoredPostDraft: Codable, Equatable { var postType: RedditPostType var subreddit: String var topic: String var tone: PostTone var title: String var body: String var linkURL: String var videoURL: String var pollOptions: [StoredPollOption] var pollDuration: PollDuration var isNSFW: Bool var isSpoiler: Bool var isOC: Bool var flair: String init(from draft: PostDraft) { postType = draft.postType subreddit = draft.subreddit topic = draft.topic tone = draft.tone title = draft.title body = draft.body linkURL = draft.linkURL videoURL = draft.videoURL pollOptions = draft.pollOptions.map(StoredPollOption.init) pollDuration = draft.pollDuration isNSFW = draft.isNSFW isSpoiler = draft.isSpoiler isOC = draft.isOC flair = draft.flair } var postDraft: PostDraft { PostDraft( postType: postType, subreddit: subreddit, topic: topic, tone: tone, title: title, body: body, linkURL: linkURL, videoURL: videoURL, imageFileURL: nil, pollOptions: pollOptions.map(\.pollOption), pollDuration: pollDuration, isNSFW: isNSFW, isSpoiler: isSpoiler, isOC: isOC, flair: flair ) } } struct StoredTitleDraft: Codable, Equatable { var subreddit: String var topic: String var tone: PostTone var titleGoal: TitleGoal var originalTitle: String var postType: RedditPostType var isNSFW: Bool var isSpoiler: Bool var isOC: Bool var flair: String var variantCount: TitleVariantCount init(from draft: TitleDraft) { subreddit = draft.subreddit topic = draft.topic tone = draft.tone titleGoal = draft.titleGoal originalTitle = draft.originalTitle postType = draft.postType isNSFW = draft.isNSFW isSpoiler = draft.isSpoiler isOC = draft.isOC flair = draft.flair variantCount = draft.variantCount } var titleDraft: TitleDraft { TitleDraft( subreddit: subreddit, topic: topic, tone: tone, titleGoal: titleGoal, originalTitle: originalTitle, postType: postType, isNSFW: isNSFW, isSpoiler: isSpoiler, isOC: isOC, flair: flair, variantCount: variantCount ) } } struct StoredTitleVariant: Codable, Equatable { let id: UUID var title: String var score: Int var reasoning: String init(from variant: TitleVariant) { id = variant.id title = variant.title score = variant.score reasoning = variant.reasoning } var titleVariant: TitleVariant { TitleVariant(id: id, title: title, score: score, reasoning: reasoning) } } struct StoredTitleAnalysis: Codable, Equatable { var overallScore: Int var lengthScore: Int var engagementScore: Int var clarityScore: Int var suggestions: [String] init(from analysis: TitleAnalysis) { overallScore = analysis.overallScore lengthScore = analysis.lengthScore engagementScore = analysis.engagementScore clarityScore = analysis.clarityScore suggestions = analysis.suggestions } var titleAnalysis: TitleAnalysis { TitleAnalysis( overallScore: overallScore, lengthScore: lengthScore, engagementScore: engagementScore, clarityScore: clarityScore, suggestions: suggestions ) } } struct StoredCommentDraft: Codable, Equatable { var commentType: CommentType var subreddit: String var postTitle: String var postBody: String var postURL: String var parentComment: String var topic: String var tone: PostTone var intent: CommentIntent var length: CommentLength var body: String var includeQuote: Bool var useMarkdown: Bool var variantCount: CommentVariantCount init(from draft: CommentDraft) { commentType = draft.commentType subreddit = draft.subreddit postTitle = draft.postTitle postBody = draft.postBody postURL = draft.postURL parentComment = draft.parentComment topic = draft.topic tone = draft.tone intent = draft.intent length = draft.length body = draft.body includeQuote = draft.includeQuote useMarkdown = draft.useMarkdown variantCount = draft.variantCount } var commentDraft: CommentDraft { CommentDraft( commentType: commentType, subreddit: subreddit, postTitle: postTitle, postBody: postBody, postURL: postURL, parentComment: parentComment, topic: topic, tone: tone, intent: intent, length: length, body: body, includeQuote: includeQuote, useMarkdown: useMarkdown, variantCount: variantCount ) } } struct StoredCommentVariant: Codable, Equatable { let id: UUID var body: String var score: Int var reasoning: String init(from variant: CommentVariant) { id = variant.id body = variant.body score = variant.score reasoning = variant.reasoning } var commentVariant: CommentVariant { CommentVariant(id: id, body: body, score: score, reasoning: reasoning) } } enum AIHistoryPayload: Codable, Equatable { case postGenerator(StoredPostDraft) case titleOptimizer( draft: StoredTitleDraft, analysis: StoredTitleAnalysis?, variants: [StoredTitleVariant], selectedVariantID: UUID? ) case commentWriter( draft: StoredCommentDraft, variants: [StoredCommentVariant], selectedVariantID: UUID? ) } struct AIHistoryEntry: Identifiable, Codable, Equatable { let id: UUID let toolKind: AIHistoryToolKind let createdAt: Date let displayTitle: String let displaySubtitle: String let payload: AIHistoryPayload init( id: UUID = UUID(), toolKind: AIHistoryToolKind, createdAt: Date = .now, displayTitle: String, displaySubtitle: String, payload: AIHistoryPayload ) { self.id = id self.toolKind = toolKind self.createdAt = createdAt self.displayTitle = displayTitle self.displaySubtitle = displaySubtitle self.payload = payload } } extension AIHistoryEntry { static func fromPostGenerator(draft: PostDraft) -> AIHistoryEntry { let title = draft.title.trimmingCharacters(in: .whitespaces) let displayTitle = title.isEmpty ? "Untitled post" : title let subreddit = formattedSubredditName(draft.subreddit) return AIHistoryEntry( toolKind: .postGenerator, displayTitle: displayTitle, displaySubtitle: subreddit, payload: .postGenerator(StoredPostDraft(from: draft)) ) } static func fromTitleOptimizer( draft: TitleDraft, analysis: TitleAnalysis?, variants: [TitleVariant], selectedVariantID: UUID? ) -> AIHistoryEntry { let activeTitle: String if let selectedVariantID, let variant = variants.first(where: { $0.id == selectedVariantID }) { activeTitle = variant.title } else { activeTitle = draft.originalTitle } let title = activeTitle.trimmingCharacters(in: .whitespaces) let displayTitle = title.isEmpty ? "Untitled optimization" : title let subreddit = formattedSubredditName(draft.subreddit) return AIHistoryEntry( toolKind: .titleOptimizer, displayTitle: displayTitle, displaySubtitle: subreddit, payload: .titleOptimizer( draft: StoredTitleDraft(from: draft), analysis: analysis.map(StoredTitleAnalysis.init), variants: variants.map(StoredTitleVariant.init), selectedVariantID: selectedVariantID ) ) } static func fromCommentWriter( draft: CommentDraft, variants: [CommentVariant], selectedVariantID: UUID? ) -> AIHistoryEntry { let activeBody: String if let selectedVariantID, let variant = variants.first(where: { $0.id == selectedVariantID }) { activeBody = variant.body } else { activeBody = draft.body } let trimmed = activeBody.trimmingCharacters(in: .whitespacesAndNewlines) let displayTitle: String if trimmed.isEmpty { displayTitle = "Untitled comment" } else { let firstLine = trimmed.split(separator: "\n", maxSplits: 1).first.map(String.init) ?? trimmed displayTitle = firstLine.count > 80 ? String(firstLine.prefix(77)) + "…" : firstLine } let subreddit = formattedSubredditName(draft.subreddit) return AIHistoryEntry( toolKind: .commentWriter, displayTitle: displayTitle, displaySubtitle: subreddit, payload: .commentWriter( draft: StoredCommentDraft(from: draft), variants: variants.map(StoredCommentVariant.init), selectedVariantID: selectedVariantID ) ) } private static func formattedSubredditName(_ subreddit: String) -> String { let name = PostDraftValidator.normalizedSubreddit(subreddit) guard !name.isEmpty else { return "r/subreddit" } return "r/\(name)" } }