Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AI Chat browsing menu #3635

Merged
merged 41 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8abb195
WIP: Ai chat browsing menu
Bunn Nov 27, 2024
da3b1d5
Improve navigation bar
Bunn Nov 27, 2024
ca543de
Title font
Bunn Nov 27, 2024
c5846d0
Don't save state
Bunn Nov 27, 2024
9987ae4
Fix chat button dark mode
Bunn Nov 27, 2024
531f766
WIP: Model
Bunn Nov 28, 2024
4e07beb
WIP: Timer for reloading AI Chat
Bunn Nov 28, 2024
081ff8b
Use 10min timer
Bunn Nov 28, 2024
9889a79
improve code
Bunn Nov 28, 2024
1bcbea0
AI Chat modal
Bunn Nov 28, 2024
912150f
Update default URL
Bunn Nov 28, 2024
7c0d131
Merge branch 'main' into bunn/aichat/browsing-menu
Bunn Nov 28, 2024
da51019
use correct feature flag
Bunn Nov 28, 2024
5aca0bb
WIP: Handle external navigation
Bunn Nov 28, 2024
ee03d80
Load chat delegate pages in new tab
Bunn Nov 29, 2024
6efd162
Use local package for AI Chat
Bunn Nov 29, 2024
926c486
Improve code comment
Bunn Nov 29, 2024
16b78c6
improve cleanup timer
Bunn Nov 29, 2024
6afa762
Fire pixels
Bunn Nov 29, 2024
54bef28
Merge branch 'main' into bunn/aichat/browsing-menu
Bunn Nov 29, 2024
26c83d7
Fix feature flag
Bunn Nov 29, 2024
d616bf9
change timer to 10min
Bunn Nov 29, 2024
06babe5
linter
Bunn Nov 29, 2024
033fa69
Fix test
Bunn Nov 29, 2024
0ae71bb
Fix memory warning issue
Bunn Nov 30, 2024
657f60d
WIP: Remove bar background
Bunn Dec 2, 2024
e56e3b6
Revert "WIP: Remove bar background"
Bunn Dec 2, 2024
251af3c
Reapply "WIP: Remove bar background"
Bunn Dec 2, 2024
1ac4b3d
Use rounded corner webview
Bunn Dec 2, 2024
d463dc7
Add loading view
Bunn Dec 2, 2024
104e738
Webview color
Bunn Dec 2, 2024
f394ff5
Enabled by default on internal users
Bunn Dec 2, 2024
d45b0da
Merge branch 'release/7.148.0' into bunn/aichat/browsing-menu
Bunn Dec 3, 2024
2f55110
Add AI Chat settings (#3665)
Bunn Dec 3, 2024
924f084
Fix user text
Bunn Dec 3, 2024
e84cb78
Add settings tests
Bunn Dec 4, 2024
b6b0ded
Update AI Chat icon
Bunn Dec 4, 2024
c075d3d
Prevent appDidShowUITime from being fired more than once
Bunn Dec 4, 2024
befdb18
Update variable name
Bunn Dec 4, 2024
28f7896
Add identifier
Bunn Dec 4, 2024
906775c
Ship review feedback
Bunn Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the action called without waiting for the dismissal was causing problems with actions that depended on the view stack, like presenting a ViewController

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
Loading