import SwiftUI struct CommentWriterView: View { @Environment(\.requirePremiumAccess) private var requirePremiumAccess @Bindable var viewModel: CommentWriterViewModel 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 ScrollView { VStack(alignment: .leading, spacing: 12) { tabBar PostPageBoundary { switch viewModel.selectedTab { case .compose: composeContent case .preview: previewContent case .variants: variantsContent } } } .padding(.horizontal, Layout.horizontalPadding) .padding(.top, 12) .padding(.bottom, 24) } } .background(AppTheme.background) .alert("Replace existing comment?", isPresented: $viewModel.showOverwriteConfirmation) { Button("Cancel", role: .cancel) { viewModel.cancelOverwriteConfirmation() } Button("Replace", role: .destructive) { guard requirePremiumAccess() else { return } Task { await viewModel.confirmOverwriteAndGenerate() } } } message: { Text("Generating will replace your current comment text.") } .onChange(of: viewModel.draft.subreddit) { _, _ in viewModel.notifyDraftEdited() } .onChange(of: viewModel.draft.postTitle) { _, _ in viewModel.notifyDraftEdited() } .onChange(of: viewModel.draft.postBody) { _, _ in viewModel.notifyDraftEdited() } .onChange(of: viewModel.draft.parentComment) { _, _ in viewModel.notifyDraftEdited() } .onChange(of: viewModel.draft.body) { _, _ in viewModel.notifyDraftEdited() } } // MARK: - Header private var header: some View { HStack(spacing: 14) { ModernSidebarIconView(kind: .commentWriter, size: 40) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text("Comment Writer") .font(.system(size: 18, weight: .semibold)) .foregroundStyle(AppTheme.textPrimary) if viewModel.hasDraftChanges { Circle() .fill(AppTheme.accentGreen) .frame(width: 6, height: 6) .help("Draft has unsaved edits") } } Text(viewModel.usesLiveAI ? "Write Reddit-ready comments with OpenAI" : "Write Reddit-ready comments with AI templates") .font(.system(size: 11)) .foregroundStyle(AppTheme.textSecondary) } Spacer() headerActions } .padding(.horizontal, Layout.horizontalPadding) .padding(.vertical, 16) .overlay(alignment: .bottom) { Rectangle() .fill(AppTheme.border) .frame(height: 1) } } @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.canExport) Button { guard requirePremiumAccess() else { return } Task { await viewModel.generateComment() } } label: { HStack(spacing: 6) { if viewModel.isGenerating { ProgressView() .controlSize(.small) .tint(.white) } else { Image(systemName: "sparkles") .font(.system(size: 11, weight: .semibold)) } Text(viewModel.isGenerating ? "Generating…" : "Generate") .font(.system(size: 11, weight: .semibold)) } .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 8) .background(viewModel.canGenerate ? AppTheme.accentGreen : AppTheme.accentGreen.opacity(0.4)) .clipShape(RoundedRectangle(cornerRadius: 8)) } .buttonStyle(AppPrimaryButtonStyle()) .disabled(!viewModel.canGenerate) } } // MARK: - Tab Bar private var tabBar: some View { HStack(spacing: 0) { ForEach(CommentWriterTab.allCases) { tab in Button { withAnimation(.easeInOut(duration: 0.15)) { viewModel.selectedTab = tab } } label: { HStack(spacing: 4) { Text(tab.title) .font(.system(size: 11, weight: .semibold)) if tab == .variants, !viewModel.variants.isEmpty { Text("\(viewModel.variants.count)") .font(.system(size: 9, weight: .bold)) .foregroundStyle(.white) .padding(.horizontal, 5) .padding(.vertical, 2) .background(AppTheme.accentGreen) .clipShape(Capsule()) } } .foregroundStyle(viewModel.selectedTab == tab ? AppTheme.textPrimary : AppTheme.textSecondary) .frame(maxWidth: .infinity) .padding(.vertical, 7) .background( viewModel.selectedTab == tab ? AppTheme.cardBackground : Color.clear ) .clipShape(RoundedRectangle(cornerRadius: 7)) } .buttonStyle(AppPlainButtonStyle()) .hoverOverlay(cornerRadius: 7, isEnabled: viewModel.selectedTab != tab) } } .padding(3) .frame(width: 300) .background(AppTheme.panelBackground) .clipShape(RoundedRectangle(cornerRadius: 9)) .overlay( RoundedRectangle(cornerRadius: 9) .stroke(AppTheme.cardBorder, lineWidth: 1) ) } // MARK: - Compose private var composeContent: some View { ViewThatFits(in: .horizontal) { composeColumns VStack(alignment: .leading, spacing: Layout.columnSpacing) { composeColumn(title: "Configuration", icon: "slider.horizontal.3") { configurationPanel } composeColumn(title: "Your Comment", icon: "bubble.left") { editorPanel } } } } private var composeColumns: some View { HStack(alignment: .top, spacing: Layout.columnSpacing) { composeColumn(title: "Configuration", icon: "slider.horizontal.3") { configurationPanel } .frame(width: Layout.columnWidth) Rectangle() .fill(AppTheme.border) .frame(width: 1) .padding(.vertical, 2) composeColumn(title: "Your Comment", icon: "bubble.left") { editorPanel } .frame(maxWidth: .infinity) } } private func composeColumn( 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.accentGreen) 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: "Comment Type", subtitle: "Top-level or reply to a comment") { CommentTypePicker(selectedType: Binding( get: { viewModel.draft.commentType }, set: { viewModel.selectCommentType($0) } )) } PostFormCard(title: "Thread Context", subtitle: "What are you commenting on?") { VStack(alignment: .leading, spacing: 12) { PostFormField( label: "Subreddit", placeholder: "technology", text: $viewModel.draft.subreddit, prefix: "r/" ) if !viewModel.draft.subreddit.isEmpty, !PostDraftValidator.isValidSubreddit(viewModel.draft.subreddit) { validationHint("Use 3–21 characters: letters, numbers, underscores only.") } PostFormField( label: "Post Title", placeholder: "Paste or type the post title", text: $viewModel.draft.postTitle ) PostFormTextEditor( label: "Post Body (optional)", placeholder: "Excerpt from the post for better context…", text: $viewModel.draft.postBody, minHeight: 70 ) if viewModel.draft.commentType == .reply { PostFormTextEditor( label: "Parent Comment", placeholder: "Paste the comment you're replying to…", text: $viewModel.draft.parentComment, minHeight: 90 ) } PostFormField( label: "Post URL (optional)", placeholder: "https://reddit.com/r/…/comments/…", text: $viewModel.draft.postURL ) if !viewModel.draft.postURL.isEmpty, !CommentDraftValidator.isValidRedditURL(viewModel.draft.postURL) { validationHint("Enter a valid reddit.com URL to open the thread.") } } } PostFormCard(title: "Comment Intent", subtitle: "What should this comment achieve?") { CommentIntentPicker(selectedIntent: $viewModel.draft.intent) } PostFormCard(title: "AI Settings", subtitle: "Tone, length, and style") { VStack(alignment: .leading, spacing: 12) { PostFormField( label: "Focus / Angle (optional)", placeholder: "e.g. share personal experience", text: $viewModel.draft.topic ) VStack(alignment: .leading, spacing: 6) { Text("Tone") .font(.system(size: 10, weight: .medium)) .foregroundStyle(AppTheme.textTertiary) CommentTonePicker(selectedTone: $viewModel.draft.tone) } VStack(alignment: .leading, spacing: 6) { Text("Length") .font(.system(size: 10, weight: .medium)) .foregroundStyle(AppTheme.textTertiary) CommentLengthPicker(selectedLength: $viewModel.draft.length) } CommentToggleRow( title: "Use Markdown", subtitle: "Bold, italic, quotes, links", isOn: $viewModel.draft.useMarkdown ) if viewModel.draft.commentType == .reply { CommentToggleRow( title: "Quote Parent", subtitle: "Include > blockquote of parent comment", isOn: $viewModel.draft.includeQuote ) } VStack(alignment: .leading, spacing: 6) { Text("Variants") .font(.system(size: 10, weight: .medium)) .foregroundStyle(AppTheme.textTertiary) CommentVariantCountPicker(selectedCount: $viewModel.draft.variantCount) } } } RedditCommentEtiquetteCard() } } private var editorPanel: some View { VStack(spacing: Layout.sectionSpacing) { PostFormCard( title: "Your Comment", subtitle: viewModel.draft.useMarkdown ? "Markdown supported — edit after generating" : "Plain text — edit after generating" ) { PostFormTextEditor( label: "Comment Body", placeholder: "Generate a comment or write your own…", text: $viewModel.draft.body, minHeight: 190 ) } characterCountFooter RedditThreadContextCard( postTitle: viewModel.draft.postTitle, postBody: viewModel.draft.postBody, parentComment: viewModel.draft.parentComment, commentType: viewModel.draft.commentType, formattedSubreddit: viewModel.formattedSubreddit ) } } private var characterCountFooter: some View { HStack(spacing: 12) { Text("\(viewModel.draft.body.count) / \(CommentDraftValidator.maxCommentLength)") .font(.system(size: 10, weight: .medium, design: .monospaced)) .foregroundStyle( viewModel.draft.body.count > CommentDraftValidator.maxCommentLength ? Color(hex: 0xF87171) : AppTheme.textTertiary ) Spacer() Text("Reddit limit: 10,000 characters") .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: - 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) { RedditCommentPreviewCard( commentBody: viewModel.activeCommentBody, formattedSubreddit: viewModel.formattedSubreddit, commentType: viewModel.draft.commentType, parentComment: viewModel.draft.parentComment ) previewMetadata } .frame(maxWidth: 560) Spacer(minLength: 0) } } } private var previewMetadata: some View { VStack(alignment: .leading, spacing: 0) { metadataRow(label: "Type", value: viewModel.draft.commentType.title) metadataDivider metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit) metadataDivider metadataRow(label: "Intent", value: viewModel.draft.intent.title) metadataDivider metadataRow(label: "Tone", value: viewModel.draft.tone.title) metadataDivider metadataRow(label: "Length", value: viewModel.draft.length.title) metadataDivider metadataRow(label: "Engine", value: viewModel.usesLiveAI ? "OpenAI" : "Local templates") if let best = viewModel.bestVariant { metadataDivider metadataRow(label: "Best Score", value: "\(best.score)/100") } } .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) ) ) } // MARK: - Variants private var variantsContent: some View { VStack(alignment: .leading, spacing: Layout.columnSpacing) { variantsHeader if viewModel.variants.isEmpty { CommentWriterEmptyState( icon: "bubble.left.and.bubble.right", title: "No variants yet", subtitle: "Fill in thread context on the Compose tab,\nthen tap Generate to get comment variants." ) .frame(maxWidth: .infinity) } else { variantsBody } } } private var variantsBody: some View { VStack(spacing: Layout.sectionSpacing) { VStack(spacing: 8) { ForEach(viewModel.variants) { variant in CommentVariantRow( variant: variant, isSelected: viewModel.selectedVariantID == variant.id, onSelect: { viewModel.selectVariant(variant) }, onApply: { viewModel.applyVariant(variant) } ) } } if !viewModel.activeCommentBody.isEmpty { PostFormCard(title: "Selected Preview", subtitle: "How this variant looks in a thread") { RedditCommentPreviewCard( commentBody: viewModel.activeCommentBody, formattedSubreddit: viewModel.formattedSubreddit, commentType: viewModel.draft.commentType, parentComment: viewModel.draft.parentComment ) } } } .frame(maxWidth: 640) .frame(maxWidth: .infinity) } private var variantsHeader: some View { HStack(alignment: .top) { sectionHeader(title: "Comment Variants", icon: "bubble.left.and.bubble.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 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 func validationHint(_ message: String) -> some View { HStack(spacing: 4) { Image(systemName: "exclamationmark.circle.fill") .font(.system(size: 9)) Text(message) .font(.system(size: 9)) } .foregroundStyle(Color(hex: 0xF87171)) } } #Preview { CommentWriterView(viewModel: CommentWriterViewModel()) .frame(width: 880, height: 720) }