Skip to content

Commit

Permalink
AI Chat browsing menu (#3635)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1204167627774280/1208794395441049/f

**Description**:
Add AI Chat entry point in the browsing menu
  • Loading branch information
Bunn authored Dec 4, 2024
1 parent a5dab98 commit edf899d
Show file tree
Hide file tree
Showing 36 changed files with 1,218 additions and 30 deletions.
1 change: 1 addition & 0 deletions Core/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public enum FeatureFlag: String {

/// https://app.asana.com/0/1208592102886666/1208613627589762/f
case crashReportOptInStatusResetting

case isPrivacyProLaunchedROW
case isPrivacyProLaunchedROWOverride

Expand Down
19 changes: 17 additions & 2 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,16 @@ extension Pixel {
case browsingMenuShare
case browsingMenuCopy
case browsingMenuPrint
case browsingMenuListPrint
case browsingMenuFindInPage
case browsingMenuZoom
case browsingMenuDisableProtection
case browsingMenuEnableProtection
case browsingMenuReportBrokenSite
case browsingMenuFireproof
case browsingMenuAutofill

case browsingMenuAIChat

case addressBarShare
case addressBarSettings
case addressBarCancelPressedOnNTP
Expand Down Expand Up @@ -895,9 +897,13 @@ extension Pixel {
case appDidShowUITime(time: BucketAggregation)
case appDidBecomeActiveTime(time: BucketAggregation)

// MARK: AI Chat
case openAIChatBefore10min
case openAIChatAfter10min
case aiChatNoRemoteSettingsFound(settings: String)

// MARK: Lifecycle
case appDidTransitionToUnexpectedState

}

}
Expand Down Expand Up @@ -959,6 +965,7 @@ extension Pixel.Event {
case .browsingMenuToggleBrowsingMode: return "mb_dm"
case .browsingMenuCopy: return "mb_cp"
case .browsingMenuPrint: return "mb_pr"

case .browsingMenuFindInPage: return "mb_fp"
case .browsingMenuZoom: return "m_menu_page_zoom_taps"
case .browsingMenuDisableProtection: return "mb_wla"
Expand All @@ -968,6 +975,8 @@ extension Pixel.Event {
case .browsingMenuAutofill: return "m_nav_autofill_menu_item_pressed"

case .browsingMenuShare: return "m_browsingmenu_share"
case .browsingMenuAIChat: return "m_aichat_menu_tab_icon"
case .browsingMenuListPrint: return "m_browsing_menu_list_print"

case .addressBarShare: return "m_addressbar_share"
case .addressBarSettings: return "m_addressbar_settings"
Expand Down Expand Up @@ -1787,6 +1796,12 @@ extension Pixel.Event {
case .appDidShowUITime(let time): return "m_debug_app-did-show-ui-time-\(time)"
case .appDidBecomeActiveTime(let time): return "m_debug_app-did-become-active-time-\(time)"

// MARK: AI Chat
case .openAIChatAfter10min: return "m_aichat_open_after_10_min"
case .openAIChatBefore10min: return "m_aichat_open_before_10_min"
case .aiChatNoRemoteSettingsFound(let settings):
return "m_aichat_no_remote_settings_found-\(settings.lowercased())"

// MARK: Lifecycle
case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state"

Expand Down
49 changes: 49 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions DuckDuckGo/AIChat/AIChatPixelHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// AIChatPixelHandler.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 AIChat
import Core

struct AIChatPixelHandler: AIChatPixelHandling {
func fire(pixel: AIChatPixel) {
switch pixel {
case .openAfter10min:
Pixel.fire(pixel: .openAIChatAfter10min)
case .openBefore10min:
Pixel.fire(pixel: .openAIChatBefore10min)
}
}
}
108 changes: 108 additions & 0 deletions DuckDuckGo/AIChat/AIChatSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// AIChatSettings.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 AIChat
import Foundation
import Core

/// 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 AIChatSettings: AIChatSettingsProvider {
enum SettingsValue: String {
case aiChatURL

var defaultValue: String {
switch self {
case .aiChatURL: return "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=4"
}
}
}

private let privacyConfigurationManager: PrivacyConfigurationManaging
private var remoteSettings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings {
privacyConfigurationManager.privacyConfig.settings(for: .aiChat)
}
private let internalUserDecider: InternalUserDecider
private let userDefaults: UserDefaults

init(privacyConfigurationManager: PrivacyConfigurationManaging, internalUserDecider: InternalUserDecider, userDefaults: UserDefaults = .standard) {
self.internalUserDecider = internalUserDecider
self.privacyConfigurationManager = privacyConfigurationManager
self.userDefaults = userDefaults
}

// MARK: - Public

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

var isAIChatBrowsingMenuUserSettingsEnabled: Bool {
userDefaults.showAIChatBrowsingMenu
}

var isAIChatFeatureEnabled: Bool {
privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .aiChat) || internalUserDecider.isInternalUser
}

var isAIChatBrowsingToolbarShortcutFeatureEnabled: Bool {
let isBrowsingToolbarShortcutFeatureFlagEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(AIChatSubfeature.browsingToolbarShortcut)
let isInternalUser = internalUserDecider.isInternalUser
let isFeatureEnabled = isBrowsingToolbarShortcutFeatureFlagEnabled || isInternalUser
return isFeatureEnabled && isAIChatBrowsingMenuUserSettingsEnabled
}

func enableAIChatBrowsingMenuUserSettings(enable: Bool) {
userDefaults.showAIChatBrowsingMenu = enable
}

// MARK: - Private

private func getSettingsData(_ value: SettingsValue) -> String {
if let value = remoteSettings[value.rawValue] as? String {
return value
} else {
Pixel.fire(pixel: .aiChatNoRemoteSettingsFound(settings: value.rawValue))
return value.defaultValue
}
}
}

private extension UserDefaults {
enum Keys {
static let showAIChatBrowsingMenu = "aichat.settings.showAIChatBrowsingMenu"
}

static let showAIChatBrowsingMenuDefaultValue = true

@objc dynamic var showAIChatBrowsingMenu: Bool {
get {
value(forKey: Keys.showAIChatBrowsingMenu) as? Bool ?? Self.showAIChatBrowsingMenuDefaultValue
}

set {
guard newValue != showAIChatBrowsingMenu else { return }
set(newValue, forKey: Keys.showAIChatBrowsingMenu)
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "AIChat-24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "AIChat.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
5 changes: 3 additions & 2 deletions DuckDuckGo/BrowsingMenu/BrowsingMenuViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,9 @@ extension BrowsingMenuViewController: UITableViewDelegate {

switch menuEntries[indexPath.row] {
case .regular(_, _, _, _, let action):
dismiss(animated: true)
action()
dismiss(animated: true) {
action()
}
case .separator:
break
}
Expand Down
6 changes: 5 additions & 1 deletion DuckDuckGo/MainViewController+Segues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ extension MainViewController {
fireproofing: fireproofing,
websiteDataManager: websiteDataManager)

let aiChatSettings = AIChatSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
internalUserDecider: AppDependencyProvider.shared.internalUserDecider)

let settingsViewModel = SettingsViewModel(legacyViewProvider: legacyViewProvider,
subscriptionManager: AppDependencyProvider.shared.subscriptionManager,
subscriptionFeatureAvailability: subscriptionFeatureAvailability,
Expand All @@ -304,7 +307,8 @@ extension MainViewController {
historyManager: historyManager,
syncPausedStateManager: syncPausedStateManager,
privacyProDataReporter: privacyProDataReporter,
textZoomCoordinator: textZoomCoordinator)
textZoomCoordinator: textZoomCoordinator,
aiChatSettings: aiChatSettings)
Pixel.fire(pixel: .settingsPresented)

if let navigationController = self.presentedViewController as? UINavigationController,
Expand Down
25 changes: 24 additions & 1 deletion DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import Onboarding
import os.log
import PageRefreshMonitor
import BrokenSitePrompt
import AIChat

class MainViewController: UIViewController {

Expand Down Expand Up @@ -186,6 +187,16 @@ class MainViewController: UIViewController {

var appDidFinishLaunchingStartTime: CFAbsoluteTime?

private lazy var aiChatNavigationController: UINavigationController = {
let settings = AIChatSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
internalUserDecider: AppDependencyProvider.shared.internalUserDecider)
let aiChatViewController = AIChatViewController(settings: settings,
webViewConfiguration: WKWebViewConfiguration.persistent(),
pixelHandler: AIChatPixelHandler())
aiChatViewController.delegate = self
return UINavigationController(rootViewController: aiChatViewController)
}()

init(
bookmarksDatabase: CoreDataDatabase,
bookmarksDatabaseCleaner: BookmarkDatabaseCleaner,
Expand Down Expand Up @@ -351,6 +362,7 @@ class MainViewController: UIViewController {
let launchTime = CFAbsoluteTimeGetCurrent() - appDidFinishLaunchingStartTime
Pixel.fire(pixel: .appDidShowUITime(time: Pixel.Event.BucketAggregation(number: launchTime)),
withAdditionalParameters: [PixelParameters.time: String(launchTime)])
self.appDidFinishLaunchingStartTime = nil /// We only want this pixel to be fired once
}
}

Expand Down Expand Up @@ -1688,7 +1700,6 @@ class MainViewController: UIViewController {

Pixel.fire(pixel: pixel, withAdditionalParameters: pixelParameters, includedParameters: [.atb])
}

}

extension MainViewController: FindInPageDelegate {
Expand Down Expand Up @@ -2347,6 +2358,11 @@ extension MainViewController: TabDelegate {
segueToReportBrokenSite(entryPoint: .toggleReport(completionHandler: completionHandler))
}

func tabDidRequestAIChat(tab: TabViewController) {
aiChatNavigationController.modalPresentationStyle = .fullScreen
tab.present(aiChatNavigationController, animated: true, completion: nil)
}

func tabDidRequestBookmarks(tab: TabViewController) {
Pixel.fire(pixel: .bookmarksButtonPressed,
withAdditionalParameters: [PixelParameters.originatedFromMenu: "1"])
Expand Down Expand Up @@ -2931,3 +2947,10 @@ extension MainViewController: AutofillLoginSettingsListViewControllerDelegate {
controller.dismiss(animated: true)
}
}

// MARK: - AIChatViewControllerDelegate
extension MainViewController: AIChatViewControllerDelegate {
func aiChatViewController(_ viewController: AIChatViewController, didRequestToLoad url: URL) {
loadUrlInNewTab(url, inheritedAttribution: nil)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "SettingsAIChat.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "SettingsAIChatHero.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Loading

0 comments on commit edf899d

Please sign in to comment.