ParaphrasingView.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import SwiftUI
  2. struct ParaphrasingView: View {
  3. @StateObject private var viewModel = ParaphrasingViewModel()
  4. @Environment(\.onSettingsTapped) private var onSettingsTapped
  5. var body: some View {
  6. GeometryReader { proxy in
  7. let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
  8. let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
  9. Group {
  10. if viewModel.isParaphrasing {
  11. loadingLayout(
  12. horizontalPadding: dynamicHorizontalPadding,
  13. topPadding: AppTheme.brandLabelTopInset,
  14. bottomPadding: dynamicBottomPadding
  15. )
  16. } else if viewModel.hasResults {
  17. VStack(alignment: .leading, spacing: 0) {
  18. header
  19. .padding(.bottom, 20)
  20. resultsLayout
  21. .frame(maxWidth: .infinity, maxHeight: .infinity)
  22. }
  23. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
  24. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  25. .padding(.horizontal, dynamicHorizontalPadding)
  26. .padding(.top, AppTheme.brandLabelTopInset)
  27. .padding(.bottom, dynamicBottomPadding)
  28. } else {
  29. VStack(alignment: .leading, spacing: 0) {
  30. header
  31. .padding(.bottom, 24)
  32. contentCard
  33. .frame(maxWidth: .infinity, maxHeight: .infinity)
  34. if let errorMessage = viewModel.errorMessage {
  35. Text(errorMessage)
  36. .font(.system(size: 14))
  37. .foregroundStyle(.red)
  38. .padding(.top, 12)
  39. }
  40. GradientButton(title: viewModel.isParaphrasing ? "Paraphrasing..." : "Paraphrase") {
  41. viewModel.paraphrase()
  42. }
  43. .disabled(!viewModel.canParaphrase)
  44. .padding(.top, 16)
  45. }
  46. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
  47. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  48. .padding(.horizontal, dynamicHorizontalPadding)
  49. .padding(.top, AppTheme.brandLabelTopInset)
  50. .padding(.bottom, dynamicBottomPadding)
  51. }
  52. }
  53. }
  54. .onChange(of: viewModel.text) { _ in
  55. viewModel.handleTextChange()
  56. }
  57. }
  58. private func loadingLayout(
  59. horizontalPadding: CGFloat,
  60. topPadding: CGFloat,
  61. bottomPadding: CGFloat
  62. ) -> some View {
  63. VStack(spacing: 14) {
  64. ProgressView()
  65. .controlSize(.regular)
  66. Text("Loading...")
  67. .font(.system(size: 16, weight: .semibold))
  68. .foregroundStyle(AppTheme.textPrimary)
  69. Text("Paraphrasing your content")
  70. .font(.system(size: 14))
  71. .foregroundStyle(AppTheme.textSecondary)
  72. }
  73. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .center)
  74. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
  75. .padding(.horizontal, horizontalPadding)
  76. .padding(.top, topPadding)
  77. .padding(.bottom, bottomPadding)
  78. }
  79. private var header: some View {
  80. VStack(alignment: .leading, spacing: 16) {
  81. if viewModel.hasResults {
  82. IconButton(iconName: "arrow.left") {
  83. viewModel.resetResults()
  84. }
  85. .accessibilityLabel("Go back")
  86. }
  87. HStack(alignment: .top) {
  88. VStack(alignment: .leading, spacing: 8) {
  89. Text("Paraphrasing")
  90. .font(.system(size: 30, weight: .bold))
  91. .foregroundStyle(AppTheme.textPrimary)
  92. HStack(spacing: 0) {
  93. Text("Say it ")
  94. .foregroundStyle(AppTheme.textSecondary)
  95. Text("differently")
  96. .fontWeight(.semibold)
  97. .foregroundStyle(AppTheme.teal)
  98. Text(". Keep it ")
  99. .foregroundStyle(AppTheme.textSecondary)
  100. Text("yours")
  101. .fontWeight(.semibold)
  102. .foregroundStyle(AppTheme.teal)
  103. Text(".")
  104. .foregroundStyle(AppTheme.textSecondary)
  105. }
  106. .font(.system(size: 15))
  107. }
  108. Spacer()
  109. IconButton(icon: { SettingsGearIcon() }, action: onSettingsTapped)
  110. .accessibilityLabel("Settings")
  111. }
  112. }
  113. }
  114. private var contentCard: some View {
  115. VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
  116. YourContentHeader()
  117. VStack(alignment: .leading, spacing: 6) {
  118. textEditor
  119. .background(Color.white)
  120. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  121. .overlay(
  122. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  123. .stroke(AppTheme.inputBorder, lineWidth: 1)
  124. )
  125. Text(viewModel.wordCountLimitLabel)
  126. .font(.system(size: 11))
  127. .foregroundStyle(AppTheme.textMuted)
  128. .frame(maxWidth: .infinity, alignment: .trailing)
  129. }
  130. .scrollSafeEditorSectionFrame(compact: false)
  131. HStack(spacing: 8) {
  132. ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
  133. viewModel.pasteText()
  134. }
  135. ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {
  136. viewModel.uploadFile()
  137. }
  138. Spacer()
  139. ClearButton {
  140. viewModel.clearText()
  141. }
  142. }
  143. }
  144. .inputContentCardPadding()
  145. .scrollSafeContentCardFrame(compact: false)
  146. .background(AppTheme.cardBackground)
  147. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  148. .overlay(
  149. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  150. .stroke(AppTheme.border, lineWidth: 1)
  151. )
  152. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  153. }
  154. private var textEditor: some View {
  155. ThinCaretTextEditor(
  156. text: $viewModel.text,
  157. placeholder: "Type or paste your text here and press the paraphrase button...",
  158. maxWords: ParaphrasingViewModel.maxWords,
  159. onWordLimitReached: viewModel.notifyWordLimitReached
  160. )
  161. .font(.system(size: 14))
  162. .foregroundStyle(AppTheme.textPrimary)
  163. .scrollSafeTextEditorFrame(compact: false)
  164. }
  165. private var resultsLayout: some View {
  166. paraphrasedTextCard
  167. }
  168. private var paraphrasedTextCard: some View {
  169. VStack(alignment: .leading, spacing: 12) {
  170. HStack {
  171. Text("Paraphrased Text")
  172. .font(.system(size: 13, weight: .semibold))
  173. .foregroundStyle(AppTheme.textPrimary)
  174. Spacer()
  175. ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
  176. viewModel.copyParaphrasedText()
  177. }
  178. .disabled(viewModel.paraphrasedText.isEmpty)
  179. .opacity(viewModel.paraphrasedText.isEmpty ? 0.55 : 1)
  180. }
  181. ScrollView(.vertical, showsIndicators: true) {
  182. Text(viewModel.paraphrasedText)
  183. .font(.system(size: 14))
  184. .foregroundStyle(AppTheme.textPrimary)
  185. .frame(maxWidth: .infinity, alignment: .leading)
  186. .textSelection(.enabled)
  187. .padding(16)
  188. }
  189. .frame(maxHeight: .infinity, alignment: .top)
  190. .background(AppTheme.tealLight.opacity(0.35))
  191. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  192. .overlay(
  193. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  194. .stroke(AppTheme.teal.opacity(0.2), lineWidth: 1)
  195. )
  196. }
  197. .padding(AppTheme.contentCardPadding)
  198. .background(AppTheme.cardBackground)
  199. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  200. .overlay(
  201. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  202. .stroke(AppTheme.border, lineWidth: 1)
  203. )
  204. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  205. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  206. }
  207. }