| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- import SwiftUI
- // MARK: - Pickers
- struct CommentTypePicker: View {
- @Binding var selectedType: CommentType
- var body: some View {
- HStack(spacing: 8) {
- ForEach(CommentType.allCases) { type in
- Button {
- selectedType = type
- } label: {
- HStack(spacing: 8) {
- Image(systemName: type.systemImage)
- .font(.system(size: 12, weight: .semibold))
- .foregroundStyle(
- selectedType == type ? AppTheme.accentGreen : AppTheme.textSecondary
- )
- .frame(width: 16)
- VStack(alignment: .leading, spacing: 1) {
- Text(type.title)
- .font(.system(size: 11, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text(type.subtitle)
- .font(.system(size: 9))
- .foregroundStyle(AppTheme.textTertiary)
- .lineLimit(1)
- }
- Spacer(minLength: 0)
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal, 10)
- .padding(.vertical, 8)
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(
- selectedType == type
- ? AppTheme.accentGreen.opacity(0.15)
- : AppTheme.panelBackground
- )
- )
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(
- selectedType == type
- ? AppTheme.accentGreen.opacity(0.5)
- : AppTheme.cardBorder,
- lineWidth: 1
- )
- )
- }
- .buttonStyle(AppPlainButtonStyle())
- .hoverOverlay(cornerRadius: 8)
- }
- }
- }
- }
- struct CommentIntentPicker: View {
- @Binding var selectedIntent: CommentIntent
- var body: some View {
- LazyVGrid(
- columns: [GridItem(.flexible()), GridItem(.flexible())],
- spacing: 8
- ) {
- ForEach(CommentIntent.allCases) { intent in
- Button {
- selectedIntent = intent
- } label: {
- HStack(spacing: 8) {
- Image(systemName: intent.systemImage)
- .font(.system(size: 11, weight: .semibold))
- .foregroundStyle(
- selectedIntent == intent ? AppTheme.accentGreen : AppTheme.textSecondary
- )
- .frame(width: 14)
- VStack(alignment: .leading, spacing: 1) {
- Text(intent.title)
- .font(.system(size: 11, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text(intent.subtitle)
- .font(.system(size: 9))
- .foregroundStyle(AppTheme.textTertiary)
- .lineLimit(1)
- }
- Spacer(minLength: 0)
- }
- .padding(.horizontal, 10)
- .padding(.vertical, 8)
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(
- selectedIntent == intent
- ? AppTheme.accentGreen.opacity(0.15)
- : AppTheme.panelBackground
- )
- )
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(
- selectedIntent == intent
- ? AppTheme.accentGreen.opacity(0.5)
- : AppTheme.cardBorder,
- lineWidth: 1
- )
- )
- }
- .buttonStyle(AppPlainButtonStyle())
- .hoverOverlay(cornerRadius: 8)
- }
- }
- }
- }
- struct CommentLengthPicker: View {
- @Binding var selectedLength: CommentLength
- var body: some View {
- HStack(spacing: 8) {
- ForEach(CommentLength.allCases) { length in
- Button {
- selectedLength = length
- } label: {
- VStack(spacing: 2) {
- Text(length.title)
- .font(.system(size: 10, weight: .semibold))
- Text(length.wordRange)
- .font(.system(size: 8))
- .foregroundStyle(
- selectedLength == length ? .white.opacity(0.8) : AppTheme.textTertiary
- )
- }
- .foregroundStyle(selectedLength == length ? .white : AppTheme.textSecondary)
- .padding(.horizontal, 12)
- .padding(.vertical, 8)
- .frame(maxWidth: .infinity)
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(
- selectedLength == length
- ? AppTheme.accentGreen
- : AppTheme.panelBackground
- )
- )
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(AppTheme.cardBorder, lineWidth: 1)
- )
- }
- .buttonStyle(AppPlainButtonStyle())
- .hoverOverlay(cornerRadius: 8)
- }
- }
- }
- }
- struct CommentTonePicker: View {
- @Binding var selectedTone: PostTone
- var body: some View {
- ScrollView(.horizontal, showsIndicators: false) {
- HStack(spacing: 6) {
- ForEach(PostTone.allCases) { tone in
- Button {
- selectedTone = tone
- } label: {
- Text(tone.title)
- .font(.system(size: 10, weight: .semibold))
- .foregroundStyle(selectedTone == tone ? .white : AppTheme.textSecondary)
- .padding(.horizontal, 10)
- .padding(.vertical, 6)
- .background(
- Capsule()
- .fill(
- selectedTone == tone
- ? AppTheme.accentGreen
- : AppTheme.panelBackground
- )
- )
- .overlay(
- Capsule()
- .stroke(
- selectedTone == tone
- ? AppTheme.accentGreen.opacity(0.5)
- : AppTheme.cardBorder,
- lineWidth: 1
- )
- )
- }
- .buttonStyle(AppPlainButtonStyle())
- .hoverOverlay(cornerRadius: 50)
- }
- }
- }
- }
- }
- struct CommentVariantCountPicker: View {
- @Binding var selectedCount: CommentVariantCount
- var body: some View {
- HStack(spacing: 8) {
- ForEach(CommentVariantCount.allCases) { count in
- Button {
- selectedCount = count
- } label: {
- Text(count.label)
- .font(.system(size: 10, weight: .semibold))
- .foregroundStyle(selectedCount == count ? .white : AppTheme.textSecondary)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 7)
- .background(
- RoundedRectangle(cornerRadius: 7)
- .fill(
- selectedCount == count
- ? AppTheme.accentGreen
- : AppTheme.panelBackground
- )
- )
- .overlay(
- RoundedRectangle(cornerRadius: 7)
- .stroke(AppTheme.cardBorder, lineWidth: 1)
- )
- }
- .buttonStyle(AppPlainButtonStyle())
- .hoverOverlay(cornerRadius: 8)
- }
- }
- }
- }
- struct CommentToggleRow: View {
- let title: String
- let subtitle: String
- @Binding var isOn: Bool
- var body: some View {
- Toggle(isOn: $isOn) {
- VStack(alignment: .leading, spacing: 2) {
- Text(title)
- .font(.system(size: 11, weight: .medium))
- .foregroundStyle(AppTheme.textPrimary)
- Text(subtitle)
- .font(.system(size: 9))
- .foregroundStyle(AppTheme.textTertiary)
- }
- }
- .toggleStyle(.switch)
- .tint(AppTheme.accentGreen)
- .padding(.horizontal, 10)
- .padding(.vertical, 8)
- .hoverOverlay(cornerRadius: 8)
- .hoverCursor()
- }
- }
- // MARK: - Reddit Etiquette Card
- struct RedditCommentEtiquetteCard: View {
- var body: some View {
- VStack(alignment: .leading, spacing: 8) {
- HStack(spacing: 6) {
- Image(systemName: "info.circle.fill")
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.accentGreen)
- Text("Reddit Comment Tips")
- .font(.system(size: 11, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- }
- VStack(alignment: .leading, spacing: 4) {
- etiquetteRow("10,000 character limit per comment")
- etiquetteRow("Markdown: **bold**, *italic*, > quotes, `code`")
- etiquetteRow("Stay on-topic for the subreddit")
- etiquetteRow("Be civil — disagree with ideas, not people")
- etiquetteRow("Edit with \"Edit:\" to clarify, don't bait-and-switch")
- }
- }
- .padding(12)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .fill(AppTheme.accentGreen.opacity(0.06))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .stroke(AppTheme.accentGreen.opacity(0.2), lineWidth: 1)
- )
- )
- }
- private func etiquetteRow(_ text: String) -> some View {
- HStack(alignment: .top, spacing: 6) {
- Text("•")
- .font(.system(size: 10))
- .foregroundStyle(AppTheme.accentGreen)
- Text(text)
- .font(.system(size: 10))
- .foregroundStyle(AppTheme.textSecondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- }
- // MARK: - Thread Context Card
- struct RedditThreadContextCard: View {
- let postTitle: String
- let postBody: String
- let parentComment: String
- let commentType: CommentType
- let formattedSubreddit: String
- var body: some View {
- VStack(alignment: .leading, spacing: 10) {
- HStack(spacing: 6) {
- Image(systemName: "doc.text")
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.accentOrange)
- Text("Thread Context")
- .font(.system(size: 11, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- }
- VStack(alignment: .leading, spacing: 8) {
- contextBlock(
- label: formattedSubreddit,
- content: postTitle.isEmpty ? "Post title" : postTitle,
- isTitle: true
- )
- if !postBody.isEmpty {
- contextBlock(label: "Post body", content: postBody, isTitle: false)
- }
- if commentType == .reply, !parentComment.isEmpty {
- contextBlock(label: "Replying to", content: parentComment, isTitle: false, isQuoted: true)
- }
- }
- }
- .padding(12)
- .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 contextBlock(
- label: String,
- content: String,
- isTitle: Bool,
- isQuoted: Bool = false
- ) -> some View {
- VStack(alignment: .leading, spacing: 4) {
- Text(label)
- .font(.system(size: 9, weight: .semibold))
- .foregroundStyle(AppTheme.textTertiary)
- if isQuoted {
- HStack(spacing: 0) {
- Rectangle()
- .fill(AppTheme.accentGreen.opacity(0.5))
- .frame(width: 3)
- Text(content)
- .font(.system(size: 10))
- .foregroundStyle(AppTheme.textSecondary)
- .lineLimit(3)
- .padding(.leading, 8)
- }
- .padding(.vertical, 4)
- } else {
- Text(content)
- .font(.system(size: isTitle ? 11 : 10, weight: isTitle ? .semibold : .regular))
- .foregroundStyle(isTitle ? AppTheme.textPrimary : AppTheme.textSecondary)
- .lineLimit(isTitle ? 2 : 3)
- }
- }
- }
- }
- // MARK: - Comment Preview
- struct RedditCommentPreviewCard: View {
- let commentBody: String
- let formattedSubreddit: String
- let commentType: CommentType
- let parentComment: String
- var isReply: Bool = false
- var body: some View {
- VStack(alignment: .leading, spacing: 0) {
- if commentType == .reply, !parentComment.isEmpty {
- parentCommentRow
- .padding(.leading, 14)
- }
- commentRow
- .padding(.leading, commentType == .reply ? 28 : 0)
- }
- .background(AppTheme.panelBackground)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.cornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.cornerRadius)
- .stroke(AppTheme.cardBorder, lineWidth: 1)
- )
- }
- private var parentCommentRow: some View {
- HStack(alignment: .top, spacing: 8) {
- voteColumn
- VStack(alignment: .leading, spacing: 6) {
- commentHeader(username: "u/OriginalPoster")
- Text(parentComment)
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textSecondary)
- .lineLimit(3)
- .fixedSize(horizontal: false, vertical: true)
- commentActions
- }
- }
- .padding(.horizontal, 14)
- .padding(.vertical, 12)
- .opacity(0.7)
- }
- private var commentRow: some View {
- HStack(alignment: .top, spacing: 8) {
- voteColumn
- VStack(alignment: .leading, spacing: 6) {
- commentHeader(username: "u/ReddoraUser")
- if commentBody.isEmpty {
- Text("Your comment will appear here…")
- .font(.system(size: 12))
- .foregroundStyle(AppTheme.textTertiary)
- .italic()
- } else {
- PostMarkdownText(text: commentBody, fontSize: 12, color: AppTheme.textPrimary)
- }
- commentActions
- }
- }
- .padding(.horizontal, 14)
- .padding(.vertical, 12)
- .background(
- commentType == .reply
- ? AppTheme.accentGreen.opacity(0.04)
- : Color.clear
- )
- }
- private var voteColumn: some View {
- VStack(spacing: 2) {
- Image(systemName: "arrow.up")
- .font(.system(size: 12, weight: .semibold))
- .foregroundStyle(AppTheme.textTertiary)
- Text("1")
- .font(.system(size: 10, weight: .semibold))
- .foregroundStyle(AppTheme.textSecondary)
- Image(systemName: "arrow.down")
- .font(.system(size: 12, weight: .semibold))
- .foregroundStyle(AppTheme.textTertiary)
- }
- .frame(width: 24)
- }
- private func commentHeader(username: String) -> some View {
- HStack(spacing: 4) {
- Text(username)
- .font(.system(size: 10, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text("·")
- .foregroundStyle(AppTheme.textTertiary)
- Text("just now")
- .font(.system(size: 9))
- .foregroundStyle(AppTheme.textTertiary)
- if commentType == .reply, username == "u/ReddoraUser" {
- Text("·")
- .foregroundStyle(AppTheme.textTertiary)
- Text(formattedSubreddit)
- .font(.system(size: 9, weight: .medium))
- .foregroundStyle(AppTheme.accentGreen)
- }
- }
- }
- private var commentActions: some View {
- HStack(spacing: 12) {
- actionButton("Reply", icon: "arrowshape.turn.up.left")
- actionButton("Share", icon: "square.and.arrow.up")
- actionButton("Award", icon: "seal")
- actionButton("⋯", icon: nil)
- }
- .padding(.top, 2)
- }
- private func actionButton(_ title: String, icon: String?) -> some View {
- HStack(spacing: 4) {
- if let icon {
- Image(systemName: icon)
- .font(.system(size: 9, weight: .semibold))
- }
- Text(title)
- .font(.system(size: 9, weight: .semibold))
- }
- .foregroundStyle(AppTheme.textTertiary)
- }
- }
- // MARK: - Variant Row
- struct CommentVariantRow: View {
- let variant: CommentVariant
- let isSelected: Bool
- let onSelect: () -> Void
- let onApply: () -> Void
- var body: some View {
- VStack(alignment: .leading, spacing: 8) {
- HStack {
- CommentScoreBadge(score: variant.score)
- Text(variant.reasoning)
- .font(.system(size: 9))
- .foregroundStyle(AppTheme.textTertiary)
- .lineLimit(1)
- Spacer()
- HStack(spacing: 6) {
- Button("Select", action: onSelect)
- .font(.system(size: 9, weight: .semibold))
- .foregroundStyle(isSelected ? AppTheme.accentGreen : AppTheme.textSecondary)
- .buttonStyle(AppPlainButtonStyle())
- .hoverOverlay(cornerRadius: 8)
- Button("Apply", action: onApply)
- .font(.system(size: 9, weight: .semibold))
- .foregroundStyle(.white)
- .padding(.horizontal, 8)
- .padding(.vertical, 4)
- .background(AppTheme.accentGreen.opacity(0.8))
- .clipShape(RoundedRectangle(cornerRadius: 5))
- .buttonStyle(AppPlainButtonStyle())
- .hoverOverlay(cornerRadius: 8)
- }
- }
- Text(variant.body)
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textSecondary)
- .lineLimit(4)
- .fixedSize(horizontal: false, vertical: true)
- }
- .padding(12)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(isSelected ? AppTheme.accentGreen.opacity(0.1) : AppTheme.panelBackground)
- )
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(
- isSelected ? AppTheme.accentGreen.opacity(0.4) : AppTheme.cardBorder,
- lineWidth: 1
- )
- )
- .hoverCard(cornerRadius: 8, isSelected: isSelected)
- .hoverCursor()
- .onTapGesture(perform: onSelect)
- }
- }
- struct CommentScoreBadge: View {
- let score: Int
- var body: some View {
- Text("\(score)")
- .font(.system(size: 10, weight: .bold))
- .foregroundStyle(scoreColor)
- .padding(.horizontal, 6)
- .padding(.vertical, 3)
- .background(scoreColor.opacity(0.15))
- .clipShape(RoundedRectangle(cornerRadius: 4))
- }
- private var scoreColor: Color {
- switch score {
- case 80...: AppTheme.accentGreen
- case 60..<80: AppTheme.accentYellow
- default: AppTheme.textTertiary
- }
- }
- }
- struct CommentWriterEmptyState: 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.accentGreen.opacity(0.6))
- Text(title)
- .font(.system(size: 12, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text(subtitle)
- .font(.system(size: 10))
- .foregroundStyle(AppTheme.textSecondary)
- .multilineTextAlignment(.center)
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 32)
- }
- }
- struct CommentWriterEmptyResultPlaceholder: View {
- var body: some View {
- VStack(spacing: 12) {
- Image(systemName: "sparkles")
- .font(.system(size: 28, weight: .medium))
- .foregroundStyle(AppTheme.accentGreen)
- VStack(spacing: 6) {
- Text("Fill Configuration, then press Generate")
- .font(.system(size: 13, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- .multilineTextAlignment(.center)
- Text("AI writes your comment from thread context and settings on the left. You don't need to fill anything here first.")
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textSecondary)
- .multilineTextAlignment(.center)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- .padding(.horizontal, 24)
- .padding(.vertical, 36)
- .frame(maxWidth: .infinity, minHeight: 180)
- .background(AppTheme.panelBackground)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .strokeBorder(
- AppTheme.accentGreen.opacity(0.3),
- style: StrokeStyle(lineWidth: 1, dash: [6, 4])
- )
- )
- }
- }
- struct CommentSecondaryButtonStyle: ButtonStyle {
- func makeBody(configuration: Configuration) -> some View {
- AppSecondaryButtonStyle().makeBody(configuration: configuration)
- }
- }
|