|
|
@@ -279,6 +279,9 @@ final class ViewController: NSViewController {
|
|
279
|
279
|
let startedAt: Date
|
|
280
|
280
|
let endedAt: Date
|
|
281
|
281
|
let audioFilePath: String
|
|
|
282
|
+ /// Google account email that was signed in when this recording was saved.
|
|
|
283
|
+ /// Used to scope AI Companion history per-account.
|
|
|
284
|
+ var accountEmail: String?
|
|
282
|
285
|
var microphoneAudioFilePath: String?
|
|
283
|
286
|
var systemAudioFilePath: String?
|
|
284
|
287
|
var transcriptStatusRaw: String?
|
|
|
@@ -1562,7 +1565,20 @@ private extension ViewController {
|
|
1562
|
1565
|
}
|
|
1563
|
1566
|
let decoder = JSONDecoder()
|
|
1564
|
1567
|
if let decoded = try? decoder.decode([MeetingRecordingSummary].self, from: raw) {
|
|
1565
|
|
- aiCompanionLocalRecordings = decoded.sorted(by: { $0.endedAt > $1.endedAt })
|
|
|
1568
|
+ var migrated = decoded
|
|
|
1569
|
+ let testEmail = "mqlmac1@gmail.com"
|
|
|
1570
|
+ var didMigrate = false
|
|
|
1571
|
+ for idx in migrated.indices {
|
|
|
1572
|
+ let existing = migrated[idx].accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
1573
|
+ if existing?.isEmpty != false {
|
|
|
1574
|
+ migrated[idx].accountEmail = testEmail
|
|
|
1575
|
+ didMigrate = true
|
|
|
1576
|
+ }
|
|
|
1577
|
+ }
|
|
|
1578
|
+ aiCompanionLocalRecordings = migrated.sorted(by: { $0.endedAt > $1.endedAt })
|
|
|
1579
|
+ if didMigrate {
|
|
|
1580
|
+ persistAiCompanionLocalRecordings()
|
|
|
1581
|
+ }
|
|
1566
|
1582
|
} else {
|
|
1567
|
1583
|
aiCompanionLocalRecordings = []
|
|
1568
|
1584
|
}
|
|
|
@@ -1574,6 +1590,18 @@ private extension ViewController {
|
|
1574
|
1590
|
UserDefaults.standard.set(encoded, forKey: aiCompanionLocalRecordingsDefaultsKey)
|
|
1575
|
1591
|
}
|
|
1576
|
1592
|
|
|
|
1593
|
+ private func aiCompanionActiveAccountEmail() -> String? {
|
|
|
1594
|
+ guard hasGoogleSessionAvailable() else { return nil }
|
|
|
1595
|
+ let raw = scheduleCurrentProfile?.email.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
1596
|
+ guard let raw, raw.isEmpty == false, raw.contains("@") else { return nil }
|
|
|
1597
|
+ return raw.lowercased()
|
|
|
1598
|
+ }
|
|
|
1599
|
+
|
|
|
1600
|
+ private func aiCompanionRecordingsForActiveAccount() -> [MeetingRecordingSummary] {
|
|
|
1601
|
+ guard let email = aiCompanionActiveAccountEmail() else { return [] }
|
|
|
1602
|
+ return aiCompanionLocalRecordings.filter { ($0.accountEmail ?? "").lowercased() == email }
|
|
|
1603
|
+ }
|
|
|
1604
|
+
|
|
1577
|
1605
|
private func requestSpeechRecognitionAuthorizationIfNeeded() async throws {
|
|
1578
|
1606
|
switch SFSpeechRecognizer.authorizationStatus() {
|
|
1579
|
1607
|
case .authorized:
|
|
|
@@ -1921,6 +1949,24 @@ private extension ViewController {
|
|
1921
|
1949
|
microphoneURL: session.microphoneAudioFileURL,
|
|
1922
|
1950
|
recordingID: session.id
|
|
1923
|
1951
|
)
|
|
|
1952
|
+ var accountEmail = await MainActor.run { self.aiCompanionActiveAccountEmail() }
|
|
|
1953
|
+ if accountEmail == nil, self.hasGoogleSessionAvailable() {
|
|
|
1954
|
+ do {
|
|
|
1955
|
+ let token = try await self.googleOAuth.validAccessToken(presentingWindow: self.view.window)
|
|
|
1956
|
+ if let profile = try? await self.googleOAuth.fetchUserProfile(accessToken: token) {
|
|
|
1957
|
+ let display = await MainActor.run { self.makeGoogleProfileDisplay(from: profile) }
|
|
|
1958
|
+ await MainActor.run { [weak self] in
|
|
|
1959
|
+ self?.applyGoogleProfile(display)
|
|
|
1960
|
+ }
|
|
|
1961
|
+ let cleaned = profile.email?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
1962
|
+ if let cleaned, cleaned.isEmpty == false {
|
|
|
1963
|
+ accountEmail = cleaned.lowercased()
|
|
|
1964
|
+ }
|
|
|
1965
|
+ }
|
|
|
1966
|
+ } catch {
|
|
|
1967
|
+ // If profile lookup fails, persist recording without account scoping (it will be hidden in AI Companion).
|
|
|
1968
|
+ }
|
|
|
1969
|
+ }
|
|
1924
|
1970
|
await MainActor.run {
|
|
1925
|
1971
|
let summary = MeetingRecordingSummary(
|
|
1926
|
1972
|
id: session.id,
|
|
|
@@ -1929,6 +1975,7 @@ private extension ViewController {
|
|
1929
|
1975
|
startedAt: session.startedAt,
|
|
1930
|
1976
|
endedAt: Date(),
|
|
1931
|
1977
|
audioFilePath: finalized.mixedURL.path,
|
|
|
1978
|
+ accountEmail: accountEmail,
|
|
1932
|
1979
|
microphoneAudioFilePath: finalized.microphoneURL?.path,
|
|
1933
|
1980
|
systemAudioFilePath: finalized.systemURL?.path,
|
|
1934
|
1981
|
transcriptStatusRaw: MeetingTranscriptStatus.notRequested.rawValue,
|
|
|
@@ -2870,15 +2917,92 @@ private extension ViewController {
|
|
2870
|
2917
|
titleLabel.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2871
|
2918
|
subtitle.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2872
|
2919
|
|
|
|
2920
|
+ let signedIn = hasGoogleSessionAvailable()
|
|
|
2921
|
+ let activeEmail = aiCompanionActiveAccountEmail()
|
|
|
2922
|
+
|
|
|
2923
|
+ if signedIn == false || activeEmail == nil {
|
|
|
2924
|
+ let locked = roundedContainer(cornerRadius: 14, color: palette.sectionCard)
|
|
|
2925
|
+ locked.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2926
|
+ styleSurface(locked, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
2927
|
+
|
|
|
2928
|
+ let lockedStack = NSStackView()
|
|
|
2929
|
+ lockedStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2930
|
+ lockedStack.orientation = .vertical
|
|
|
2931
|
+ lockedStack.alignment = .leading
|
|
|
2932
|
+ lockedStack.spacing = 10
|
|
|
2933
|
+
|
|
|
2934
|
+ let msg = textLabel(
|
|
|
2935
|
+ "Connect your Google account to see your AI Companion meeting history.",
|
|
|
2936
|
+ font: NSFont.systemFont(ofSize: 14, weight: .semibold),
|
|
|
2937
|
+ color: palette.textPrimary
|
|
|
2938
|
+ )
|
|
|
2939
|
+ msg.alignment = .left
|
|
|
2940
|
+ msg.maximumNumberOfLines = 2
|
|
|
2941
|
+ msg.lineBreakMode = .byWordWrapping
|
|
|
2942
|
+
|
|
|
2943
|
+ lockedStack.addArrangedSubview(msg)
|
|
|
2944
|
+ locked.addSubview(lockedStack)
|
|
|
2945
|
+ NSLayoutConstraint.activate([
|
|
|
2946
|
+ lockedStack.leadingAnchor.constraint(equalTo: locked.leadingAnchor, constant: 14),
|
|
|
2947
|
+ lockedStack.trailingAnchor.constraint(equalTo: locked.trailingAnchor, constant: -14),
|
|
|
2948
|
+ lockedStack.topAnchor.constraint(equalTo: locked.topAnchor, constant: 14),
|
|
|
2949
|
+ lockedStack.bottomAnchor.constraint(equalTo: locked.bottomAnchor, constant: -14)
|
|
|
2950
|
+ ])
|
|
|
2951
|
+
|
|
|
2952
|
+ contentStack.addArrangedSubview(locked)
|
|
|
2953
|
+ locked.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
2954
|
+
|
|
|
2955
|
+ // If tokens exist but we haven't fetched the profile yet, try to hydrate it so we can scope history correctly.
|
|
|
2956
|
+ if signedIn, scheduleCurrentProfile == nil {
|
|
|
2957
|
+ Task { [weak self] in
|
|
|
2958
|
+ guard let self else { return }
|
|
|
2959
|
+ do {
|
|
|
2960
|
+ let token = try await self.googleOAuth.validAccessToken(presentingWindow: self.view.window)
|
|
|
2961
|
+ if let profile = try? await self.googleOAuth.fetchUserProfile(accessToken: token) {
|
|
|
2962
|
+ await MainActor.run { [weak self] in
|
|
|
2963
|
+ guard let self else { return }
|
|
|
2964
|
+ self.applyGoogleProfile(self.makeGoogleProfileDisplay(from: profile))
|
|
|
2965
|
+ self.redrawAiCompanionPageIfNeeded()
|
|
|
2966
|
+ }
|
|
|
2967
|
+ }
|
|
|
2968
|
+ } catch {
|
|
|
2969
|
+ // Leave UI in connect state; schedule page handles showing auth errors.
|
|
|
2970
|
+ }
|
|
|
2971
|
+ }
|
|
|
2972
|
+ }
|
|
|
2973
|
+
|
|
|
2974
|
+ content.addSubview(contentStack)
|
|
|
2975
|
+ NSLayoutConstraint.activate([
|
|
|
2976
|
+ scroll.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
|
|
|
2977
|
+ scroll.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
|
|
|
2978
|
+ scroll.topAnchor.constraint(equalTo: panel.topAnchor),
|
|
|
2979
|
+ scroll.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
|
|
|
2980
|
+
|
|
|
2981
|
+ content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
|
2982
|
+ content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
|
|
|
2983
|
+ content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
|
|
|
2984
|
+ content.bottomAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.bottomAnchor),
|
|
|
2985
|
+ content.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor),
|
|
|
2986
|
+
|
|
|
2987
|
+ contentStack.leftAnchor.constraint(equalTo: content.leftAnchor, constant: 28),
|
|
|
2988
|
+ contentStack.rightAnchor.constraint(equalTo: content.rightAnchor, constant: -28),
|
|
|
2989
|
+ contentStack.topAnchor.constraint(equalTo: content.topAnchor, constant: 16),
|
|
|
2990
|
+ content.bottomAnchor.constraint(greaterThanOrEqualTo: contentStack.bottomAnchor, constant: 16)
|
|
|
2991
|
+ ])
|
|
|
2992
|
+
|
|
|
2993
|
+ return panel
|
|
|
2994
|
+ }
|
|
|
2995
|
+
|
|
2873
|
2996
|
if let session = activeMeetingRecordingSession {
|
|
2874
|
2997
|
let activeCard = aiCompanionActiveRecordingCard(session: session)
|
|
2875
|
2998
|
contentStack.addArrangedSubview(activeCard)
|
|
2876
|
2999
|
activeCard.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2877
|
3000
|
}
|
|
2878
|
3001
|
|
|
2879
|
|
- if aiCompanionLocalRecordings.isEmpty {
|
|
|
3002
|
+ let scopedRecordings = aiCompanionRecordingsForActiveAccount()
|
|
|
3003
|
+ if scopedRecordings.isEmpty {
|
|
2880
|
3004
|
let emptyLabel = textLabel(
|
|
2881
|
|
- "No previous meetings yet. End a meeting to save it here and generate notes.",
|
|
|
3005
|
+ "No previous meetings yet for \(activeEmail ?? "this account"). End a meeting to save it here and generate notes.",
|
|
2882
|
3006
|
font: typography.fieldLabel,
|
|
2883
|
3007
|
color: palette.textMuted
|
|
2884
|
3008
|
)
|
|
|
@@ -3059,7 +3183,7 @@ private extension ViewController {
|
|
3059
|
3183
|
private func filteredAiCompanionRecordings() -> [MeetingRecordingSummary] {
|
|
3060
|
3184
|
let calendar = Calendar.current
|
|
3061
|
3185
|
let now = Date()
|
|
3062
|
|
- let recordings = aiCompanionLocalRecordings
|
|
|
3186
|
+ let recordings = aiCompanionRecordingsForActiveAccount()
|
|
3063
|
3187
|
|
|
3064
|
3188
|
func previousCalendarWeekBounds(reference: Date) -> (start: Date, end: Date)? {
|
|
3065
|
3189
|
guard let thisWeek = calendar.dateInterval(of: .weekOfYear, for: reference),
|
|
|
@@ -9936,6 +10060,8 @@ private extension ViewController {
|
|
9936
|
10060
|
scheduleCurrentProfile = profile
|
|
9937
|
10061
|
|
|
9938
|
10062
|
updateGoogleAuthButtonTitle()
|
|
|
10063
|
+ pageCache[.aiCompanion] = nil
|
|
|
10064
|
+ redrawAiCompanionPageIfNeeded()
|
|
9939
|
10065
|
|
|
9940
|
10066
|
guard let profile, let pictureURL = profile.pictureURL else { return }
|
|
9941
|
10067
|
let avatarDiameter = scheduleGoogleSignedInAvatarSize
|