From 182f740778ed99942d7b4a0e1a19a12659a73169 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 15 Jan 2025 15:22:46 +0000 Subject: [PATCH 1/2] WIP: Debug option --- DuckDuckGo.xcodeproj/project.pbxproj | 8 ++ .../AIChatDebugSettingsHandling.swift | 47 ++++++++++ .../AIChat/UserScript/AIChatUserScript.swift | 6 +- DuckDuckGo/AIChatDebugView.swift | 94 +++++++++++++++++++ DuckDuckGo/Debug.storyboard | 31 +++--- DuckDuckGo/RootDebugViewController.swift | 4 + DuckDuckGo/UserScripts.swift | 8 +- 7 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 DuckDuckGo/AIChat/UserScript/AIChatDebugSettingsHandling.swift create mode 100644 DuckDuckGo/AIChatDebugView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index affb71c084..584a3c8503 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -142,6 +142,8 @@ 311BD1AD2836BB3900AEF6C1 /* AutofillItemsEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311BD1AC2836BB3900AEF6C1 /* AutofillItemsEmptyView.swift */; }; 311BD1AF2836BB4200AEF6C1 /* AutofillItemsLockedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311BD1AE2836BB4200AEF6C1 /* AutofillItemsLockedView.swift */; }; 311BD1B12836C0CA00AEF6C1 /* AutofillLoginListAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311BD1B02836C0CA00AEF6C1 /* AutofillLoginListAuthenticator.swift */; }; + 31206F702D3804E800A95D76 /* AIChatDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31206F6F2D3804E800A95D76 /* AIChatDebugView.swift */; }; + 31206F722D38072100A95D76 /* AIChatDebugSettingsHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31206F712D38071000A95D76 /* AIChatDebugSettingsHandling.swift */; }; 312E5746283BB04A00C18FA0 /* AutofillEmptySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312E5745283BB04A00C18FA0 /* AutofillEmptySearchView.swift */; }; 3132FA2627A0784600DD7A12 /* FilePreviewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3132FA2527A0784600DD7A12 /* FilePreviewHelper.swift */; }; 3132FA2827A0788400DD7A12 /* PassKitPreviewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3132FA2727A0788400DD7A12 /* PassKitPreviewHelper.swift */; }; @@ -1558,6 +1560,8 @@ 311BD1AC2836BB3900AEF6C1 /* AutofillItemsEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillItemsEmptyView.swift; sourceTree = ""; }; 311BD1AE2836BB4200AEF6C1 /* AutofillItemsLockedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillItemsLockedView.swift; sourceTree = ""; }; 311BD1B02836C0CA00AEF6C1 /* AutofillLoginListAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginListAuthenticator.swift; sourceTree = ""; }; + 31206F6F2D3804E800A95D76 /* AIChatDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatDebugView.swift; sourceTree = ""; }; + 31206F712D38071000A95D76 /* AIChatDebugSettingsHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatDebugSettingsHandling.swift; sourceTree = ""; }; 312E5745283BB04A00C18FA0 /* AutofillEmptySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillEmptySearchView.swift; sourceTree = ""; }; 3132FA2527A0784600DD7A12 /* FilePreviewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewHelper.swift; sourceTree = ""; }; 3132FA2727A0788400DD7A12 /* PassKitPreviewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassKitPreviewHelper.swift; sourceTree = ""; }; @@ -3927,6 +3931,7 @@ 31C314A32D2EF9DF009A412A /* UserScript */ = { isa = PBXGroup; children = ( + 31206F712D38071000A95D76 /* AIChatDebugSettingsHandling.swift */, 318C5B482D3022FE00DAA5FC /* AIChatPayloadHandling.swift */, 31C3149B2D2EEB44009A412A /* AIChatScriptUserValues.swift */, 31C3149C2D2EEB44009A412A /* AIChatUserScript.swift */, @@ -4758,6 +4763,7 @@ 858566E7252E4F56007501B8 /* Debug.storyboard */, D6F93E3B2B4FFA97004C268D /* SubscriptionDebugViewController.swift */, 8590CB602684D0600089F6BF /* CookieDebugViewController.swift */, + 31206F6F2D3804E800A95D76 /* AIChatDebugView.swift */, 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */, 858566FA252E55D6007501B8 /* ImageCacheDebugViewController.swift */, 8590CB66268A2E520089F6BF /* RootDebugViewController.swift */, @@ -8182,6 +8188,7 @@ 7B4F87E72D0734090010B18F /* ControlCenterWidget.swift in Sources */, 9F7CFF762C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift in Sources */, D68A21442B7EC08500BB372E /* SubscriptionExternalLinkView.swift in Sources */, + 31206F702D3804E800A95D76 /* AIChatDebugView.swift in Sources */, BDD3B3552B8EF8DB005857A8 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, BD862E0B2B30F9300073E2EE /* VPNFeedbackFormView.swift in Sources */, 850365F323DE087800D0F787 /* UIImageViewExtension.swift in Sources */, @@ -8294,6 +8301,7 @@ 1DEAADFF2BA7832F00E25A97 /* EmailProtectionView.swift in Sources */, 31C314A22D2EF614009A412A /* AIChatViewControllerManager.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, + 31206F722D38072100A95D76 /* AIChatDebugSettingsHandling.swift in Sources */, D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */, 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, diff --git a/DuckDuckGo/AIChat/UserScript/AIChatDebugSettingsHandling.swift b/DuckDuckGo/AIChat/UserScript/AIChatDebugSettingsHandling.swift new file mode 100644 index 0000000000..c6978d5d1f --- /dev/null +++ b/DuckDuckGo/AIChat/UserScript/AIChatDebugSettingsHandling.swift @@ -0,0 +1,47 @@ +// +// AIChatDebugSettingsHandling.swift +// DuckDuckGo +// +// Copyright © 2025 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 + +protocol AIChatDebugSettingsHandling { + var messagePolicyHostname: String? { get set } +} + +struct AIChatDebugSettings: AIChatDebugSettingsHandling { + private let userDefaultsKey = "aichat.debug.messagePolicyHostname" + private let userDefault: UserDefaults + + init(userDefault: UserDefaults = .standard) { + self.userDefault = userDefault + } + + var messagePolicyHostname: String? { + get { + let value = userDefault.string(forKey: userDefaultsKey) + return value?.isEmpty == true ? nil : value + } + set { + if let newValue = newValue, !newValue.isEmpty { + userDefault.set(newValue, forKey: userDefaultsKey) + } else { + userDefault.removeObject(forKey: userDefaultsKey) + } + } + } +} diff --git a/DuckDuckGo/AIChat/UserScript/AIChatUserScript.swift b/DuckDuckGo/AIChat/UserScript/AIChatUserScript.swift index 577c211c41..290b01cb68 100644 --- a/DuckDuckGo/AIChat/UserScript/AIChatUserScript.swift +++ b/DuckDuckGo/AIChat/UserScript/AIChatUserScript.swift @@ -34,7 +34,7 @@ final class AIChatUserScript: NSObject, Subfeature { weak var broker: UserScriptMessageBroker? private(set) var messageOriginPolicy: MessageOriginPolicy - init(handler: AIChatUserScriptHandling) { + init(handler: AIChatUserScriptHandling, debugSettings: AIChatDebugSettingsHandling) { self.handler = handler var rules = [HostnameMatchingRule]() @@ -43,6 +43,10 @@ final class AIChatUserScript: NSObject, Subfeature { rules.append(.exact(hostname: ddgDomain)) } + if let debugHostname = debugSettings.messagePolicyHostname { + rules.append(.exact(hostname: debugHostname)) + } + self.messageOriginPolicy = .only(rules: rules) } diff --git a/DuckDuckGo/AIChatDebugView.swift b/DuckDuckGo/AIChatDebugView.swift new file mode 100644 index 0000000000..c81c40c5e0 --- /dev/null +++ b/DuckDuckGo/AIChatDebugView.swift @@ -0,0 +1,94 @@ +// +// AIChatDebugView.swift +// DuckDuckGo +// +// Copyright © 2025 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 Combine + +struct AIChatDebugView: View { + @StateObject private var viewModel = AIChatDebugViewModel() + + var body: some View { + List { + Section(footer: Text("Stored Hostname: \(viewModel.enteredHostname)")) { + NavigationLink(destination: AIChatDebugHostnameEntryView(viewModel: viewModel)) { + Text("Message policy hostname") + } + } + } + .navigationTitle("AI Chat") + } +} + +private final class AIChatDebugViewModel: ObservableObject { + private var debugSettings = AIChatDebugSettings() + + @Published var enteredHostname: String { + didSet { + debugSettings.messagePolicyHostname = enteredHostname + } + } + + init() { + self.enteredHostname = debugSettings.messagePolicyHostname ?? "" + } + + func resetHostname() { + enteredHostname = "" + } +} + +private struct AIChatDebugHostnameEntryView: View { + @ObservedObject var viewModel: AIChatDebugViewModel + @State private var policyHostname: String = "" + @Environment(\.presentationMode) var presentationMode + + var body: some View { + Form { + Section { + TextField("Hostname", text: $policyHostname) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Button(action: { + viewModel.enteredHostname = policyHostname + presentationMode.wrappedValue.dismiss() + }) { + Text("Confirm") + } + + Button(action: { + viewModel.resetHostname() + policyHostname = "" + presentationMode.wrappedValue.dismiss() + }) { + Text("Reset") + } + } + .navigationTitle("Edit Hostname") + .onAppear { + policyHostname = viewModel.enteredHostname + } + } +} + +#Preview { + AIChatDebugView() +} diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index b09aefbf25..5481428753 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -306,7 +306,7 @@ - + @@ -360,7 +360,7 @@ - + @@ -383,6 +383,15 @@ + + + + + + + + + @@ -1047,34 +1056,34 @@ - + - + - + - + @@ -1142,7 +1151,7 @@ - + diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index ac9ac306d4..fcf3614fe0 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -48,6 +48,7 @@ class RootDebugViewController: UITableViewController { case onboarding = 676 case resetSyncPromoPrompts = 677 case resetTipKit = 681 + case aiChat = 682 } @IBOutlet weak var shareButton: UIBarButtonItem! @@ -193,6 +194,9 @@ class RootDebugViewController: UITableViewController { ActionMessageView.present(message: "Sync Promos reset") case .resetTipKit: tipKitUIActionHandler.resetTipKitTapped() + case .aiChat: + let controller = UIHostingController(rootView: AIChatDebugView()) + navigationController?.pushViewController(controller, animated: true) } } } diff --git a/DuckDuckGo/UserScripts.swift b/DuckDuckGo/UserScripts.swift index f21891f44a..645157cbc8 100644 --- a/DuckDuckGo/UserScripts.swift +++ b/DuckDuckGo/UserScripts.swift @@ -55,7 +55,8 @@ final class UserScripts: UserScriptsProvider { init(with sourceProvider: ScriptSourceProviding, appSettings: AppSettings = AppDependencyProvider.shared.appSettings, - featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger) { + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, + aiChatDebugSettings: AIChatDebugSettingsHandling = AIChatDebugSettings()) { contentBlockerUserScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig) @@ -69,7 +70,10 @@ final class UserScripts: UserScriptsProvider { properties: sourceProvider.contentScopeProperties, isIsolated: true) autoconsentUserScript = AutoconsentUserScript(config: sourceProvider.privacyConfigurationManager.privacyConfig) - aiChatUserScript = AIChatUserScript(handler: AIChatUserScriptHandler(featureFlagger: featureFlagger)) + + let aiChatScriptHandler = AIChatUserScriptHandler(featureFlagger: featureFlagger) + aiChatUserScript = AIChatUserScript(handler: aiChatScriptHandler, + debugSettings: aiChatDebugSettings) contentScopeUserScriptIsolated.registerSubfeature(delegate: aiChatUserScript) // Special pages - Such as Duck Player From ec6a4bac0260bf1c2ed474b857918d7f502d8866 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 15 Jan 2025 16:16:04 +0000 Subject: [PATCH 2/2] Linter --- DuckDuckGo/AIChatDebugView.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/AIChatDebugView.swift b/DuckDuckGo/AIChatDebugView.swift index c81c40c5e0..a7e372d6c7 100644 --- a/DuckDuckGo/AIChatDebugView.swift +++ b/DuckDuckGo/AIChatDebugView.swift @@ -66,19 +66,18 @@ private struct AIChatDebugHostnameEntryView: View { .autocorrectionDisabled(true) .textInputAutocapitalization(.never) } - - Button(action: { + Button { viewModel.enteredHostname = policyHostname presentationMode.wrappedValue.dismiss() - }) { + } label: { Text("Confirm") } - Button(action: { + Button { viewModel.resetHostname() policyHostname = "" presentationMode.wrappedValue.dismiss() - }) { + } label: { Text("Reset") } }