SummarizerView.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import SwiftUI
  2. struct SummarizerView: View {
  3. @StateObject private var viewModel = SummarizerViewModel()
  4. var body: some View {
  5. GeometryReader { proxy in
  6. let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
  7. let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
  8. Group {
  9. if viewModel.summary.isEmpty {
  10. VStack(alignment: .leading, spacing: 0) {
  11. header
  12. .padding(.bottom, 24)
  13. contentCard
  14. .frame(maxWidth: .infinity, maxHeight: .infinity)
  15. if let errorMessage = viewModel.errorMessage {
  16. Text(errorMessage)
  17. .font(.system(size: 14))
  18. .foregroundStyle(.red)
  19. .padding(.top, 12)
  20. }
  21. GradientButton(title: viewModel.isSummarizing ? "Summarizing..." : "Summarize") {
  22. viewModel.summarize()
  23. }
  24. .disabled(!viewModel.canSummarize)
  25. .padding(.top, 16)
  26. }
  27. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
  28. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  29. .padding(.horizontal, dynamicHorizontalPadding)
  30. .padding(.top, AppTheme.brandLabelTopInset)
  31. .padding(.bottom, dynamicBottomPadding)
  32. } else {
  33. ScrollView(.vertical, showsIndicators: false) {
  34. VStack(alignment: .leading, spacing: 0) {
  35. header
  36. .padding(.bottom, 24)
  37. contentCard
  38. .padding(.bottom, 24)
  39. outputCard
  40. .padding(.bottom, 24)
  41. if let errorMessage = viewModel.errorMessage {
  42. Text(errorMessage)
  43. .font(.system(size: 14))
  44. .foregroundStyle(.red)
  45. .padding(.bottom, 12)
  46. }
  47. GradientButton(title: viewModel.isSummarizing ? "Summarizing..." : "Summarize") {
  48. viewModel.summarize()
  49. }
  50. .disabled(!viewModel.canSummarize)
  51. }
  52. .frame(maxWidth: 1200, alignment: .topLeading)
  53. .frame(maxWidth: .infinity, alignment: .topLeading)
  54. .padding(.horizontal, dynamicHorizontalPadding)
  55. .padding(.top, AppTheme.brandLabelTopInset)
  56. .padding(.bottom, dynamicBottomPadding)
  57. }
  58. }
  59. }
  60. }
  61. .onChange(of: viewModel.text) { newValue in
  62. if newValue.wordCount > SummarizerViewModel.maxWords {
  63. viewModel.text = newValue.truncated(toMaxWords: SummarizerViewModel.maxWords)
  64. }
  65. }
  66. }
  67. private var header: some View {
  68. HStack(alignment: .top) {
  69. VStack(alignment: .leading, spacing: 8) {
  70. Text("Summarizer")
  71. .font(.system(size: 30, weight: .bold))
  72. .foregroundStyle(AppTheme.textPrimary)
  73. HStack(spacing: 0) {
  74. Text("Read ")
  75. .foregroundStyle(AppTheme.textSecondary)
  76. Text("less")
  77. .fontWeight(.semibold)
  78. .foregroundStyle(AppTheme.teal)
  79. Text(". Understand ")
  80. .foregroundStyle(AppTheme.textSecondary)
  81. Text("more")
  82. .fontWeight(.semibold)
  83. .foregroundStyle(AppTheme.teal)
  84. Text(".")
  85. .foregroundStyle(AppTheme.textSecondary)
  86. }
  87. .font(.system(size: 15))
  88. }
  89. Spacer()
  90. IconButton {
  91. SettingsGearIcon()
  92. } action: {}
  93. .accessibilityLabel("Settings")
  94. }
  95. }
  96. private var contentCard: some View {
  97. VStack(alignment: .leading, spacing: 16) {
  98. Image("YourContentLogo")
  99. .resizable()
  100. .aspectRatio(contentMode: .fit)
  101. .frame(height: 52, alignment: .topLeading)
  102. .frame(height: 46, alignment: .topLeading)
  103. .clipped()
  104. .accessibilityLabel("Your Content")
  105. VStack(alignment: .leading, spacing: 6) {
  106. ThinCaretTextEditor(
  107. text: $viewModel.text,
  108. placeholder: "Type or paste your text here and press the summarize button..."
  109. )
  110. .font(.system(size: 14))
  111. .foregroundStyle(AppTheme.textPrimary)
  112. .frame(maxWidth: .infinity, maxHeight: .infinity)
  113. .background(Color.white)
  114. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  115. .overlay(
  116. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  117. .stroke(AppTheme.inputBorder, lineWidth: 1)
  118. )
  119. Text(viewModel.wordCountLimitLabel)
  120. .font(.system(size: 11))
  121. .foregroundStyle(AppTheme.textMuted)
  122. .frame(maxWidth: .infinity, alignment: .trailing)
  123. }
  124. .frame(maxWidth: .infinity, maxHeight: .infinity)
  125. HStack(spacing: 8) {
  126. ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
  127. viewModel.pasteText()
  128. }
  129. ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {}
  130. Spacer()
  131. ClearButton {
  132. viewModel.clearText()
  133. }
  134. }
  135. }
  136. .padding(22)
  137. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  138. .background(AppTheme.cardBackground)
  139. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  140. .overlay(
  141. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  142. .stroke(AppTheme.border, lineWidth: 1)
  143. )
  144. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  145. }
  146. private var outputCard: some View {
  147. VStack(alignment: .leading, spacing: 12) {
  148. HStack {
  149. Text("Summary")
  150. .font(.system(size: 13, weight: .semibold))
  151. .foregroundStyle(AppTheme.textPrimary)
  152. Spacer()
  153. ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
  154. viewModel.copySummary()
  155. }
  156. }
  157. Text(viewModel.summary)
  158. .font(.system(size: 14))
  159. .foregroundStyle(AppTheme.textPrimary)
  160. .frame(maxWidth: .infinity, alignment: .leading)
  161. .padding(16)
  162. .background(AppTheme.tealLight.opacity(0.35))
  163. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  164. .overlay(
  165. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  166. .stroke(AppTheme.teal.opacity(0.2), lineWidth: 1)
  167. )
  168. }
  169. .padding(22)
  170. .background(AppTheme.cardBackground)
  171. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  172. .overlay(
  173. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  174. .stroke(AppTheme.border, lineWidth: 1)
  175. )
  176. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  177. }
  178. }