Ver Fonte

Show local meeting transcript in a sheet with Transcript button

Replace inline transcript cards with Transcript + Play/Stop actions.
Opening Transcript presents a Meeting Notes-style modal sheet (Copy,
Close, scrollable text) that live-updates while Apple Speech completes.
Handle panel close via the sheet host and windowShouldClose.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 há 2 meses atrás
pai
commit
2960dc73b2
1 ficheiros alterados com 220 adições e 53 exclusões
  1. 220 53
      zoom_app/ViewController.swift

+ 220 - 53
zoom_app/ViewController.swift

@@ -337,6 +337,12 @@ class ViewController: NSViewController {
     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?
+    private weak var aiCompanionLocalTranscriptHeaderView: NSView?
+    private weak var aiCompanionLocalTranscriptSubtitleLabel: NSTextField?
+    private var aiCompanionLocalTranscriptPanelRecordingID: String?
     private weak var meetingsPageCategoryFilter: NSPopUpButton?
     private weak var meetingsPageFromDatePicker: NSDatePicker?
     private weak var meetingsPageToDatePicker: NSDatePicker?
@@ -7158,6 +7164,7 @@ class ViewController: NSViewController {
             stack.addArrangedSubview(row)
             row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
         }
+        refreshLocalRecordingTranscriptPanelContentIfNeeded()
     }
 
     @MainActor
@@ -7182,6 +7189,18 @@ class ViewController: NSViewController {
         let dateLabel = makeLabel(dateText, size: 11, color: secondaryText, weight: .regular, centered: false)
         dateLabel.translatesAutoresizingMaskIntoConstraints = false
 
+        let transcriptBtn = NSButton(title: "Transcript", target: self, action: #selector(recordingTranscriptTapped(_:)))
+        transcriptBtn.translatesAutoresizingMaskIntoConstraints = false
+        transcriptBtn.isBordered = false
+        transcriptBtn.wantsLayer = true
+        transcriptBtn.layer?.backgroundColor = (palette.isDarkMode ? NSColor.white.withAlphaComponent(0.08) : NSColor.black.withAlphaComponent(0.06)).cgColor
+        transcriptBtn.layer?.cornerRadius = 10
+        transcriptBtn.layer?.borderWidth = 1
+        transcriptBtn.layer?.borderColor = palette.inputBorder.cgColor
+        transcriptBtn.contentTintColor = primaryText
+        transcriptBtn.font = .systemFont(ofSize: 12, weight: .semibold)
+        transcriptBtn.tag = index
+
         let play = NSButton(title: recordedMeetingsPlayingID == recording.id ? "Stop" : "Play", target: self, action: #selector(recordingPlayTapped(_:)))
         play.translatesAutoresizingMaskIntoConstraints = false
         play.isBordered = false
@@ -7192,12 +7211,17 @@ class ViewController: NSViewController {
         play.font = .systemFont(ofSize: 12, weight: .semibold)
         play.tag = index
 
+        let buttonsRow = NSStackView(views: [transcriptBtn, play])
+        buttonsRow.translatesAutoresizingMaskIntoConstraints = false
+        buttonsRow.orientation = .horizontal
+        buttonsRow.alignment = .centerY
+        buttonsRow.spacing = 8
+
         card.addSubview(title)
         card.addSubview(dateLabel)
-        card.addSubview(play)
+        card.addSubview(buttonsRow)
 
         var lastStackBottom: NSView = dateLabel
-        var pinnedTranscriptScrollToCardBottom = false
 
         if aiCompanionTranscriptionActiveRecordingIDs.contains(recording.id) {
             let busy = makeLabel("Generating transcript…", size: 11, color: secondaryText, weight: .regular, centered: false)
@@ -7213,7 +7237,7 @@ class ViewController: NSViewController {
         } else if let err = recording.transcriptError?.trimmingCharacters(in: .whitespacesAndNewlines), err.isEmpty == false {
             let errLabel = makeLabel("Transcript: \(err)", size: 11, color: NSColor.systemRed, weight: .regular, centered: false)
             errLabel.translatesAutoresizingMaskIntoConstraints = false
-            errLabel.maximumNumberOfLines = 3
+            errLabel.maximumNumberOfLines = 2
             errLabel.lineBreakMode = .byWordWrapping
             if let cell = errLabel.cell as? NSTextFieldCell {
                 cell.wraps = true
@@ -7227,67 +7251,202 @@ class ViewController: NSViewController {
             lastStackBottom = errLabel
         }
 
-        if let transcript = recording.transcriptText?.trimmingCharacters(in: .whitespacesAndNewlines), transcript.isEmpty == false {
-            let header = makeLabel("Transcript", size: 11, color: secondaryText, weight: .semibold, centered: false)
-            header.translatesAutoresizingMaskIntoConstraints = false
-            card.addSubview(header)
-
-            let scroll = NSScrollView()
-            scroll.translatesAutoresizingMaskIntoConstraints = false
-            scroll.drawsBackground = false
-            scroll.hasVerticalScroller = true
-            scroll.borderType = .noBorder
-            scroll.autohidesScrollers = true
-            scroll.scrollerStyle = .overlay
-
-            let tv = NSTextView()
-            tv.translatesAutoresizingMaskIntoConstraints = false
-            tv.string = transcript
-            tv.isEditable = false
-            tv.isSelectable = true
-            tv.drawsBackground = false
-            tv.textColor = primaryText
-            tv.font = .systemFont(ofSize: 11)
-            tv.textContainerInset = NSSize(width: 4, height: 4)
-            tv.textContainer?.widthTracksTextView = true
-            scroll.documentView = tv
-
-            card.addSubview(scroll)
-
-            NSLayoutConstraint.activate([
-                header.topAnchor.constraint(equalTo: lastStackBottom.bottomAnchor, constant: 8),
-                header.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
-                header.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12),
-
-                scroll.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 4),
-                scroll.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
-                scroll.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -10),
-                scroll.heightAnchor.constraint(equalToConstant: 120),
-                scroll.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -10)
-            ])
-            pinnedTranscriptScrollToCardBottom = true
-        }
-
         NSLayoutConstraint.activate([
             title.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
             title.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
-            title.trailingAnchor.constraint(lessThanOrEqualTo: play.leadingAnchor, constant: -10),
+            title.trailingAnchor.constraint(lessThanOrEqualTo: buttonsRow.leadingAnchor, constant: -10),
+
+            buttonsRow.centerYAnchor.constraint(equalTo: title.centerYAnchor),
+            buttonsRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12),
+
+            transcriptBtn.heightAnchor.constraint(equalToConstant: 30),
+            transcriptBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 92),
+            play.heightAnchor.constraint(equalToConstant: 30),
+            play.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
 
             dateLabel.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 4),
             dateLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
-            dateLabel.trailingAnchor.constraint(equalTo: title.trailingAnchor),
+            dateLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12),
 
-            play.centerYAnchor.constraint(equalTo: title.centerYAnchor),
-            play.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12),
-            play.heightAnchor.constraint(equalToConstant: 30),
-            play.widthAnchor.constraint(greaterThanOrEqualToConstant: 72)
+            lastStackBottom.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -10)
         ])
 
-        if pinnedTranscriptScrollToCardBottom == false {
-            lastStackBottom.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -10).isActive = true
+        return card
+    }
+
+    private func bodyTextForLocalRecordingTranscript(recording: MeetingRecordingManager.RecordedMeeting) -> String {
+        if aiCompanionTranscriptionActiveRecordingIDs.contains(recording.id) {
+            return "Generating transcript…"
         }
+        if let err = recording.transcriptError?.trimmingCharacters(in: .whitespacesAndNewlines), err.isEmpty == false {
+            return "Transcript could not be generated.\n\n\(err)"
+        }
+        if let text = recording.transcriptText?.trimmingCharacters(in: .whitespacesAndNewlines), text.isEmpty == false {
+            return text
+        }
+        return "Transcript will appear here after the audio is processed. You can leave this window open; it updates when the transcript is ready. If nothing appears, check Speech Recognition in System Settings → Privacy & Security."
+    }
 
-        return card
+    @MainActor
+    private func refreshLocalRecordingTranscriptPanelContentIfNeeded() {
+        guard let recordingID = aiCompanionLocalTranscriptPanelRecordingID,
+              let textView = aiCompanionLocalTranscriptTextView,
+              let recording = recordedMeetings.first(where: { $0.id == recordingID }) else { return }
+        textView.string = bodyTextForLocalRecordingTranscript(recording: recording)
+    }
+
+    @MainActor
+    private func presentLocalRecordingTranscriptPanel(recording: MeetingRecordingManager.RecordedMeeting) {
+        guard let hostWindow = view.window else { return }
+        let meetingTitle = recording.resolvedDisplayTitle
+        let body = bodyTextForLocalRecordingTranscript(recording: recording)
+
+        let hostIsDarkMode = darkModeEnabled
+        let panelAppearanceName: NSAppearance.Name = hostIsDarkMode ? .darkAqua : .aqua
+        let outerBackgroundColor: NSColor = hostIsDarkMode ? .windowBackgroundColor : NSColor(calibratedWhite: 0.98, alpha: 1.0)
+        let headerBackgroundColor: NSColor = hostIsDarkMode ? .controlBackgroundColor : NSColor(calibratedWhite: 0.93, alpha: 1.0)
+
+        if let panel = aiCompanionLocalTranscriptPanel, let textView = aiCompanionLocalTranscriptTextView {
+            panel.title = "Transcript - \(meetingTitle)"
+            panel.appearance = NSAppearance(named: panelAppearanceName)
+            panel.backgroundColor = outerBackgroundColor
+            textView.string = body
+            aiCompanionLocalTranscriptPanelRecordingID = recording.id
+            aiCompanionLocalTranscriptRootView?.layer?.backgroundColor = outerBackgroundColor.cgColor
+            aiCompanionLocalTranscriptHeaderView?.layer?.backgroundColor = headerBackgroundColor.cgColor
+            aiCompanionLocalTranscriptSubtitleLabel?.stringValue = meetingTitle
+            if hostWindow.attachedSheet !== panel {
+                hostWindow.beginSheet(panel, completionHandler: nil)
+            }
+            panel.makeKeyAndOrderFront(nil)
+            NSApp.activate(ignoringOtherApps: true)
+            return
+        }
+
+        let panelWidth: CGFloat = 720
+        let panelHeight: CGFloat = 540
+        let panel = NSPanel(
+            contentRect: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight),
+            styleMask: [.titled, .closable],
+            backing: .buffered,
+            defer: false
+        )
+        panel.isReleasedWhenClosed = false
+        panel.title = "Transcript - \(meetingTitle)"
+        panel.isFloatingPanel = false
+        panel.hidesOnDeactivate = false
+        panel.delegate = self
+        panel.appearance = NSAppearance(named: panelAppearanceName)
+        panel.backgroundColor = outerBackgroundColor
+        panel.standardWindowButton(.zoomButton)?.isHidden = true
+        panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
+
+        let root = NSView()
+        root.translatesAutoresizingMaskIntoConstraints = false
+        root.wantsLayer = true
+        root.layer?.backgroundColor = outerBackgroundColor.cgColor
+
+        let header = NSView()
+        header.translatesAutoresizingMaskIntoConstraints = false
+        header.wantsLayer = true
+        header.layer?.backgroundColor = headerBackgroundColor.cgColor
+        header.layer?.cornerRadius = 10
+
+        let headingColor: NSColor = hostIsDarkMode ? .labelColor : .black
+        let titleLabel = textLabel("Transcript", font: .systemFont(ofSize: 15, weight: .semibold), color: headingColor)
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
+        let subtitleLabel = textLabel(meetingTitle, font: .systemFont(ofSize: 12, weight: .regular), color: .secondaryLabelColor)
+        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
+        subtitleLabel.lineBreakMode = .byTruncatingTail
+
+        let scroll = NSScrollView()
+        scroll.translatesAutoresizingMaskIntoConstraints = false
+        scroll.drawsBackground = true
+        scroll.backgroundColor = NSColor.textBackgroundColor
+        scroll.hasVerticalScroller = true
+        scroll.hasHorizontalScroller = false
+        scroll.borderType = .bezelBorder
+
+        let textView = NSTextView()
+        textView.isEditable = false
+        textView.isSelectable = true
+        textView.backgroundColor = NSColor.textBackgroundColor
+        textView.textColor = NSColor.labelColor
+        textView.font = .systemFont(ofSize: 13, weight: .regular)
+        textView.string = body
+        textView.textContainer?.widthTracksTextView = true
+        textView.textContainerInset = NSSize(width: 10, height: 10)
+
+        let copyButton = NSButton(title: "Copy", target: self, action: #selector(aiCompanionLocalTranscriptCopyTapped(_:)))
+        copyButton.translatesAutoresizingMaskIntoConstraints = false
+        copyButton.bezelStyle = .rounded
+
+        let closeButton = NSButton(title: "Close", target: self, action: #selector(aiCompanionLocalTranscriptCloseTapped(_:)))
+        closeButton.translatesAutoresizingMaskIntoConstraints = false
+        closeButton.bezelStyle = .rounded
+
+        scroll.documentView = textView
+        header.addSubview(titleLabel)
+        header.addSubview(subtitleLabel)
+        header.addSubview(copyButton)
+        header.addSubview(closeButton)
+        root.addSubview(header)
+        root.addSubview(scroll)
+
+        NSLayoutConstraint.activate([
+            header.topAnchor.constraint(equalTo: root.topAnchor, constant: 12),
+            header.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 16),
+            header.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
+
+            titleLabel.topAnchor.constraint(equalTo: header.topAnchor, constant: 12),
+            titleLabel.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 12),
+            titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: copyButton.leadingAnchor, constant: -12),
+
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
+            subtitleLabel.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 12),
+            subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: copyButton.leadingAnchor, constant: -12),
+            subtitleLabel.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -12),
+
+            copyButton.centerYAnchor.constraint(equalTo: header.centerYAnchor),
+            copyButton.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -8),
+
+            closeButton.centerYAnchor.constraint(equalTo: header.centerYAnchor),
+            closeButton.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -12),
+
+            scroll.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 12),
+            scroll.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 16),
+            scroll.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
+            scroll.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -16)
+        ])
+
+        panel.contentView = root
+        aiCompanionLocalTranscriptPanel = panel
+        aiCompanionLocalTranscriptTextView = textView
+        aiCompanionLocalTranscriptRootView = root
+        aiCompanionLocalTranscriptHeaderView = header
+        aiCompanionLocalTranscriptSubtitleLabel = subtitleLabel
+        aiCompanionLocalTranscriptPanelRecordingID = recording.id
+
+        hostWindow.beginSheet(panel, completionHandler: nil)
+        panel.makeKeyAndOrderFront(nil)
+        NSApp.activate(ignoringOtherApps: true)
+    }
+
+    @objc private func recordingTranscriptTapped(_ sender: NSButton) {
+        let idx = sender.tag
+        guard recordedMeetings.indices.contains(idx) else { return }
+        presentLocalRecordingTranscriptPanel(recording: recordedMeetings[idx])
+    }
+
+    @objc private func aiCompanionLocalTranscriptCopyTapped(_ sender: NSButton) {
+        let text = aiCompanionLocalTranscriptTextView?.string ?? ""
+        NSPasteboard.general.clearContents()
+        NSPasteboard.general.setString(text, forType: .string)
+    }
+
+    @objc private func aiCompanionLocalTranscriptCloseTapped(_ sender: NSButton) {
+        guard let panel = aiCompanionLocalTranscriptPanel, let host = view.window else { return }
+        host.endSheet(panel)
     }
 
     @objc
@@ -8620,6 +8779,14 @@ extension ViewController: WKNavigationDelegate, WKUIDelegate {
 }
 
 extension ViewController: NSWindowDelegate {
+    func windowShouldClose(_ sender: NSWindow) -> Bool {
+        if let panel = aiCompanionLocalTranscriptPanel, sender === panel, let host = view.window {
+            host.endSheet(panel)
+            return false
+        }
+        return true
+    }
+
     func windowWillClose(_ notification: Notification) {
         guard let window = notification.object as? NSWindow else { return }
         if window === joinMeetingWindow {