Переглянути джерело

Fix stuck Ai Assistant transcripts and polish related UI.

Resume Apple Speech for recordings that never finished transcribing, report when no speech is detected, remove the Show folder button from Ai Assistant, fix the terms privacy policy link, and open privacy/support/terms URLs from settings and paywall.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 місяців тому
батько
коміт
fd2572a2a1

+ 1 - 1
website/terms.html

@@ -102,7 +102,7 @@
     <li>Verifying the accuracy of AI-generated summaries before relying on them.</li>
   </ul>
   <p>
-  Ai-generated content may be incomplete or inaccurate. It does <strong>not</strong> constitute legal, medical, financial, or professional advice. Transcription uses Apple’s on-device speech recognition where supported; note generation may send transcript text to OpenAI when you request it, as described in our <a href="YOUR_PRIVACY_POLICY_URL">Privacy Policy</a>.
+  Ai-generated content may be incomplete or inaccurate. It does <strong>not</strong> constitute legal, medical, financial, or professional advice. Transcription uses Apple’s on-device speech recognition where supported; note generation may send transcript text to OpenAI when you request it, as described in our <a href="https://sites.google.com/view/aicompanionforzoom/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.
   </p>
 
   <h2>8. Intellectual property</h2>

+ 36 - 2
zoom_app/MeetingRecordingManager.swift

@@ -123,10 +123,12 @@ final class MeetingRecordingManager: NSObject {
     /// Set by the embedded `WKWebView` when its URL indicates an active join/in-call web client page.
     private var embeddedWebClientCallUIVisible: Bool = false
     private var stopRecordingInFlight: Bool = false
+    private var transcriptionInFlightRecordingIDs: Set<String> = []
 
     private override init() {
         super.init()
         loadRecordingsFromDisk()
+        resumePendingTranscriptions()
     }
 
     // MARK: - Public API
@@ -411,9 +413,30 @@ final class MeetingRecordingManager: NSObject {
         }
     }
 
+    /// Queues Apple Speech for recordings that still need a transcript (new saves and resume after launch).
+    private func resumePendingTranscriptions() {
+        for recording in recordings where recordingNeedsTranscription(recording) {
+            scheduleAppleTranscription(for: recording)
+        }
+    }
+
+    private func recordingNeedsTranscription(_ recording: RecordedMeeting) -> Bool {
+        let hasText = recording.transcriptText?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
+        if hasText { return false }
+        let hasError = recording.transcriptError?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
+        if hasError { return false }
+        guard let dir = try? recordingsDirectoryURL() else { return false }
+        let audioURL = dir.appendingPathComponent(recording.audioFilename)
+        return FileManager.default.fileExists(atPath: audioURL.path)
+    }
+
     /// Runs Apple Speech on the saved mixed audio; updates `recordings` and posts `recordingsDidChangeNotification` when finished.
     private func scheduleAppleTranscription(for recording: RecordedMeeting) {
         let recordingId = recording.id
+        guard transcriptionInFlightRecordingIDs.contains(recordingId) == false else { return }
+        guard recordingNeedsTranscription(recording) else { return }
+
+        transcriptionInFlightRecordingIDs.insert(recordingId)
         let filename = recording.audioFilename
         let locales = TranscriptionLanguagePreferences.localesForAppleTranscription()
         Task {
@@ -428,6 +451,9 @@ final class MeetingRecordingManager: NSObject {
             userInfo: ["recordingId": recordingId]
         )
         defer {
+            Task { @MainActor in
+                self.transcriptionInFlightRecordingIDs.remove(recordingId)
+            }
             NotificationCenter.default.post(
                 name: Self.transcriptionDidEndNotification,
                 object: self,
@@ -440,9 +466,17 @@ final class MeetingRecordingManager: NSObject {
             let dir = try recordingsDirectoryURL()
             let audioURL = dir.appendingPathComponent(audioFilename)
             let segments = try await service.transcribeMeeting(micURL: audioURL, systemURL: nil, locales: locales)
-            let text = segments.renderedTimelineText()
+            let text = segments.renderedTimelineText().trimmingCharacters(in: .whitespacesAndNewlines)
             await MainActor.run {
-                applyTranscript(recordingId: recordingId, text: text.isEmpty ? nil : text, error: nil)
+                if text.isEmpty {
+                    applyTranscript(
+                        recordingId: recordingId,
+                        text: nil,
+                        error: "No speech was detected in this recording."
+                    )
+                } else {
+                    applyTranscript(recordingId: recordingId, text: text, error: nil)
+                }
             }
         } catch {
             await MainActor.run {

+ 47 - 44
zoom_app/ViewController.swift

@@ -21,6 +21,12 @@ private enum AppWindowMetrics {
     static let defaultContentHeight: CGFloat = 690
 }
 
+private enum AppExternalURLs {
+    static let privacyPolicy = URL(string: "https://sites.google.com/view/aicompanionforzoom/privacy-policy")!
+    static let termsOfServices = URL(string: "https://sites.google.com/view/aicompanionforzoom/terms-and-condition")!
+    static let support = URL(string: "https://sites.google.com/view/aicompanionforzoom/get-support")!
+}
+
 /// Zoom shows "browser not supported" for the default macOS `WKWebView` UA. Use a desktop Safari string so join/start (including `/s/` host links) load in-app.
 private let embeddedZoomWebViewUserAgent =
     "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15"
@@ -358,7 +364,6 @@ class ViewController: NSViewController {
     private var recordedMeetingsPlayer: AVAudioPlayer?
     private var recordedMeetingsPlayingID: String?
     private weak var aiCompanionStopRecordingButton: NSButton?
-    private weak var aiCompanionOpenRecordingsFolderButton: NSButton?
     private weak var aiCompanionLocalTranscriptPanel: NSPanel?
     private weak var aiCompanionLocalTranscriptTextView: NSTextView?
     private weak var aiCompanionLocalTranscriptRootView: NSView?
@@ -447,7 +452,7 @@ class ViewController: NSViewController {
     private var paywallIsDesktopLayout = false
     private var paywallPanelWidthConstraint: NSLayoutConstraint?
     private var paywallPanelHeightConstraint: NSLayoutConstraint?
-    private var joinMeetingWindow: NSWindow?
+    private var joinMeetingWindow: NSPanel?
     private var scheduleMeetingWindow: NSWindow?
     private weak var scheduleTopicField: NSTextField?
     private weak var scheduleDateField: NSTextField?
@@ -923,7 +928,6 @@ class ViewController: NSViewController {
         aiCompanionOpenDocButton = nil
         aiCompanionCopyMarkdownButton = nil
         aiCompanionStopRecordingButton = nil
-        aiCompanionOpenRecordingsFolderButton = nil
         aiCompanionFilterPopup = nil
         aiCompanionFilterFromPicker = nil
         aiCompanionFilterToPicker = nil
@@ -1549,9 +1553,30 @@ class ViewController: NSViewController {
         )
     }
 
+    private func presentJoinMeetingPanel(_ panel: NSPanel) {
+        guard let host = view.window else {
+            panel.center()
+            panel.makeKeyAndOrderFront(nil)
+            return
+        }
+        if host.attachedSheet !== panel {
+            host.beginSheet(panel, completionHandler: nil)
+        }
+        panel.makeKeyAndOrderFront(nil)
+    }
+
+    private func dismissJoinMeetingPanel() {
+        guard let panel = joinMeetingWindow else { return }
+        if let host = view.window, host.attachedSheet === panel {
+            host.endSheet(panel)
+        } else {
+            panel.performClose(nil)
+        }
+    }
+
     @objc private func joinMeetingTapped() {
         if let existing = joinMeetingWindow {
-            existing.makeKeyAndOrderFront(nil)
+            presentJoinMeetingPanel(existing)
             NSApp.activate(ignoringOtherApps: true)
             DispatchQueue.main.async { [weak self] in
                 _ = self?.joinURLField?.becomeFirstResponder()
@@ -1577,17 +1602,18 @@ class ViewController: NSViewController {
         panel.standardWindowButton(.closeButton)?.isHidden = true
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
         panel.standardWindowButton(.zoomButton)?.isHidden = true
-        panel.isMovableByWindowBackground = true
+        panel.isFloatingPanel = false
+        panel.isMovable = false
+        panel.isMovableByWindowBackground = false
         // Allow the panel to become key immediately so text fields receive keyboard input (avoids "can't type" with floating/key heuristics).
         panel.becomesKeyOnlyIfNeeded = false
         panel.appearance = NSAppearance(named: palette.isDarkMode ? .darkAqua : .aqua)
-        panel.center()
         panel.contentViewController = controller
         panel.delegate = self
         applyWindowBackgroundForCurrentTheme(panel)
-        panel.makeKeyAndOrderFront(nil)
-        NSApp.activate(ignoringOtherApps: true)
         joinMeetingWindow = panel
+        presentJoinMeetingPanel(panel)
+        NSApp.activate(ignoringOtherApps: true)
         DispatchQueue.main.async { [weak self] in
             guard let self else { return }
             _ = self.joinURLField?.becomeFirstResponder()
@@ -1777,7 +1803,7 @@ class ViewController: NSViewController {
     }
 
     @objc private func joinMeetingCancelTapped() {
-        joinMeetingWindow?.performClose(nil)
+        dismissJoinMeetingPanel()
     }
 
     @objc private func joinMeetingSubmitTapped() {
@@ -2100,7 +2126,7 @@ class ViewController: NSViewController {
             opened = openURLInDefaultBrowser(url)
         }
         if opened {
-            joinMeetingWindow?.performClose(nil)
+            dismissJoinMeetingPanel()
             if shouldRecordLocally {
                 beginZoomMeetingRecordingIfAppropriate(sourceURL: url, meetingDisplayName: zoomMeetingDisplayName)
             }
@@ -4885,11 +4911,11 @@ class ViewController: NSViewController {
         case .upgrade:
             settingsUpgradePremiumTapped()
         case .privacyPolicy:
-            showSimpleAlert(title: "Privacy Policy", message: "Add your Privacy Policy URL in the app and open it here.")
+            openURLInDefaultBrowser(AppExternalURLs.privacyPolicy)
         case .support:
-            showSimpleAlert(title: "Support", message: "Add your Support URL/email in the app and open it here.")
+            openURLInDefaultBrowser(AppExternalURLs.support)
         case .termsOfServices:
-            showSimpleAlert(title: "Terms", message: "Add your Terms URL in the app and open it here.")
+            openURLInDefaultBrowser(AppExternalURLs.termsOfServices)
         }
     }
 
@@ -6242,24 +6268,15 @@ class ViewController: NSViewController {
     }
 
     @objc private func paywallPrivacyPolicyClicked(_ sender: Any?) {
-        showSimpleAlert(
-            title: "Privacy Policy",
-            message: "Placeholder: Privacy Policy content/link will be added before publishing."
-        )
+        openURLInDefaultBrowser(AppExternalURLs.privacyPolicy)
     }
 
     @objc private func paywallSupportClicked(_ sender: Any?) {
-        showSimpleAlert(
-            title: "Support",
-            message: "Placeholder: Support contact details/link will be added before publishing."
-        )
+        openURLInDefaultBrowser(AppExternalURLs.support)
     }
 
     @objc private func paywallTermsOfServicesClicked(_ sender: Any?) {
-        showSimpleAlert(
-            title: "Terms of Services",
-            message: "Placeholder: Terms of Services content/link will be added before publishing."
-        )
+        openURLInDefaultBrowser(AppExternalURLs.termsOfServices)
     }
 
     @objc private func paywallManageSubscriptionClicked(_ sender: Any?) {
@@ -7388,18 +7405,7 @@ class ViewController: NSViewController {
         stopRecording.font = .systemFont(ofSize: 13, weight: .semibold)
         stopRecording.isHidden = true
 
-        let openFolder = NSButton(title: "Show folder", target: self, action: #selector(aiCompanionOpenRecordingsFolderTapped))
-        openFolder.translatesAutoresizingMaskIntoConstraints = false
-        openFolder.isBordered = false
-        openFolder.wantsLayer = true
-        openFolder.layer?.backgroundColor = (palette.isDarkMode ? NSColor.white.withAlphaComponent(0.08) : NSColor.black.withAlphaComponent(0.06)).cgColor
-        openFolder.layer?.cornerRadius = 10
-        openFolder.layer?.borderWidth = 1
-        openFolder.layer?.borderColor = palette.inputBorder.cgColor
-        openFolder.contentTintColor = primaryText
-        openFolder.font = .systemFont(ofSize: 13, weight: .semibold)
-
-        let toolbarRight = NSStackView(views: [openFolder, stopRecording, loadingIndicator])
+        let toolbarRight = NSStackView(views: [stopRecording, loadingIndicator])
         toolbarRight.orientation = .horizontal
         toolbarRight.alignment = .centerY
         toolbarRight.spacing = 10
@@ -7556,8 +7562,6 @@ class ViewController: NSViewController {
 
             stopRecording.heightAnchor.constraint(equalToConstant: 30),
             stopRecording.widthAnchor.constraint(greaterThanOrEqualToConstant: 110),
-            openFolder.heightAnchor.constraint(equalToConstant: 30),
-            openFolder.widthAnchor.constraint(greaterThanOrEqualToConstant: 110),
 
             filterBar.topAnchor.constraint(equalTo: titleStack.bottomAnchor, constant: 14),
             filterBar.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 18),
@@ -7603,7 +7607,6 @@ class ViewController: NSViewController {
         aiCompanionDetailScroll = nil
         aiCompanionDetailStack = nil
         aiCompanionStopRecordingButton = stopRecording
-        aiCompanionOpenRecordingsFolderButton = openFolder
         aiCompanionFilterPopup = categoryFilter
         aiCompanionFilterFromPicker = fromDatePicker
         aiCompanionFilterToPicker = toDatePicker
@@ -7735,10 +7738,6 @@ class ViewController: NSViewController {
         }
     }
 
-    @objc private func aiCompanionOpenRecordingsFolderTapped() {
-        MeetingRecordingManager.shared.revealRecordingsInFinder()
-    }
-
     // MARK: - AI Companion list / detail rendering
 
     @MainActor
@@ -9911,6 +9910,10 @@ extension ViewController: WKNavigationDelegate, WKUIDelegate {
 
 extension ViewController: NSWindowDelegate {
     func windowShouldClose(_ sender: NSWindow) -> Bool {
+        if let panel = joinMeetingWindow, sender === panel, let host = view.window {
+            host.endSheet(panel)
+            return false
+        }
         if let panel = aiCompanionLocalTranscriptPanel, sender === panel, let host = view.window {
             host.endSheet(panel)
             return false