Просмотр исходного кода

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 месяцев назад
Родитель
Сommit
bb6bc91d79
1 измененных файлов с 220 добавлено и 14 удалено
  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()
         }
     }