diff --git a/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift b/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift index b9acf8784..98ee89d4e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift @@ -40,6 +40,7 @@ public class DigestConfigViewModel: ObservableObject { try await dataService.setupUserDigestConfig() try await dataService.refreshDigest() digestEnabled = true + dataService.featureFlags.digestEnabled = true } catch { if error is IneligibleError { isIneligible = true @@ -109,20 +110,17 @@ struct DigestConfigView: View { } .padding(.top, 50) } else if viewModel.digestEnabled { - VStack(spacing: 15) { + VStack(spacing: 25) { Spacer() - Text(""" - You've been added to the AI Digest demo. Your first issue should be ready soon. - When a new digest is ready the icon in the library header will change color. - You can close this window now. - """) + // swiftlint:disable:next line_length + Text("You've been added to the AI Digest demo. Your first issue should be ready soon. When a new digest is ready the icon in the library header will change color. You can close this window now.") if !viewModel.notificationsEnabled { if viewModel.isTryingToEnableNotifications { ProgressView() } else { Button(action: { viewModel.tryEnableNotifications(dataService: dataService) - }, label: { Text("Notify me when its ready") }) + }, label: { Text("Enable digest notifications") }) .buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white)) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift index fc92a1db9..94ea44818 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift @@ -12,17 +12,19 @@ func getChapterData(digest: DigestResult) -> [(DigestChapter, DigestChapterData) var currentAudioIndex = 0 var currentWordCount = 0.0 - for (index, speechFile) in digest.speechFiles.enumerated() { - let chapter = digest.chapters[index] + for (index, speechFile) in (digest.speechFiles ?? []).enumerated() { + let chapter = digest.chapters?[index] let duration = currentWordCount / SpeechDocument.averageWPM / speed * 60.0 - chapterData.append((chapter, DigestChapterData( - time: formatTimeInterval(duration) ?? "00:00", - start: Int(currentAudioIndex), - end: currentAudioIndex + Int(speechFile.utterances.count) - ))) - currentAudioIndex += Int(speechFile.utterances.count) - currentWordCount += chapter.wordCount + if let chapter = chapter { + chapterData.append((chapter, DigestChapterData( + time: formatTimeInterval(duration) ?? "00:00", + start: Int(currentAudioIndex), + end: currentAudioIndex + Int(speechFile.utterances.count) + ))) + currentAudioIndex += Int(speechFile.utterances.count) + currentWordCount += chapter.wordCount + } } return chapterData } @@ -39,6 +41,7 @@ func formatTimeInterval(_ time: TimeInterval) -> String? { public class FullScreenDigestViewModel: ObservableObject { @Published var isLoading = false @Published var hasError = false + @Published var isRunning = false @Published var digest: DigestResult? @Published var chapterInfo: [(DigestChapter, DigestChapterData)]? @Published var presentedLibraryItem: String? @@ -49,6 +52,7 @@ public class FullScreenDigestViewModel: ObservableObject { func load(dataService: DataService, audioController: AudioController) async { hasError = false isLoading = true + isRunning = false if !dataService.digestNeedsRefresh() { if let digest = dataService.loadStoredDigest() { @@ -69,6 +73,8 @@ public class FullScreenDigestViewModel: ObservableObject { self.digest = digest self.chapterInfo = getChapterData(digest: digest) self.lastVisitedDigestId = digest.id + self.isRunning = digest.jobState == "RUNNING" || digest.jobState == "PENDING" + self.hasError = digest.jobState == "FAILED" if let playingDigest = audioController.itemAudioProperties as? DigestAudioItem, playingDigest.digest.id == digest.id { // Don't think we need to do anything here @@ -179,9 +185,11 @@ struct FullScreenDigestView: View { await viewModel.load(dataService: dataService, audioController: audioController) } }, label: { Text("Try again") }) - .buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white)) + .buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white)) Spacer() } + } else if viewModel.isRunning { + jobRunningText } else { itemBody } @@ -192,6 +200,19 @@ struct FullScreenDigestView: View { await viewModel.load(dataService: dataService, audioController: audioController) } } + + var jobRunningText: some View { + VStack { + Spacer() + Text(""" + You've been added to the AI Digest demo. Your first issue should be ready soon. + When a new digest is ready the icon in the library header will change color. + You can close this window now. + """) + .padding(20) + Spacer() + } + } var closeButton: some View { Button(action: { @@ -218,7 +239,7 @@ struct FullScreenDigestView: View { Spacer() } if let digest = viewModel.digest { - Text(digest.title) + Text(digest.title ?? "") .font(Font.system(size: 17, weight: .semibold)) .lineSpacing(5) .lineLimit(3) @@ -274,14 +295,14 @@ struct FullScreenDigestView: View { .padding(.top, 20) } - if let digest = viewModel.digest { + if let digest = viewModel.digest, let content = digest.content { Text("Transcript") .font(Font.system(size: 17, weight: .semibold)) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 20) VStack { - Markdown(digest.content) + Markdown(content) .foregroundColor(Color.appGrayTextContrast) } .padding(15) diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift index 21b10a54b..ce28e4536 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift @@ -44,7 +44,7 @@ public struct DigestAudioItem: AudioItemProperties { public init(digest: DigestResult, chapters: [DigestChapterData]) { self.digest = digest self.itemID = digest.id - self.title = digest.title + self.title = digest.title ?? "Omnivore digest" self.chapters = chapters self.startIndex = 0 @@ -52,7 +52,7 @@ public struct DigestAudioItem: AudioItemProperties { self.imageURL = nil - if let first = digest.speechFiles.first { + if let first = digest.speechFiles?.first { self.language = first.language self.byline = digest.byline } @@ -1033,7 +1033,7 @@ public struct DigestAudioItem: AudioItemProperties { } func combineSpeechFiles(from digest: DigestResult) -> ([Utterance], Double) { - let allUtterances = digest.speechFiles.flatMap { $0.utterances } + let allUtterances = digest.speechFiles?.flatMap { $0.utterances } ?? [] var updatedUtterances: [Utterance] = [] var currentWordOffset = 0.0 @@ -1053,7 +1053,7 @@ public struct DigestAudioItem: AudioItemProperties { } func downloadDigestItemSpeechFile(itemID: String, priority: DownloadPriority) async throws -> SpeechDocument? { - if let digestItem = itemAudioProperties as? DigestAudioItem, let firstFile = digestItem.digest.speechFiles.first { + if let digestItem = itemAudioProperties as? DigestAudioItem, let firstFile = digestItem.digest.speechFiles?.first { let (utterances, wordCount) = combineSpeechFiles(from: digestItem.digest) let document = SpeechDocument( diff --git a/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift b/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift index b529929f4..088e83b5e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift @@ -9,16 +9,16 @@ struct AITaskRequest: Decodable { public struct DigestResult: Codable { public let id: String - public let title: String - public let byline: String - public let content: String - public let description: String - public let urlsToAudio: [String] - public let chapters: [DigestChapter] - public let speechFiles: [SpeechDocument] + public let title: String? + public let byline: String? + public let content: String? + public let description: String? + public let urlsToAudio: [String]? + public let chapters: [DigestChapter]? + public let speechFiles: [SpeechDocument]? - public let jobState: String - public let createdAt: String + public let jobState: String? + public let createdAt: String? } public struct DigestChapter: Codable { @@ -169,9 +169,10 @@ extension DataService { do { let digest = try await networker.urlSession.performRequest(resource: resource) - let oldDigest = loadStoredDigest() - saveDigest(digest) + if digest.jobState == "SUCCEEDED" { + saveDigest(digest) + } return digest } catch { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateUserPersonalization.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateUserPersonalization.swift index 4f6f98e4d..25508cb7b 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateUserPersonalization.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateUserPersonalization.swift @@ -40,7 +40,7 @@ public extension DataService { try $0.setUserPersonalization( input: InputObjects.SetUserPersonalizationInput( digestConfig: OptionalArgument( - InputObjects.DigestConfigInput(channels: OptionalArgument([OptionalArgument("email")])) + InputObjects.DigestConfigInput(channels: OptionalArgument([OptionalArgument("push")])) ) ), selection: selection