瀏覽代碼

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
共有 3 個文件被更改,包括 84 次插入47 次删除
  1. 1 1
      website/terms.html
  2. 36 2
      zoom_app/MeetingRecordingManager.swift
  3. 47 44
      zoom_app/ViewController.swift

+ 1 - 1
website/terms.html

@@ -102,7 +102,7 @@
     <li>Verifying the accuracy of AI-generated summaries before relying on them.</li>
     <li>Verifying the accuracy of AI-generated summaries before relying on them.</li>
   </ul>
   </ul>
   <p>
   <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>
   </p>
 
 
   <h2>8. Intellectual property</h2>
   <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.
     /// Set by the embedded `WKWebView` when its URL indicates an active join/in-call web client page.
     private var embeddedWebClientCallUIVisible: Bool = false
     private var embeddedWebClientCallUIVisible: Bool = false
     private var stopRecordingInFlight: Bool = false
     private var stopRecordingInFlight: Bool = false
+    private var transcriptionInFlightRecordingIDs: Set<String> = []
 
 
     private override init() {
     private override init() {
         super.init()
         super.init()
         loadRecordingsFromDisk()
         loadRecordingsFromDisk()
+        resumePendingTranscriptions()
     }
     }
 
 
     // MARK: - Public API
     // 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.
     /// Runs Apple Speech on the saved mixed audio; updates `recordings` and posts `recordingsDidChangeNotification` when finished.
     private func scheduleAppleTranscription(for recording: RecordedMeeting) {
     private func scheduleAppleTranscription(for recording: RecordedMeeting) {
         let recordingId = recording.id
         let recordingId = recording.id
+        guard transcriptionInFlightRecordingIDs.contains(recordingId) == false else { return }
+        guard recordingNeedsTranscription(recording) else { return }
+
+        transcriptionInFlightRecordingIDs.insert(recordingId)
         let filename = recording.audioFilename
         let filename = recording.audioFilename
         let locales = TranscriptionLanguagePreferences.localesForAppleTranscription()
         let locales = TranscriptionLanguagePreferences.localesForAppleTranscription()
         Task {
         Task {
@@ -428,6 +451,9 @@ final class MeetingRecordingManager: NSObject {
             userInfo: ["recordingId": recordingId]
             userInfo: ["recordingId": recordingId]
         )
         )
         defer {
         defer {
+            Task { @MainActor in
+                self.transcriptionInFlightRecordingIDs.remove(recordingId)
+            }
             NotificationCenter.default.post(
             NotificationCenter.default.post(
                 name: Self.transcriptionDidEndNotification,
                 name: Self.transcriptionDidEndNotification,
                 object: self,
                 object: self,
@@ -440,9 +466,17 @@ final class MeetingRecordingManager: NSObject {
             let dir = try recordingsDirectoryURL()
             let dir = try recordingsDirectoryURL()
             let audioURL = dir.appendingPathComponent(audioFilename)
             let audioURL = dir.appendingPathComponent(audioFilename)
             let segments = try await service.transcribeMeeting(micURL: audioURL, systemURL: nil, locales: locales)
             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 {
             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 {
         } catch {
             await MainActor.run {
             await MainActor.run {

+ 47 - 44
zoom_app/ViewController.swift

@@ -21,6 +21,12 @@ private enum AppWindowMetrics {
     static let defaultContentHeight: CGFloat = 690
     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.
 /// 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 =
 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"
     "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 recordedMeetingsPlayer: AVAudioPlayer?
     private var recordedMeetingsPlayingID: String?
     private var recordedMeetingsPlayingID: String?
     private weak var aiCompanionStopRecordingButton: NSButton?
     private weak var aiCompanionStopRecordingButton: NSButton?
-    private weak var aiCompanionOpenRecordingsFolderButton: NSButton?
     private weak var aiCompanionLocalTranscriptPanel: NSPanel?
     private weak var aiCompanionLocalTranscriptPanel: NSPanel?
     private weak var aiCompanionLocalTranscriptTextView: NSTextView?
     private weak var aiCompanionLocalTranscriptTextView: NSTextView?
     private weak var aiCompanionLocalTranscriptRootView: NSView?
     private weak var aiCompanionLocalTranscriptRootView: NSView?
@@ -447,7 +452,7 @@ class ViewController: NSViewController {
     private var paywallIsDesktopLayout = false
     private var paywallIsDesktopLayout = false
     private var paywallPanelWidthConstraint: NSLayoutConstraint?
     private var paywallPanelWidthConstraint: NSLayoutConstraint?
     private var paywallPanelHeightConstraint: NSLayoutConstraint?
     private var paywallPanelHeightConstraint: NSLayoutConstraint?
-    private var joinMeetingWindow: NSWindow?
+    private var joinMeetingWindow: NSPanel?
     private var scheduleMeetingWindow: NSWindow?
     private var scheduleMeetingWindow: NSWindow?
     private weak var scheduleTopicField: NSTextField?
     private weak var scheduleTopicField: NSTextField?
     private weak var scheduleDateField: NSTextField?
     private weak var scheduleDateField: NSTextField?
@@ -923,7 +928,6 @@ class ViewController: NSViewController {
         aiCompanionOpenDocButton = nil
         aiCompanionOpenDocButton = nil
         aiCompanionCopyMarkdownButton = nil
         aiCompanionCopyMarkdownButton = nil
         aiCompanionStopRecordingButton = nil
         aiCompanionStopRecordingButton = nil
-        aiCompanionOpenRecordingsFolderButton = nil
         aiCompanionFilterPopup = nil
         aiCompanionFilterPopup = nil
         aiCompanionFilterFromPicker = nil
         aiCompanionFilterFromPicker = nil
         aiCompanionFilterToPicker = 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() {
     @objc private func joinMeetingTapped() {
         if let existing = joinMeetingWindow {
         if let existing = joinMeetingWindow {
-            existing.makeKeyAndOrderFront(nil)
+            presentJoinMeetingPanel(existing)
             NSApp.activate(ignoringOtherApps: true)
             NSApp.activate(ignoringOtherApps: true)
             DispatchQueue.main.async { [weak self] in
             DispatchQueue.main.async { [weak self] in
                 _ = self?.joinURLField?.becomeFirstResponder()
                 _ = self?.joinURLField?.becomeFirstResponder()
@@ -1577,17 +1602,18 @@ class ViewController: NSViewController {
         panel.standardWindowButton(.closeButton)?.isHidden = true
         panel.standardWindowButton(.closeButton)?.isHidden = true
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
         panel.standardWindowButton(.zoomButton)?.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).
         // 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.becomesKeyOnlyIfNeeded = false
         panel.appearance = NSAppearance(named: palette.isDarkMode ? .darkAqua : .aqua)
         panel.appearance = NSAppearance(named: palette.isDarkMode ? .darkAqua : .aqua)
-        panel.center()
         panel.contentViewController = controller
         panel.contentViewController = controller
         panel.delegate = self
         panel.delegate = self
         applyWindowBackgroundForCurrentTheme(panel)
         applyWindowBackgroundForCurrentTheme(panel)
-        panel.makeKeyAndOrderFront(nil)
-        NSApp.activate(ignoringOtherApps: true)
         joinMeetingWindow = panel
         joinMeetingWindow = panel
+        presentJoinMeetingPanel(panel)
+        NSApp.activate(ignoringOtherApps: true)
         DispatchQueue.main.async { [weak self] in
         DispatchQueue.main.async { [weak self] in
             guard let self else { return }
             guard let self else { return }
             _ = self.joinURLField?.becomeFirstResponder()
             _ = self.joinURLField?.becomeFirstResponder()
@@ -1777,7 +1803,7 @@ class ViewController: NSViewController {
     }
     }
 
 
     @objc private func joinMeetingCancelTapped() {
     @objc private func joinMeetingCancelTapped() {
-        joinMeetingWindow?.performClose(nil)
+        dismissJoinMeetingPanel()
     }
     }
 
 
     @objc private func joinMeetingSubmitTapped() {
     @objc private func joinMeetingSubmitTapped() {
@@ -2100,7 +2126,7 @@ class ViewController: NSViewController {
             opened = openURLInDefaultBrowser(url)
             opened = openURLInDefaultBrowser(url)
         }
         }
         if opened {
         if opened {
-            joinMeetingWindow?.performClose(nil)
+            dismissJoinMeetingPanel()
             if shouldRecordLocally {
             if shouldRecordLocally {
                 beginZoomMeetingRecordingIfAppropriate(sourceURL: url, meetingDisplayName: zoomMeetingDisplayName)
                 beginZoomMeetingRecordingIfAppropriate(sourceURL: url, meetingDisplayName: zoomMeetingDisplayName)
             }
             }
@@ -4885,11 +4911,11 @@ class ViewController: NSViewController {
         case .upgrade:
         case .upgrade:
             settingsUpgradePremiumTapped()
             settingsUpgradePremiumTapped()
         case .privacyPolicy:
         case .privacyPolicy:
-            showSimpleAlert(title: "Privacy Policy", message: "Add your Privacy Policy URL in the app and open it here.")
+            openURLInDefaultBrowser(AppExternalURLs.privacyPolicy)
         case .support:
         case .support:
-            showSimpleAlert(title: "Support", message: "Add your Support URL/email in the app and open it here.")
+            openURLInDefaultBrowser(AppExternalURLs.support)
         case .termsOfServices:
         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?) {
     @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?) {
     @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?) {
     @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?) {
     @objc private func paywallManageSubscriptionClicked(_ sender: Any?) {
@@ -7388,18 +7405,7 @@ class ViewController: NSViewController {
         stopRecording.font = .systemFont(ofSize: 13, weight: .semibold)
         stopRecording.font = .systemFont(ofSize: 13, weight: .semibold)
         stopRecording.isHidden = true
         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.orientation = .horizontal
         toolbarRight.alignment = .centerY
         toolbarRight.alignment = .centerY
         toolbarRight.spacing = 10
         toolbarRight.spacing = 10
@@ -7556,8 +7562,6 @@ class ViewController: NSViewController {
 
 
             stopRecording.heightAnchor.constraint(equalToConstant: 30),
             stopRecording.heightAnchor.constraint(equalToConstant: 30),
             stopRecording.widthAnchor.constraint(greaterThanOrEqualToConstant: 110),
             stopRecording.widthAnchor.constraint(greaterThanOrEqualToConstant: 110),
-            openFolder.heightAnchor.constraint(equalToConstant: 30),
-            openFolder.widthAnchor.constraint(greaterThanOrEqualToConstant: 110),
 
 
             filterBar.topAnchor.constraint(equalTo: titleStack.bottomAnchor, constant: 14),
             filterBar.topAnchor.constraint(equalTo: titleStack.bottomAnchor, constant: 14),
             filterBar.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 18),
             filterBar.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 18),
@@ -7603,7 +7607,6 @@ class ViewController: NSViewController {
         aiCompanionDetailScroll = nil
         aiCompanionDetailScroll = nil
         aiCompanionDetailStack = nil
         aiCompanionDetailStack = nil
         aiCompanionStopRecordingButton = stopRecording
         aiCompanionStopRecordingButton = stopRecording
-        aiCompanionOpenRecordingsFolderButton = openFolder
         aiCompanionFilterPopup = categoryFilter
         aiCompanionFilterPopup = categoryFilter
         aiCompanionFilterFromPicker = fromDatePicker
         aiCompanionFilterFromPicker = fromDatePicker
         aiCompanionFilterToPicker = toDatePicker
         aiCompanionFilterToPicker = toDatePicker
@@ -7735,10 +7738,6 @@ class ViewController: NSViewController {
         }
         }
     }
     }
 
 
-    @objc private func aiCompanionOpenRecordingsFolderTapped() {
-        MeetingRecordingManager.shared.revealRecordingsInFinder()
-    }
-
     // MARK: - AI Companion list / detail rendering
     // MARK: - AI Companion list / detail rendering
 
 
     @MainActor
     @MainActor
@@ -9911,6 +9910,10 @@ extension ViewController: WKNavigationDelegate, WKUIDelegate {
 
 
 extension ViewController: NSWindowDelegate {
 extension ViewController: NSWindowDelegate {
     func windowShouldClose(_ sender: NSWindow) -> Bool {
     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 {
         if let panel = aiCompanionLocalTranscriptPanel, sender === panel, let host = view.window {
             host.endSheet(panel)
             host.endSheet(panel)
             return false
             return false