Sfoglia il codice sorgente

Fix Ai Companion tab and add recordings date filter.

Stop refreshing unused Zoom AI summaries on open so the error string no longer replaces local recording UI; add a Meetings-style filter row for local recordings and enable Get notes when notes exist without a status field.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 mesi fa
parent
commit
bb6bc91d79
1 ha cambiato i file con 220 aggiunte e 14 eliminazioni
  1. 220 14
      zoom_app/ViewController.swift

+ 220 - 14
zoom_app/ViewController.swift

@@ -349,6 +349,24 @@ class ViewController: NSViewController {
     private weak var aiCompanionLocalNotesHeaderView: NSView?
     private weak var aiCompanionLocalNotesSubtitleLabel: NSTextField?
     private var aiCompanionLocalNotesPanelRecordingID: String?
+
+    private enum AiCompanionRecordingFilter: String, CaseIterable {
+        case all = "All"
+        case today = "Today's meetings"
+        case previousWeek = "Previous week"
+        case thisWeek = "This week"
+        case previousMonth = "Previous month"
+        case custom = "Custom date"
+    }
+
+    private var aiCompanionRecordingFilter: AiCompanionRecordingFilter = .all
+    private weak var aiCompanionFilterPopup: NSPopUpButton?
+    private weak var aiCompanionFilterFromPicker: NSDatePicker?
+    private weak var aiCompanionFilterToPicker: NSDatePicker?
+    private weak var aiCompanionFilterApplyButton: NSButton?
+    private weak var aiCompanionFilterResetButton: NSButton?
+    private weak var aiCompanionFilterRefreshButton: NSButton?
+
     private let meetingNotesService = MeetingNotesService()
     private var localRecordingNotesTaskByRecordingId: [String: Task<Void, Never>] = [:]
     private var localRecordingNotesRequestIdByRecordingId: [String: UUID] = [:]
@@ -862,6 +880,12 @@ class ViewController: NSViewController {
         aiCompanionCopyMarkdownButton = nil
         aiCompanionStopRecordingButton = nil
         aiCompanionOpenRecordingsFolderButton = nil
+        aiCompanionFilterPopup = nil
+        aiCompanionFilterFromPicker = nil
+        aiCompanionFilterToPicker = nil
+        aiCompanionFilterApplyButton = nil
+        aiCompanionFilterResetButton = nil
+        aiCompanionFilterRefreshButton = nil
         aiCompanionRowButtons.removeAll()
         aiCompanionLastLoadedAt = nil
         homeView?.removeFromSuperview()
@@ -7021,7 +7045,73 @@ class ViewController: NSViewController {
         container.addSubview(titleStack)
         container.addSubview(toolbarRight)
 
-        // Status label below header.
+        // Filter row matches `makeMeetingsPageView` styling; preset list is Ai Companion–specific.
+        let categoryFilter = NSPopUpButton()
+        categoryFilter.translatesAutoresizingMaskIntoConstraints = false
+        categoryFilter.addItems(withTitles: AiCompanionRecordingFilter.allCases.map(\.rawValue))
+        categoryFilter.selectItem(withTitle: AiCompanionRecordingFilter.all.rawValue)
+        categoryFilter.target = self
+        categoryFilter.action = #selector(aiCompanionFilterPresetChanged(_:))
+
+        let fromDatePicker = NSDatePicker()
+        fromDatePicker.translatesAutoresizingMaskIntoConstraints = false
+        fromDatePicker.datePickerStyle = .textFieldAndStepper
+        fromDatePicker.datePickerMode = .single
+        fromDatePicker.datePickerElements = [.yearMonthDay]
+        fromDatePicker.dateValue = Date()
+        fromDatePicker.font = .systemFont(ofSize: 13, weight: .regular)
+
+        let toDatePicker = NSDatePicker()
+        toDatePicker.translatesAutoresizingMaskIntoConstraints = false
+        toDatePicker.datePickerStyle = .textFieldAndStepper
+        toDatePicker.datePickerMode = .single
+        toDatePicker.datePickerElements = [.yearMonthDay]
+        toDatePicker.dateValue = Date()
+        toDatePicker.font = .systemFont(ofSize: 13, weight: .regular)
+
+        let applyFiltersButton = NSButton(title: "Apply", target: self, action: #selector(aiCompanionFilterApplyTapped))
+        applyFiltersButton.translatesAutoresizingMaskIntoConstraints = false
+        applyFiltersButton.isBordered = false
+        applyFiltersButton.wantsLayer = true
+        applyFiltersButton.layer?.backgroundColor = accentBlue.cgColor
+        applyFiltersButton.layer?.cornerRadius = 10
+        applyFiltersButton.contentTintColor = .white
+        applyFiltersButton.font = .systemFont(ofSize: 13, weight: .semibold)
+
+        let resetFiltersButton = NSButton(title: "Reset", target: self, action: #selector(aiCompanionFilterResetTapped))
+        resetFiltersButton.translatesAutoresizingMaskIntoConstraints = false
+        resetFiltersButton.isBordered = false
+        resetFiltersButton.wantsLayer = true
+        resetFiltersButton.layer?.backgroundColor = palette.inputBackground.cgColor
+        resetFiltersButton.layer?.cornerRadius = 10
+        resetFiltersButton.layer?.borderWidth = 1
+        resetFiltersButton.layer?.borderColor = palette.inputBorder.cgColor
+        resetFiltersButton.contentTintColor = primaryText
+        resetFiltersButton.font = .systemFont(ofSize: 13, weight: .semibold)
+
+        let refreshButton = NSButton(title: "", target: self, action: #selector(aiCompanionFilterRefreshTapped))
+        refreshButton.translatesAutoresizingMaskIntoConstraints = false
+        refreshButton.isBordered = false
+        refreshButton.wantsLayer = true
+        refreshButton.layer?.backgroundColor = palette.inputBackground.cgColor
+        refreshButton.layer?.cornerRadius = 10
+        refreshButton.layer?.borderWidth = 1
+        refreshButton.layer?.borderColor = palette.inputBorder.cgColor
+        refreshButton.contentTintColor = primaryText
+        refreshButton.font = .systemFont(ofSize: 13, weight: .semibold)
+        if let image = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: "Refresh") {
+            refreshButton.image = image
+            refreshButton.imagePosition = .imageOnly
+        }
+
+        let filterBar = NSStackView(views: [categoryFilter, fromDatePicker, toDatePicker, applyFiltersButton, resetFiltersButton, refreshButton])
+        filterBar.orientation = .horizontal
+        filterBar.spacing = 10
+        filterBar.alignment = .centerY
+        filterBar.translatesAutoresizingMaskIntoConstraints = false
+        container.addSubview(filterBar)
+
+        // Hint text below filters (recording help — not Zoom API status).
         let status = makeLabel("", size: 12, color: secondaryText, weight: .regular, centered: false)
         status.translatesAutoresizingMaskIntoConstraints = false
         status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
@@ -7095,9 +7185,23 @@ class ViewController: NSViewController {
             openFolder.heightAnchor.constraint(equalToConstant: 30),
             openFolder.widthAnchor.constraint(greaterThanOrEqualToConstant: 110),
 
-            status.topAnchor.constraint(equalTo: titleStack.bottomAnchor, constant: 14),
-            status.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 24),
-            status.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -24),
+            filterBar.topAnchor.constraint(equalTo: titleStack.bottomAnchor, constant: 14),
+            filterBar.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 18),
+            filterBar.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -18),
+
+            categoryFilter.widthAnchor.constraint(equalToConstant: 170),
+            fromDatePicker.widthAnchor.constraint(equalToConstant: 170),
+            toDatePicker.widthAnchor.constraint(equalToConstant: 170),
+            applyFiltersButton.widthAnchor.constraint(equalToConstant: 96),
+            resetFiltersButton.widthAnchor.constraint(equalToConstant: 96),
+            refreshButton.widthAnchor.constraint(equalToConstant: 44),
+            applyFiltersButton.heightAnchor.constraint(equalToConstant: 34),
+            resetFiltersButton.heightAnchor.constraint(equalToConstant: 34),
+            refreshButton.heightAnchor.constraint(equalToConstant: 34),
+
+            status.topAnchor.constraint(equalTo: filterBar.bottomAnchor, constant: 12),
+            status.leadingAnchor.constraint(equalTo: filterBar.leadingAnchor),
+            status.trailingAnchor.constraint(equalTo: filterBar.trailingAnchor),
 
             listScroll.topAnchor.constraint(equalTo: status.bottomAnchor, constant: 10),
             listScroll.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
@@ -7136,12 +7240,96 @@ class ViewController: NSViewController {
         aiCompanionDetailStack = nil
         aiCompanionStopRecordingButton = stopRecording
         aiCompanionOpenRecordingsFolderButton = openFolder
-
+        aiCompanionFilterPopup = categoryFilter
+        aiCompanionFilterFromPicker = fromDatePicker
+        aiCompanionFilterToPicker = toDatePicker
+        aiCompanionFilterApplyButton = applyFiltersButton
+        aiCompanionFilterResetButton = resetFiltersButton
+        aiCompanionFilterRefreshButton = refreshButton
+
+        updateAiCompanionDatePickersEnabledState()
         renderAiCompanionList()
         updateAiCompanionRecordingUI()
         return root
     }
 
+    /// Mirrors `updateMeetingsDatePickersEnabledState` for the Ai Companion recordings filter row.
+    @MainActor
+    private func updateAiCompanionDatePickersEnabledState() {
+        let isCustomRange = aiCompanionRecordingFilter == .custom
+        aiCompanionFilterFromPicker?.isEnabled = isCustomRange
+        aiCompanionFilterToPicker?.isEnabled = isCustomRange
+        aiCompanionFilterFromPicker?.alphaValue = isCustomRange ? 1.0 : 0.55
+        aiCompanionFilterToPicker?.alphaValue = isCustomRange ? 1.0 : 0.55
+    }
+
+    @objc private func aiCompanionFilterPresetChanged(_ sender: NSPopUpButton) {
+        guard let title = sender.titleOfSelectedItem,
+              let preset = AiCompanionRecordingFilter(rawValue: title) else { return }
+        aiCompanionRecordingFilter = preset
+        updateAiCompanionDatePickersEnabledState()
+        renderAiCompanionList()
+    }
+
+    @objc private func aiCompanionFilterApplyTapped() {
+        renderAiCompanionList()
+    }
+
+    @objc private func aiCompanionFilterResetTapped() {
+        aiCompanionRecordingFilter = .all
+        aiCompanionFilterPopup?.selectItem(withTitle: AiCompanionRecordingFilter.all.rawValue)
+        aiCompanionFilterFromPicker?.dateValue = Date()
+        aiCompanionFilterToPicker?.dateValue = Date()
+        updateAiCompanionDatePickersEnabledState()
+        renderAiCompanionList()
+    }
+
+    @objc private func aiCompanionFilterRefreshTapped() {
+        recordedMeetings = MeetingRecordingManager.shared.recordings
+        renderAiCompanionList()
+        updateAiCompanionRecordingUI()
+    }
+
+    private func aiCompanionDateFilterHalfOpenInterval() -> (start: Date, endExclusive: Date)? {
+        let cal = Calendar.current
+        let now = Date()
+        switch aiCompanionRecordingFilter {
+        case .all:
+            return nil
+        case .today:
+            let d = cal.startOfDay(for: now)
+            guard let end = cal.date(byAdding: .day, value: 1, to: d) else { return nil }
+            return (d, end)
+        case .thisWeek:
+            guard let week = cal.dateInterval(of: .weekOfYear, for: now) else { return nil }
+            return (week.start, week.end)
+        case .previousWeek:
+            guard let thisWeek = cal.dateInterval(of: .weekOfYear, for: now) else { return nil }
+            let prevStart = cal.date(byAdding: .day, value: -7, to: thisWeek.start) ?? thisWeek.start
+            return (prevStart, thisWeek.start)
+        case .previousMonth:
+            let comps = cal.dateComponents([.year, .month], from: now)
+            guard let thisMonthStart = cal.date(from: comps) else { return nil }
+            guard let prevMonthStart = cal.date(byAdding: .month, value: -1, to: thisMonthStart) else { return nil }
+            return (prevMonthStart, thisMonthStart)
+        case .custom:
+            guard let fromPicker = aiCompanionFilterFromPicker, let toPicker = aiCompanionFilterToPicker else { return nil }
+            let a = cal.startOfDay(for: fromPicker.dateValue)
+            let b = cal.startOfDay(for: toPicker.dateValue)
+            let lo = min(a, b)
+            let hi = max(a, b)
+            guard let endExclusive = cal.date(byAdding: .day, value: 1, to: hi) else { return nil }
+            return (lo, endExclusive)
+        }
+    }
+
+    private func aiCompanionFilteredRecordings() -> [MeetingRecordingManager.RecordedMeeting] {
+        guard let interval = aiCompanionDateFilterHalfOpenInterval() else {
+            return recordedMeetings
+        }
+        return recordedMeetings.filter { $0.startedAt >= interval.start && $0.startedAt < interval.endExclusive }
+    }
+
     @MainActor
     private func updateAiCompanionRecordingUI() {
         let mgr = MeetingRecordingManager.shared
@@ -7194,16 +7382,27 @@ class ViewController: NSViewController {
             view.removeFromSuperview()
         }
 
-        let count = recordedMeetings.count
-        aiCompanionCountLabel?.stringValue = count == 1 ? "1 recording" : "\(count) recordings"
+        let displayed = aiCompanionFilteredRecordings()
+        let totalCount = recordedMeetings.count
+        let displayedCount = displayed.count
+        if aiCompanionRecordingFilter == .all {
+            aiCompanionCountLabel?.stringValue = totalCount == 1 ? "1 recording" : "\(totalCount) recordings"
+        } else {
+            aiCompanionCountLabel?.stringValue = "\(displayedCount) of \(totalCount) recordings"
+        }
 
-        let showEmpty = count == 0
+        let showEmpty = displayed.isEmpty
         if showEmpty {
-            aiCompanionEmptyLabel?.stringValue = "No recordings yet. Start or join a meeting to record it."
+            if totalCount == 0 {
+                aiCompanionEmptyLabel?.stringValue = "No recordings yet. Start or join a meeting to record it."
+            } else {
+                aiCompanionEmptyLabel?.stringValue = "No meetings match the selected filters."
+            }
         }
         aiCompanionEmptyLabel?.isHidden = !showEmpty
 
-        for (idx, recording) in recordedMeetings.enumerated() {
+        for recording in displayed {
+            guard let idx = recordedMeetings.firstIndex(where: { $0.id == recording.id }) else { continue }
             let row = makeRecordedMeetingCard(recording: recording, index: idx)
             stack.addArrangedSubview(row)
             row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
@@ -7213,10 +7412,18 @@ class ViewController: NSViewController {
     }
 
     private func localRecordingNotesStatus(for recording: MeetingRecordingManager.RecordedMeeting) -> LocalRecordingNotesStatus {
-        guard let raw = recording.notesStatusRaw, let s = LocalRecordingNotesStatus(rawValue: raw) else {
-            return .notRequested
+        if let raw = recording.notesStatusRaw, let s = LocalRecordingNotesStatus(rawValue: raw) {
+            if s == .notRequested,
+               let t = recording.notesText?.trimmingCharacters(in: .whitespacesAndNewlines),
+               t.isEmpty == false {
+                return .ready
+            }
+            return s
+        }
+        if let t = recording.notesText?.trimmingCharacters(in: .whitespacesAndNewlines), t.isEmpty == false {
+            return .ready
         }
-        return s
+        return .notRequested
     }
 
     @MainActor
@@ -8825,7 +9032,6 @@ class ViewController: NSViewController {
             recordedMeetings = MeetingRecordingManager.shared.recordings
             renderAiCompanionList()
             updateAiCompanionRecordingUI()
-            aiCompanionRefreshIfStale()
         }
     }