Skip to content

Commit

Permalink
Add AI Chat settings (#3665)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1204167627774280/1208896297203534/f

**Description**:
Add AI Chat settings UI to enable/disable the feature
  • Loading branch information
Bunn authored Dec 3, 2024
1 parent d45b0da commit 2f55110
Show file tree
Hide file tree
Showing 17 changed files with 202 additions and 38 deletions.
5 changes: 0 additions & 5 deletions Core/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ public enum FeatureFlag: String {
/// https://app.asana.com/0/1208592102886666/1208613627589762/f
case crashReportOptInStatusResetting

/// https://app.asana.com/0/1204167627774280/1208794395441049/f
case aiChatBrowsingToolbarShortcut

case isPrivacyProLaunchedROW
case isPrivacyProLaunchedROWOverride

Expand Down Expand Up @@ -140,8 +137,6 @@ extension FeatureFlag: FeatureFlagDescribing {
return .remoteReleasable(.feature(.adAttributionReporting))
case .crashReportOptInStatusResetting:
return .internalOnly()
case .aiChatBrowsingToolbarShortcut:
return .remoteReleasable(.subfeature(AIChatSubfeature.browsingToolbarShortcut))
case .isPrivacyProLaunchedROW:
return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROW))
case .isPrivacyProLaunchedROWOverride:
Expand Down
12 changes: 8 additions & 4 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,10 @@
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 */; };
316AA45A2CF8E31F00A2ED28 /* AIChatRemoteSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettings.swift */; };
316AA45A2CF8E31F00A2ED28 /* AIChatSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA4592CF8E31F00A2ED28 /* AIChatSettings.swift */; };
3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; };
317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; };
317CA3432CFF82E100F88848 /* SettingsAIChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */; };
317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; };
31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */; };
31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; };
Expand Down Expand Up @@ -1527,10 +1528,11 @@
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>"; };
316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatRemoteSettings.swift; sourceTree = "<group>"; };
316AA4592CF8E31F00A2ED28 /* AIChatSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatSettings.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>"; };
317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAIChatView.swift; sourceTree = "<group>"; };
317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackStorage.swift; sourceTree = "<group>"; };
31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerStorage.swift; sourceTree = "<group>"; };
31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3348,6 +3350,7 @@
1DEAADEB2BA45B4400E25A97 /* SettingsAccessibilityView.swift */,
1DEAADED2BA45DFE00E25A97 /* SettingsDataClearingView.swift */,
D65625A02C232F5E006EF297 /* SettingsDuckPlayerView.swift */,
317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */,
);
name = MainSettings;
sourceTree = "<group>";
Expand Down Expand Up @@ -3599,7 +3602,7 @@
isa = PBXGroup;
children = (
31043B152CFA5B890028A97F /* AIChatPixelHandler.swift */,
316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettings.swift */,
316AA4592CF8E31F00A2ED28 /* AIChatSettings.swift */,
);
path = AIChat;
sourceTree = "<group>";
Expand Down Expand Up @@ -7820,7 +7823,7 @@
D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */,
85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */,
3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */,
316AA45A2CF8E31F00A2ED28 /* AIChatRemoteSettings.swift in Sources */,
316AA45A2CF8E31F00A2ED28 /* AIChatSettings.swift in Sources */,
F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */,
BDE91CDC2C62AA3A0005CB74 /* DefaultMetadataCollector.swift in Sources */,
D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */,
Expand Down Expand Up @@ -8132,6 +8135,7 @@
8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */,
1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */,
B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */,
317CA3432CFF82E100F88848 /* SettingsAIChatView.swift in Sources */,
9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */,
985892522260B1B200EEB31B /* ProgressView.swift in Sources */,
85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// AIChatRemoteSettings.swift
// AIChatSettings.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
Expand All @@ -24,7 +24,7 @@ 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 AIChatRemoteSettings: AIChatRemoteSettingsProvider {
struct AIChatSettings: AIChatSettingsProvider {
enum SettingsValue: String {
case aiChatURL

Expand All @@ -36,12 +36,16 @@ struct AIChatRemoteSettings: AIChatRemoteSettingsProvider {
}

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

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

// MARK: - Public
Expand All @@ -53,14 +57,52 @@ struct AIChatRemoteSettings: AIChatRemoteSettingsProvider {
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 = settings[value.rawValue] as? 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)
}
}
}
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
5 changes: 3 additions & 2 deletions DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ class MainViewController: UIViewController {
var appDidFinishLaunchingStartTime: CFAbsoluteTime?

private lazy var aiChatNavigationController: UINavigationController = {
let remoteSettings = AIChatRemoteSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager)
let aiChatViewController = AIChatViewController(remoteSettings: remoteSettings,
let settings = AIChatSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
internalUserDecider: AppDependencyProvider.shared.internalUserDecider)
let aiChatViewController = AIChatViewController(settings: settings,
webViewConfiguration: WKWebViewConfiguration.persistent(),
pixelHandler: AIChatPixelHandler())
aiChatViewController.delegate = self
Expand Down
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.
54 changes: 54 additions & 0 deletions DuckDuckGo/SettingsAIChatView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// SettingsAIChatView.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 SwiftUI
import DesignResourcesKit

struct SettingsAIChatView: View {
@EnvironmentObject var viewModel: SettingsViewModel

var body: some View {
List {

VStack(alignment: .center) {
Image("SettingsDuckPlayerHero")
.padding(.top, -20) // Change this to AI Chat image

Text(UserText.aiChatFeatureName)
.daxTitle3()

Text(.init(UserText.aiChatPreferencesCaptionWithLinkMarkdown))
.tint(Color.init(designSystemColor: .accent))
.daxBodyRegular()
.multilineTextAlignment(.center)
.foregroundColor(Color(designSystemColor: .textSecondary))
.padding(.top, 12)
}
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)

Section {
SettingsCellView(label: UserText.aiChatSettingsEnableBrowsingMenuToggle,
accessory: .toggle(isOn: viewModel.aiChatEnabledBinding))
}
}.applySettingsListModifiers(title: UserText.aiChatFeatureName,
displayMode: .inline,
viewModel: viewModel)
}
}
10 changes: 9 additions & 1 deletion DuckDuckGo/SettingsMainSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@ struct SettingsMainSettingsView: View {
SettingsCellView(label: UserText.dataClearing,
image: Image("SettingsDataClearing"))
}


// AI Chat
if viewModel.state.aiChatEnabled {
NavigationLink(destination: SettingsAIChatView().environmentObject(viewModel)) {
SettingsCellView(label: UserText.aiChatFeatureName,
image: Image("SettingsAIChat"))
}
}

// Duck Player
// We need to hide the settings until the user is enrolled in the experiment
if viewModel.state.duckPlayerEnabled {
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/SettingsRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ struct SettingsRootView: View {
SettingsDuckPlayerView().environmentObject(viewModel)
case .netP:
NetworkProtectionRootView()
case .aiChat:
SettingsAIChatView().environmentObject(viewModel)
}
}

Expand Down
8 changes: 6 additions & 2 deletions DuckDuckGo/SettingsState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ struct SettingsState {
var duckPlayerMode: DuckPlayerMode?
var duckPlayerOpenInNewTab: Bool
var duckPlayerOpenInNewTabEnabled: Bool


// AI Chat
var aiChatEnabled: Bool

static var defaults: SettingsState {
return SettingsState(
appTheme: .systemDefault,
Expand Down Expand Up @@ -142,7 +145,8 @@ struct SettingsState {
duckPlayerEnabled: false,
duckPlayerMode: .alwaysAsk,
duckPlayerOpenInNewTab: true,
duckPlayerOpenInNewTabEnabled: false
duckPlayerOpenInNewTabEnabled: false,
aiChatEnabled: false
)
}
}
Loading

0 comments on commit 2f55110

Please sign in to comment.