ソースを参照

Add Title Optimizer tool with analysis, variants, and preview workflow.

Gives users a dedicated page to improve Reddit titles with scored suggestions, side-by-side comparison, and feed preview—matching the Post Generator experience.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 ヶ月 前
コミット
b9427686b1

+ 101 - 0
Reddit App/Models/TitleOptimizerModels.swift

@@ -0,0 +1,101 @@
+import Foundation
+
+enum TitleOptimizerTab: String, CaseIterable, Identifiable {
+    case optimize
+    case compare
+    case preview
+
+    var id: String { rawValue }
+
+    var title: String {
+        switch self {
+        case .optimize: "Optimize"
+        case .compare: "Compare"
+        case .preview: "Preview"
+        }
+    }
+}
+
+enum TitleGoal: String, CaseIterable, Identifiable {
+    case curiosity
+    case question
+    case listicle
+    case emotional
+    case direct
+    case provocative
+
+    var id: String { rawValue }
+
+    var title: String {
+        switch self {
+        case .curiosity: "Curiosity"
+        case .question: "Question"
+        case .listicle: "Listicle"
+        case .emotional: "Emotional"
+        case .direct: "Direct"
+        case .provocative: "Bold"
+        }
+    }
+
+    var subtitle: String {
+        switch self {
+        case .curiosity: "Create intrigue"
+        case .question: "Ask the community"
+        case .listicle: "Numbers & lists"
+        case .emotional: "Personal & relatable"
+        case .direct: "Clear & concise"
+        case .provocative: "Spark debate"
+        }
+    }
+}
+
+enum TitleVariantCount: Int, CaseIterable, Identifiable {
+    case three = 3
+    case five = 5
+    case eight = 8
+
+    var id: Int { rawValue }
+
+    var label: String { "\(rawValue) titles" }
+}
+
+struct TitleDraft: Equatable {
+    var subreddit: String = ""
+    var topic: String = ""
+    var tone: PostTone = .casual
+    var titleGoal: TitleGoal = .curiosity
+    var originalTitle: String = ""
+    var postType: RedditPostType = .text
+    var isNSFW: Bool = false
+    var isSpoiler: Bool = false
+    var isOC: Bool = false
+    var flair: String = ""
+    var variantCount: TitleVariantCount = .five
+}
+
+struct TitleVariant: Identifiable, Equatable {
+    let id: UUID
+    var title: String
+    var score: Int
+    var reasoning: String
+
+    init(id: UUID = UUID(), title: String, score: Int, reasoning: String) {
+        self.id = id
+        self.title = title
+        self.score = score
+        self.reasoning = reasoning
+    }
+}
+
+struct TitleAnalysis: Equatable {
+    var overallScore: Int
+    var lengthScore: Int
+    var engagementScore: Int
+    var clarityScore: Int
+    var suggestions: [String]
+}
+
+struct TitleOptimizationResult: Equatable {
+    var analysis: TitleAnalysis
+    var variants: [TitleVariant]
+}

+ 262 - 0
Reddit App/Services/TitleOptimizationService.swift

@@ -0,0 +1,262 @@
+import Foundation
+
+protocol TitleOptimizationServiceProtocol: Sendable {
+    func optimizeTitles(from draft: TitleDraft) async throws -> TitleOptimizationResult
+}
+
+enum TitleOptimizationError: LocalizedError {
+    case emptyTopic
+    case emptySubreddit
+    case emptyTitle
+
+    var errorDescription: String? {
+        switch self {
+        case .emptyTopic: "Enter a topic or keywords to guide title generation."
+        case .emptySubreddit: "Enter a subreddit to tailor titles for that community."
+        case .emptyTitle: "Enter a title to optimize, or provide a topic to generate from scratch."
+        }
+    }
+}
+
+struct MockTitleOptimizationService: TitleOptimizationServiceProtocol {
+    func optimizeTitles(from draft: TitleDraft) async throws -> TitleOptimizationResult {
+        guard !draft.subreddit.trimmingCharacters(in: .whitespaces).isEmpty else {
+            throw TitleOptimizationError.emptySubreddit
+        }
+        guard !draft.topic.trimmingCharacters(in: .whitespaces).isEmpty else {
+            throw TitleOptimizationError.emptyTopic
+        }
+
+        let hasTitle = !draft.originalTitle.trimmingCharacters(in: .whitespaces).isEmpty
+        if !hasTitle {
+            throw TitleOptimizationError.emptyTitle
+        }
+
+        try await Task.sleep(for: .milliseconds(900))
+
+        let subreddit = draft.subreddit.replacingOccurrences(of: "r/", with: "")
+        let topic = draft.topic.trimmingCharacters(in: .whitespaces)
+        let original = draft.originalTitle.trimmingCharacters(in: .whitespaces)
+
+        let analysis = analyzeTitle(original, goal: draft.titleGoal)
+        let variants = makeVariants(
+            original: original,
+            topic: topic,
+            subreddit: subreddit,
+            tone: draft.tone,
+            goal: draft.titleGoal,
+            count: draft.variantCount.rawValue
+        )
+
+        return TitleOptimizationResult(analysis: analysis, variants: variants)
+    }
+
+    private func analyzeTitle(_ title: String, goal: TitleGoal) -> TitleAnalysis {
+        let length = title.count
+        let lengthScore = scoreLength(length)
+        let engagementScore = scoreEngagement(title, goal: goal)
+        let clarityScore = scoreClarity(title)
+        let overall = (lengthScore + engagementScore + clarityScore) / 3
+        let suggestions = makeSuggestions(title: title, length: length, goal: goal)
+
+        return TitleAnalysis(
+            overallScore: overall,
+            lengthScore: lengthScore,
+            engagementScore: engagementScore,
+            clarityScore: clarityScore,
+            suggestions: suggestions
+        )
+    }
+
+    private func scoreLength(_ length: Int) -> Int {
+        switch length {
+        case 0: 0
+        case 1...50: 95
+        case 51...80: 85
+        case 81...120: 70
+        case 121...200: 50
+        case 201...300: 30
+        default: 10
+        }
+    }
+
+    private func scoreEngagement(_ title: String, goal: TitleGoal) -> Int {
+        var score = 50
+        let lower = title.lowercased()
+
+        if title.contains("?") { score += 10 }
+        if title.range(of: #"\d+"#, options: .regularExpression) != nil { score += 8 }
+        if lower.contains("how") || lower.contains("why") || lower.contains("what") { score += 6 }
+        if lower.contains("you") || lower.contains("your") { score += 5 }
+        if title == title.uppercased() && title.count > 5 { score -= 15 }
+
+        switch goal {
+        case .question where !title.contains("?"): score -= 10
+        case .listicle where title.range(of: #"\d+"#, options: .regularExpression) == nil: score -= 8
+        case .curiosity where !lower.contains("secret") && !lower.contains("nobody") && !lower.contains("actually"): score -= 3
+        default: break
+        }
+
+        return min(100, max(0, score))
+    }
+
+    private func scoreClarity(_ title: String) -> Int {
+        var score = 70
+        let words = title.split(separator: " ").count
+
+        if words >= 4 && words <= 12 { score += 15 }
+        if words < 3 { score -= 20 }
+        if words > 20 { score -= 15 }
+        if title.contains("  ") { score -= 5 }
+        if title.hasPrefix(" ") || title.hasSuffix(" ") { score -= 10 }
+
+        return min(100, max(0, score))
+    }
+
+    private func makeSuggestions(title: String, length: Int, goal: TitleGoal) -> [String] {
+        var tips: [String] = []
+
+        if length > 120 {
+            tips.append("Shorten to under 80 characters for better mobile visibility.")
+        } else if length < 20 {
+            tips.append("Add more context — very short titles can feel vague in feeds.")
+        }
+
+        if !title.contains("?") && goal == .question {
+            tips.append("Reframe as a question to invite comments.")
+        }
+
+        if title.range(of: #"\d+"#, options: .regularExpression) == nil && goal == .listicle {
+            tips.append("Include a number (e.g. \"5 tips\") for listicle-style engagement.")
+        }
+
+        let lower = title.lowercased()
+        if goal == .curiosity && !lower.contains("?") {
+            tips.append("Add a curiosity hook — hint at a surprising outcome.")
+        }
+
+        if tips.isEmpty {
+            tips.append("Strong base title — try variants with different hooks below.")
+        }
+
+        return tips
+    }
+
+    private func makeVariants(
+        original: String,
+        topic: String,
+        subreddit: String,
+        tone: PostTone,
+        goal: TitleGoal,
+        count: Int
+    ) -> [TitleVariant] {
+        let templates = variantTemplates(
+            original: original,
+            topic: topic,
+            subreddit: subreddit,
+            tone: tone,
+            goal: goal
+        )
+
+        return Array(templates.prefix(count)).enumerated().map { index, template in
+            TitleVariant(
+                title: template.title,
+                score: template.score - index * 2,
+                reasoning: template.reasoning
+            )
+        }
+    }
+
+    private func variantTemplates(
+        original: String,
+        topic: String,
+        subreddit: String,
+        tone: PostTone,
+        goal: TitleGoal
+    ) -> [(title: String, score: Int, reasoning: String)] {
+        let base = original.isEmpty ? topic : original
+
+        var results: [(title: String, score: Int, reasoning: String)] = []
+
+        switch goal {
+        case .curiosity:
+            results = [
+                ("The \(topic) trick nobody on r/\(subreddit) talks about", 92, "Curiosity gap with community reference"),
+                ("I tried \(base) for 30 days — here's what actually happened", 89, "Personal experiment hook"),
+                ("Why \(topic) is quietly changing everything (and most people miss it)", 87, "Insider knowledge angle"),
+                ("\(base) — am I the only one who noticed this?", 84, "Relatability + intrigue"),
+                ("What r/\(subreddit) taught me about \(topic) that I wish I knew sooner", 82, "Community wisdom framing"),
+                ("The uncomfortable truth about \(topic)", 80, "Bold curiosity opener"),
+                ("\(base): a deep dive you didn't know you needed", 78, "Value promise"),
+                ("This \(topic) approach changed my mind completely", 76, "Transformation narrative"),
+            ]
+        case .question:
+            results = [
+                ("What's your experience with \(topic)?", 91, "Direct community question"),
+                ("Am I wrong about \(base)?", 88, "Invites debate and replies"),
+                ("How do you approach \(topic) on r/\(subreddit)?", 86, "Subreddit-specific ask"),
+                ("Is \(topic) actually worth it in 2026?", 84, "Timely relevance question"),
+                ("Anyone else struggling with \(base)?", 82, "Shared struggle hook"),
+                ("What's the best \(topic) advice you've ever received?", 80, "Experience-sharing prompt"),
+                ("\(base) — what am I missing here?", 78, "Humble knowledge seeker"),
+                ("Hot take or common knowledge: \(topic)?", 76, "Opinion solicitation"),
+            ]
+        case .listicle:
+            results = [
+                ("7 \(topic) tips that actually work on r/\(subreddit)", 93, "Numbered list with community anchor"),
+                ("5 mistakes everyone makes with \(base)", 90, "Problem-awareness listicle"),
+                ("3 reasons \(topic) is underrated right now", 87, "Concise numbered hook"),
+                ("10 things I learned about \(topic) the hard way", 85, "Lessons-learned format"),
+                ("The top 5 \(topic) resources you need in 2026", 83, "Resource roundup angle"),
+                ("\(base): 4 key takeaways from my experience", 81, "Summary-style list"),
+                ("6 signs you're ready for \(topic)", 79, "Checklist engagement"),
+                ("8 \(topic) hacks r/\(subreddit) swears by", 77, "Community-endorsed list"),
+            ]
+        case .emotional:
+            results = [
+                ("\(base) hit me harder than I expected", 91, "Emotional vulnerability"),
+                ("I wasn't ready for what \(topic) taught me", 88, "Personal revelation"),
+                ("This \(topic) moment changed everything for me", 86, "Transformation story"),
+                ("Honestly, \(base) has been on my mind lately", 84, "Authentic confession"),
+                ("The \(topic) journey nobody prepares you for", 82, "Empathy-driven framing"),
+                ("Why \(base) still keeps me up at night", 80, "Emotional intensity"),
+                ("\(topic) — a love letter to r/\(subreddit)", 78, "Community appreciation"),
+                ("I finally understand \(base) and it's bittersweet", 76, "Reflective tone"),
+            ]
+        case .direct:
+            results = [
+                ("\(base) — a complete guide for r/\(subreddit)", 92, "Clear value proposition"),
+                ("Everything you need to know about \(topic)", 89, "Comprehensive promise"),
+                ("\(topic): what works, what doesn't, and why", 87, "Balanced direct framing"),
+                ("My honest take on \(base)", 85, "Straightforward opinion"),
+                ("\(topic) explained simply", 83, "Accessibility focus"),
+                ("The definitive \(base) breakdown", 81, "Authority positioning"),
+                ("\(topic) for beginners — start here", 79, "Audience targeting"),
+                ("\(base): facts, not hype", 77, "No-nonsense angle"),
+            ]
+        case .provocative:
+            results = [
+                ("Unpopular opinion: \(base) is overrated", 93, "Classic debate starter"),
+                ("\(topic) is a scam and here's why", 90, "Strong contrarian hook"),
+                ("Hot take: r/\(subreddit) is wrong about \(topic)", 87, "Community challenge"),
+                ("Stop doing \(base) — seriously", 85, "Command attention"),
+                ("\(topic) culture has gone too far", 83, "Cultural critique"),
+                ("I'll say it: \(base) isn't that impressive", 81, "Mild controversy"),
+                ("Why everyone is sleeping on the real \(topic) issue", 79, "Contrarian insight"),
+                ("\(base) — fight me", 77, "Maximum engagement bait"),
+            ]
+        }
+
+        if tone == .humorous {
+            results = results.map { variant in
+                (title: variant.title + " 😅", score: variant.score - 3, reasoning: variant.reasoning + " (humorous tone)")
+            }
+        } else if tone == .professional {
+            results = results.map { variant in
+                (title: "[Analysis] " + variant.title, score: variant.score - 2, reasoning: variant.reasoning + " (professional tone)")
+            }
+        }
+
+        return results.sorted { $0.score > $1.score }
+    }
+}

+ 149 - 0
Reddit App/ViewModels/TitleOptimizerViewModel.swift

@@ -0,0 +1,149 @@
+import AppKit
+import SwiftUI
+
+@MainActor
+@Observable
+final class TitleOptimizerViewModel {
+    var draft = TitleDraft()
+    var selectedTab: TitleOptimizerTab = .optimize
+    var isOptimizing = false
+    var errorMessage: String?
+    var successMessage: String?
+    var analysis: TitleAnalysis?
+    var variants: [TitleVariant] = []
+    var selectedVariantID: UUID?
+
+    private let optimizationService: any TitleOptimizationServiceProtocol
+
+    init(optimizationService: any TitleOptimizationServiceProtocol = MockTitleOptimizationService()) {
+        self.optimizationService = optimizationService
+    }
+
+    var formattedSubreddit: String {
+        let trimmed = draft.subreddit.trimmingCharacters(in: .whitespaces)
+        guard !trimmed.isEmpty else { return "r/subreddit" }
+        return trimmed.hasPrefix("r/") ? trimmed : "r/\(trimmed)"
+    }
+
+    var canOptimize: Bool {
+        !draft.topic.trimmingCharacters(in: .whitespaces).isEmpty
+            && !draft.subreddit.trimmingCharacters(in: .whitespaces).isEmpty
+            && !draft.originalTitle.trimmingCharacters(in: .whitespaces).isEmpty
+            && !isOptimizing
+    }
+
+    var activeTitle: String {
+        if let selectedID = selectedVariantID,
+           let variant = variants.first(where: { $0.id == selectedID }) {
+            return variant.title
+        }
+        return draft.originalTitle
+    }
+
+    var bestVariant: TitleVariant? {
+        variants.max(by: { $0.score < $1.score })
+    }
+
+    var previewDraft: PostDraft {
+        PostDraft(
+            postType: draft.postType,
+            subreddit: draft.subreddit,
+            topic: draft.topic,
+            tone: draft.tone,
+            title: activeTitle,
+            isNSFW: draft.isNSFW,
+            isSpoiler: draft.isSpoiler,
+            isOC: draft.isOC,
+            flair: draft.flair
+        )
+    }
+
+    func selectPostType(_ type: RedditPostType) {
+        draft.postType = type
+        clearMessages()
+    }
+
+    func selectVariant(_ variant: TitleVariant) {
+        selectedVariantID = variant.id
+        clearMessages()
+    }
+
+    func applyVariant(_ variant: TitleVariant) {
+        draft.originalTitle = variant.title
+        selectedVariantID = variant.id
+        successMessage = "Title applied."
+        errorMessage = nil
+    }
+
+    func optimizeTitles() async {
+        guard canOptimize else { return }
+
+        isOptimizing = true
+        errorMessage = nil
+        successMessage = nil
+
+        do {
+            let result = try await optimizationService.optimizeTitles(from: draft)
+            analysis = result.analysis
+            variants = result.variants
+            selectedVariantID = result.variants.first?.id
+            successMessage = "Generated \(result.variants.count) title suggestions."
+        } catch {
+            errorMessage = error.localizedDescription
+        }
+
+        isOptimizing = false
+    }
+
+    func copyToClipboard() {
+        let title = activeTitle.trimmingCharacters(in: .whitespaces)
+        guard !title.isEmpty else { return }
+
+        NSPasteboard.general.clearContents()
+        NSPasteboard.general.setString(title, forType: .string)
+        successMessage = "Copied to clipboard."
+        errorMessage = nil
+    }
+
+    func resetDraft() {
+        draft = TitleDraft()
+        selectedTab = .optimize
+        analysis = nil
+        variants = []
+        selectedVariantID = nil
+        clearMessages()
+    }
+
+    func exportText() -> String {
+        var lines: [String] = []
+        lines.append("Subreddit: \(formattedSubreddit)")
+        lines.append("Topic: \(draft.topic)")
+        lines.append("Goal: \(draft.titleGoal.title)")
+        lines.append("Tone: \(draft.tone.title)")
+        lines.append("")
+        lines.append("Original: \(draft.originalTitle)")
+
+        if let analysis {
+            lines.append("")
+            lines.append("Analysis Score: \(analysis.overallScore)/100")
+            lines.append("  Length: \(analysis.lengthScore)")
+            lines.append("  Engagement: \(analysis.engagementScore)")
+            lines.append("  Clarity: \(analysis.clarityScore)")
+        }
+
+        if !variants.isEmpty {
+            lines.append("")
+            lines.append("Suggestions:")
+            for (index, variant) in variants.enumerated() {
+                lines.append("\(index + 1). [\(variant.score)] \(variant.title)")
+            }
+        }
+
+        return lines.joined(separator: "\n")
+    }
+
+    private func clearMessages() {
+        errorMessage = nil
+        successMessage = nil
+    }
+}

+ 437 - 0
Reddit App/Views/Components/TitleOptimizerComponents.swift

@@ -0,0 +1,437 @@
+import SwiftUI
+
+// MARK: - Title Goal Picker
+
+struct TitleGoalPicker: View {
+    @Binding var selectedGoal: TitleGoal
+
+    var body: some View {
+        LazyVGrid(
+            columns: [GridItem(.flexible()), GridItem(.flexible())],
+            spacing: 8
+        ) {
+            ForEach(TitleGoal.allCases) { goal in
+                Button {
+                    selectedGoal = goal
+                } label: {
+                    VStack(alignment: .leading, spacing: 2) {
+                        Text(goal.title)
+                            .font(.system(size: 11, weight: .semibold))
+                            .foregroundStyle(AppTheme.textPrimary)
+                        Text(goal.subtitle)
+                            .font(.system(size: 9))
+                            .foregroundStyle(AppTheme.textTertiary)
+                            .lineLimit(1)
+                    }
+                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .padding(.horizontal, 10)
+                    .padding(.vertical, 8)
+                    .background(
+                        RoundedRectangle(cornerRadius: 8)
+                            .fill(
+                                selectedGoal == goal
+                                    ? AppTheme.accentBlue.opacity(0.15)
+                                    : AppTheme.panelBackground
+                            )
+                    )
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 8)
+                            .stroke(
+                                selectedGoal == goal
+                                    ? AppTheme.accentBlue.opacity(0.5)
+                                    : AppTheme.cardBorder,
+                                lineWidth: 1
+                            )
+                    )
+                }
+                .buttonStyle(.plain)
+            }
+        }
+    }
+}
+
+// MARK: - Variant Count Picker
+
+struct TitleVariantCountPicker: View {
+    @Binding var selectedCount: TitleVariantCount
+
+    var body: some View {
+        HStack(spacing: 6) {
+            ForEach(TitleVariantCount.allCases) { count in
+                Button {
+                    selectedCount = count
+                } label: {
+                    Text(count.label)
+                        .font(.system(size: 10, weight: .semibold))
+                        .foregroundStyle(selectedCount == count ? .white : AppTheme.textSecondary)
+                        .padding(.horizontal, 12)
+                        .padding(.vertical, 6)
+                        .background(
+                            Capsule()
+                                .fill(
+                                    selectedCount == count
+                                        ? AppTheme.accentBlue
+                                        : AppTheme.panelBackground
+                                )
+                        )
+                        .overlay(
+                            Capsule()
+                                .stroke(AppTheme.cardBorder, lineWidth: 1)
+                        )
+                }
+                .buttonStyle(.plain)
+            }
+        }
+    }
+}
+
+// MARK: - Title Analysis Card
+
+struct TitleAnalysisCard: View {
+    let analysis: TitleAnalysis
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 14) {
+            HStack {
+                VStack(alignment: .leading, spacing: 2) {
+                    Text("Title Analysis")
+                        .font(.system(size: 12, weight: .semibold))
+                        .foregroundStyle(AppTheme.textPrimary)
+                    Text("How your current title performs")
+                        .font(.system(size: 10))
+                        .foregroundStyle(AppTheme.textSecondary)
+                }
+
+                Spacer()
+
+                TitleScoreBadge(score: analysis.overallScore, size: .large)
+            }
+
+            HStack(spacing: 12) {
+                scoreMetric(label: "Length", score: analysis.lengthScore)
+                scoreMetric(label: "Engagement", score: analysis.engagementScore)
+                scoreMetric(label: "Clarity", score: analysis.clarityScore)
+            }
+
+            if !analysis.suggestions.isEmpty {
+                VStack(alignment: .leading, spacing: 6) {
+                    Text("Suggestions")
+                        .font(.system(size: 10, weight: .medium))
+                        .foregroundStyle(AppTheme.textTertiary)
+
+                    ForEach(analysis.suggestions, id: \.self) { suggestion in
+                        HStack(alignment: .top, spacing: 6) {
+                            Image(systemName: "lightbulb.fill")
+                                .font(.system(size: 9))
+                                .foregroundStyle(AppTheme.accentYellow)
+                                .padding(.top, 2)
+                            Text(suggestion)
+                                .font(.system(size: 10))
+                                .foregroundStyle(AppTheme.textSecondary)
+                                .fixedSize(horizontal: false, vertical: true)
+                        }
+                    }
+                }
+            }
+        }
+        .padding(14)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(AppTheme.cardBackground)
+                .overlay(
+                    RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                        .stroke(AppTheme.cardBorder, lineWidth: 1)
+                )
+        )
+    }
+
+    private func scoreMetric(label: String, score: Int) -> some View {
+        VStack(spacing: 4) {
+            TitleScoreBadge(score: score, size: .small)
+            Text(label)
+                .font(.system(size: 9, weight: .medium))
+                .foregroundStyle(AppTheme.textTertiary)
+        }
+        .frame(maxWidth: .infinity)
+    }
+}
+
+// MARK: - Score Badge
+
+struct TitleScoreBadge: View {
+    enum Size {
+        case small
+        case large
+
+        var fontSize: CGFloat {
+            switch self {
+            case .small: 11
+            case .large: 18
+            }
+        }
+
+        var padding: CGFloat {
+            switch self {
+            case .small: 6
+            case .large: 10
+            }
+        }
+    }
+
+    let score: Int
+    var size: Size = .small
+
+    var body: some View {
+        Text("\(score)")
+            .font(.system(size: size.fontSize, weight: .bold, design: .rounded))
+            .foregroundStyle(scoreColor)
+            .padding(size.padding)
+            .background(
+                Circle()
+                    .fill(scoreColor.opacity(0.15))
+            )
+            .overlay(
+                Circle()
+                    .stroke(scoreColor.opacity(0.3), lineWidth: 1)
+            )
+    }
+
+    private var scoreColor: Color {
+        switch score {
+        case 80...100: AppTheme.accentGreen
+        case 60..<80: AppTheme.accentYellow
+        case 40..<60: AppTheme.accentOrange
+        default: Color(hex: 0xEF4444)
+        }
+    }
+}
+
+// MARK: - Title Variant Row
+
+struct TitleVariantRow: View {
+    let variant: TitleVariant
+    let isSelected: Bool
+    let onSelect: () -> Void
+    let onApply: () -> Void
+
+    var body: some View {
+        HStack(alignment: .top, spacing: 12) {
+            Button(action: onSelect) {
+                Circle()
+                    .stroke(isSelected ? AppTheme.accentBlue : AppTheme.cardBorder, lineWidth: 2)
+                    .frame(width: 16, height: 16)
+                    .overlay {
+                        if isSelected {
+                            Circle()
+                                .fill(AppTheme.accentBlue)
+                                .frame(width: 8, height: 8)
+                        }
+                    }
+            }
+            .buttonStyle(.plain)
+            .padding(.top, 2)
+
+            VStack(alignment: .leading, spacing: 6) {
+                HStack(alignment: .top) {
+                    Text(variant.title)
+                        .font(.system(size: 12, weight: .medium))
+                        .foregroundStyle(AppTheme.textPrimary)
+                        .fixedSize(horizontal: false, vertical: true)
+
+                    Spacer(minLength: 8)
+
+                    TitleScoreBadge(score: variant.score)
+                }
+
+                Text(variant.reasoning)
+                    .font(.system(size: 10))
+                    .foregroundStyle(AppTheme.textTertiary)
+
+                HStack(spacing: 8) {
+                    Text("\(variant.title.count) chars")
+                        .font(.system(size: 9))
+                        .foregroundStyle(
+                            variant.title.count > 300 ? Color(hex: 0xF87171) : AppTheme.textTertiary
+                        )
+
+                    Spacer()
+
+                    Button(action: onApply) {
+                        Text("Use Title")
+                            .font(.system(size: 10, weight: .semibold))
+                            .foregroundStyle(AppTheme.accentBlue)
+                    }
+                    .buttonStyle(.plain)
+                }
+            }
+        }
+        .padding(12)
+        .background(
+            RoundedRectangle(cornerRadius: 8)
+                .fill(isSelected ? AppTheme.accentBlue.opacity(0.08) : AppTheme.panelBackground)
+        )
+        .overlay(
+            RoundedRectangle(cornerRadius: 8)
+                .stroke(
+                    isSelected ? AppTheme.accentBlue.opacity(0.4) : AppTheme.cardBorder,
+                    lineWidth: 1
+                )
+        )
+    }
+}
+
+// MARK: - Compare Card
+
+struct TitleCompareCard: View {
+    let title: String
+    let score: Int?
+    let label: String
+    let isOriginal: Bool
+    var isSelected: Bool = false
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            HStack {
+                Text(label)
+                    .font(.system(size: 10, weight: .semibold))
+                    .foregroundStyle(isOriginal ? AppTheme.textSecondary : AppTheme.accentBlue)
+                    .padding(.horizontal, 8)
+                    .padding(.vertical, 3)
+                    .background(
+                        Capsule()
+                            .fill(
+                                isOriginal
+                                    ? AppTheme.panelBackground
+                                    : AppTheme.accentBlue.opacity(0.15)
+                            )
+                    )
+
+                Spacer()
+
+                if let score {
+                    TitleScoreBadge(score: score)
+                }
+            }
+
+            Text(title.isEmpty ? "No title" : title)
+                .font(.system(size: 13, weight: .semibold))
+                .foregroundStyle(title.isEmpty ? AppTheme.textTertiary : AppTheme.textPrimary)
+                .fixedSize(horizontal: false, vertical: true)
+
+            HStack {
+                Text("\(title.count) / 300 characters")
+                    .font(.system(size: 9))
+                    .foregroundStyle(
+                        title.count > 300 ? Color(hex: 0xF87171) : AppTheme.textTertiary
+                    )
+
+                Spacer()
+
+                if isSelected {
+                    Label("Active", systemImage: "checkmark.circle.fill")
+                        .font(.system(size: 9, weight: .semibold))
+                        .foregroundStyle(AppTheme.accentGreen)
+                }
+            }
+        }
+        .padding(14)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(AppTheme.cardBackground)
+                .overlay(
+                    RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                        .stroke(
+                            isSelected ? AppTheme.accentBlue.opacity(0.5) : AppTheme.cardBorder,
+                            lineWidth: isSelected ? 1.5 : 1
+                        )
+                )
+        )
+    }
+}
+
+// MARK: - Feed Preview Row
+
+struct TitleFeedPreviewRow: View {
+    let title: String
+    let subreddit: String
+    var isHighlighted: Bool = false
+
+    var body: some View {
+        HStack(alignment: .top, spacing: 10) {
+            RoundedRectangle(cornerRadius: 4)
+                .fill(AppTheme.cardBackground)
+                .frame(width: 72, height: 52)
+                .overlay {
+                    Image(systemName: "photo")
+                        .font(.system(size: 16))
+                        .foregroundStyle(AppTheme.textTertiary)
+                }
+
+            VStack(alignment: .leading, spacing: 4) {
+                Text(subreddit)
+                    .font(.system(size: 9, weight: .semibold))
+                    .foregroundStyle(AppTheme.textTertiary)
+
+                Text(title.isEmpty ? "Title preview" : title)
+                    .font(.system(size: 11, weight: .semibold))
+                    .foregroundStyle(title.isEmpty ? AppTheme.textTertiary : AppTheme.textPrimary)
+                    .lineLimit(2)
+                    .fixedSize(horizontal: false, vertical: true)
+            }
+
+            Spacer(minLength: 0)
+        }
+        .padding(10)
+        .background(
+            RoundedRectangle(cornerRadius: 8)
+                .fill(isHighlighted ? AppTheme.accentBlue.opacity(0.08) : AppTheme.panelBackground)
+        )
+        .overlay(
+            RoundedRectangle(cornerRadius: 8)
+                .stroke(
+                    isHighlighted ? AppTheme.accentBlue.opacity(0.4) : AppTheme.cardBorder,
+                    lineWidth: 1
+                )
+        )
+    }
+}
+
+// MARK: - Empty State
+
+struct TitleOptimizerEmptyState: View {
+    let icon: String
+    let title: String
+    let subtitle: String
+
+    var body: some View {
+        VStack(spacing: 10) {
+            Image(systemName: icon)
+                .font(.system(size: 28))
+                .foregroundStyle(AppTheme.accentBlue.opacity(0.6))
+
+            Text(title)
+                .font(.system(size: 12, weight: .semibold))
+                .foregroundStyle(AppTheme.textSecondary)
+
+            Text(subtitle)
+                .font(.system(size: 10))
+                .foregroundStyle(AppTheme.textTertiary)
+                .multilineTextAlignment(.center)
+        }
+        .frame(maxWidth: .infinity)
+        .padding(.vertical, 32)
+        .background(
+            RoundedRectangle(cornerRadius: 8)
+                .fill(AppTheme.panelBackground)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 8)
+                        .strokeBorder(
+                            AppTheme.accentBlue.opacity(0.2),
+                            style: StrokeStyle(lineWidth: 1, dash: [6, 4])
+                        )
+                )
+        )
+    }
+}

+ 3 - 0
Reddit App/Views/FrontPageView.swift

@@ -3,6 +3,7 @@ import SwiftUI
 struct FrontPageView: View {
     @State private var viewModel = FrontPageViewModel()
     @State private var postGeneratorViewModel = PostGeneratorViewModel()
+    @State private var titleOptimizerViewModel = TitleOptimizerViewModel()
 
     var body: some View {
         HStack(spacing: 0) {
@@ -39,6 +40,8 @@ struct FrontPageView: View {
         switch item.iconKind {
         case .postGenerator:
             PostGeneratorView(viewModel: postGeneratorViewModel)
+        case .titleOptimizer:
+            TitleOptimizerView(viewModel: titleOptimizerViewModel)
         default:
             ToolPlaceholderView(item: item)
         }

+ 503 - 0
Reddit App/Views/TitleOptimizerView.swift

@@ -0,0 +1,503 @@
+import SwiftUI
+
+struct TitleOptimizerView: View {
+    @Bindable var viewModel: TitleOptimizerViewModel
+
+    var body: some View {
+        VStack(spacing: 0) {
+            header
+            tabBar
+
+            ScrollView {
+                PostPageBoundary {
+                    switch viewModel.selectedTab {
+                    case .optimize:
+                        optimizeContent
+                    case .compare:
+                        compareContent
+                    case .preview:
+                        previewContent
+                    }
+                }
+                .padding(.horizontal, 24)
+                .padding(.top, 8)
+                .padding(.bottom, 24)
+            }
+        }
+        .background(AppTheme.background)
+    }
+
+    // MARK: - Header
+
+    private var header: some View {
+        HStack(spacing: 14) {
+            ModernSidebarIconView(kind: .titleOptimizer, size: 40)
+
+            VStack(alignment: .leading, spacing: 2) {
+                Text("Title Optimizer")
+                    .font(.system(size: 18, weight: .semibold))
+                    .foregroundStyle(AppTheme.textPrimary)
+                Text("Craft viral Reddit titles with AI")
+                    .font(.system(size: 11))
+                    .foregroundStyle(AppTheme.textSecondary)
+            }
+
+            Spacer()
+
+            headerActions
+        }
+        .padding(.horizontal, 24)
+        .padding(.vertical, 16)
+        .overlay(alignment: .bottom) {
+            Rectangle()
+                .fill(AppTheme.border)
+                .frame(height: 1)
+        }
+    }
+
+    private var headerActions: some View {
+        HStack(spacing: 8) {
+            Button {
+                viewModel.resetDraft()
+            } label: {
+                Label("Reset", systemImage: "arrow.counterclockwise")
+                    .font(.system(size: 11, weight: .medium))
+            }
+            .buttonStyle(TitleSecondaryButtonStyle())
+
+            Button {
+                viewModel.copyToClipboard()
+            } label: {
+                Label("Copy", systemImage: "doc.on.doc")
+                    .font(.system(size: 11, weight: .medium))
+            }
+            .buttonStyle(TitleSecondaryButtonStyle())
+            .disabled(viewModel.activeTitle.trimmingCharacters(in: .whitespaces).isEmpty)
+
+            Button {
+                Task { await viewModel.optimizeTitles() }
+            } label: {
+                HStack(spacing: 6) {
+                    if viewModel.isOptimizing {
+                        ProgressView()
+                            .controlSize(.small)
+                            .tint(.white)
+                    } else {
+                        Image(systemName: "sparkles")
+                            .font(.system(size: 11, weight: .semibold))
+                    }
+                    Text(viewModel.isOptimizing ? "Optimizing…" : "Optimize")
+                        .font(.system(size: 11, weight: .semibold))
+                }
+                .foregroundStyle(.white)
+                .padding(.horizontal, 14)
+                .padding(.vertical, 8)
+                .background(viewModel.canOptimize ? AppTheme.accentBlue : AppTheme.accentBlue.opacity(0.4))
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+            }
+            .buttonStyle(.plain)
+            .disabled(!viewModel.canOptimize)
+        }
+    }
+
+    // MARK: - Tab Bar
+
+    private var tabBar: some View {
+        HStack(spacing: 4) {
+            ForEach(TitleOptimizerTab.allCases) { tab in
+                Button {
+                    viewModel.selectedTab = tab
+                } label: {
+                    Text(tab.title)
+                        .font(.system(size: 11, weight: .semibold))
+                        .foregroundStyle(viewModel.selectedTab == tab ? AppTheme.textPrimary : AppTheme.textSecondary)
+                        .padding(.horizontal, 14)
+                        .padding(.vertical, 8)
+                        .background(
+                            viewModel.selectedTab == tab
+                                ? AppTheme.cardBackground
+                                : Color.clear
+                        )
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                }
+                .buttonStyle(.plain)
+            }
+
+            Spacer()
+        }
+        .padding(.horizontal, 24)
+        .padding(.top, 12)
+        .padding(.bottom, 4)
+    }
+
+    // MARK: - Optimize
+
+    private var optimizeContent: some View {
+        HStack(alignment: .top, spacing: 16) {
+            configurationPanel
+            editorPanel
+        }
+    }
+
+    private var configurationPanel: some View {
+        VStack(spacing: 12) {
+            PostFormCard(title: "Target Subreddit", subtitle: "Tailor titles for the community") {
+                PostFormField(
+                    label: "Subreddit",
+                    placeholder: "technology",
+                    text: $viewModel.draft.subreddit,
+                    prefix: "r/"
+                )
+            }
+
+            PostFormCard(title: "AI Settings", subtitle: "Guide topic, tone, and style") {
+                VStack(alignment: .leading, spacing: 12) {
+                    PostFormField(
+                        label: "Topic / Keywords",
+                        placeholder: "e.g. macOS productivity tips",
+                        text: $viewModel.draft.topic
+                    )
+
+                    VStack(alignment: .leading, spacing: 6) {
+                        Text("Tone")
+                            .font(.system(size: 10, weight: .medium))
+                            .foregroundStyle(AppTheme.textTertiary)
+                        TonePicker(selectedTone: $viewModel.draft.tone)
+                    }
+                }
+            }
+
+            PostFormCard(title: "Title Goal", subtitle: "What kind of hook do you want?") {
+                TitleGoalPicker(selectedGoal: $viewModel.draft.titleGoal)
+            }
+
+            PostFormCard(title: "Post Context", subtitle: "Optional — affects preview style") {
+                VStack(alignment: .leading, spacing: 12) {
+                    PostTypePicker(selectedType: Binding(
+                        get: { viewModel.draft.postType },
+                        set: { viewModel.selectPostType($0) }
+                    ))
+
+                    PostFormField(
+                        label: "Flair",
+                        placeholder: "Discussion, Question, Meme…",
+                        text: $viewModel.draft.flair
+                    )
+                }
+            }
+
+            PostFormCard(title: "Post Tags", subtitle: "Reddit content labels") {
+                HStack(spacing: 6) {
+                    PostTagToggle(
+                        title: "NSFW",
+                        systemImage: "exclamationmark.triangle.fill",
+                        activeColor: Color(hex: 0xEF4444),
+                        isOn: $viewModel.draft.isNSFW
+                    )
+                    PostTagToggle(
+                        title: "Spoiler",
+                        systemImage: "eye.slash.fill",
+                        activeColor: AppTheme.textSecondary,
+                        isOn: $viewModel.draft.isSpoiler
+                    )
+                    PostTagToggle(
+                        title: "OC",
+                        systemImage: "star.fill",
+                        activeColor: AppTheme.accentGreen,
+                        isOn: $viewModel.draft.isOC
+                    )
+                }
+            }
+
+            PostFormCard(title: "Suggestions", subtitle: "How many variants to generate") {
+                TitleVariantCountPicker(selectedCount: $viewModel.draft.variantCount)
+            }
+
+            if let error = viewModel.errorMessage {
+                PostGeneratorMessageBanner(message: error, isError: true)
+            } else if let success = viewModel.successMessage {
+                PostGeneratorMessageBanner(message: success, isError: false)
+            }
+        }
+        .frame(width: 300)
+    }
+
+    private var editorPanel: some View {
+        VStack(spacing: 12) {
+            PostFormCard(title: "Original Title", subtitle: "The title you want to improve") {
+                PostFormField(
+                    label: "Post Title",
+                    placeholder: "Enter your draft title here",
+                    text: $viewModel.draft.originalTitle
+                )
+            }
+
+            characterCountFooter
+
+            if let analysis = viewModel.analysis {
+                TitleAnalysisCard(analysis: analysis)
+            }
+
+            suggestionsSection
+        }
+        .frame(maxWidth: .infinity)
+    }
+
+    @ViewBuilder
+    private var suggestionsSection: some View {
+        PostFormCard(
+            title: "AI Suggestions",
+            subtitle: viewModel.variants.isEmpty
+                ? "Hit Optimize to generate title variants"
+                : "\(viewModel.variants.count) optimized titles ranked by score"
+        ) {
+            if viewModel.variants.isEmpty {
+                TitleOptimizerEmptyState(
+                    icon: "textformat.size",
+                    title: "No suggestions yet",
+                    subtitle: "Fill in your subreddit, topic, and title,\nthen tap Optimize to get AI-powered variants."
+                )
+            } else {
+                VStack(spacing: 8) {
+                    ForEach(viewModel.variants) { variant in
+                        TitleVariantRow(
+                            variant: variant,
+                            isSelected: viewModel.selectedVariantID == variant.id,
+                            onSelect: { viewModel.selectVariant(variant) },
+                            onApply: { viewModel.applyVariant(variant) }
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    private var characterCountFooter: some View {
+        HStack {
+            Text("\(viewModel.draft.originalTitle.count) / 300")
+                .font(.system(size: 10))
+                .foregroundStyle(
+                    viewModel.draft.originalTitle.count > 300 ? Color(hex: 0xF87171) : AppTheme.textTertiary
+                )
+
+            Spacer()
+
+            Text("Reddit limit: 300 characters for title")
+                .font(.system(size: 10))
+                .foregroundStyle(AppTheme.textTertiary)
+        }
+    }
+
+    // MARK: - Compare
+
+    private var compareContent: some View {
+        VStack(spacing: 16) {
+            if viewModel.variants.isEmpty {
+                TitleOptimizerEmptyState(
+                    icon: "arrow.left.arrow.right",
+                    title: "Nothing to compare yet",
+                    subtitle: "Generate title suggestions on the Optimize tab first."
+                )
+            } else {
+                compareHeader
+
+                TitleCompareCard(
+                    title: viewModel.draft.originalTitle,
+                    score: viewModel.analysis?.overallScore,
+                    label: "Original",
+                    isOriginal: true,
+                    isSelected: viewModel.selectedVariantID == nil
+                )
+
+                HStack {
+                    Rectangle()
+                        .fill(AppTheme.border)
+                        .frame(height: 1)
+                    Text("vs")
+                        .font(.system(size: 10, weight: .semibold))
+                        .foregroundStyle(AppTheme.textTertiary)
+                        .padding(.horizontal, 8)
+                    Rectangle()
+                        .fill(AppTheme.border)
+                        .frame(height: 1)
+                }
+
+                VStack(spacing: 8) {
+                    ForEach(viewModel.variants) { variant in
+                        TitleCompareCard(
+                            title: variant.title,
+                            score: variant.score,
+                            label: "Variant #\(variantRank(variant))",
+                            isOriginal: false,
+                            isSelected: viewModel.selectedVariantID == variant.id
+                        )
+                        .onTapGesture {
+                            viewModel.selectVariant(variant)
+                        }
+                    }
+                }
+
+                compareMetadata
+            }
+        }
+        .frame(maxWidth: 640)
+        .frame(maxWidth: .infinity)
+    }
+
+    private var compareHeader: some View {
+        HStack {
+            VStack(alignment: .leading, spacing: 2) {
+                Text("Side-by-Side Comparison")
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(AppTheme.textPrimary)
+                Text("Tap a variant to select it as active")
+                    .font(.system(size: 10))
+                    .foregroundStyle(AppTheme.textSecondary)
+            }
+
+            Spacer()
+
+            if let best = viewModel.bestVariant {
+                HStack(spacing: 6) {
+                    Image(systemName: "trophy.fill")
+                        .font(.system(size: 10))
+                        .foregroundStyle(AppTheme.accentYellow)
+                    Text("Best: \(best.score)/100")
+                        .font(.system(size: 10, weight: .semibold))
+                        .foregroundStyle(AppTheme.textSecondary)
+                }
+            }
+        }
+    }
+
+    private var compareMetadata: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit)
+            metadataRow(label: "Goal", value: viewModel.draft.titleGoal.title)
+            metadataRow(label: "Tone", value: viewModel.draft.tone.title)
+            metadataRow(label: "Active", value: viewModel.activeTitle)
+        }
+        .padding(14)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(AppTheme.cardBackground)
+                .overlay(
+                    RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                        .stroke(AppTheme.cardBorder, lineWidth: 1)
+                )
+        )
+    }
+
+    private func variantRank(_ variant: TitleVariant) -> Int {
+        (viewModel.variants.firstIndex(where: { $0.id == variant.id }) ?? 0) + 1
+    }
+
+    // MARK: - Preview
+
+    private var previewContent: some View {
+        VStack(spacing: 16) {
+            Text("Live Preview")
+                .font(.system(size: 12, weight: .semibold))
+                .foregroundStyle(AppTheme.textSecondary)
+                .frame(maxWidth: .infinity, alignment: .leading)
+
+            RedditPostPreviewCard(
+                draft: viewModel.previewDraft,
+                formattedSubreddit: viewModel.formattedSubreddit
+            )
+
+            PostFormCard(title: "Feed Preview", subtitle: "How your title looks in a crowded feed") {
+                VStack(spacing: 6) {
+                    TitleFeedPreviewRow(
+                        title: "Other trending post in this subreddit right now",
+                        subreddit: viewModel.formattedSubreddit
+                    )
+                    TitleFeedPreviewRow(
+                        title: viewModel.activeTitle,
+                        subreddit: viewModel.formattedSubreddit,
+                        isHighlighted: true
+                    )
+                    TitleFeedPreviewRow(
+                        title: "Another popular discussion happening nearby",
+                        subreddit: viewModel.formattedSubreddit
+                    )
+                }
+            }
+
+            previewMetadata
+        }
+        .frame(maxWidth: 560)
+        .frame(maxWidth: .infinity)
+    }
+
+    private var previewMetadata: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            metadataRow(label: "Active Title", value: viewModel.activeTitle)
+            metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit)
+            metadataRow(label: "Goal", value: viewModel.draft.titleGoal.title)
+            metadataRow(label: "Tone", value: viewModel.draft.tone.title)
+            metadataRow(label: "Type", value: viewModel.draft.postType.title)
+
+            if let score = viewModel.analysis?.overallScore {
+                metadataRow(label: "Score", value: "\(score)/100")
+            }
+
+            if viewModel.draft.isNSFW || viewModel.draft.isSpoiler || viewModel.draft.isOC {
+                metadataRow(
+                    label: "Tags",
+                    value: [
+                        viewModel.draft.isNSFW ? "NSFW" : nil,
+                        viewModel.draft.isSpoiler ? "Spoiler" : nil,
+                        viewModel.draft.isOC ? "OC" : nil,
+                    ]
+                    .compactMap { $0 }
+                    .joined(separator: ", ")
+                )
+            }
+        }
+        .padding(14)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(AppTheme.cardBackground)
+                .overlay(
+                    RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                        .stroke(AppTheme.cardBorder, lineWidth: 1)
+                )
+        )
+    }
+
+    private func metadataRow(label: String, value: String) -> some View {
+        HStack(alignment: .top) {
+            Text(label)
+                .font(.system(size: 10, weight: .medium))
+                .foregroundStyle(AppTheme.textTertiary)
+                .frame(width: 80, alignment: .leading)
+            Text(value)
+                .font(.system(size: 10))
+                .foregroundStyle(AppTheme.textSecondary)
+                .fixedSize(horizontal: false, vertical: true)
+        }
+    }
+}
+
+private struct TitleSecondaryButtonStyle: ButtonStyle {
+    func makeBody(configuration: Configuration) -> some View {
+        configuration.label
+            .foregroundStyle(AppTheme.textSecondary)
+            .padding(.horizontal, 12)
+            .padding(.vertical, 8)
+            .background(AppTheme.cardBackground.opacity(configuration.isPressed ? 0.6 : 1))
+            .clipShape(RoundedRectangle(cornerRadius: 8))
+            .overlay(
+                RoundedRectangle(cornerRadius: 8)
+                    .stroke(AppTheme.cardBorder, lineWidth: 1)
+            )
+    }
+}
+
+#Preview {
+    TitleOptimizerView(viewModel: TitleOptimizerViewModel())
+        .frame(width: 880, height: 720)
+}