|
@@ -349,6 +349,24 @@ class ViewController: NSViewController {
|
|
|
private weak var aiCompanionLocalNotesHeaderView: NSView?
|
|
private weak var aiCompanionLocalNotesHeaderView: NSView?
|
|
|
private weak var aiCompanionLocalNotesSubtitleLabel: NSTextField?
|
|
private weak var aiCompanionLocalNotesSubtitleLabel: NSTextField?
|
|
|
private var aiCompanionLocalNotesPanelRecordingID: String?
|
|
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 let meetingNotesService = MeetingNotesService()
|
|
|
private var localRecordingNotesTaskByRecordingId: [String: Task<Void, Never>] = [:]
|
|
private var localRecordingNotesTaskByRecordingId: [String: Task<Void, Never>] = [:]
|
|
|
private var localRecordingNotesRequestIdByRecordingId: [String: UUID] = [:]
|
|
private var localRecordingNotesRequestIdByRecordingId: [String: UUID] = [:]
|
|
@@ -862,6 +880,12 @@ class ViewController: NSViewController {
|
|
|
aiCompanionCopyMarkdownButton = nil
|
|
aiCompanionCopyMarkdownButton = nil
|
|
|
aiCompanionStopRecordingButton = nil
|
|
aiCompanionStopRecordingButton = nil
|
|
|
aiCompanionOpenRecordingsFolderButton = nil
|
|
aiCompanionOpenRecordingsFolderButton = nil
|
|
|
|
|
+ aiCompanionFilterPopup = nil
|
|
|
|
|
+ aiCompanionFilterFromPicker = nil
|
|
|
|
|
+ aiCompanionFilterToPicker = nil
|
|
|
|
|
+ aiCompanionFilterApplyButton = nil
|
|
|
|
|
+ aiCompanionFilterResetButton = nil
|
|
|
|
|
+ aiCompanionFilterRefreshButton = nil
|
|
|
aiCompanionRowButtons.removeAll()
|
|
aiCompanionRowButtons.removeAll()
|
|
|
aiCompanionLastLoadedAt = nil
|
|
aiCompanionLastLoadedAt = nil
|
|
|
homeView?.removeFromSuperview()
|
|
homeView?.removeFromSuperview()
|
|
@@ -7021,7 +7045,73 @@ class ViewController: NSViewController {
|
|
|
container.addSubview(titleStack)
|
|
container.addSubview(titleStack)
|
|
|
container.addSubview(toolbarRight)
|
|
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)
|
|
let status = makeLabel("", size: 12, color: secondaryText, weight: .regular, centered: false)
|
|
|
status.translatesAutoresizingMaskIntoConstraints = false
|
|
status.translatesAutoresizingMaskIntoConstraints = false
|
|
|
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
@@ -7095,9 +7185,23 @@ class ViewController: NSViewController {
|
|
|
openFolder.heightAnchor.constraint(equalToConstant: 30),
|
|
openFolder.heightAnchor.constraint(equalToConstant: 30),
|
|
|
openFolder.widthAnchor.constraint(greaterThanOrEqualToConstant: 110),
|
|
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.topAnchor.constraint(equalTo: status.bottomAnchor, constant: 10),
|
|
|
listScroll.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
|
listScroll.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
|
@@ -7136,12 +7240,96 @@ class ViewController: NSViewController {
|
|
|
aiCompanionDetailStack = nil
|
|
aiCompanionDetailStack = nil
|
|
|
aiCompanionStopRecordingButton = stopRecording
|
|
aiCompanionStopRecordingButton = stopRecording
|
|
|
aiCompanionOpenRecordingsFolderButton = openFolder
|
|
aiCompanionOpenRecordingsFolderButton = openFolder
|
|
|
-
|
|
|
|
|
|
|
+ aiCompanionFilterPopup = categoryFilter
|
|
|
|
|
+ aiCompanionFilterFromPicker = fromDatePicker
|
|
|
|
|
+ aiCompanionFilterToPicker = toDatePicker
|
|
|
|
|
+ aiCompanionFilterApplyButton = applyFiltersButton
|
|
|
|
|
+ aiCompanionFilterResetButton = resetFiltersButton
|
|
|
|
|
+ aiCompanionFilterRefreshButton = refreshButton
|
|
|
|
|
+
|
|
|
|
|
+ updateAiCompanionDatePickersEnabledState()
|
|
|
renderAiCompanionList()
|
|
renderAiCompanionList()
|
|
|
updateAiCompanionRecordingUI()
|
|
updateAiCompanionRecordingUI()
|
|
|
return root
|
|
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
|
|
@MainActor
|
|
|
private func updateAiCompanionRecordingUI() {
|
|
private func updateAiCompanionRecordingUI() {
|
|
|
let mgr = MeetingRecordingManager.shared
|
|
let mgr = MeetingRecordingManager.shared
|
|
@@ -7194,16 +7382,27 @@ class ViewController: NSViewController {
|
|
|
view.removeFromSuperview()
|
|
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 {
|
|
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
|
|
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)
|
|
let row = makeRecordedMeetingCard(recording: recording, index: idx)
|
|
|
stack.addArrangedSubview(row)
|
|
stack.addArrangedSubview(row)
|
|
|
row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
@@ -7213,10 +7412,18 @@ class ViewController: NSViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func localRecordingNotesStatus(for recording: MeetingRecordingManager.RecordedMeeting) -> LocalRecordingNotesStatus {
|
|
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
|
|
@MainActor
|
|
@@ -8825,7 +9032,6 @@ class ViewController: NSViewController {
|
|
|
recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
|
renderAiCompanionList()
|
|
renderAiCompanionList()
|
|
|
updateAiCompanionRecordingUI()
|
|
updateAiCompanionRecordingUI()
|
|
|
- aiCompanionRefreshIfStale()
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|