import SwiftUI struct TitleOptimizerView: View { @Bindable var viewModel: TitleOptimizerViewModel private enum Layout { static let horizontalPadding: CGFloat = 24 static let columnWidth: CGFloat = 300 static let columnSpacing: CGFloat = 20 static let sectionSpacing: CGFloat = 12 } var body: some View { VStack(spacing: 0) { header messageBanners ScrollViewReader { scrollProxy in ScrollView { VStack(alignment: .leading, spacing: 12) { tabBar PostPageBoundary { switch viewModel.selectedTab { case .optimize: optimizeContent case .compare: compareContent case .preview: previewContent } } } .padding(.horizontal, Layout.horizontalPadding) .padding(.top, 12) .padding(.bottom, 24) } .onChange(of: viewModel.variants.count) { _, count in guard count > 0, viewModel.selectedTab == .optimize else { return } withAnimation(.easeInOut(duration: 0.25)) { scrollProxy.scrollTo("title-suggestions", anchor: .top) } } } } .background(AppTheme.background) } // MARK: - Header private var header: some View { VStack(spacing: 0) { 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(viewModel.generationEngineSubtitle) .font(.system(size: 11)) .foregroundStyle(AppTheme.textSecondary) } Spacer() headerActions } .padding(.horizontal, Layout.horizontalPadding) .padding(.vertical, 16) FullWidthDivider() } } @ViewBuilder private var messageBanners: some View { if let error = viewModel.errorMessage { PostGeneratorMessageBanner(message: error, isError: true) .padding(.horizontal, Layout.horizontalPadding) .padding(.top, 8) } else if let success = viewModel.successMessage { PostGeneratorMessageBanner(message: success, isError: false) .padding(.horizontal, Layout.horizontalPadding) .padding(.top, 8) } } private var headerActions: some View { HStack(spacing: 8) { Button { viewModel.resetDraft() } label: { Label("Reset", systemImage: "arrow.counterclockwise") .font(.system(size: 11, weight: .medium)) } .buttonStyle(AppSecondaryButtonStyle()) Button { viewModel.copyToClipboard() } label: { Label("Copy", systemImage: "doc.on.doc") .font(.system(size: 11, weight: .medium)) } .buttonStyle(AppSecondaryButtonStyle()) .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(AppPrimaryButtonStyle()) .disabled(!viewModel.canOptimize) } } // MARK: - Tab Bar private var tabBar: some View { ToolSegmentedTabBar( selection: $viewModel.selectedTab, accentColor: AppTheme.accentBlue, width: 300 ) } // MARK: - Optimize private var optimizeContent: some View { optimizeColumns } private var optimizeColumns: some View { HStack(alignment: .top, spacing: Layout.columnSpacing) { optimizeColumn(title: "Configuration", icon: "slider.horizontal.3") { configurationPanel } .frame(width: Layout.columnWidth) .layoutPriority(1) Rectangle() .fill(AppTheme.border) .frame(width: 1) .padding(.vertical, 2) optimizeColumn(title: "Title & Suggestions", icon: "textformat") { editorPanel } .frame(minWidth: 0, maxWidth: .infinity) .layoutPriority(0) } } private func optimizeColumn( title: String, icon: String, @ViewBuilder content: () -> Content ) -> some View { VStack(alignment: .leading, spacing: Layout.sectionSpacing) { sectionHeader(title: title, icon: icon) content() } } private func sectionHeader(title: String, icon: String) -> some View { HStack(spacing: 6) { Image(systemName: icon) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(AppTheme.accentBlue) Text(title) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(AppTheme.textSecondary) } .textCase(.uppercase) .tracking(0.4) } private var configurationPanel: some View { VStack(spacing: Layout.sectionSpacing) { PostFormCard(title: "Target Subreddit", subtitle: "Tailor titles for the community") { PostFormField( label: "Subreddit", placeholder: "technology", text: $viewModel.draft.subreddit, prefix: "r/", isRequired: true ) } 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, isRequired: true ) 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: "Preview Context", subtitle: "Optional — affects preview style") { VStack(alignment: .leading, spacing: 12) { PostTypePicker(selectedType: Binding( get: { viewModel.draft.postType }, set: { viewModel.selectPostType($0) } )) HStack(spacing: 8) { PostTagToggle( title: "NSFW", systemImage: "exclamationmark.triangle.fill", activeColor: Color(hex: 0xEF4444), isOn: $viewModel.draft.isNSFW ) .frame(maxWidth: .infinity) PostTagToggle( title: "Spoiler", systemImage: "eye.slash.fill", activeColor: AppTheme.textSecondary, isOn: $viewModel.draft.isSpoiler ) .frame(maxWidth: .infinity) PostTagToggle( title: "OC", systemImage: "star.fill", activeColor: AppTheme.accentGreen, isOn: $viewModel.draft.isOC ) .frame(maxWidth: .infinity) } PostFormField( label: "Flair", placeholder: "Discussion, Question, Meme…", text: $viewModel.draft.flair ) } } PostFormCard(title: "Suggestions", subtitle: "How many variants to generate") { TitleVariantCountPicker(selectedCount: $viewModel.draft.variantCount) } } } private var editorPanel: some View { VStack(spacing: Layout.sectionSpacing) { 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, isRequired: true ) } characterCountFooter if let analysis = viewModel.analysis { TitleAnalysisCard(analysis: analysis) } suggestionsSection .id("title-suggestions") } } @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, onApply: { viewModel.applyVariant(variant) } ) } } } } } private var characterCountFooter: some View { HStack(spacing: 12) { Text("\(viewModel.draft.originalTitle.count) / 300") .font(.system(size: 10, weight: .medium, design: .monospaced)) .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) } .padding(.horizontal, 12) .padding(.vertical, 9) .background( RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .fill(AppTheme.cardBackground) .overlay( RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .stroke(AppTheme.cardBorder, lineWidth: 1) ) ) } // MARK: - Compare private var compareContent: some View { VStack(alignment: .leading, spacing: Layout.columnSpacing) { compareHeader if viewModel.variants.isEmpty { TitleOptimizerEmptyState( icon: "arrow.left.arrow.right", title: "Nothing to compare yet", subtitle: "Generate title suggestions on the Optimize tab first." ) .frame(maxWidth: .infinity) } else { compareBody } } } private var compareBody: some View { VStack(spacing: Layout.sectionSpacing) { 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.applyVariant(variant) } } } compareMetadata } .frame(maxWidth: 640) .frame(maxWidth: .infinity) } private var compareHeader: some View { HStack(alignment: .top) { sectionHeader(title: "Side-by-Side Comparison", icon: "arrow.left.arrow.right") 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) } .padding(.top, 1) } } } private var compareMetadata: some View { VStack(alignment: .leading, spacing: 0) { metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit) metadataDivider metadataRow(label: "Goal", value: viewModel.draft.titleGoal.title) metadataDivider metadataRow(label: "Tone", value: viewModel.draft.tone.title) metadataDivider metadataRow(label: "Engine", value: viewModel.usesLiveAI ? "AI" : "Local templates") metadataDivider 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(alignment: .leading, spacing: Layout.columnSpacing) { sectionHeader(title: "Live Preview", icon: "eye") HStack { Spacer(minLength: 0) VStack(spacing: Layout.sectionSpacing) { 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) Spacer(minLength: 0) } } } private var previewMetadata: some View { VStack(alignment: .leading, spacing: 0) { metadataRow(label: "Active Title", value: viewModel.activeTitle) metadataDivider metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit) metadataDivider metadataRow(label: "Goal", value: viewModel.draft.titleGoal.title) metadataDivider metadataRow(label: "Tone", value: viewModel.draft.tone.title) metadataDivider metadataRow(label: "Type", value: viewModel.draft.postType.title) metadataDivider metadataRow(label: "Engine", value: viewModel.usesLiveAI ? "AI" : "Local templates") if let score = viewModel.analysis?.overallScore { metadataDivider metadataRow(label: "Score", value: "\(score)/100") } if viewModel.draft.isNSFW || viewModel.draft.isSpoiler || viewModel.draft.isOC { metadataDivider 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 var metadataDivider: some View { Rectangle() .fill(AppTheme.border) .frame(height: 1) .padding(.vertical, 6) } private func metadataRow(label: String, value: String) -> some View { HStack(alignment: .top, spacing: 12) { Text(label) .font(.system(size: 10, weight: .medium)) .foregroundStyle(AppTheme.textTertiary) .frame(width: 72, 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 { AppSecondaryButtonStyle().makeBody(configuration: configuration) } } #Preview { TitleOptimizerView(viewModel: TitleOptimizerViewModel()) .frame(width: 880, height: 720) }