|
|
@@ -343,6 +343,16 @@ class ViewController: NSViewController {
|
|
|
private weak var aiCompanionLocalTranscriptHeaderView: NSView?
|
|
|
private weak var aiCompanionLocalTranscriptSubtitleLabel: NSTextField?
|
|
|
private var aiCompanionLocalTranscriptPanelRecordingID: String?
|
|
|
+ private weak var aiCompanionLocalNotesPanel: NSPanel?
|
|
|
+ private weak var aiCompanionLocalNotesTextView: NSTextView?
|
|
|
+ private weak var aiCompanionLocalNotesRootView: NSView?
|
|
|
+ private weak var aiCompanionLocalNotesHeaderView: NSView?
|
|
|
+ private weak var aiCompanionLocalNotesSubtitleLabel: NSTextField?
|
|
|
+ private var aiCompanionLocalNotesPanelRecordingID: String?
|
|
|
+ private let meetingNotesService = MeetingNotesService()
|
|
|
+ private var localRecordingNotesTaskByRecordingId: [String: Task<Void, Never>] = [:]
|
|
|
+ private var localRecordingNotesRequestIdByRecordingId: [String: UUID] = [:]
|
|
|
+ private var localRecordingNotesProgressByRecordingId: [String: String] = [:]
|
|
|
private weak var meetingsPageCategoryFilter: NSPopUpButton?
|
|
|
private weak var meetingsPageFromDatePicker: NSDatePicker?
|
|
|
private weak var meetingsPageToDatePicker: NSDatePicker?
|
|
|
@@ -420,6 +430,13 @@ class ViewController: NSViewController {
|
|
|
private weak var meetingConsentSecondaryLanguagePopup: NSPopUpButton?
|
|
|
|
|
|
private var aiCompanionTranscriptionActiveRecordingIDs: Set<String> = []
|
|
|
+
|
|
|
+ private enum LocalRecordingNotesStatus: String {
|
|
|
+ case notRequested
|
|
|
+ case processing
|
|
|
+ case ready
|
|
|
+ case failed
|
|
|
+ }
|
|
|
private let paywallContentWidth: CGFloat = 520
|
|
|
private var selectedPremiumPlan: PremiumPlan = .yearly
|
|
|
private var paywallPlanViews: [PremiumPlan: [NSView]] = [:]
|
|
|
@@ -679,6 +696,8 @@ class ViewController: NSViewController {
|
|
|
self.aiCompanionTranscriptionActiveRecordingIDs.remove(rec.id)
|
|
|
}
|
|
|
self.renderAiCompanionList()
|
|
|
+ self.refreshLocalRecordingTranscriptPanelContentIfNeeded()
|
|
|
+ self.refreshLocalRecordingNotesPanelContentIfNeeded()
|
|
|
}
|
|
|
}
|
|
|
NotificationCenter.default.addObserver(
|
|
|
@@ -7165,6 +7184,14 @@ class ViewController: NSViewController {
|
|
|
row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
}
|
|
|
refreshLocalRecordingTranscriptPanelContentIfNeeded()
|
|
|
+ refreshLocalRecordingNotesPanelContentIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func localRecordingNotesStatus(for recording: MeetingRecordingManager.RecordedMeeting) -> LocalRecordingNotesStatus {
|
|
|
+ guard let raw = recording.notesStatusRaw, let s = LocalRecordingNotesStatus(rawValue: raw) else {
|
|
|
+ return .notRequested
|
|
|
+ }
|
|
|
+ return s
|
|
|
}
|
|
|
|
|
|
@MainActor
|
|
|
@@ -7201,6 +7228,25 @@ class ViewController: NSViewController {
|
|
|
transcriptBtn.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
|
transcriptBtn.tag = index
|
|
|
|
|
|
+ let getNotesBtn = NSButton(title: "Get notes", 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.tag = index
|
|
|
+
|
|
|
+ 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))
|
|
|
+ getNotesBtn.isEnabled = getNotesEnabled
|
|
|
+
|
|
|
let play = NSButton(title: recordedMeetingsPlayingID == recording.id ? "Stop" : "Play", target: self, action: #selector(recordingPlayTapped(_:)))
|
|
|
play.translatesAutoresizingMaskIntoConstraints = false
|
|
|
play.isBordered = false
|
|
|
@@ -7211,7 +7257,10 @@ class ViewController: NSViewController {
|
|
|
play.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
|
play.tag = index
|
|
|
|
|
|
- let buttonsRow = NSStackView(views: [transcriptBtn, play])
|
|
|
+ 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
|
|
|
@@ -7251,6 +7300,36 @@ class ViewController: NSViewController {
|
|
|
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
|
|
|
+ }
|
|
|
+
|
|
|
NSLayoutConstraint.activate([
|
|
|
title.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
|
|
|
title.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
|
|
|
@@ -7260,9 +7339,11 @@ class ViewController: NSViewController {
|
|
|
buttonsRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12),
|
|
|
|
|
|
transcriptBtn.heightAnchor.constraint(equalToConstant: 30),
|
|
|
- transcriptBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 92),
|
|
|
+ transcriptBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 80),
|
|
|
+ getNotesBtn.heightAnchor.constraint(equalToConstant: 30),
|
|
|
+ getNotesBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 88),
|
|
|
play.heightAnchor.constraint(equalToConstant: 30),
|
|
|
- play.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
|
|
|
+ play.widthAnchor.constraint(greaterThanOrEqualToConstant: 68),
|
|
|
|
|
|
dateLabel.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 4),
|
|
|
dateLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
|
@@ -7287,6 +7368,296 @@ class ViewController: NSViewController {
|
|
|
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."
|
|
|
}
|
|
|
|
|
|
+ private func bodyTextForLocalRecordingNotes(recording: MeetingRecordingManager.RecordedMeeting) -> String {
|
|
|
+ switch localRecordingNotesStatus(for: recording) {
|
|
|
+ case .processing:
|
|
|
+ return localRecordingNotesProgressByRecordingId[recording.id] ?? "Preparing notes…"
|
|
|
+ case .failed:
|
|
|
+ let err = recording.notesError?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
+ return err.isEmpty ? "Notes could not be generated." : "Notes unavailable.\n\n\(err)"
|
|
|
+ case .ready:
|
|
|
+ return recording.notesText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
+ case .notRequested:
|
|
|
+ return "Tap Get notes on the recording card after the transcript is ready."
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func refreshLocalRecordingNotesPanelContentIfNeeded() {
|
|
|
+ guard let recordingID = aiCompanionLocalNotesPanelRecordingID,
|
|
|
+ let textView = aiCompanionLocalNotesTextView,
|
|
|
+ let recording = recordedMeetings.first(where: { $0.id == recordingID }) else { return }
|
|
|
+ textView.string = bodyTextForLocalRecordingNotes(recording: recording)
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func presentLocalRecordingNotesPanel(recording: MeetingRecordingManager.RecordedMeeting, initialText: String?) {
|
|
|
+ guard let hostWindow = view.window else { return }
|
|
|
+ let meetingTitle = recording.resolvedDisplayTitle
|
|
|
+ let trimmedInitial = initialText?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ let body: String = {
|
|
|
+ if let trimmedInitial, trimmedInitial.isEmpty == false { return trimmedInitial }
|
|
|
+ return bodyTextForLocalRecordingNotes(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 = aiCompanionLocalNotesPanel, let textView = aiCompanionLocalNotesTextView {
|
|
|
+ panel.title = "Notes - \(meetingTitle)"
|
|
|
+ panel.appearance = NSAppearance(named: panelAppearanceName)
|
|
|
+ panel.backgroundColor = outerBackgroundColor
|
|
|
+ textView.string = body
|
|
|
+ aiCompanionLocalNotesPanelRecordingID = recording.id
|
|
|
+ aiCompanionLocalNotesRootView?.layer?.backgroundColor = outerBackgroundColor.cgColor
|
|
|
+ aiCompanionLocalNotesHeaderView?.layer?.backgroundColor = headerBackgroundColor.cgColor
|
|
|
+ aiCompanionLocalNotesSubtitleLabel?.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 = "Notes - \(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("Meeting notes", 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(aiCompanionLocalNotesCopyTapped(_:)))
|
|
|
+ copyButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ copyButton.bezelStyle = .rounded
|
|
|
+
|
|
|
+ let closeButton = NSButton(title: "Close", target: self, action: #selector(aiCompanionLocalNotesCloseTapped(_:)))
|
|
|
+ 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
|
|
|
+ aiCompanionLocalNotesPanel = panel
|
|
|
+ aiCompanionLocalNotesTextView = textView
|
|
|
+ aiCompanionLocalNotesRootView = root
|
|
|
+ aiCompanionLocalNotesHeaderView = header
|
|
|
+ aiCompanionLocalNotesSubtitleLabel = subtitleLabel
|
|
|
+ aiCompanionLocalNotesPanelRecordingID = recording.id
|
|
|
+
|
|
|
+ hostWindow.beginSheet(panel, completionHandler: nil)
|
|
|
+ panel.makeKeyAndOrderFront(nil)
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func startLocalRecordingNotesProcessing(recordingId: String, transcript: String, requestId: UUID, autoPresentOnReady: Bool) {
|
|
|
+ localRecordingNotesTaskByRecordingId[recordingId]?.cancel()
|
|
|
+ localRecordingNotesRequestIdByRecordingId[recordingId] = requestId
|
|
|
+ localRecordingNotesProgressByRecordingId[recordingId] = "Preparing notes, please wait..."
|
|
|
+ MeetingRecordingManager.shared.setMeetingNotes(
|
|
|
+ recordingId: recordingId,
|
|
|
+ notesStatusRaw: LocalRecordingNotesStatus.processing.rawValue,
|
|
|
+ notesText: nil,
|
|
|
+ notesError: nil
|
|
|
+ )
|
|
|
+ recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
|
+ renderAiCompanionList()
|
|
|
+
|
|
|
+ let task = Task { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ do {
|
|
|
+ // Notes language must follow the transcript text, not Apple Speech locale prefs
|
|
|
+ // (e.g. Urdu selected for recognition must not force Urdu notes for an English transcript).
|
|
|
+ let notes = try await self.meetingNotesService.generateNotes(from: transcript)
|
|
|
+ if Task.isCancelled {
|
|
|
+ await MainActor.run { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ if self.localRecordingNotesRequestIdByRecordingId[recordingId] == requestId {
|
|
|
+ self.localRecordingNotesTaskByRecordingId[recordingId] = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await MainActor.run { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ guard self.localRecordingNotesRequestIdByRecordingId[recordingId] == requestId else { return }
|
|
|
+ self.localRecordingNotesProgressByRecordingId[recordingId] = nil
|
|
|
+ MeetingRecordingManager.shared.setMeetingNotes(
|
|
|
+ recordingId: recordingId,
|
|
|
+ notesStatusRaw: LocalRecordingNotesStatus.ready.rawValue,
|
|
|
+ notesText: notes,
|
|
|
+ notesError: nil
|
|
|
+ )
|
|
|
+ self.recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
|
+ self.refreshLocalRecordingNotesPanelContentIfNeeded()
|
|
|
+ self.renderAiCompanionList()
|
|
|
+ if autoPresentOnReady, let rec = self.recordedMeetings.first(where: { $0.id == recordingId }) {
|
|
|
+ self.presentLocalRecordingNotesPanel(recording: rec, initialText: notes)
|
|
|
+ }
|
|
|
+ if self.localRecordingNotesRequestIdByRecordingId[recordingId] == requestId {
|
|
|
+ self.localRecordingNotesTaskByRecordingId[recordingId] = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ if error is CancellationError {
|
|
|
+ await MainActor.run { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ if self.localRecordingNotesRequestIdByRecordingId[recordingId] == requestId {
|
|
|
+ self.localRecordingNotesTaskByRecordingId[recordingId] = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await MainActor.run { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ guard self.localRecordingNotesRequestIdByRecordingId[recordingId] == requestId else { return }
|
|
|
+ self.localRecordingNotesProgressByRecordingId[recordingId] = nil
|
|
|
+ let msg = error.localizedDescription.isEmpty ? "Failed to generate notes." : error.localizedDescription
|
|
|
+ MeetingRecordingManager.shared.setMeetingNotes(
|
|
|
+ recordingId: recordingId,
|
|
|
+ notesStatusRaw: LocalRecordingNotesStatus.failed.rawValue,
|
|
|
+ notesText: nil,
|
|
|
+ notesError: msg
|
|
|
+ )
|
|
|
+ self.recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
|
+ self.refreshLocalRecordingNotesPanelContentIfNeeded()
|
|
|
+ self.renderAiCompanionList()
|
|
|
+ if self.localRecordingNotesRequestIdByRecordingId[recordingId] == requestId {
|
|
|
+ self.localRecordingNotesTaskByRecordingId[recordingId] = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ localRecordingNotesTaskByRecordingId[recordingId] = task
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func recordingGetNotesTapped(_ sender: NSButton) {
|
|
|
+ let idx = sender.tag
|
|
|
+ guard recordedMeetings.indices.contains(idx) else { return }
|
|
|
+ let recording = recordedMeetings[idx]
|
|
|
+ let status = localRecordingNotesStatus(for: recording)
|
|
|
+
|
|
|
+ if status == .ready,
|
|
|
+ let text = recording.notesText?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
|
+ text.isEmpty == false {
|
|
|
+ presentLocalRecordingNotesPanel(recording: recording, initialText: text)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if status == .processing { return }
|
|
|
+
|
|
|
+ if aiCompanionTranscriptionActiveRecordingIDs.contains(recording.id) {
|
|
|
+ showSimpleAlert(title: "Transcript not ready", message: "Wait until the transcript finishes generating, then try Get notes again.")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let transcript = recording.transcriptText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
+ if transcript.isEmpty {
|
|
|
+ if let err = recording.transcriptError?.trimmingCharacters(in: .whitespacesAndNewlines), err.isEmpty == false {
|
|
|
+ showSimpleAlert(title: "No transcript", message: "Transcription failed for this recording, so notes cannot be generated.")
|
|
|
+ } else {
|
|
|
+ showSimpleAlert(title: "No transcript yet", message: "Wait until the transcript is available (or open Transcript to check progress), then tap Get notes.")
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let requestId = UUID()
|
|
|
+ startLocalRecordingNotesProcessing(recordingId: recording.id, transcript: transcript, requestId: requestId, autoPresentOnReady: true)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func aiCompanionLocalNotesCopyTapped(_ sender: NSButton) {
|
|
|
+ let text = aiCompanionLocalNotesTextView?.string ?? ""
|
|
|
+ NSPasteboard.general.clearContents()
|
|
|
+ NSPasteboard.general.setString(text, forType: .string)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func aiCompanionLocalNotesCloseTapped(_ sender: NSButton) {
|
|
|
+ guard let panel = aiCompanionLocalNotesPanel, let host = view.window else { return }
|
|
|
+ host.endSheet(panel)
|
|
|
+ }
|
|
|
+
|
|
|
@MainActor
|
|
|
private func refreshLocalRecordingTranscriptPanelContentIfNeeded() {
|
|
|
guard let recordingID = aiCompanionLocalTranscriptPanelRecordingID,
|
|
|
@@ -8784,6 +9155,10 @@ extension ViewController: NSWindowDelegate {
|
|
|
host.endSheet(panel)
|
|
|
return false
|
|
|
}
|
|
|
+ if let panel = aiCompanionLocalNotesPanel, sender === panel, let host = view.window {
|
|
|
+ host.endSheet(panel)
|
|
|
+ return false
|
|
|
+ }
|
|
|
return true
|
|
|
}
|
|
|
|