Преглед изворни кода

Show full-screen loading and separate results page for summarizer.

Align Summarizer with the grammar-check flow by presenting a dedicated loading screen during processing and a distinct results view with back navigation once the summary is ready.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 пре 1 месец
родитељ
комит
b4fd6b6ecd
2 измењених фајлова са 99 додато и 82 уклоњено
  1. 8 0
      gramora/ViewModels/SummarizerViewModel.swift
  2. 91 82
      gramora/Views/SummarizerView.swift

+ 8 - 0
gramora/ViewModels/SummarizerViewModel.swift

@@ -30,6 +30,10 @@ final class SummarizerViewModel: ObservableObject {
         !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSummarizing
     }
 
+    var hasResults: Bool {
+        !summary.isEmpty
+    }
+
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
         text = WordLimit.applyImported(clipboard, maxWords: Self.maxWords, setError: { errorMessage = $0 })
@@ -67,6 +71,10 @@ final class SummarizerViewModel: ObservableObject {
 
     func clearText() {
         text = ""
+        resetResults()
+    }
+
+    func resetResults() {
         summary = ""
         errorMessage = nil
     }

+ 91 - 82
gramora/Views/SummarizerView.swift

@@ -3,17 +3,32 @@ import SwiftUI
 struct SummarizerView: View {
     @StateObject private var viewModel = SummarizerViewModel()
 
-    private var showsOutputLayout: Bool {
-        !viewModel.summary.isEmpty || viewModel.isSummarizing
-    }
-
     var body: some View {
         GeometryReader { proxy in
             let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
             let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
 
             Group {
-                if !showsOutputLayout {
+                if viewModel.isSummarizing {
+                    loadingLayout(
+                        horizontalPadding: dynamicHorizontalPadding,
+                        topPadding: AppTheme.brandLabelTopInset,
+                        bottomPadding: dynamicBottomPadding
+                    )
+                } else if viewModel.hasResults {
+                    VStack(alignment: .leading, spacing: 0) {
+                        header
+                            .padding(.bottom, 20)
+
+                        resultsLayout
+                            .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    }
+                    .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
+                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+                    .padding(.horizontal, dynamicHorizontalPadding)
+                    .padding(.top, AppTheme.brandLabelTopInset)
+                    .padding(.bottom, dynamicBottomPadding)
+                } else {
                     VStack(alignment: .leading, spacing: 0) {
                         header
                             .padding(.bottom, 24)
@@ -39,40 +54,6 @@ struct SummarizerView: View {
                     .padding(.horizontal, dynamicHorizontalPadding)
                     .padding(.top, AppTheme.brandLabelTopInset)
                     .padding(.bottom, dynamicBottomPadding)
-                } else {
-                    VStack(alignment: .leading, spacing: 0) {
-                        header
-                            .padding(.bottom, 24)
-
-                        ScrollView(.vertical, showsIndicators: false) {
-                            VStack(alignment: .leading, spacing: 0) {
-                                contentCard
-                                    .padding(.bottom, 24)
-
-                                outputCard
-                                    .padding(.bottom, 24)
-
-                                if let errorMessage = viewModel.errorMessage {
-                                    Text(errorMessage)
-                                        .font(.system(size: 14))
-                                        .foregroundStyle(.red)
-                                        .padding(.bottom, 12)
-                                }
-
-                                GradientButton(title: viewModel.isSummarizing ? "Summarizing..." : "Summarize") {
-                                    viewModel.summarize()
-                                }
-                                .disabled(!viewModel.canSummarize)
-                            }
-                            .padding(.bottom, dynamicBottomPadding)
-                        }
-                        .frame(maxWidth: .infinity, maxHeight: .infinity)
-                    }
-                    .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
-                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
-                    .padding(.horizontal, dynamicHorizontalPadding)
-                    .padding(.top, AppTheme.brandLabelTopInset)
-                    .padding(.bottom, dynamicBottomPadding)
                 }
             }
         }
@@ -81,43 +62,74 @@ struct SummarizerView: View {
         }
     }
 
-    private var header: some View {
-        HStack(alignment: .top) {
-            VStack(alignment: .leading, spacing: 8) {
-                Text("Summarizer")
-                    .font(.system(size: 30, weight: .bold))
-                    .foregroundStyle(AppTheme.textPrimary)
+    private func loadingLayout(
+        horizontalPadding: CGFloat,
+        topPadding: CGFloat,
+        bottomPadding: CGFloat
+    ) -> some View {
+        VStack(spacing: 14) {
+            ProgressView()
+                .controlSize(.regular)
+
+            Text("Loading...")
+                .font(.system(size: 16, weight: .semibold))
+                .foregroundStyle(AppTheme.textPrimary)
+
+            Text("Summarizing your content")
+                .font(.system(size: 14))
+                .foregroundStyle(AppTheme.textSecondary)
+        }
+        .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .center)
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
+        .padding(.horizontal, horizontalPadding)
+        .padding(.top, topPadding)
+        .padding(.bottom, bottomPadding)
+    }
 
-                HStack(spacing: 0) {
-                    Text("Read ")
-                        .foregroundStyle(AppTheme.textSecondary)
-                    Text("less")
-                        .fontWeight(.semibold)
-                        .foregroundStyle(AppTheme.teal)
-                    Text(". Understand ")
-                        .foregroundStyle(AppTheme.textSecondary)
-                    Text("more")
-                        .fontWeight(.semibold)
-                        .foregroundStyle(AppTheme.teal)
-                    Text(".")
-                        .foregroundStyle(AppTheme.textSecondary)
+    private var header: some View {
+        VStack(alignment: .leading, spacing: 16) {
+            if viewModel.hasResults {
+                IconButton(iconName: "arrow.left") {
+                    viewModel.resetResults()
                 }
-                .font(.system(size: 15))
+                .accessibilityLabel("Go back")
             }
 
-            Spacer()
+            HStack(alignment: .top) {
+                VStack(alignment: .leading, spacing: 8) {
+                    Text("Summarizer")
+                        .font(.system(size: 30, weight: .bold))
+                        .foregroundStyle(AppTheme.textPrimary)
+
+                    HStack(spacing: 0) {
+                        Text("Read ")
+                            .foregroundStyle(AppTheme.textSecondary)
+                        Text("less")
+                            .fontWeight(.semibold)
+                            .foregroundStyle(AppTheme.teal)
+                        Text(". Understand ")
+                            .foregroundStyle(AppTheme.textSecondary)
+                        Text("more")
+                            .fontWeight(.semibold)
+                            .foregroundStyle(AppTheme.teal)
+                        Text(".")
+                            .foregroundStyle(AppTheme.textSecondary)
+                    }
+                    .font(.system(size: 15))
+                }
 
-            IconButton {
-                SettingsGearIcon()
-            } action: {}
-            .accessibilityLabel("Settings")
+                Spacer()
+
+                IconButton {
+                    SettingsGearIcon()
+                } action: {}
+                .accessibilityLabel("Settings")
+            }
         }
     }
 
     private var contentCard: some View {
-        let compactEditor = showsOutputLayout
-
-        return VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
+        VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
             YourContentHeader()
 
             VStack(alignment: .leading, spacing: 6) {
@@ -134,7 +146,7 @@ struct SummarizerView: View {
                     .foregroundStyle(AppTheme.textMuted)
                     .frame(maxWidth: .infinity, alignment: .trailing)
             }
-            .scrollSafeEditorSectionFrame(compact: compactEditor)
+            .scrollSafeEditorSectionFrame(compact: false)
 
             HStack(spacing: 8) {
                 ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
@@ -153,7 +165,7 @@ struct SummarizerView: View {
             }
         }
         .inputContentCardPadding()
-        .scrollSafeContentCardFrame(compact: compactEditor)
+        .scrollSafeContentCardFrame(compact: false)
         .background(AppTheme.cardBackground)
         .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
         .overlay(
@@ -172,10 +184,14 @@ struct SummarizerView: View {
         )
         .font(.system(size: 14))
         .foregroundStyle(AppTheme.textPrimary)
-        .scrollSafeTextEditorFrame(compact: showsOutputLayout)
+        .scrollSafeTextEditorFrame(compact: false)
+    }
+
+    private var resultsLayout: some View {
+        summaryCard
     }
 
-    private var outputCard: some View {
+    private var summaryCard: some View {
         VStack(alignment: .leading, spacing: 12) {
             HStack {
                 Text("Summary")
@@ -191,17 +207,9 @@ struct SummarizerView: View {
                 .opacity(viewModel.summary.isEmpty ? 0.55 : 1)
             }
 
-            Group {
-                if viewModel.isSummarizing && viewModel.summary.isEmpty {
-                    Text("Summarizing...")
-                        .font(.system(size: 14))
-                        .foregroundStyle(AppTheme.textMuted)
-                } else {
-                    Text(viewModel.summary)
-                        .font(.system(size: 14))
-                        .foregroundStyle(AppTheme.textPrimary)
-                }
-            }
+            Text(viewModel.summary)
+                .font(.system(size: 14))
+                .foregroundStyle(AppTheme.textPrimary)
                 .frame(maxWidth: .infinity, alignment: .leading)
                 .padding(16)
                 .background(AppTheme.tealLight.opacity(0.35))
@@ -219,5 +227,6 @@ struct SummarizerView: View {
                 .stroke(AppTheme.border, lineWidth: 1)
         )
         .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
     }
 }