Skip to content

Commit

Permalink
AI Chat modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Bunn committed Nov 28, 2024
1 parent 9889a79 commit 1bcbea0
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 23 deletions.
16 changes: 12 additions & 4 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@
316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */; };
316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */; };
316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */; };
316AA4582CF8AABA00A2ED28 /* AIChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA4572CF8AAA800A2ED28 /* AIChatModel.swift */; };
316AA4582CF8AABA00A2ED28 /* AIChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA4572CF8AAA800A2ED28 /* AIChatViewModel.swift */; };
316AA45A2CF8E31F00A2ED28 /* AIChatRemoteSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettingsProvider.swift */; };
316AA45C2CF8E82700A2ED28 /* Logger+AIChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA45B2CF8E82300A2ED28 /* Logger+AIChat.swift */; };
3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; };
317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; };
317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; };
Expand Down Expand Up @@ -1513,7 +1515,9 @@
316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManagerTests.swift; sourceTree = "<group>"; };
316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToDownloadsAlert.swift; sourceTree = "<group>"; };
316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadActionMessageViewHelper.swift; sourceTree = "<group>"; };
316AA4572CF8AAA800A2ED28 /* AIChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatModel.swift; sourceTree = "<group>"; };
316AA4572CF8AAA800A2ED28 /* AIChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatViewModel.swift; sourceTree = "<group>"; };
316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatRemoteSettingsProvider.swift; sourceTree = "<group>"; };
316AA45B2CF8E82300A2ED28 /* Logger+AIChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+AIChat.swift"; sourceTree = "<group>"; };
3170048127A9504F00C03F35 /* DownloadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMocks.swift; sourceTree = "<group>"; };
317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillInterfaceEmailTruncatorTests.swift; sourceTree = "<group>"; };
31794BFF2821DFB600F18633 /* DuckUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DuckUI; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3572,7 +3576,9 @@
311C79E22CF790270021196A /* AIChat */ = {
isa = PBXGroup;
children = (
316AA4572CF8AAA800A2ED28 /* AIChatModel.swift */,
316AA45B2CF8E82300A2ED28 /* Logger+AIChat.swift */,
316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettingsProvider.swift */,
316AA4572CF8AAA800A2ED28 /* AIChatViewModel.swift */,
311C79E52CF790380021196A /* AIChatWebViewController.swift */,
311C79E32CF7902F0021196A /* AIChatViewController.swift */,
);
Expand Down Expand Up @@ -7762,6 +7768,7 @@
D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */,
85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */,
3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */,
316AA45A2CF8E31F00A2ED28 /* AIChatRemoteSettingsProvider.swift in Sources */,
F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */,
BDE91CDC2C62AA3A0005CB74 /* DefaultMetadataCollector.swift in Sources */,
D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */,
Expand Down Expand Up @@ -7792,7 +7799,7 @@
981FED6E22025151008488D7 /* BlankSnapshotViewController.swift in Sources */,
D66F683D2BB333C100AE93E2 /* SubscriptionContainerView.swift in Sources */,
851B128822200575004781BC /* Onboarding.swift in Sources */,
316AA4582CF8AABA00A2ED28 /* AIChatModel.swift in Sources */,
316AA4582CF8AABA00A2ED28 /* AIChatViewModel.swift in Sources */,
9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */,
859DB8142CE6263C001F7210 /* TextZoomController.swift in Sources */,
F1EFB0062C5B8B8E009AB44B /* StatusIndicatorView.swift in Sources */,
Expand Down Expand Up @@ -7983,6 +7990,7 @@
F130D73A1E5776C500C45811 /* OmniBarDelegate.swift in Sources */,
85DFEDEF24C7EA3B00973FE7 /* SmallOmniBarState.swift in Sources */,
1E908BF129827C480008C8F3 /* AutoconsentUserScript.swift in Sources */,
316AA45C2CF8E82700A2ED28 /* Logger+AIChat.swift in Sources */,
1D200C972BA3157A00108701 /* SettingsNextStepsView.swift in Sources */,
4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */,
1E7A71192934EC6100B7EA19 /* OmniBarNotificationContainerView.swift in Sources */,
Expand Down
86 changes: 86 additions & 0 deletions DuckDuckGo/AIChat/AIChatRemoteSettingsProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// AIChatRemoteSettingsProvider.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import BrowserServicesKit
import Foundation

protocol AIChatRemoteSettingsProvider {
var aiChatURL: URL { get }
var isAIChatEnabled: Bool { get }
var isBrowsingToolbarShortcutEnabled: Bool { get }
var isAddressBarShortcutEnabled: Bool { get }
}

/// This struct serves as a wrapper for PrivacyConfigurationManaging, enabling the retrieval of data relevant to AIChat.
/// It also fire pixels when necessary data is missing.
struct AIChatRemoteSettings: AIChatRemoteSettingsProvider {
enum SettingsValue: String {
case aiChatURL

var defaultValue: String {
switch self {
case .aiChatURL: return "https://duck.ai"
}
}
}

private let privacyConfigurationManager: PrivacyConfigurationManaging
private var settings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings {
privacyConfigurationManager.privacyConfig.settings(for: .aiChat)
}

init(privacyConfigurationManager: PrivacyConfigurationManaging) {
self.privacyConfigurationManager = privacyConfigurationManager
}

// MARK: - Public

var aiChatURL: URL {
guard let url = URL(string: getSettingsData(.aiChatURL)) else {
return URL(string: SettingsValue.aiChatURL.defaultValue)!
}
return url
}

var isAIChatEnabled: Bool {
privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .aiChat)
}

// browsingToolbarShortcut
var isBrowsingToolbarShortcutEnabled: Bool {
privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(AIChatSubfeature.toolbarShortcut)
}

// addressBarShortcut
var isAddressBarShortcutEnabled: Bool {
privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(AIChatSubfeature.applicationMenuShortcut)
}

// MARK: - Private

private func getSettingsData(_ value: SettingsValue) -> String {
if let value = settings[value.rawValue] as? String {
return value
} else {
Logger.aiChat.debug("No remote settings found \(value.rawValue)")
// PixelKit.fire(GeneralPixel.aichatNoRemoteSettingsFound(value), includeAppVersionParameter: true)
return value.defaultValue
}
}
}
28 changes: 20 additions & 8 deletions DuckDuckGo/AIChat/AIChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ import UIKit
import Combine

final class AIChatViewController: UIViewController {
private let chatModel: AIChatModel
private let chatModel: AIChatViewModel
private var webViewController: AIChatWebViewController?
private var cleanupCancellable: AnyCancellable?

init(chatModel: AIChatModel) {
init(chatModel: AIChatViewModel) {
self.chatModel = chatModel
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

// MARK: - Lifecycle
extension AIChatViewController {

override func viewDidLoad() {
super.viewDidLoad()
Expand All @@ -49,6 +53,16 @@ final class AIChatViewController: UIViewController {
chatModel.cancelTimer()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
chatModel.cancelTimer()
removeWebViewController()
}
}

// MARK: - Views Setup
extension AIChatViewController {

private func setupNavigationBar() {
let imageView = UIImageView(image: UIImage(named: "Logo"))
imageView.contentMode = .scaleAspectFit
Expand Down Expand Up @@ -110,6 +124,10 @@ final class AIChatViewController: UIViewController {
webViewController?.view.removeFromSuperview()
webViewController = nil
}
}

// MARK: - Event handling
extension AIChatViewController {

private func subscribeToCleanupPublisher() {
cleanupCancellable = chatModel.cleanupPublisher
Expand All @@ -123,10 +141,4 @@ final class AIChatViewController: UIViewController {
chatModel.startCleanupTimer()
dismiss(animated: true)
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
chatModel.cancelTimer()
removeWebViewController()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// AIChatModel.swift
// AIChatViewModel.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
Expand All @@ -20,30 +20,38 @@
import WebKit
import Combine

final class AIChatModel {
final class AIChatViewModel {
private let remoteSettings: AIChatRemoteSettingsProvider
private var cleanupTimerCancellable: AnyCancellable?

let webViewConfiguration: WKWebViewConfiguration
let cleanupPublisher = PassthroughSubject<Void, Never>()

init(webViewConfiguration: WKWebViewConfiguration) {
init(webViewConfiguration: WKWebViewConfiguration, remoteSettings: AIChatRemoteSettingsProvider) {
self.webViewConfiguration = webViewConfiguration
self.remoteSettings = remoteSettings
}

func cancelTimer() {
Logger.aiChat.debug("Cancelling cleanup timer")
cleanupTimerCancellable?.cancel()
}

/// Starts a 10-minute timer to trigger cleanup after AI Chat is closed.
/// Cancels any existing timer before starting a new one.
func startCleanupTimer() {
print("Start timer")
Logger.aiChat.debug("Starting cleanup timer")
cancelTimer()

cleanupTimerCancellable = Just(())
.delay(for: .seconds(600), scheduler: RunLoop.main)
.sink { [weak self] in
Logger.aiChat.debug("Cleanup timer done")
self?.cleanupPublisher.send()
}
}

var aiChatURL: URL {
remoteSettings.aiChatURL
}
}
15 changes: 9 additions & 6 deletions DuckDuckGo/AIChat/AIChatWebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import UIKit
import WebKit

final class AIChatWebViewController: UIViewController {
private let chatModel: AIChatModel
private let chatModel: AIChatViewModel

private lazy var webView: WKWebView = {
return WKWebView(frame: .zero, configuration: chatModel.webViewConfiguration)
}()

init(chatModel: AIChatModel) {
init(chatModel: AIChatViewModel) {
self.chatModel = chatModel
super.init(nibName: nil, bundle: nil)
}
Expand All @@ -54,15 +54,18 @@ final class AIChatWebViewController: UIViewController {
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}

// MARK: - WebView functions

extension AIChatWebViewController {

func reload() {
loadWebsite()
}

private func loadWebsite() {
if let url = URL(string: "https://duck.ai") {
let request = URLRequest(url: url)
webView.load(request)
}
let request = URLRequest(url: chatModel.aiChatURL)
webView.load(request)
}
}
25 changes: 25 additions & 0 deletions DuckDuckGo/AIChat/Logger+AIChat.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Logger+AIChat.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import os.log

public extension Logger {
static var aiChat = { Logger(subsystem: "AI Chat", category: "") }()
}
3 changes: 2 additions & 1 deletion DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ class MainViewController: UIViewController {
var appDidFinishLaunchingStartTime: CFAbsoluteTime?

private lazy var aiChatNavigationController: UINavigationController = {
let chatModel = AIChatModel(webViewConfiguration: WKWebViewConfiguration.persistent())
let remoteSettings = AIChatRemoteSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager)
let chatModel = AIChatViewModel(webViewConfiguration: WKWebViewConfiguration.persistent(), remoteSettings: remoteSettings)
return UINavigationController(rootViewController: AIChatViewController(chatModel: chatModel))
}()

Expand Down

0 comments on commit 1bcbea0

Please sign in to comment.