Jelajahi Sumber

Redesign AI Companion meeting cards around Get Notes flow.

Match the mockup with date/saved-time metadata, status labels for transcript and notes states, and a single primary action while transcripts continue to generate automatically after save.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 bulan lalu
induk
melakukan
3c424cf6e2
1 mengubah file dengan 112 tambahan dan 129 penghapusan
  1. 112 129
      zoom_app/ViewController.swift

+ 112 - 129
zoom_app/ViewController.swift

@@ -7089,8 +7089,8 @@ class ViewController: NSViewController {
         root.addSubview(container)
 
         // Header row: recordings list (AI Companion mode).
-        let title = makeLabel("Ai Companion", size: 28, color: primaryText, weight: .bold, centered: false)
-        let subtitle = makeLabel("Recorded meetings and Apple transcripts", size: 13, color: secondaryText, weight: .regular, centered: false)
+        let title = makeLabel("AI Companion", size: 28, color: primaryText, weight: .bold, centered: false)
+        let subtitle = makeLabel("Get notes from your previous meetings", size: 13, color: secondaryText, weight: .regular, centered: false)
         let count = makeLabel("0 recordings", size: 12, color: secondaryText, weight: .medium, centered: false)
         let titleStack = NSStackView(views: [title, subtitle, count])
         titleStack.orientation = .vertical
@@ -7165,9 +7165,11 @@ class ViewController: NSViewController {
         applyFiltersButton.translatesAutoresizingMaskIntoConstraints = false
         applyFiltersButton.isBordered = false
         applyFiltersButton.wantsLayer = true
-        applyFiltersButton.layer?.backgroundColor = accentBlue.cgColor
+        applyFiltersButton.layer?.backgroundColor = palette.inputBackground.cgColor
         applyFiltersButton.layer?.cornerRadius = 10
-        applyFiltersButton.contentTintColor = .white
+        applyFiltersButton.layer?.borderWidth = 1
+        applyFiltersButton.layer?.borderColor = palette.inputBorder.cgColor
+        applyFiltersButton.contentTintColor = primaryText
         applyFiltersButton.font = .systemFont(ofSize: 13, weight: .semibold)
 
         let resetFiltersButton = NSButton(title: "Reset", target: self, action: #selector(aiCompanionFilterResetTapped))
@@ -7428,8 +7430,12 @@ class ViewController: NSViewController {
         if mgr.isRecording {
             aiCompanionStatusLabel?.stringValue =
                 "Recording… When you leave the meeting, click Stop & save (here or in the menu bar)."
+            aiCompanionStatusLabel?.font = .systemFont(ofSize: 12, weight: .regular)
+            aiCompanionStatusLabel?.textColor = secondaryText
         } else {
             aiCompanionStatusLabel?.stringValue = "Previous meetings"
+            aiCompanionStatusLabel?.font = .systemFont(ofSize: 15, weight: .semibold)
+            aiCompanionStatusLabel?.textColor = primaryText
         }
         refreshAiCompanionStatusPreferredMaxLayoutWidth()
     }
@@ -7516,6 +7522,52 @@ class ViewController: NSViewController {
         return .notRequested
     }
 
+    private func aiCompanionCardDateLabel(for date: Date) -> String {
+        let formatter = DateFormatter()
+        formatter.locale = Locale(identifier: "en_GB")
+        formatter.dateFormat = "d-MMMM-yyyy"
+        return "Date: \(formatter.string(from: date))"
+    }
+
+    private func aiCompanionCardSavedTimeLabel(for date: Date) -> String {
+        let formatter = DateFormatter()
+        formatter.locale = Locale(identifier: "en_US_POSIX")
+        formatter.dateStyle = .none
+        formatter.timeStyle = .short
+        return "Saved time: \(formatter.string(from: date))"
+    }
+
+    private func localRecordingNotesStatusLabel(
+        for recording: MeetingRecordingManager.RecordedMeeting,
+        notesStatus: LocalRecordingNotesStatus
+    ) -> (text: String, color: NSColor) {
+        if aiCompanionTranscriptionActiveRecordingIDs.contains(recording.id) {
+            return ("Generating transcript…", secondaryText)
+        }
+        if let err = recording.transcriptError?.trimmingCharacters(in: .whitespacesAndNewlines), err.isEmpty == false {
+            return ("Transcript unavailable", NSColor.systemRed)
+        }
+        switch notesStatus {
+        case .notRequested:
+            let hasTranscript = recording.transcriptText?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
+            if hasTranscript {
+                return ("Notes not requested", secondaryText)
+            }
+            return ("Waiting for transcript…", secondaryText)
+        case .processing:
+            let msg = localRecordingNotesProgressByRecordingId[recording.id] ?? "Generating notes…"
+            return (msg, secondaryText)
+        case .ready:
+            return ("Notes ready", secondaryText)
+        case .failed:
+            let err = recording.notesError?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+            if err.isEmpty {
+                return ("Notes could not be generated", NSColor.systemRed)
+            }
+            return (err, NSColor.systemRed)
+        }
+    }
+
     @MainActor
     private func makeRecordedMeetingCard(recording: MeetingRecordingManager.RecordedMeeting, index: Int) -> NSView {
         let card = NSView()
@@ -7526,152 +7578,83 @@ class ViewController: NSViewController {
         card.layer?.borderColor = palette.inputBorder.cgColor
         card.layer?.backgroundColor = (palette.isDarkMode ? NSColor.white.withAlphaComponent(0.04) : NSColor.black.withAlphaComponent(0.03)).cgColor
 
-        let title = makeLabel(recording.resolvedDisplayTitle, size: 14, color: primaryText, weight: .semibold, centered: false)
+        let title = makeLabel(recording.resolvedDisplayTitle, size: 15, color: primaryText, weight: .semibold, centered: false)
         title.translatesAutoresizingMaskIntoConstraints = false
-        title.maximumNumberOfLines = 1
+        title.maximumNumberOfLines = 2
         title.lineBreakMode = .byTruncatingTail
 
-        let dateFormatter = DateFormatter()
-        dateFormatter.dateStyle = .medium
-        dateFormatter.timeStyle = .short
-        let dateText = "\(dateFormatter.string(from: recording.startedAt))"
-        let dateLabel = makeLabel(dateText, size: 11, color: secondaryText, weight: .regular, centered: false)
+        let dateLabel = makeLabel(aiCompanionCardDateLabel(for: recording.startedAt), size: 12, 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 getNotesBtn = NSButton(title: "Get notes", target: self, action: #selector(recordingGetNotesTapped(_:)))
+        let savedTimeLabel = makeLabel(aiCompanionCardSavedTimeLabel(for: recording.endedAt), size: 12, color: secondaryText, weight: .regular, centered: false)
+        savedTimeLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        let notesStatus = localRecordingNotesStatus(for: recording)
+        let hasTranscript = recording.transcriptText?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
+        let transcriptionBusy = aiCompanionTranscriptionActiveRecordingIDs.contains(recording.id)
+        let notesReady = notesStatus == .ready
+        let getNotesTitle = notesReady ? "View Notes" : "Get Notes"
+        let getNotesBtn = NSButton(title: getNotesTitle, target: self, action: #selector(recordingGetNotesTapped(_:)))
         getNotesBtn.translatesAutoresizingMaskIntoConstraints = false
         getNotesBtn.isBordered = false
         getNotesBtn.wantsLayer = true
-        getNotesBtn.layer?.backgroundColor = (palette.isDarkMode ? NSColor.white.withAlphaComponent(0.08) : NSColor.black.withAlphaComponent(0.06)).cgColor
         getNotesBtn.layer?.cornerRadius = 10
-        getNotesBtn.layer?.borderWidth = 1
-        getNotesBtn.layer?.borderColor = palette.inputBorder.cgColor
-        getNotesBtn.contentTintColor = primaryText
-        getNotesBtn.font = .systemFont(ofSize: 12, weight: .semibold)
+        getNotesBtn.font = .systemFont(ofSize: 13, weight: .semibold)
         getNotesBtn.tag = index
+        if let icon = NSImage(systemSymbolName: "note.text", accessibilityDescription: nil) {
+            let config = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
+            getNotesBtn.image = icon.withSymbolConfiguration(config)
+            getNotesBtn.imagePosition = .imageLeading
+        }
 
-        let notesStatus = localRecordingNotesStatus(for: recording)
-        let hasTranscript = recording.transcriptText?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
-        let transcriptionBusy = aiCompanionTranscriptionActiveRecordingIDs.contains(recording.id)
         let getNotesEnabled = (notesStatus != .processing)
-            && (notesStatus == .ready || notesStatus == .failed || (hasTranscript && !transcriptionBusy))
+            && (notesReady || notesStatus == .failed || (hasTranscript && !transcriptionBusy))
         getNotesBtn.isEnabled = getNotesEnabled
+        if getNotesEnabled {
+            getNotesBtn.layer?.backgroundColor = accentBlue.cgColor
+            getNotesBtn.contentTintColor = .white
+        } else {
+            getNotesBtn.layer?.backgroundColor = (palette.isDarkMode ? NSColor.white.withAlphaComponent(0.08) : NSColor.black.withAlphaComponent(0.06)).cgColor
+            getNotesBtn.contentTintColor = secondaryText
+        }
 
-        let play = NSButton(title: recordedMeetingsPlayingID == recording.id ? "Stop" : "Play", target: self, action: #selector(recordingPlayTapped(_:)))
-        play.translatesAutoresizingMaskIntoConstraints = false
-        play.isBordered = false
-        play.wantsLayer = true
-        play.layer?.cornerRadius = 10
-        play.layer?.backgroundColor = accentBlue.cgColor
-        play.contentTintColor = .white
-        play.font = .systemFont(ofSize: 12, weight: .semibold)
-        play.tag = index
-
-        let buttonsRow = NSStackView(views: [transcriptBtn, getNotesBtn, play])
-        transcriptBtn.setContentHuggingPriority(.defaultLow, for: .horizontal)
-        getNotesBtn.setContentHuggingPriority(.defaultLow, for: .horizontal)
-        play.setContentHuggingPriority(.required, for: .horizontal)
-        buttonsRow.translatesAutoresizingMaskIntoConstraints = false
-        buttonsRow.orientation = .horizontal
-        buttonsRow.alignment = .centerY
-        buttonsRow.spacing = 8
+        let statusInfo = localRecordingNotesStatusLabel(for: recording, notesStatus: notesStatus)
+        let statusLabel = makeLabel(statusInfo.text, size: 12, color: statusInfo.color, weight: .regular, centered: false)
+        statusLabel.translatesAutoresizingMaskIntoConstraints = false
+        statusLabel.maximumNumberOfLines = 3
+        statusLabel.lineBreakMode = .byWordWrapping
+        if let cell = statusLabel.cell as? NSTextFieldCell {
+            cell.wraps = true
+        }
 
         card.addSubview(title)
         card.addSubview(dateLabel)
-        card.addSubview(buttonsRow)
-
-        var lastStackBottom: NSView = dateLabel
-
-        if aiCompanionTranscriptionActiveRecordingIDs.contains(recording.id) {
-            let busy = makeLabel("Generating transcript…", size: 11, color: secondaryText, weight: .regular, centered: false)
-            busy.translatesAutoresizingMaskIntoConstraints = false
-            busy.maximumNumberOfLines = 1
-            card.addSubview(busy)
-            NSLayoutConstraint.activate([
-                busy.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 6),
-                busy.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
-                busy.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
-            ])
-            lastStackBottom = busy
-        } 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 = 2
-            errLabel.lineBreakMode = .byWordWrapping
-            if let cell = errLabel.cell as? NSTextFieldCell {
-                cell.wraps = true
-            }
-            card.addSubview(errLabel)
-            NSLayoutConstraint.activate([
-                errLabel.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 6),
-                errLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
-                errLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12)
-            ])
-            lastStackBottom = errLabel
-        }
-
-        if notesStatus == .processing {
-            let msg = localRecordingNotesProgressByRecordingId[recording.id] ?? "Preparing notes…"
-            let busyNotes = makeLabel("Notes: \(msg)", size: 11, color: secondaryText, weight: .regular, centered: false)
-            busyNotes.translatesAutoresizingMaskIntoConstraints = false
-            busyNotes.maximumNumberOfLines = 2
-            card.addSubview(busyNotes)
-            NSLayoutConstraint.activate([
-                busyNotes.topAnchor.constraint(equalTo: lastStackBottom.bottomAnchor, constant: 6),
-                busyNotes.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
-                busyNotes.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
-            ])
-            lastStackBottom = busyNotes
-        } else if notesStatus == .failed,
-                  let ne = recording.notesError?.trimmingCharacters(in: .whitespacesAndNewlines), ne.isEmpty == false {
-            let notesErr = makeLabel("Notes: \(ne)", size: 11, color: NSColor.systemRed, weight: .regular, centered: false)
-            notesErr.translatesAutoresizingMaskIntoConstraints = false
-            notesErr.maximumNumberOfLines = 2
-            notesErr.lineBreakMode = .byWordWrapping
-            if let cell = notesErr.cell as? NSTextFieldCell {
-                cell.wraps = true
-            }
-            card.addSubview(notesErr)
-            NSLayoutConstraint.activate([
-                notesErr.topAnchor.constraint(equalTo: lastStackBottom.bottomAnchor, constant: 6),
-                notesErr.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
-                notesErr.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12)
-            ])
-            lastStackBottom = notesErr
-        }
+        card.addSubview(savedTimeLabel)
+        card.addSubview(getNotesBtn)
+        card.addSubview(statusLabel)
 
         NSLayoutConstraint.activate([
-            title.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
-            title.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
-            title.trailingAnchor.constraint(lessThanOrEqualTo: buttonsRow.leadingAnchor, constant: -10),
+            title.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
+            title.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
+            title.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
 
-            buttonsRow.centerYAnchor.constraint(equalTo: title.centerYAnchor),
-            buttonsRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12),
+            dateLabel.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 8),
+            dateLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
+            dateLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
 
-            transcriptBtn.heightAnchor.constraint(equalToConstant: 30),
-            transcriptBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 80),
-            getNotesBtn.heightAnchor.constraint(equalToConstant: 30),
-            getNotesBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 88),
-            play.heightAnchor.constraint(equalToConstant: 30),
-            play.widthAnchor.constraint(greaterThanOrEqualToConstant: 68),
+            savedTimeLabel.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 4),
+            savedTimeLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
+            savedTimeLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
 
-            dateLabel.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 4),
-            dateLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
-            dateLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12),
+            getNotesBtn.topAnchor.constraint(equalTo: savedTimeLabel.bottomAnchor, constant: 14),
+            getNotesBtn.leadingAnchor.constraint(equalTo: title.leadingAnchor),
+            getNotesBtn.heightAnchor.constraint(equalToConstant: 34),
+            getNotesBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 130),
 
-            lastStackBottom.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -10)
+            statusLabel.topAnchor.constraint(equalTo: getNotesBtn.bottomAnchor, constant: 8),
+            statusLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
+            statusLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
+            statusLabel.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14)
         ])
 
         return card
@@ -7693,7 +7676,7 @@ class ViewController: NSViewController {
     private func bodyTextForLocalRecordingNotes(recording: MeetingRecordingManager.RecordedMeeting) -> String {
         switch localRecordingNotesStatus(for: recording) {
         case .processing:
-            return localRecordingNotesProgressByRecordingId[recording.id] ?? "Preparing notes…"
+            return localRecordingNotesProgressByRecordingId[recording.id] ?? "Generating notes…"
         case .failed:
             let err = recording.notesError?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
             return err.isEmpty ? "Notes could not be generated." : "Notes unavailable.\n\n\(err)"
@@ -7857,7 +7840,7 @@ class ViewController: NSViewController {
     private func startLocalRecordingNotesProcessing(recordingId: String, transcript: String, requestId: UUID, autoPresentOnReady: Bool) {
         localRecordingNotesTaskByRecordingId[recordingId]?.cancel()
         localRecordingNotesRequestIdByRecordingId[recordingId] = requestId
-        localRecordingNotesProgressByRecordingId[recordingId] = "Preparing notes, please wait..."
+        localRecordingNotesProgressByRecordingId[recordingId] = "Generating notes…"
         MeetingRecordingManager.shared.setMeetingNotes(
             recordingId: recordingId,
             notesStatusRaw: LocalRecordingNotesStatus.processing.rawValue,