diff --git a/app/brave_settings_strings.grdp b/app/brave_settings_strings.grdp
index e18de2ee52fc..4ae6b4c4106b 100644
--- a/app/brave_settings_strings.grdp
+++ b/app/brave_settings_strings.grdp
@@ -712,6 +712,17 @@
Allowed to access localhost resources
+
+
+ Leo AI chat
+
+
+ Sites can ask to open Leo AI chat
+
+
+ Don't allow site to open Leo AI chat
+
+
Standard protection is on.
diff --git a/browser/about_flags.cc b/browser/about_flags.cc
index bb6539e69f48..207582ab5e87 100644
--- a/browser/about_flags.cc
+++ b/browser/about_flags.cc
@@ -361,44 +361,47 @@
#endif
#if BUILDFLAG(ENABLE_AI_CHAT)
-#define BRAVE_AI_CHAT \
- EXPAND_FEATURE_ENTRIES({ \
- "brave-ai-chat", \
- "Brave AI Chat", \
- "Summarize articles and engage in conversation with AI", \
- kOsWin | kOsMac | kOsLinux | kOsAndroid, \
- FEATURE_VALUE_TYPE(ai_chat::features::kAIChat), \
- })
-#define BRAVE_AI_CHAT_HISTORY \
- EXPAND_FEATURE_ENTRIES({ \
- "brave-ai-chat-history", \
- "Brave AI Chat History", \
- "Enables AI Chat History persistence and management", \
- kOsWin | kOsMac | kOsLinux, \
- FEATURE_VALUE_TYPE(ai_chat::features::kAIChatHistory), \
- })
-#define BRAVE_AI_CHAT_CONTEXT_MENU_REWRITE_IN_PLACE \
- EXPAND_FEATURE_ENTRIES({ \
- "brave-ai-chat-context-menu-rewrite-in-place", \
- "Brave AI Chat Rewrite In Place From Context Menu", \
- "Enables AI Chat rewrite in place feature from the context menu", \
- kOsDesktop, \
- FEATURE_VALUE_TYPE(ai_chat::features::kContextMenuRewriteInPlace), \
- })
-#define BRAVE_AI_CHAT_PAGE_CONTENT_REFINE \
- EXPAND_FEATURE_ENTRIES({ \
- "brave-ai-chat-page-content-refine", \
- "Brave AI Chat Page Content Refine", \
- "Enable local text embedding for long page content in order to find " \
- "most relevant parts to the prompt within context limit.", \
- kOsDesktop | kOsAndroid, \
- FEATURE_VALUE_TYPE(ai_chat::features::kPageContentRefine), \
- })
+#define BRAVE_AI_CHAT_FEATURE_ENTRIES \
+ EXPAND_FEATURE_ENTRIES( \
+ { \
+ "brave-ai-chat", \
+ "Brave AI Chat", \
+ "Summarize articles and engage in conversation with AI", \
+ kOsWin | kOsMac | kOsLinux | kOsAndroid, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kAIChat), \
+ }, \
+ { \
+ "brave-ai-chat-history", \
+ "Brave AI Chat History", \
+ "Enables AI Chat History persistence and management", \
+ kOsWin | kOsMac | kOsLinux, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kAIChatHistory), \
+ }, \
+ { \
+ "brave-ai-chat-context-menu-rewrite-in-place", \
+ "Brave AI Chat Rewrite In Place From Context Menu", \
+ "Enables AI Chat rewrite in place feature from the context menu", \
+ kOsDesktop, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kContextMenuRewriteInPlace), \
+ }, \
+ { \
+ "brave-ai-chat-page-content-refine", \
+ "Brave AI Chat Page Content Refine", \
+ "Enable local text embedding for long page content in order to " \
+ "find " \
+ "most relevant parts to the prompt within context limit.", \
+ kOsDesktop | kOsAndroid, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kPageContentRefine), \
+ }, \
+ { \
+ "brave-ai-chat-open-leo-from-brave-search", \
+ "Open Leo AI Chat from Brave Search", \
+ "Enables opening Leo AI Chat from Brave Search", \
+ kOsDesktop | kOsAndroid, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kOpenAIChatFromBraveSearch), \
+ })
#else
-#define BRAVE_AI_CHAT
-#define BRAVE_AI_CHAT_HISTORY
-#define BRAVE_AI_CHAT_CONTEXT_MENU_REWRITE_IN_PLACE
-#define BRAVE_AI_CHAT_PAGE_CONTENT_REFINE
+#define BRAVE_AI_CHAT_FEATURE_ENTRIES
#endif
#if BUILDFLAG(ENABLE_AI_REWRITER)
#define BRAVE_AI_REWRITER \
@@ -980,10 +983,7 @@
BRAVE_SAFE_BROWSING_ANDROID \
BRAVE_CHANGE_ACTIVE_TAB_ON_SCROLL_EVENT_FEATURE_ENTRIES \
BRAVE_TABS_FEATURE_ENTRIES \
- BRAVE_AI_CHAT \
- BRAVE_AI_CHAT_HISTORY \
- BRAVE_AI_CHAT_CONTEXT_MENU_REWRITE_IN_PLACE \
- BRAVE_AI_CHAT_PAGE_CONTENT_REFINE \
+ BRAVE_AI_CHAT_FEATURE_ENTRIES \
BRAVE_AI_REWRITER \
BRAVE_OMNIBOX_FEATURES \
BRAVE_MIDDLE_CLICK_AUTOSCROLL_FEATURE_ENTRY \
diff --git a/browser/ai_chat/BUILD.gn b/browser/ai_chat/BUILD.gn
index f882ac93f149..219a13500fb5 100644
--- a/browser/ai_chat/BUILD.gn
+++ b/browser/ai_chat/BUILD.gn
@@ -10,12 +10,12 @@ assert(enable_ai_chat)
static_library("ai_chat") {
sources = [
- "//brave/browser/ai_chat/ai_chat_service_factory.cc",
- "//brave/browser/ai_chat/ai_chat_service_factory.h",
- "//brave/browser/ai_chat/ai_chat_settings_helper.cc",
- "//brave/browser/ai_chat/ai_chat_settings_helper.h",
- "//brave/browser/ai_chat/ai_chat_utils.cc",
- "//brave/browser/ai_chat/ai_chat_utils.h",
+ "ai_chat_service_factory.cc",
+ "ai_chat_service_factory.h",
+ "ai_chat_settings_helper.cc",
+ "ai_chat_settings_helper.h",
+ "ai_chat_utils.cc",
+ "ai_chat_utils.h",
]
deps = [
@@ -46,7 +46,10 @@ static_library("ai_chat") {
source_set("unit_tests") {
testonly = true
- sources = [ "ai_chat_throttle_unittest.cc" ]
+ sources = [
+ "ai_chat_throttle_unittest.cc",
+ "brave_open_ai_chat_permission_context_unittest.cc",
+ ]
deps = [
"//base",
@@ -70,6 +73,7 @@ source_set("browser_tests") {
sources = [
"//chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.cc",
"//chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h",
+ "ai_chat_brave_search_throttle_browsertest.cc",
"ai_chat_browsertests.cc",
"ai_chat_metrics_browsertest.cc",
"ai_chat_policy_browsertest.cc",
diff --git a/browser/ai_chat/ai_chat_brave_search_throttle_browsertest.cc b/browser/ai_chat/ai_chat_brave_search_throttle_browsertest.cc
new file mode 100644
index 000000000000..f183830c4d17
--- /dev/null
+++ b/browser/ai_chat/ai_chat_brave_search_throttle_browsertest.cc
@@ -0,0 +1,242 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "brave/components/ai_chat/content/browser/ai_chat_brave_search_throttle.h"
+
+#include
+#include
+
+#include "base/files/file_path.h"
+#include "base/location.h"
+#include "base/path_service.h"
+#include "base/strings/string_util.h"
+#include "brave/browser/ui/brave_browser.h"
+#include "brave/browser/ui/sidebar/sidebar_controller.h"
+#include "brave/browser/ui/sidebar/sidebar_model.h"
+#include "brave/components/constants/brave_paths.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "components/permissions/permission_request_manager.h"
+#include "components/permissions/test/mock_permission_prompt_factory.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_mock_cert_verifier.h"
+#include "content/public/test/test_navigation_observer.h"
+#include "content/public/test/test_utils.h"
+#include "net/base/net_errors.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+constexpr char kBraveSearchHost[] = "search.brave.com";
+constexpr char kLeoPath[] = "/leo";
+constexpr char kOpenAIChatButtonValidPath[] = "/open_ai_chat_button_valid.html";
+constexpr char kOpenAIChatButtonInvalidPath[] =
+ "/open_ai_chat_button_invalid.html";
+
+} // namespace
+
+// TODO(jocelyn): This should be changed to PlatformBrowserTest when we support
+// Android. https://github.com/brave/brave-browser/issues/41905
+class AIChatBraveSearchThrottleBrowserTest : public InProcessBrowserTest {
+ public:
+ AIChatBraveSearchThrottleBrowserTest()
+ : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
+
+ void SetUpOnMainThread() override {
+ InProcessBrowserTest::SetUpOnMainThread();
+
+ mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
+ host_resolver()->AddRule("*", "127.0.0.1");
+ content::SetupCrossSiteRedirector(&https_server_);
+
+ base::FilePath test_data_dir =
+ base::PathService::CheckedGet(brave::DIR_TEST_DATA);
+ test_data_dir = test_data_dir.AppendASCII("leo");
+ https_server_.ServeFilesFromDirectory(test_data_dir);
+ ASSERT_TRUE(https_server_.Start());
+
+ permissions::PermissionRequestManager* manager =
+ permissions::PermissionRequestManager::FromWebContents(
+ GetActiveWebContents());
+ prompt_factory_ =
+ std::make_unique(manager);
+ }
+
+ void TearDownOnMainThread() override {
+ prompt_factory_.reset();
+ InProcessBrowserTest::TearDownOnMainThread();
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ InProcessBrowserTest::SetUpCommandLine(command_line);
+ mock_cert_verifier_.SetUpCommandLine(command_line);
+ }
+
+ void SetUpInProcessBrowserTestFixture() override {
+ InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
+ mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
+ }
+
+ void TearDownInProcessBrowserTestFixture() override {
+ mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
+ InProcessBrowserTest::TearDownInProcessBrowserTestFixture();
+ }
+
+ content::WebContents* GetActiveWebContents() {
+ return browser()->tab_strip_model()->GetActiveWebContents();
+ }
+
+ void ClickOpenAIChatButton() {
+ // Modify the href to have test server port and click it.
+ ASSERT_TRUE(content::ExecJs(GetActiveWebContents()->GetPrimaryMainFrame(),
+ content::JsReplace(R"(
+ const link = document.getElementById('continue-with-leo')
+ const url = new URL(link.href)
+ url.port = $1
+ link.href = url.href
+ link.click())",
+ https_server_.port())));
+ }
+
+ bool IsLeoOpened() {
+ sidebar::SidebarController* controller =
+ static_cast(browser())->sidebar_controller();
+ auto index = controller->model()->GetIndexOf(
+ sidebar::SidebarItem::BuiltInItemType::kChatUI);
+ return index.has_value() && controller->IsActiveIndex(index);
+ }
+
+ void CloseLeoPanel(const base::Location& location) {
+ SCOPED_TRACE(testing::Message() << location.ToString());
+ sidebar::SidebarController* controller =
+ static_cast(browser())->sidebar_controller();
+ controller->DeactivateCurrentPanel();
+ ASSERT_FALSE(IsLeoOpened());
+ }
+
+ void NavigateToTestPage(const base::Location& location,
+ const std::string& host,
+ const std::string& path,
+ int expected_prompt_count) {
+ SCOPED_TRACE(testing::Message() << location.ToString());
+ ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(),
+ https_server_.GetURL(host, path)));
+ EXPECT_FALSE(IsLeoOpened());
+ EXPECT_EQ(expected_prompt_count, prompt_factory_->show_count());
+ }
+
+ void ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ const base::Location& location,
+ int expected_prompt_count,
+ bool expected_leo_opened,
+ const std::string& expected_last_committed_path =
+ kOpenAIChatButtonValidPath) {
+ SCOPED_TRACE(testing::Message() << location.ToString());
+ content::TestNavigationObserver observer(
+ GetActiveWebContents(), net::ERR_ABORTED,
+ content::MessageLoopRunner::QuitMode::IMMEDIATE,
+ false /* ignore_uncommitted_navigations */);
+ ClickOpenAIChatButton();
+ observer.Wait();
+
+ EXPECT_EQ(IsLeoOpened(), expected_leo_opened);
+ EXPECT_EQ(expected_prompt_count, prompt_factory_->show_count());
+ EXPECT_EQ(observer.last_navigation_url().path_piece(), kLeoPath);
+ EXPECT_EQ(GetActiveWebContents()->GetLastCommittedURL().path_piece(),
+ expected_last_committed_path);
+ }
+
+ protected:
+ net::test_server::EmbeddedTestServer https_server_;
+ std::unique_ptr prompt_factory_;
+
+ private:
+ content::ContentMockCertVerifier mock_cert_verifier_;
+};
+
+IN_PROC_BROWSER_TEST_F(AIChatBraveSearchThrottleBrowserTest,
+ OpenAIChat_AskAndAccept) {
+ int cur_prompt_count = 0;
+ prompt_factory_->set_response_type(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+ NavigateToTestPage(FROM_HERE, kBraveSearchHost, kOpenAIChatButtonValidPath,
+ cur_prompt_count);
+ ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ FROM_HERE, ++cur_prompt_count,
+ /*expected_leo_opened=*/true);
+
+ CloseLeoPanel(FROM_HERE);
+ ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ FROM_HERE, cur_prompt_count,
+ /*expected_leo_opened=*/true);
+}
+
+IN_PROC_BROWSER_TEST_F(AIChatBraveSearchThrottleBrowserTest,
+ OpenAIChat_AskAndDeny) {
+ int cur_prompt_count = 0;
+ prompt_factory_->set_response_type(
+ permissions::PermissionRequestManager::DENY_ALL);
+ NavigateToTestPage(FROM_HERE, kBraveSearchHost, kOpenAIChatButtonValidPath,
+ cur_prompt_count);
+ ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ FROM_HERE, ++cur_prompt_count, /*expected_leo_opened=*/false);
+
+ // Clicking a button again to test no new permission prompt should be shown
+ // when the permission setting is denied.
+ ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ FROM_HERE, cur_prompt_count,
+ /*expected_leo_opened=*/false);
+}
+
+IN_PROC_BROWSER_TEST_F(AIChatBraveSearchThrottleBrowserTest,
+ OpenAIChat_AskAndDismiss) {
+ int cur_prompt_count = 0;
+ prompt_factory_->set_response_type(
+ permissions::PermissionRequestManager::DISMISS);
+ NavigateToTestPage(FROM_HERE, kBraveSearchHost, kOpenAIChatButtonValidPath,
+ cur_prompt_count);
+ ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ FROM_HERE, ++cur_prompt_count, /*expected_leo_opened=*/false);
+
+ // Click a button again after dismissing the permission, permission prompt
+ // should be shown again.
+ prompt_factory_->set_response_type(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+ NavigateToTestPage(FROM_HERE, kBraveSearchHost, kOpenAIChatButtonValidPath,
+ cur_prompt_count);
+ ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ FROM_HERE, ++cur_prompt_count,
+ /*expected_leo_opened=*/true);
+}
+
+IN_PROC_BROWSER_TEST_F(AIChatBraveSearchThrottleBrowserTest,
+ OpenAIChat_MismatchedNonce) {
+ int cur_prompt_count = 0;
+ NavigateToTestPage(FROM_HERE, kBraveSearchHost, kOpenAIChatButtonInvalidPath,
+ cur_prompt_count);
+ // No permission prompt should be shown.
+ ClickOpenAIChatAndCheckLeoOpenedAndNavigationCancelled(
+ FROM_HERE, cur_prompt_count, /*expected_leo_opened=*/false,
+ kOpenAIChatButtonInvalidPath);
+}
+
+IN_PROC_BROWSER_TEST_F(AIChatBraveSearchThrottleBrowserTest,
+ OpenAIChat_NotBraveSearchURL) {
+ // The behavior should be the same as without the throttle.
+ NavigateToTestPage(FROM_HERE, "brave.com", kOpenAIChatButtonValidPath, 0);
+ content::TestNavigationObserver observer(GetActiveWebContents());
+ ClickOpenAIChatButton();
+ observer.Wait();
+
+ EXPECT_FALSE(IsLeoOpened());
+ EXPECT_EQ(0, prompt_factory_->show_count());
+ EXPECT_TRUE(observer.last_navigation_succeeded());
+ EXPECT_EQ(observer.last_navigation_url().path_piece(), kLeoPath);
+ EXPECT_EQ(GetActiveWebContents()->GetLastCommittedURL().path_piece(),
+ kLeoPath);
+}
diff --git a/browser/ai_chat/brave_open_ai_chat_permission_context_unittest.cc b/browser/ai_chat/brave_open_ai_chat_permission_context_unittest.cc
new file mode 100644
index 000000000000..25fce629fc05
--- /dev/null
+++ b/browser/ai_chat/brave_open_ai_chat_permission_context_unittest.cc
@@ -0,0 +1,115 @@
+// Copyright (c) 2024 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/permissions/contexts/brave_open_ai_chat_permission_context.h"
+
+#include
+
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/permissions/permission_request_data.h"
+#include "components/permissions/permission_request_id.h"
+#include "components/permissions/permission_request_manager.h"
+#include "components/permissions/test/mock_permission_prompt_factory.h"
+#include "content/public/browser/permission_result.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace permissions {
+
+class BraveOpenAIChatPermissionContextTest
+ : public ChromeRenderViewHostTestHarness {
+ public:
+ BraveOpenAIChatPermissionContextTest() = default;
+ ~BraveOpenAIChatPermissionContextTest() override = default;
+
+ ContentSetting RequestPermission(
+ BraveOpenAIChatPermissionContext* permission_context,
+ const GURL& url) {
+ NavigateAndCommit(url);
+ prompt_factory_->DocumentOnLoadCompletedInPrimaryMainFrame();
+
+ const PermissionRequestID id(
+ web_contents()->GetPrimaryMainFrame()->GetGlobalId(),
+ PermissionRequestID::RequestLocalId());
+ ContentSetting setting = ContentSetting::CONTENT_SETTING_DEFAULT;
+ base::RunLoop run_loop;
+ permission_context->RequestPermission(
+ PermissionRequestData(permission_context, id, /*user_gesture=*/true,
+ url),
+ base::BindLambdaForTesting([&](ContentSetting result) {
+ setting = result;
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+
+ return setting;
+ }
+
+ protected:
+ void SetUp() override {
+ ChromeRenderViewHostTestHarness::SetUp();
+ PermissionRequestManager::CreateForWebContents(web_contents());
+ PermissionRequestManager* manager =
+ PermissionRequestManager::FromWebContents(web_contents());
+ prompt_factory_ = std::make_unique(manager);
+ }
+
+ void TearDown() override {
+ prompt_factory_.reset();
+ ChromeRenderViewHostTestHarness::TearDown();
+ }
+
+ std::unique_ptr prompt_factory_;
+};
+
+TEST_F(BraveOpenAIChatPermissionContextTest, PromptForBraveSearch) {
+ GURL brave_search_url("https://search.brave.com");
+
+ prompt_factory_->set_response_type(PermissionRequestManager::ACCEPT_ALL);
+ BraveOpenAIChatPermissionContext context(browser_context());
+ EXPECT_EQ(ContentSetting::CONTENT_SETTING_ALLOW,
+ RequestPermission(&context, brave_search_url));
+ EXPECT_EQ(prompt_factory_->show_count(), 1);
+}
+
+TEST_F(BraveOpenAIChatPermissionContextTest, BlockForNonBraveSearch) {
+ GURL brave_url("https://brave.com");
+
+ prompt_factory_->set_response_type(PermissionRequestManager::ACCEPT_ALL);
+ BraveOpenAIChatPermissionContext context(browser_context());
+ EXPECT_EQ(ContentSetting::CONTENT_SETTING_BLOCK,
+ RequestPermission(&context, brave_url));
+ EXPECT_EQ(prompt_factory_->show_count(), 0);
+}
+
+TEST_F(BraveOpenAIChatPermissionContextTest, NotAllowedInInsecureOrigins) {
+ BraveOpenAIChatPermissionContext permission_context(browser_context());
+ GURL insecure_url("http://search.brave.com");
+ GURL secure_url("https://search.brave.com");
+
+ EXPECT_EQ(content::PermissionStatus::DENIED,
+ permission_context
+ .GetPermissionStatus(nullptr /* render_frame_host */,
+ insecure_url, insecure_url)
+ .status);
+
+ EXPECT_EQ(content::PermissionStatus::DENIED,
+ permission_context
+ .GetPermissionStatus(nullptr /* render_frame_host */,
+ insecure_url, secure_url)
+ .status);
+
+ EXPECT_EQ(content::PermissionStatus::ASK,
+ permission_context
+ .GetPermissionStatus(nullptr /* render_frame_host */,
+ secure_url, secure_url)
+ .status);
+}
+
+} // namespace permissions
diff --git a/browser/ai_chat/page_content_fetcher_browsertest.cc b/browser/ai_chat/page_content_fetcher_browsertest.cc
index 8d6cfdcd19b5..e9d96d9e2e4a 100644
--- a/browser/ai_chat/page_content_fetcher_browsertest.cc
+++ b/browser/ai_chat/page_content_fetcher_browsertest.cc
@@ -5,6 +5,8 @@
#include "brave/components/ai_chat/content/browser/page_content_fetcher.h"
+#include
+
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
@@ -133,6 +135,20 @@ class PageContentFetcherBrowserTest : public InProcessBrowserTest {
run_loop.Run();
}
+ std::optional GetOpenAIChatButtonNonce() {
+ std::optional ret_nonce;
+ base::RunLoop run_loop;
+ page_content_fetcher_ =
+ std::make_unique(GetActiveWebContents());
+ page_content_fetcher_->GetOpenAIChatButtonNonce(base::BindLambdaForTesting(
+ [&run_loop, &ret_nonce](const std::optional& nonce) {
+ ret_nonce = nonce;
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+ return ret_nonce;
+ }
+
// Handles returning a .patch file if the user is on a github.com pull request
void SetGithubInterceptor() {
GURL expected_patch_url =
@@ -269,3 +285,35 @@ IN_PROC_BROWSER_TEST_F(PageContentFetcherBrowserTest, GetSearchSummarizerKey) {
GetSearchSummarizerKey(FROM_HERE, expected_result);
}
}
+
+IN_PROC_BROWSER_TEST_F(PageContentFetcherBrowserTest,
+ GetOpenAIChatButtonNonce) {
+ // Test no open Leo button with continue-with-leo ID present.
+ GURL url = https_server_.GetURL("a.com", "/open_ai_chat_button.html");
+ NavigateURL(url);
+ EXPECT_FALSE(GetOpenAIChatButtonNonce());
+
+ // Test valid case.
+ NavigateURL(url);
+ ASSERT_TRUE(content::ExecJs(GetActiveWebContents()->GetPrimaryMainFrame(),
+ "document.getElementById('valid').setAttribute('"
+ "id', 'continue-with-leo')"));
+ EXPECT_EQ(GetOpenAIChatButtonNonce(), "5566");
+
+ // Test invalid cases.
+ const auto invalid_cases = std::to_array(
+ {"invalid", "not-a-tag", "no-href", "no-nonce", "empty-nonce",
+ "empty-nonce2", "empty-nonce3", "empty-nonce4", "empty-nonce5",
+ "empty-nonce6", "not-https-url", "not-search-url", "not-open-leo-url"});
+
+ for (const auto& invalid_case : invalid_cases) {
+ SCOPED_TRACE(testing::Message() << "Invalid case: " << invalid_case);
+ NavigateURL(url);
+ ASSERT_TRUE(content::ExecJs(
+ GetActiveWebContents()->GetPrimaryMainFrame(),
+ content::JsReplace("document.getElementById($1)."
+ "setAttribute('id', 'continue-with-leo')",
+ invalid_case)));
+ EXPECT_FALSE(GetOpenAIChatButtonNonce());
+ }
+}
diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc
index 32dddb9f7b56..02c5a592a299 100644
--- a/browser/brave_content_browser_client.cc
+++ b/browser/brave_content_browser_client.cc
@@ -156,7 +156,9 @@ using extensions::ChromeContentBrowserClientExtensionsPart;
#endif
#if BUILDFLAG(ENABLE_AI_CHAT)
+#include "brave/browser/ai_chat/ai_chat_service_factory.h"
#include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h"
+#include "brave/components/ai_chat/content/browser/ai_chat_brave_search_throttle.h"
#include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h"
#include "brave/components/ai_chat/content/browser/ai_chat_throttle.h"
#include "brave/components/ai_chat/core/browser/utils.h"
@@ -164,6 +166,9 @@ using extensions::ChromeContentBrowserClientExtensionsPart;
#include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h"
#include "brave/components/ai_chat/core/common/mojom/page_content_extractor.mojom.h"
#include "brave/components/ai_chat/core/common/mojom/settings_helper.mojom.h"
+#if !BUILDFLAG(IS_ANDROID)
+#include "brave/browser/ui/ai_chat/utils.h"
+#endif
#if BUILDFLAG(IS_ANDROID)
#include "brave/components/ai_chat/core/browser/android/ai_chat_iap_subscription_android.h"
#endif
@@ -1268,6 +1273,16 @@ BraveContentBrowserClient::CreateThrottlesForNavigation(
throttles.push_back(std::move(ai_chat_throttle));
}
}
+
+#if !BUILDFLAG(IS_ANDROID)
+ if (auto ai_chat_brave_search_throttle =
+ ai_chat::AIChatBraveSearchThrottle::MaybeCreateThrottleFor(
+ base::BindOnce(&ai_chat::OpenAIChatForTab), handle,
+ ai_chat::AIChatServiceFactory::GetForBrowserContext(context),
+ user_prefs::UserPrefs::Get(context))) {
+ throttles.push_back(std::move(ai_chat_brave_search_throttle));
+ }
+#endif
#endif // ENABLE_AI_CHAT
return throttles;
diff --git a/browser/resources/settings/brave_overrides/privacy_page.ts b/browser/resources/settings/brave_overrides/privacy_page.ts
index 1379800d2b60..27b011b2d2ab 100644
--- a/browser/resources/settings/brave_overrides/privacy_page.ts
+++ b/browser/resources/settings/brave_overrides/privacy_page.ts
@@ -181,6 +181,36 @@ function InsertShieldsSubpage (pages: Element)
`)
}
+function InsertBraveOpenAIChatSubpage (pages: Element)
+{
+ pages.appendChild(
+ html`
+
+
+
+
+
+
+
+
+ `)
+}
+
RegisterPolymerTemplateModifications({
'settings-privacy-page': (templateContent) => {
const pages = templateContent.getElementById('pages')
@@ -208,6 +238,11 @@ RegisterPolymerTemplateModifications({
if (isLocalhostAccessFeatureEnabled) {
InsertLocalhostAccessSubpage(pages)
}
+ const isOpenAIChatFromBraveSearchEnabled =
+ loadTimeData.getBoolean('isOpenAIChatFromBraveSearchEnabled')
+ if (isOpenAIChatFromBraveSearchEnabled) {
+ InsertBraveOpenAIChatSubpage(pages)
+ }
InsertAutoplaySubpage(pages)
const isNativeBraveWalletEnabled =
loadTimeData.getBoolean('isNativeBraveWalletFeatureEnabled')
diff --git a/browser/resources/settings/brave_overrides/site_details.ts b/browser/resources/settings/brave_overrides/site_details.ts
index 4d6f70cb96d7..06c6621063bc 100644
--- a/browser/resources/settings/brave_overrides/site_details.ts
+++ b/browser/resources/settings/brave_overrides/site_details.ts
@@ -99,6 +99,32 @@ RegisterPolymerTemplateModifications({
}
curChild++
}
+ // AI Chat feature
+ const isOpenLeoFromBraveSearchFeatureEnabled =
+ loadTimeData.getBoolean('isOpenLeoFromBraveSearchFeatureEnabled')
+ const isOpenAIChatFromBraveSearchEnabled =
+ loadTimeData.getBoolean('isOpenAIChatFromBraveSearchEnabled')
+ if (isOpenAIChatFromBraveSearchEnabled && isOpenLeoFromBraveSearchFeatureEnabled) {
+ firstPermissionItem.insertAdjacentHTML(
+ 'beforebegin',
+ getTrustedHTML`
+
+
+ `)
+ const braveAIChatSettings = templateContent.
+ querySelector(`div.list-frame > site-details-permission:nth-child(${curChild})`)
+ if (!braveAIChatSettings) {
+ console.error(
+ '[Brave Settings Overrides] Couldn\'t find Brave AI chat settings')
+ }
+ else {
+ braveAIChatSettings.setAttribute(
+ 'label', loadTimeData.getString('siteSettingsBraveOpenAIChat'))
+ }
+ curChild++
+ }
const isNativeBraveWalletEnabled = loadTimeData.getBoolean('isNativeBraveWalletFeatureEnabled')
if (isNativeBraveWalletEnabled) {
firstPermissionItem.insertAdjacentHTML(
diff --git a/browser/resources/settings/brave_overrides/site_settings_page.ts b/browser/resources/settings/brave_overrides/site_settings_page.ts
index 64eaa1c2c804..24540db1216b 100644
--- a/browser/resources/settings/brave_overrides/site_settings_page.ts
+++ b/browser/resources/settings/brave_overrides/site_settings_page.ts
@@ -137,6 +137,21 @@ RegisterPolymerComponentReplacement(
lists_.permissionsAdvanced.splice(currentIndex, 0,
localhostAccessItem)
}
+ const isOpenAIChatFromBraveSearchEnabled =
+ loadTimeData.getBoolean('isOpenAIChatFromBraveSearchEnabled')
+ if (isOpenAIChatFromBraveSearchEnabled) {
+ currentIndex++
+ const AIChatItem = {
+ route: routes.SITE_SETTINGS_BRAVE_OPEN_AI_CHAT,
+ id: 'braveOpenAIChat',
+ label: 'siteSettingsBraveOpenAIChat',
+ icon: 'product-brave-leo',
+ enabledLabel: 'siteSettingsBraveOpenAIChatAsk',
+ disabledLabel: 'siteSettingsBraveOpenAIChatBlock'
+ }
+ lists_.permissionsAdvanced.splice(currentIndex, 0,
+ AIChatItem)
+ }
const isNativeBraveWalletEnabled = loadTimeData.getBoolean('isNativeBraveWalletFeatureEnabled')
if (isNativeBraveWalletEnabled) {
currentIndex++
diff --git a/browser/resources/settings/brave_routes.ts b/browser/resources/settings/brave_routes.ts
index 1d2106d728d4..bce4d58943d9 100644
--- a/browser/resources/settings/brave_routes.ts
+++ b/browser/resources/settings/brave_routes.ts
@@ -75,6 +75,10 @@ export default function addBraveRoutes(r: Partial) {
r.SITE_SETTINGS_LOCALHOST_ACCESS = r.SITE_SETTINGS
.createChild('localhostAccess')
}
+ const isOpenAIChatFromBraveSearchEnabled = loadTimeData.getBoolean('isOpenAIChatFromBraveSearchEnabled')
+ if (isOpenAIChatFromBraveSearchEnabled) {
+ r.SITE_SETTINGS_BRAVE_OPEN_AI_CHAT = r.SITE_SETTINGS.createChild('braveOpenAIChat')
+ }
const isNativeBraveWalletFeatureEnabled = loadTimeData.getBoolean('isNativeBraveWalletFeatureEnabled')
if (isNativeBraveWalletFeatureEnabled) {
r.SITE_SETTINGS_ETHEREUM = r.SITE_SETTINGS.createChild('ethereum')
diff --git a/browser/sources.gni b/browser/sources.gni
index 7eb5d58e8855..e98a3a6217e5 100644
--- a/browser/sources.gni
+++ b/browser/sources.gni
@@ -348,6 +348,10 @@ if (enable_ai_chat) {
"//brave/components/ai_chat/core/common/mojom",
]
+ if (!is_android) {
+ brave_chrome_browser_deps += [ "//brave/browser/ui/ai_chat" ]
+ }
+
if (is_android) {
brave_chrome_browser_sources += brave_browser_ai_chat_android_sources
brave_chrome_browser_deps += brave_browser_ai_chat_android_deps
diff --git a/browser/ui/ai_chat/BUILD.gn b/browser/ui/ai_chat/BUILD.gn
index e230199538ee..04e79ce41222 100644
--- a/browser/ui/ai_chat/BUILD.gn
+++ b/browser/ui/ai_chat/BUILD.gn
@@ -3,6 +3,24 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
+import("//brave/components/ai_chat/core/common/buildflags/buildflags.gni")
+assert(enable_ai_chat)
+
+if (!is_android) {
+ source_set("ai_chat") {
+ sources = [
+ "utils.cc",
+ "utils.h",
+ ]
+
+ deps = [
+ "//chrome/browser/ui/browser_window",
+ "//chrome/browser/ui/tabs",
+ "//chrome/browser/ui/views/side_panel",
+ ]
+ }
+}
+
source_set("unit_tests") {
testonly = true
sources = [ "ai_chat_tab_helper_unittest.cc" ]
diff --git a/browser/ui/ai_chat/ai_chat_tab_helper_unittest.cc b/browser/ui/ai_chat/ai_chat_tab_helper_unittest.cc
index 4fa38922747c..cfc84ec71659 100644
--- a/browser/ui/ai_chat/ai_chat_tab_helper_unittest.cc
+++ b/browser/ui/ai_chat/ai_chat_tab_helper_unittest.cc
@@ -51,6 +51,10 @@ class MockPageContentFetcher
GetSearchSummarizerKey,
(mojom::PageContentExtractor::GetSearchSummarizerKeyCallback),
(override));
+ MOCK_METHOD(void,
+ GetOpenAIChatButtonNonce,
+ (mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback),
+ (override));
};
class MockAssociatedContentObserver : public AssociatedContentDriver::Observer {
diff --git a/browser/ui/ai_chat/utils.cc b/browser/ui/ai_chat/utils.cc
new file mode 100644
index 000000000000..5c5d5053f14c
--- /dev/null
+++ b/browser/ui/ai_chat/utils.cc
@@ -0,0 +1,28 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "brave/browser/ui/ai_chat/utils.h"
+
+#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
+#include "chrome/browser/ui/tabs/public/tab_interface.h"
+#include "chrome/browser/ui/views/side_panel/side_panel_coordinator.h"
+#include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h"
+
+namespace ai_chat {
+
+void OpenAIChatForTab(content::WebContents* web_contents) {
+ if (!web_contents) {
+ return;
+ }
+
+ auto* tab = tabs::TabInterface::GetFromContents(web_contents);
+ auto* coordinator =
+ tab->GetBrowserWindowInterface()->GetFeatures().side_panel_coordinator();
+ CHECK(coordinator);
+ coordinator->Show(SidePanelEntryId::kChatUI);
+}
+
+} // namespace ai_chat
diff --git a/browser/ui/ai_chat/utils.h b/browser/ui/ai_chat/utils.h
new file mode 100644
index 000000000000..075c08fb7b45
--- /dev/null
+++ b/browser/ui/ai_chat/utils.h
@@ -0,0 +1,19 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef BRAVE_BROWSER_UI_AI_CHAT_UTILS_H_
+#define BRAVE_BROWSER_UI_AI_CHAT_UTILS_H_
+
+namespace content {
+class WebContents;
+}
+
+namespace ai_chat {
+
+void OpenAIChatForTab(content::WebContents* web_contents);
+
+} // namespace ai_chat
+
+#endif // BRAVE_BROWSER_UI_AI_CHAT_UTILS_H_
diff --git a/browser/ui/webui/settings/brave_privacy_handler.cc b/browser/ui/webui/settings/brave_privacy_handler.cc
index 34abdd43200d..b7ffe98637f7 100644
--- a/browser/ui/webui/settings/brave_privacy_handler.cc
+++ b/browser/ui/webui/settings/brave_privacy_handler.cc
@@ -7,6 +7,7 @@
#include "base/functional/bind.h"
#include "base/values.h"
+#include "brave/components/ai_chat/core/common/buildflags/buildflags.h"
#include "brave/components/brave_shields/core/common/features.h"
#include "brave/components/constants/pref_names.h"
#include "brave/components/de_amp/common/features.h"
@@ -22,6 +23,11 @@
#include "content/public/browser/web_ui_data_source.h"
#include "third_party/blink/public/common/peerconnection/webrtc_ip_handling_policy.h"
+#if BUILDFLAG(ENABLE_AI_CHAT)
+#include "brave/components/ai_chat/core/browser/utils.h"
+#include "brave/components/ai_chat/core/common/features.h"
+#endif
+
#if BUILDFLAG(ENABLE_REQUEST_OTR)
#include "brave/components/request_otr/common/features.h"
#endif
@@ -96,6 +102,14 @@ void BravePrivacyHandler::AddLoadTimeData(content::WebUIDataSource* data_source,
"isLocalhostAccessFeatureEnabled",
base::FeatureList::IsEnabled(
brave_shields::features::kBraveLocalhostAccessPermission));
+ data_source->AddBoolean(
+ "isOpenAIChatFromBraveSearchEnabled",
+#if BUILDFLAG(ENABLE_AI_CHAT)
+ ai_chat::IsAIChatEnabled(profile->GetPrefs()) &&
+ ai_chat::features::IsOpenAIChatFromBraveSearchEnabled());
+#else
+ false);
+#endif
}
void BravePrivacyHandler::SetLocalStateBooleanEnabled(
diff --git a/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc b/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc
index 8c9a3f5307a4..e3c2fdf5d762 100644
--- a/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc
+++ b/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc
@@ -105,6 +105,13 @@ void BraveAddCommonStrings(content::WebUIDataSource* html_source,
{"siteSettingsGoogleSignInAllowExceptions",
IDS_SETTINGS_SITE_SETTINGS_GOOGLE_SIGN_IN_ALLOW_EXCEPTIONS},
+ {"siteSettingsBraveOpenAIChat",
+ IDS_SETTINGS_SITE_SETTINGS_BRAVE_OPEN_AI_CHAT},
+ {"siteSettingsBraveOpenAIChatAsk",
+ IDS_SETTINGS_SITE_SETTINGS_BRAVE_OPEN_AI_CHAT_ASK},
+ {"siteSettingsBraveOpenAIChatBlock",
+ IDS_SETTINGS_SITE_SETTINGS_BRAVE_OPEN_AI_CHAT_BLOCK},
+
{"siteSettingsLocalhostAccess",
IDS_SETTINGS_SITE_SETTINGS_LOCALHOST_ACCESS},
{"siteSettingsCategoryLocalhostAccess",
diff --git a/chromium_src/android_webview/browser/aw_permission_manager.cc b/chromium_src/android_webview/browser/aw_permission_manager.cc
index 5c7dc387e4a3..661e47662003 100644
--- a/chromium_src/android_webview/browser/aw_permission_manager.cc
+++ b/chromium_src/android_webview/browser/aw_permission_manager.cc
@@ -20,6 +20,7 @@
case PermissionType::BRAVE_SOLANA: \
case PermissionType::BRAVE_GOOGLE_SIGN_IN: \
case PermissionType::BRAVE_LOCALHOST_ACCESS: \
+ case PermissionType::BRAVE_OPEN_AI_CHAT: \
case PermissionType::NUM
#include "src/android_webview/browser/aw_permission_manager.cc"
diff --git a/chromium_src/chrome/browser/permissions/permission_manager_factory.cc b/chromium_src/chrome/browser/permissions/permission_manager_factory.cc
index 81fe9feee122..f93f211f8e6a 100644
--- a/chromium_src/chrome/browser/permissions/permission_manager_factory.cc
+++ b/chromium_src/chrome/browser/permissions/permission_manager_factory.cc
@@ -10,6 +10,7 @@
#include "brave/components/permissions/brave_permission_manager.h"
#include "brave/components/permissions/contexts/brave_google_sign_in_permission_context.h"
#include "brave/components/permissions/contexts/brave_localhost_permission_context.h"
+#include "brave/components/permissions/contexts/brave_open_ai_chat_permission_context.h"
#include "brave/components/permissions/contexts/brave_wallet_permission_context.h"
#include "brave/components/permissions/permission_lifetime_manager.h"
#include "components/permissions/features.h"
@@ -42,6 +43,8 @@ PermissionManagerFactory::BuildServiceInstanceForBrowserContext(
profile);
permission_contexts[ContentSettingsType::BRAVE_LOCALHOST_ACCESS] =
std::make_unique(profile);
+ permission_contexts[ContentSettingsType::BRAVE_OPEN_AI_CHAT] =
+ std::make_unique(profile);
if (base::FeatureList::IsEnabled(
permissions::features::kPermissionLifetime)) {
diff --git a/chromium_src/chrome/browser/ui/webui/settings/site_settings_helper.cc b/chromium_src/chrome/browser/ui/webui/settings/site_settings_helper.cc
index e02ea6f2eecf..7b68d03b6d2e 100644
--- a/chromium_src/chrome/browser/ui/webui/settings/site_settings_helper.cc
+++ b/chromium_src/chrome/browser/ui/webui/settings/site_settings_helper.cc
@@ -31,6 +31,7 @@
{ContentSettingsType::BRAVE_HTTPS_UPGRADE, nullptr}, \
{ContentSettingsType::BRAVE_REMEMBER_1P_STORAGE, nullptr}, \
{ContentSettingsType::BRAVE_LOCALHOST_ACCESS, "localhostAccess"}, \
+ {ContentSettingsType::BRAVE_OPEN_AI_CHAT, "braveOpenAIChat"}, \
{ContentSettingsType::BRAVE_WEBCOMPAT_NONE, nullptr}, \
{ContentSettingsType::BRAVE_WEBCOMPAT_AUDIO, nullptr}, \
{ContentSettingsType::BRAVE_WEBCOMPAT_CANVAS, nullptr}, \
@@ -87,19 +88,24 @@
namespace site_settings {
bool HasRegisteredGroupName(ContentSettingsType type) {
- if (type == ContentSettingsType::AUTOPLAY)
+ if (type == ContentSettingsType::AUTOPLAY) {
return true;
- if (type == ContentSettingsType::BRAVE_GOOGLE_SIGN_IN)
+ }
+ if (type == ContentSettingsType::BRAVE_GOOGLE_SIGN_IN) {
return true;
+ }
if (type == ContentSettingsType::BRAVE_LOCALHOST_ACCESS) {
return true;
}
- if (type == ContentSettingsType::BRAVE_ETHEREUM)
+ if (type == ContentSettingsType::BRAVE_ETHEREUM) {
return true;
- if (type == ContentSettingsType::BRAVE_SOLANA)
+ }
+ if (type == ContentSettingsType::BRAVE_SOLANA) {
return true;
- if (type == ContentSettingsType::BRAVE_SHIELDS)
+ }
+ if (type == ContentSettingsType::BRAVE_SHIELDS) {
return true;
+ }
return HasRegisteredGroupName_ChromiumImpl(type);
}
@@ -112,6 +118,7 @@ std::vector GetVisiblePermissionCategories(
ContentSettingsType::BRAVE_SOLANA,
ContentSettingsType::BRAVE_GOOGLE_SIGN_IN,
ContentSettingsType::BRAVE_LOCALHOST_ACCESS,
+ ContentSettingsType::BRAVE_OPEN_AI_CHAT,
};
auto types = GetVisiblePermissionCategories_ChromiumImpl(origin, profile);
diff --git a/chromium_src/components/content_settings/core/browser/content_settings_registry.cc b/chromium_src/components/content_settings/core/browser/content_settings_registry.cc
index 047ade3d68e2..bbd7636e5c36 100644
--- a/chromium_src/components/content_settings/core/browser/content_settings_registry.cc
+++ b/chromium_src/components/content_settings/core/browser/content_settings_registry.cc
@@ -228,6 +228,18 @@ void ContentSettingsRegistry::BraveInit() {
ContentSettingsInfo::INHERIT_IF_LESS_PERMISSIVE,
ContentSettingsInfo::EXCEPTIONS_ON_SECURE_AND_INSECURE_ORIGINS);
+ // Register AI chat permission default value as Ask.
+ Register(ContentSettingsType::BRAVE_OPEN_AI_CHAT, "brave_open_ai_chat",
+ CONTENT_SETTING_ASK, WebsiteSettingsInfo::UNSYNCABLE,
+ /*allowlisted_schemes=*/{},
+ /*valid_settings=*/
+ {CONTENT_SETTING_ALLOW, CONTENT_SETTING_BLOCK, CONTENT_SETTING_ASK},
+ WebsiteSettingsInfo::TOP_ORIGIN_ONLY_SCOPE,
+ WebsiteSettingsRegistry::DESKTOP |
+ WebsiteSettingsRegistry::PLATFORM_ANDROID,
+ ContentSettingsInfo::INHERIT_IF_LESS_PERMISSIVE,
+ ContentSettingsInfo::EXCEPTIONS_ON_SECURE_ORIGINS_ONLY);
+
// Disable background sync by default (brave/brave-browser#4709)
content_settings_info_.erase(ContentSettingsType::BACKGROUND_SYNC);
website_settings_registry_->UnRegister(ContentSettingsType::BACKGROUND_SYNC);
diff --git a/chromium_src/components/content_settings/core/browser/content_settings_uma_util.cc b/chromium_src/components/content_settings/core/browser/content_settings_uma_util.cc
index a85492a344e2..fd74be15d2b7 100644
--- a/chromium_src/components/content_settings/core/browser/content_settings_uma_util.cc
+++ b/chromium_src/components/content_settings/core/browser/content_settings_uma_util.cc
@@ -44,6 +44,7 @@ static_assert(static_cast(ContentSettingsType::kMaxValue) <
{ContentSettingsType::BRAVE_HTTPS_UPGRADE, brave_value(12)}, \
{ContentSettingsType::BRAVE_REMEMBER_1P_STORAGE, brave_value(13)}, \
{ContentSettingsType::BRAVE_LOCALHOST_ACCESS, brave_value(14)}, \
+ {ContentSettingsType::BRAVE_OPEN_AI_CHAT, brave_value(15)}, \
/* Begin webcompat items */ \
{ContentSettingsType::BRAVE_WEBCOMPAT_NONE, brave_value(50)}, \
{ContentSettingsType::BRAVE_WEBCOMPAT_AUDIO, brave_value(51)}, \
diff --git a/chromium_src/components/content_settings/core/common/content_settings_types.mojom b/chromium_src/components/content_settings/core/common/content_settings_types.mojom
index 8f061e6343f7..9f598acb0e19 100644
--- a/chromium_src/components/content_settings/core/common/content_settings_types.mojom
+++ b/chromium_src/components/content_settings/core/common/content_settings_types.mojom
@@ -24,6 +24,9 @@ enum ContentSettingsType {
BRAVE_HTTPS_UPGRADE,
BRAVE_REMEMBER_1P_STORAGE,
BRAVE_LOCALHOST_ACCESS,
+ // Allow a site to open AI Chat (in side panel on Desktop).
+ // This is limited to Brave Search only.
+ BRAVE_OPEN_AI_CHAT,
BRAVE_WEBCOMPAT_NONE,
BRAVE_WEBCOMPAT_AUDIO,
diff --git a/chromium_src/components/permissions/permission_request.cc b/chromium_src/components/permissions/permission_request.cc
index ff8c282d9c7f..e661dce8a9d1 100644
--- a/chromium_src/components/permissions/permission_request.cc
+++ b/chromium_src/components/permissions/permission_request.cc
@@ -3,13 +3,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
+#include "components/permissions/permission_request.h"
+
#include
#include
#include "base/containers/contains.h"
#include "build/build_config.h"
#include "components/grit/brave_components_strings.h"
-#include "components/permissions/permission_request.h"
#include "components/strings/grit/components_strings.h"
#include "third_party/widevine/cdm/buildflags.h"
@@ -49,6 +50,9 @@
break; \
case RequestType::kBraveLocalhostAccessPermission: \
message_id = IDS_LOCALHOST_ACCESS_PERMISSION_FRAGMENT; \
+ break; \
+ case RequestType::kBraveOpenAIChat: \
+ message_id = IDS_OPEN_AI_CHAT_PERMISSION_FRAGMENT; \
break;
#define BRAVE_ENUM_ITEMS_FOR_SWITCH_ANDROID \
@@ -58,6 +62,9 @@
break; \
case RequestType::kBraveLocalhostAccessPermission: \
message_id = IDS_LOCALHOST_ACCESS_INFOBAR_TEXT; \
+ break; \
+ case RequestType::kBraveOpenAIChat: \
+ message_id = IDS_OPEN_AI_CHAT_INFOBAR_TEXT; \
break;
namespace {
@@ -154,16 +161,15 @@ PermissionRequest::GetDialogAnnotatedMessageText(
#endif
bool PermissionRequest::SupportsLifetime() const {
- const RequestType kExcludedTypes[] = {
- RequestType::kDiskQuota,
- RequestType::kMultipleDownloads,
+ const RequestType kExcludedTypes[] = {RequestType::kDiskQuota,
+ RequestType::kMultipleDownloads,
#if BUILDFLAG(IS_ANDROID)
- RequestType::kProtectedMediaIdentifier,
+ RequestType::kProtectedMediaIdentifier,
#else
RequestType::kRegisterProtocolHandler,
#endif // BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(ENABLE_WIDEVINE)
- RequestType::kWidevine
+ RequestType::kWidevine
#endif // BUILDFLAG(ENABLE_WIDEVINE)
};
return !base::Contains(kExcludedTypes, request_type());
diff --git a/chromium_src/components/permissions/permission_request_data.cc b/chromium_src/components/permissions/permission_request_data.cc
index 981030bb2b15..84b92fdfd790 100644
--- a/chromium_src/components/permissions/permission_request_data.cc
+++ b/chromium_src/components/permissions/permission_request_data.cc
@@ -3,10 +3,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
+#include "components/permissions/permission_request_data.h"
+
#include
#include "components/permissions/permission_context_base.h"
-#include "components/permissions/permission_request_data.h"
namespace permissions {
@@ -21,6 +22,8 @@ std::optional ContentSettingsTypeToRequestTypeIfExists_BraveImpl(
return RequestType::kBraveGoogleSignInPermission;
case ContentSettingsType::BRAVE_LOCALHOST_ACCESS:
return RequestType::kBraveLocalhostAccessPermission;
+ case ContentSettingsType::BRAVE_OPEN_AI_CHAT:
+ return RequestType::kBraveOpenAIChat;
default:
return ContentSettingsTypeToRequestTypeIfExists(content_settings_type);
}
diff --git a/chromium_src/components/permissions/permission_uma_util.cc b/chromium_src/components/permissions/permission_uma_util.cc
index d85b221cdc27..655169236821 100644
--- a/chromium_src/components/permissions/permission_uma_util.cc
+++ b/chromium_src/components/permissions/permission_uma_util.cc
@@ -14,15 +14,16 @@
case RequestType::kBraveSolana: \
case RequestType::kBraveGoogleSignInPermission: \
case RequestType::kBraveLocalhostAccessPermission: \
+ case RequestType::kBraveOpenAIChat: \
return RequestTypeForUma::PERMISSION_VR;
// These requests may be batched together, so we must handle them explicitly as
// GetUmaValueForRequests expects only a few specific request types to be
// batched
-#define BRAVE_GET_UMA_VALUE_FOR_REQUESTS \
- if (request_type >= RequestType::kWidevine && \
- request_type <= RequestType::kBraveLocalhostAccessPermission) { \
- return GetUmaValueForRequestType(request_type); \
+#define BRAVE_GET_UMA_VALUE_FOR_REQUESTS \
+ if (request_type >= RequestType::kBraveMinValue && \
+ request_type <= RequestType::kBraveMaxValue) { \
+ return GetUmaValueForRequestType(request_type); \
}
// We do not record permissions UKM and this can save us from patching
diff --git a/chromium_src/components/permissions/permission_util.cc b/chromium_src/components/permissions/permission_util.cc
index 3eb284556106..0aea75c5f39a 100644
--- a/chromium_src/components/permissions/permission_util.cc
+++ b/chromium_src/components/permissions/permission_util.cc
@@ -4,6 +4,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "components/permissions/permission_util.h"
+
#include "components/permissions/permission_uma_util.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
@@ -35,7 +36,9 @@
case PermissionType::BRAVE_GOOGLE_SIGN_IN: \
return ContentSettingsType::BRAVE_GOOGLE_SIGN_IN; \
case PermissionType::BRAVE_LOCALHOST_ACCESS: \
- return ContentSettingsType::BRAVE_LOCALHOST_ACCESS;
+ return ContentSettingsType::BRAVE_LOCALHOST_ACCESS; \
+ case PermissionType::BRAVE_OPEN_AI_CHAT: \
+ return ContentSettingsType::BRAVE_OPEN_AI_CHAT;
#include "src/components/permissions/permission_util.cc"
#undef PermissionUtil
@@ -55,6 +58,8 @@ std::string PermissionUtil::GetPermissionString(
return "BraveGoogleSignInPermission";
case ContentSettingsType::BRAVE_LOCALHOST_ACCESS:
return "BraveLocalhostAccessPermission";
+ case ContentSettingsType::BRAVE_OPEN_AI_CHAT:
+ return "BraveOpenAIChatPermission";
default:
return PermissionUtil_ChromiumImpl::GetPermissionString(content_type);
}
@@ -76,6 +81,10 @@ bool PermissionUtil::GetPermissionType(ContentSettingsType type,
*out = PermissionType::BRAVE_LOCALHOST_ACCESS;
return true;
}
+ if (type == ContentSettingsType::BRAVE_OPEN_AI_CHAT) {
+ *out = PermissionType::BRAVE_OPEN_AI_CHAT;
+ return true;
+ }
return PermissionUtil_ChromiumImpl::GetPermissionType(type, out);
}
@@ -87,6 +96,7 @@ bool PermissionUtil::IsPermission(ContentSettingsType type) {
case ContentSettingsType::BRAVE_SOLANA:
case ContentSettingsType::BRAVE_GOOGLE_SIGN_IN:
case ContentSettingsType::BRAVE_LOCALHOST_ACCESS:
+ case ContentSettingsType::BRAVE_OPEN_AI_CHAT:
return true;
default:
return PermissionUtil_ChromiumImpl::IsPermission(type);
@@ -122,6 +132,8 @@ PermissionType PermissionUtil::ContentSettingTypeToPermissionType(
return PermissionType::BRAVE_GOOGLE_SIGN_IN;
case ContentSettingsType::BRAVE_LOCALHOST_ACCESS:
return PermissionType::BRAVE_LOCALHOST_ACCESS;
+ case ContentSettingsType::BRAVE_OPEN_AI_CHAT:
+ return PermissionType::BRAVE_OPEN_AI_CHAT;
default:
return PermissionUtil_ChromiumImpl::ContentSettingTypeToPermissionType(
permission);
@@ -133,8 +145,9 @@ GURL PermissionUtil::GetCanonicalOrigin(ContentSettingsType permission,
const GURL& embedding_origin) {
// Use requesting_origin which will have ethereum or solana address info.
if (permission == ContentSettingsType::BRAVE_ETHEREUM ||
- permission == ContentSettingsType::BRAVE_SOLANA)
+ permission == ContentSettingsType::BRAVE_SOLANA) {
return requesting_origin;
+ }
return PermissionUtil_ChromiumImpl::GetCanonicalOrigin(
permission, requesting_origin, embedding_origin);
diff --git a/chromium_src/components/permissions/request_type.cc b/chromium_src/components/permissions/request_type.cc
index 53371229d804..13843d09a0f7 100644
--- a/chromium_src/components/permissions/request_type.cc
+++ b/chromium_src/components/permissions/request_type.cc
@@ -3,10 +3,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "components/permissions/request_type.h"
+
#include
#include "build/build_config.h"
-#include "components/permissions/request_type.h"
#if BUILDFLAG(IS_ANDROID)
#include "components/resources/android/theme_resources.h"
@@ -31,6 +32,7 @@ constexpr auto kAndroidStorageAccess = IDR_ANDROID_STORAGE_ACCESS;
case RequestType::kBraveSolana: \
case RequestType::kBraveGoogleSignInPermission: \
case RequestType::kBraveLocalhostAccessPermission: \
+ case RequestType::kBraveOpenAIChat: \
return IDR_ANDROID_INFOBAR_PERMISSION_COOKIE
// Add Brave cases into GetIconIdDesktop.
@@ -41,6 +43,7 @@ constexpr auto kAndroidStorageAccess = IDR_ANDROID_STORAGE_ACCESS;
case RequestType::kBraveSolana: \
case RequestType::kBraveGoogleSignInPermission: \
case RequestType::kBraveLocalhostAccessPermission: \
+ case RequestType::kBraveOpenAIChat: \
return vector_icons::kExtensionIcon
#define BRAVE_PERMISSION_KEY_FOR_REQUEST_TYPE \
@@ -53,7 +56,9 @@ constexpr auto kAndroidStorageAccess = IDR_ANDROID_STORAGE_ACCESS;
case permissions::RequestType::kBraveGoogleSignInPermission: \
return "brave_google_sign_in"; \
case permissions::RequestType::kBraveLocalhostAccessPermission: \
- return "brave_localhost_access";
+ return "brave_localhost_access"; \
+ case permissions::RequestType::kBraveOpenAIChat: \
+ return "brave_ai_chat";
#define ContentSettingsTypeToRequestType \
ContentSettingsTypeToRequestType_ChromiumImpl
@@ -85,6 +90,8 @@ RequestType ContentSettingsTypeToRequestType(
return RequestType::kBraveGoogleSignInPermission;
case ContentSettingsType::BRAVE_LOCALHOST_ACCESS:
return RequestType::kBraveLocalhostAccessPermission;
+ case ContentSettingsType::BRAVE_OPEN_AI_CHAT:
+ return RequestType::kBraveOpenAIChat;
case ContentSettingsType::DEFAULT:
// Currently we have only one DEFAULT type that is
// not mapped, which is Widevine, it's used for
@@ -107,6 +114,8 @@ std::optional RequestTypeToContentSettingsType(
return ContentSettingsType::BRAVE_ETHEREUM;
case RequestType::kBraveSolana:
return ContentSettingsType::BRAVE_SOLANA;
+ case RequestType::kBraveOpenAIChat:
+ return ContentSettingsType::BRAVE_OPEN_AI_CHAT;
default:
return RequestTypeToContentSettingsType_ChromiumImpl(request_type);
}
@@ -118,6 +127,7 @@ bool IsRequestablePermissionType(ContentSettingsType content_settings_type) {
case ContentSettingsType::BRAVE_LOCALHOST_ACCESS:
case ContentSettingsType::BRAVE_ETHEREUM:
case ContentSettingsType::BRAVE_SOLANA:
+ case ContentSettingsType::BRAVE_OPEN_AI_CHAT:
return true;
default:
return IsRequestablePermissionType_ChromiumImpl(content_settings_type);
diff --git a/chromium_src/components/permissions/request_type.h b/chromium_src/components/permissions/request_type.h
index db0c865a41b0..0d3cfd9bf018 100644
--- a/chromium_src/components/permissions/request_type.h
+++ b/chromium_src/components/permissions/request_type.h
@@ -6,9 +6,11 @@
#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_PERMISSIONS_REQUEST_TYPE_H_
#define BRAVE_CHROMIUM_SRC_COMPONENTS_PERMISSIONS_REQUEST_TYPE_H_
-#define kStorageAccess \
- kStorageAccess, kWidevine, kBraveEthereum, kBraveSolana, \
- kBraveGoogleSignInPermission, kBraveLocalhostAccessPermission
+#define kStorageAccess \
+ kStorageAccess, kWidevine, kBraveEthereum, kBraveSolana, kBraveOpenAIChat, \
+ kBraveGoogleSignInPermission, kBraveLocalhostAccessPermission, \
+ kBraveMinValue = kWidevine, \
+ kBraveMaxValue = kBraveLocalhostAccessPermission
#define ContentSettingsTypeToRequestType \
ContentSettingsTypeToRequestType_ChromiumImpl
diff --git a/chromium_src/content/browser/permissions/permission_controller_impl.cc b/chromium_src/content/browser/permissions/permission_controller_impl.cc
index d3b970d12e8b..c446b7c54a90 100644
--- a/chromium_src/content/browser/permissions/permission_controller_impl.cc
+++ b/chromium_src/content/browser/permissions/permission_controller_impl.cc
@@ -4,6 +4,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "content/browser/permissions/permission_controller_impl.h"
+
#include "content/browser/permissions/permission_util.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
@@ -21,6 +22,7 @@
case PermissionType::BRAVE_SOLANA: \
case PermissionType::BRAVE_GOOGLE_SIGN_IN: \
case PermissionType::BRAVE_LOCALHOST_ACCESS: \
+ case PermissionType::BRAVE_OPEN_AI_CHAT: \
case PermissionType::NUM
#include "src/content/browser/permissions/permission_controller_impl.cc"
diff --git a/chromium_src/content/shell/browser/shell_permission_manager.cc b/chromium_src/content/shell/browser/shell_permission_manager.cc
index 020fe013b9e6..0942ae8bda57 100644
--- a/chromium_src/content/shell/browser/shell_permission_manager.cc
+++ b/chromium_src/content/shell/browser/shell_permission_manager.cc
@@ -20,6 +20,7 @@
case PermissionType::BRAVE_SOLANA: \
case PermissionType::BRAVE_GOOGLE_SIGN_IN: \
case PermissionType::BRAVE_LOCALHOST_ACCESS: \
+ case PermissionType::BRAVE_OPEN_AI_CHAT: \
case PermissionType::NUM
#include "src/content/shell/browser/shell_permission_manager.cc"
diff --git a/chromium_src/third_party/blink/common/permissions/permission_utils.cc b/chromium_src/third_party/blink/common/permissions/permission_utils.cc
index f5baf50f24a3..049063bc445f 100644
--- a/chromium_src/third_party/blink/common/permissions/permission_utils.cc
+++ b/chromium_src/third_party/blink/common/permissions/permission_utils.cc
@@ -30,6 +30,8 @@
return "BraveGoogleSignInPermission"; \
case PermissionType::BRAVE_LOCALHOST_ACCESS: \
return "BraveLocalhostAccessPermission"; \
+ case PermissionType::BRAVE_OPEN_AI_CHAT: \
+ return "BraveOpenAIChatPermission"; \
case PermissionType::BRAVE_ETHEREUM: \
return "BraveEthereum"; \
case PermissionType::BRAVE_SOLANA: \
@@ -52,6 +54,7 @@
case PermissionType::BRAVE_SPEEDREADER: \
case PermissionType::BRAVE_GOOGLE_SIGN_IN: \
case PermissionType::BRAVE_LOCALHOST_ACCESS: \
+ case PermissionType::BRAVE_OPEN_AI_CHAT: \
return std::nullopt
#include "src/third_party/blink/common/permissions/permission_utils.cc"
diff --git a/chromium_src/third_party/blink/public/common/permissions/permission_utils.h b/chromium_src/third_party/blink/public/common/permissions/permission_utils.h
index 5e19636be6f2..350d2342197b 100644
--- a/chromium_src/third_party/blink/public/common/permissions/permission_utils.h
+++ b/chromium_src/third_party/blink/public/common/permissions/permission_utils.h
@@ -20,7 +20,8 @@
BRAVE_ETHEREUM, \
BRAVE_SOLANA, \
BRAVE_GOOGLE_SIGN_IN, \
- BRAVE_LOCALHOST_ACCESS, \
+ BRAVE_LOCALHOST_ACCESS, \
+ BRAVE_OPEN_AI_CHAT, \
NUM
// clang-format on
diff --git a/components/ai_chat/content/browser/BUILD.gn b/components/ai_chat/content/browser/BUILD.gn
index 1e3a609d705f..5f771501d52c 100644
--- a/components/ai_chat/content/browser/BUILD.gn
+++ b/components/ai_chat/content/browser/BUILD.gn
@@ -10,6 +10,8 @@ assert(enable_ai_chat)
static_library("browser") {
sources = [
+ "ai_chat_brave_search_throttle.cc",
+ "ai_chat_brave_search_throttle.h",
"ai_chat_tab_helper.cc",
"ai_chat_tab_helper.h",
"ai_chat_throttle.cc",
diff --git a/components/ai_chat/content/browser/ai_chat_brave_search_throttle.cc b/components/ai_chat/content/browser/ai_chat_brave_search_throttle.cc
new file mode 100644
index 000000000000..921344850b89
--- /dev/null
+++ b/components/ai_chat/content/browser/ai_chat_brave_search_throttle.cc
@@ -0,0 +1,147 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "brave/components/ai_chat/content/browser/ai_chat_brave_search_throttle.h"
+
+#include
+#include
+
+#include "base/check.h"
+#include "base/functional/bind.h"
+#include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h"
+#include "brave/components/ai_chat/content/browser/page_content_fetcher.h"
+#include "brave/components/ai_chat/core/browser/conversation_handler.h"
+#include "brave/components/ai_chat/core/browser/utils.h"
+#include "brave/components/ai_chat/core/common/features.h"
+#include "brave/components/ai_chat/core/common/utils.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/permission_controller.h"
+#include "content/public/browser/permission_request_description.h"
+#include "content/public/browser/web_contents.h"
+
+namespace ai_chat {
+
+// static
+std::unique_ptr
+AIChatBraveSearchThrottle::MaybeCreateThrottleFor(
+ base::OnceCallback open_leo_delegate,
+ content::NavigationHandle* navigation_handle,
+ AIChatService* ai_chat_service,
+ PrefService* pref_service) {
+ auto* web_contents = navigation_handle->GetWebContents();
+ if (!web_contents) {
+ return nullptr;
+ }
+
+ if (!open_leo_delegate || !ai_chat_service ||
+ !IsAIChatEnabled(pref_service) ||
+ !features::IsOpenAIChatFromBraveSearchEnabled() ||
+ !IsOpenAIChatButtonFromBraveSearchURL(navigation_handle->GetURL())) {
+ return nullptr;
+ }
+
+ return std::make_unique(
+ std::move(open_leo_delegate), navigation_handle, ai_chat_service);
+}
+
+AIChatBraveSearchThrottle::AIChatBraveSearchThrottle(
+ base::OnceCallback open_leo_delegate,
+ content::NavigationHandle* handle,
+ AIChatService* ai_chat_service)
+ : content::NavigationThrottle(handle),
+ open_ai_chat_delegate_(std::move(open_leo_delegate)),
+ ai_chat_service_(ai_chat_service) {
+ CHECK(open_ai_chat_delegate_);
+ CHECK(ai_chat_service_);
+}
+
+AIChatBraveSearchThrottle::~AIChatBraveSearchThrottle() = default;
+
+AIChatBraveSearchThrottle::ThrottleCheckResult
+AIChatBraveSearchThrottle::WillStartRequest() {
+ content::WebContents* web_contents = navigation_handle()->GetWebContents();
+ if (!web_contents || !navigation_handle()->IsInPrimaryMainFrame() ||
+ !IsOpenAIChatButtonFromBraveSearchURL(navigation_handle()->GetURL()) ||
+ !IsBraveSearchURL(web_contents->GetLastCommittedURL())) {
+ // Uninterested navigation for this throttle.
+ return content::NavigationThrottle::PROCEED;
+ }
+
+ // Check if nonce in HTML tag matches the one in the URL.
+ AIChatTabHelper::FromWebContents(web_contents)
+ ->GetOpenAIChatButtonNonce(
+ base::BindOnce(&AIChatBraveSearchThrottle::OnGetOpenAIChatButtonNonce,
+ weak_factory_.GetWeakPtr()));
+ return content::NavigationThrottle::DEFER;
+}
+
+void AIChatBraveSearchThrottle::OpenAIChatWithStagedEntries() {
+ content::WebContents* web_contents = navigation_handle()->GetWebContents();
+ if (!web_contents) {
+ return;
+ }
+
+ ai_chat_service_->OpenConversationWithStagedEntries(
+ AIChatTabHelper::FromWebContents(web_contents)->GetWeakPtr(),
+ base::BindOnce(&AIChatBraveSearchThrottle::OnOpenAIChat,
+ weak_factory_.GetWeakPtr()));
+}
+
+void AIChatBraveSearchThrottle::OnOpenAIChat() {
+ std::move(open_ai_chat_delegate_).Run(navigation_handle()->GetWebContents());
+}
+
+void AIChatBraveSearchThrottle::OnGetOpenAIChatButtonNonce(
+ const std::optional& nonce) {
+ if (!nonce || nonce->empty() ||
+ *nonce != navigation_handle()->GetURL().ref()) {
+ CancelDeferredNavigation(content::NavigationThrottle::CANCEL);
+ return;
+ }
+
+ // Check if the user has granted permission to open AI Chat.
+ content::WebContents* web_contents = navigation_handle()->GetWebContents();
+ if (!web_contents) {
+ CancelDeferredNavigation(content::NavigationThrottle::CANCEL);
+ return;
+ }
+
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ content::PermissionController* permission_controller =
+ web_contents->GetBrowserContext()->GetPermissionController();
+ content::PermissionResult permission_status =
+ permission_controller->GetPermissionResultForCurrentDocument(
+ blink::PermissionType::BRAVE_OPEN_AI_CHAT, rfh);
+
+ if (permission_status.status == content::PermissionStatus::DENIED) {
+ CancelDeferredNavigation(content::NavigationThrottle::CANCEL);
+ } else if (permission_status.status == content::PermissionStatus::GRANTED) {
+ OpenAIChatWithStagedEntries();
+ CancelDeferredNavigation(content::NavigationThrottle::CANCEL);
+ } else { // ask
+ permission_controller->RequestPermissionFromCurrentDocument(
+ rfh,
+ content::PermissionRequestDescription(
+ blink::PermissionType::BRAVE_OPEN_AI_CHAT, /*user_gesture=*/true),
+ base::BindOnce(&AIChatBraveSearchThrottle::OnPermissionPromptResult,
+ weak_factory_.GetWeakPtr()));
+ }
+}
+
+void AIChatBraveSearchThrottle::OnPermissionPromptResult(
+ content::PermissionStatus status) {
+ if (status == content::PermissionStatus::GRANTED) {
+ OpenAIChatWithStagedEntries();
+ }
+
+ CancelDeferredNavigation(content::NavigationThrottle::CANCEL);
+}
+
+const char* AIChatBraveSearchThrottle::GetNameForLogging() {
+ return "AIChatBraveSearchThrottle";
+}
+
+} // namespace ai_chat
diff --git a/components/ai_chat/content/browser/ai_chat_brave_search_throttle.h b/components/ai_chat/content/browser/ai_chat_brave_search_throttle.h
new file mode 100644
index 000000000000..d9183e2d5aac
--- /dev/null
+++ b/components/ai_chat/content/browser/ai_chat_brave_search_throttle.h
@@ -0,0 +1,71 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef BRAVE_COMPONENTS_AI_CHAT_CONTENT_BROWSER_AI_CHAT_BRAVE_SEARCH_THROTTLE_H_
+#define BRAVE_COMPONENTS_AI_CHAT_CONTENT_BROWSER_AI_CHAT_BRAVE_SEARCH_THROTTLE_H_
+
+#include
+#include
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "brave/components/ai_chat/core/browser/ai_chat_service.h"
+#include "content/public/browser/navigation_throttle.h"
+#include "content/public/browser/permission_result.h"
+
+namespace content {
+class WebContents;
+}
+
+class PrefService;
+
+namespace ai_chat {
+
+// A network throttle which intercepts Brave Search requests.
+// Currently the only use case is to intercept requests to open Leo AI chat, so
+// it is only created when navigating to open Leo button URL from Brave Search.
+// It could be extended to other Brave Search URLs in the future.
+//
+// For Open Leo feature, we check:
+// 1) If AI chat is enabled.
+// 2) If the request is from Brave Search and is trying to navigate to open Leo
+// button URL.
+// 3) If the nonce property in the a tag element is equal to the one in url ref.
+// 4) If the user has granted permission to open Leo.
+// The navigation to the specific Open Leo URL will be cancelled, and Leo AI
+// chat will be opened only if all the above conditions are met.
+class AIChatBraveSearchThrottle : public content::NavigationThrottle {
+ public:
+ AIChatBraveSearchThrottle(
+ base::OnceCallback open_leo_delegate,
+ content::NavigationHandle* handle,
+ AIChatService* ai_chat_service);
+ ~AIChatBraveSearchThrottle() override;
+
+ static std::unique_ptr MaybeCreateThrottleFor(
+ base::OnceCallback open_leo_delegate,
+ content::NavigationHandle* navigation_handle,
+ AIChatService* ai_chat_service,
+ PrefService* pref_service);
+
+ ThrottleCheckResult WillStartRequest() override;
+ const char* GetNameForLogging() override;
+
+ private:
+ void OnGetOpenAIChatButtonNonce(const std::optional& nonce);
+ void OnPermissionPromptResult(blink::mojom::PermissionStatus status);
+ void OnOpenAIChat();
+
+ void OpenAIChatWithStagedEntries();
+
+ base::OnceCallback open_ai_chat_delegate_;
+ const raw_ptr ai_chat_service_ = nullptr;
+
+ base::WeakPtrFactory weak_factory_{this};
+};
+
+} // namespace ai_chat
+
+#endif // BRAVE_COMPONENTS_AI_CHAT_CONTENT_BROWSER_AI_CHAT_BRAVE_SEARCH_THROTTLE_H_
diff --git a/components/ai_chat/content/browser/ai_chat_tab_helper.cc b/components/ai_chat/content/browser/ai_chat_tab_helper.cc
index 46b728ea0ea4..bcead876887e 100644
--- a/components/ai_chat/content/browser/ai_chat_tab_helper.cc
+++ b/components/ai_chat/content/browser/ai_chat_tab_helper.cc
@@ -27,6 +27,9 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/permission_controller.h"
+#include "content/public/browser/permission_request_description.h"
+#include "content/public/browser/permission_result.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
@@ -414,6 +417,21 @@ void AIChatTabHelper::GetSearchSummarizerKey(
page_content_fetcher_delegate_->GetSearchSummarizerKey(std::move(callback));
}
+void AIChatTabHelper::GetOpenAIChatButtonNonce(
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback callback) {
+ page_content_fetcher_delegate_->GetOpenAIChatButtonNonce(std::move(callback));
+}
+
+bool AIChatTabHelper::HasOpenAIChatPermission() const {
+ content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame();
+ content::PermissionController* permission_controller =
+ web_contents()->GetBrowserContext()->GetPermissionController();
+ content::PermissionResult permission_status =
+ permission_controller->GetPermissionResultForCurrentDocument(
+ blink::PermissionType::BRAVE_OPEN_AI_CHAT, rfh);
+ return permission_status.status == content::PermissionStatus::GRANTED;
+}
+
WEB_CONTENTS_USER_DATA_KEY_IMPL(AIChatTabHelper);
} // namespace ai_chat
diff --git a/components/ai_chat/content/browser/ai_chat_tab_helper.h b/components/ai_chat/content/browser/ai_chat_tab_helper.h
index b671d90bed28..1c6d9d532a5f 100644
--- a/components/ai_chat/content/browser/ai_chat_tab_helper.h
+++ b/components/ai_chat/content/browser/ai_chat_tab_helper.h
@@ -7,8 +7,8 @@
#define BRAVE_COMPONENTS_AI_CHAT_CONTENT_BROWSER_AI_CHAT_TAB_HELPER_H_
#include
-#include
#include
+#include
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
@@ -74,6 +74,12 @@ class AIChatTabHelper : public content::WebContentsObserver,
// Attempts to find a search summarizer key for the page.
virtual void GetSearchSummarizerKey(
GetSearchSummarizerKeyCallback callback) = 0;
+
+ // Fetches the nonce for the OpenLeo button from the page HTML and validate
+ // if it matches the href URL and the passed in nonce.
+ virtual void GetOpenAIChatButtonNonce(
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback
+ callback) = 0;
};
AIChatTabHelper(const AIChatTabHelper&) = delete;
@@ -97,6 +103,9 @@ class AIChatTabHelper : public content::WebContentsObserver,
// mojom::PageContentExtractorHost
void OnInterceptedPageContentChanged() override;
+ void GetOpenAIChatButtonNonce(
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback callback);
+
private:
friend class content::WebContentsUserData;
friend class ::AIChatUIBrowserTest;
@@ -157,6 +166,8 @@ class AIChatTabHelper : public content::WebContentsObserver,
void GetSearchSummarizerKey(GetSearchSummarizerKeyCallback callback) override;
+ bool HasOpenAIChatPermission() const override;
+
void OnFetchPageContentComplete(GetPageContentCallback callback,
std::string content,
bool is_video,
diff --git a/components/ai_chat/content/browser/page_content_fetcher.cc b/components/ai_chat/content/browser/page_content_fetcher.cc
index 9a29de3f306a..7ceb61f9a8c0 100644
--- a/components/ai_chat/content/browser/page_content_fetcher.cc
+++ b/components/ai_chat/content/browser/page_content_fetcher.cc
@@ -135,9 +135,20 @@ class PageContentFetcherInternal {
content_extractor_->GetSearchSummarizerKey(std::move(callback));
}
- void StartGithub(
- GURL patch_url,
- FetchPageContentCallback callback) {
+ void GetOpenAIChatButtonNonce(
+ mojo::Remote content_extractor,
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback callback) {
+ content_extractor_ = std::move(content_extractor);
+ if (!content_extractor_) {
+ DeleteSelf();
+ return;
+ }
+ content_extractor_.set_disconnect_handler(base::BindOnce(
+ &PageContentFetcherInternal::DeleteSelf, base::Unretained(this)));
+ content_extractor_->GetOpenAIChatButtonNonce(std::move(callback));
+ }
+
+ void StartGithub(GURL patch_url, FetchPageContentCallback callback) {
auto request = std::make_unique();
request->url = patch_url;
request->load_flags = net::LOAD_DO_NOT_SAVE_COOKIES;
@@ -460,4 +471,16 @@ void PageContentFetcher::GetSearchSummarizerKey(
fetcher->GetSearchSummarizerKey(std::move(extractor), std::move(callback));
}
+void PageContentFetcher::GetOpenAIChatButtonNonce(
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback callback) {
+ auto* primary_rfh = web_contents_->GetPrimaryMainFrame();
+ DCHECK(primary_rfh->IsRenderFrameLive());
+
+ auto* fetcher = new PageContentFetcherInternal(nullptr);
+ mojo::Remote extractor;
+ primary_rfh->GetRemoteInterfaces()->GetInterface(
+ extractor.BindNewPipeAndPassReceiver());
+ fetcher->GetOpenAIChatButtonNonce(std::move(extractor), std::move(callback));
+}
+
} // namespace ai_chat
diff --git a/components/ai_chat/content/browser/page_content_fetcher.h b/components/ai_chat/content/browser/page_content_fetcher.h
index 18712ff096b7..1825a328e5d8 100644
--- a/components/ai_chat/content/browser/page_content_fetcher.h
+++ b/components/ai_chat/content/browser/page_content_fetcher.h
@@ -39,6 +39,10 @@ class PageContentFetcher : public AIChatTabHelper::PageContentFetcherDelegate {
mojom::PageContentExtractor::GetSearchSummarizerKeyCallback callback)
override;
+ void GetOpenAIChatButtonNonce(
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback callback)
+ override;
+
void SetURLLoaderFactoryForTesting(
scoped_refptr url_loader_factory) {
url_loader_factory_ = url_loader_factory;
diff --git a/components/ai_chat/core/browser/ai_chat_service.cc b/components/ai_chat/core/browser/ai_chat_service.cc
index 80d59baddf71..f19954747b27 100644
--- a/components/ai_chat/core/browser/ai_chat_service.cc
+++ b/components/ai_chat/core/browser/ai_chat_service.cc
@@ -414,4 +414,21 @@ void AIChatService::OnConversationListChanged() {
}
}
+void AIChatService::OpenConversationWithStagedEntries(
+ base::WeakPtr
+ associated_content,
+ base::OnceClosure open_ai_chat) {
+ if (!associated_content || !associated_content->HasOpenAIChatPermission()) {
+ return;
+ }
+
+ ConversationHandler* conversation = GetOrCreateConversationHandlerForContent(
+ associated_content->GetContentId(), associated_content);
+ CHECK(conversation);
+
+ // Open AI Chat and trigger a fetch of staged conversations from Brave Search.
+ std::move(open_ai_chat).Run();
+ conversation->MaybeFetchOrClearContentStagedConversation();
+}
+
} // namespace ai_chat
diff --git a/components/ai_chat/core/browser/ai_chat_service.h b/components/ai_chat/core/browser/ai_chat_service.h
index fb4d16e80d31..728b3e2e7439 100644
--- a/components/ai_chat/core/browser/ai_chat_service.h
+++ b/components/ai_chat/core/browser/ai_chat_service.h
@@ -87,6 +87,11 @@ class AIChatService : public KeyedService,
base::WeakPtr
associated_content);
+ void OpenConversationWithStagedEntries(
+ base::WeakPtr
+ associated_content,
+ base::OnceClosure open_ai_chat);
+
// mojom::Service
void MarkAgreementAccepted() override;
void GetVisibleConversations(
diff --git a/components/ai_chat/core/browser/ai_chat_service_unittest.cc b/components/ai_chat/core/browser/ai_chat_service_unittest.cc
index 884c470db48c..a67e287fbfac 100644
--- a/components/ai_chat/core/browser/ai_chat_service_unittest.cc
+++ b/components/ai_chat/core/browser/ai_chat_service_unittest.cc
@@ -201,6 +201,7 @@ class MockAssociatedContent
GetStagedEntriesFromContent,
(ConversationHandler::GetStagedEntriesCallback),
(override));
+ MOCK_METHOD(bool, HasOpenAIChatPermission, (), (const, override));
base::WeakPtr GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
@@ -495,4 +496,61 @@ TEST_P(AIChatServiceUnitTest,
run_loop.Run();
}
+TEST_P(AIChatServiceUnitTest, OpenConversationWithStagedEntries_NoPermission) {
+ NiceMock associated_content{};
+ ConversationHandler* conversation =
+ ai_chat_service_->CreateConversationHandlerForContent(
+ associated_content.GetContentId(), associated_content.GetWeakPtr());
+ auto conversation_client = CreateConversationClient(conversation);
+
+ ON_CALL(associated_content, HasOpenAIChatPermission)
+ .WillByDefault(testing::Return(false));
+ EXPECT_CALL(associated_content, GetStagedEntriesFromContent).Times(0);
+
+ bool opened = false;
+ ai_chat_service_->OpenConversationWithStagedEntries(
+ associated_content.GetWeakPtr(),
+ base::BindLambdaForTesting([&]() { opened = true; }));
+ EXPECT_FALSE(opened);
+ testing::Mock::VerifyAndClearExpectations(&associated_content);
+}
+
+TEST_P(AIChatServiceUnitTest, OpenConversationWithStagedEntries) {
+ NiceMock associated_content{};
+ ConversationHandler* conversation =
+ ai_chat_service_->CreateConversationHandlerForContent(
+ associated_content.GetContentId(), associated_content.GetWeakPtr());
+ auto conversation_client = CreateConversationClient(conversation);
+
+ ON_CALL(associated_content, GetStagedEntriesFromContent)
+ .WillByDefault(
+ [](ConversationHandler::GetStagedEntriesCallback callback) {
+ std::move(callback).Run(std::vector{
+ SearchQuerySummary("query", "summary")});
+ });
+ ON_CALL(associated_content, HasOpenAIChatPermission)
+ .WillByDefault(testing::Return(true));
+
+ // Allowed scheme to be associated with a conversation
+ ON_CALL(associated_content, GetURL())
+ .WillByDefault(testing::Return(GURL("https://example.com")));
+
+ // One from setting up a connected client, one from
+ // OpenConversationWithStagedEntries.
+ EXPECT_CALL(associated_content, GetStagedEntriesFromContent).Times(2);
+
+ bool opened = false;
+ ai_chat_service_->OpenConversationWithStagedEntries(
+ associated_content.GetWeakPtr(),
+ base::BindLambdaForTesting([&]() { opened = true; }));
+
+ base::RunLoop().RunUntilIdle();
+ auto& history = conversation->GetConversationHistory();
+ ASSERT_EQ(history.size(), 2u);
+ EXPECT_EQ(history[0]->text, "query");
+ EXPECT_EQ(history[1]->text, "summary");
+ EXPECT_TRUE(opened);
+ testing::Mock::VerifyAndClearExpectations(&associated_content);
+}
+
} // namespace ai_chat
diff --git a/components/ai_chat/core/browser/associated_content_driver.cc b/components/ai_chat/core/browser/associated_content_driver.cc
index a63dc2799733..5dbe90b46f67 100644
--- a/components/ai_chat/core/browser/associated_content_driver.cc
+++ b/components/ai_chat/core/browser/associated_content_driver.cc
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
@@ -19,9 +20,9 @@
#include "base/strings/string_util.h"
#include "brave/brave_domains/service_domains.h"
#include "brave/components/ai_chat/core/browser/brave_search_responses.h"
-#include "brave/components/ai_chat/core/browser/constants.h"
#include "brave/components/ai_chat/core/browser/conversation_handler.h"
#include "brave/components/ai_chat/core/browser/utils.h"
+#include "brave/components/ai_chat/core/common/constants.h"
#include "net/base/url_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
diff --git a/components/ai_chat/core/browser/constants.h b/components/ai_chat/core/browser/constants.h
index 6eaab67f9f30..25ce8bca9952 100644
--- a/components/ai_chat/core/browser/constants.h
+++ b/components/ai_chat/core/browser/constants.h
@@ -36,8 +36,6 @@ constexpr float kMaxContentLengthThreshold = 0.6f;
constexpr size_t kReservedTokensForPrompt = 300;
constexpr size_t kReservedTokensForMaxNewTokens = 400;
-inline constexpr char kBraveSearchURLPrefix[] = "search";
-
} // namespace ai_chat
#endif // BRAVE_COMPONENTS_AI_CHAT_CORE_BROWSER_CONSTANTS_H_
diff --git a/components/ai_chat/core/browser/conversation_handler.cc b/components/ai_chat/core/browser/conversation_handler.cc
index 7362cabc90a0..744a10afd914 100644
--- a/components/ai_chat/core/browser/conversation_handler.cc
+++ b/components/ai_chat/core/browser/conversation_handler.cc
@@ -63,6 +63,10 @@ void AssociatedContentDelegate::GetStagedEntriesFromContent(
std::move(callback).Run(std::nullopt);
}
+bool AssociatedContentDelegate::HasOpenAIChatPermission() const {
+ return false;
+}
+
void AssociatedContentDelegate::GetTopSimilarityWithPromptTilContextLimit(
const std::string& prompt,
const std::string& text,
@@ -1048,23 +1052,16 @@ void ConversationHandler::MaybeFetchOrClearContentStagedConversation() {
if (!can_check_for_staged_conversation) {
// Clear any staged conversation entries since user might have unassociated
// content with this conversation
- if (chat_history_.empty()) {
- return;
- }
-
- const auto& last_turn = chat_history_.back();
- if (last_turn->from_brave_search_SERP) {
- chat_history_.clear(); // Clear the staged entries.
+ size_t num_entries = chat_history_.size();
+ std::erase_if(chat_history_, [](const mojom::ConversationTurnPtr& turn) {
+ return turn->from_brave_search_SERP;
+ });
+ if (num_entries != chat_history_.size()) {
OnHistoryUpdate();
}
return;
}
- // Can only have staged entries at the start of a conversation.
- if (!chat_history_.empty()) {
- return;
- }
-
associated_content_delegate_->GetStagedEntriesFromContent(
base::BindOnce(&ConversationHandler::OnGetStagedEntriesFromContent,
weak_ptr_factory_.GetWeakPtr()));
@@ -1073,11 +1070,16 @@ void ConversationHandler::MaybeFetchOrClearContentStagedConversation() {
void ConversationHandler::OnGetStagedEntriesFromContent(
const std::optional>& entries) {
// Check if all requirements are still met.
- if (!entries || !chat_history_.empty() || !IsContentAssociationPossible() ||
+ if (is_request_in_progress_ || !entries || !IsContentAssociationPossible() ||
!should_send_page_contents_ || !ai_chat_service_->HasUserOptedIn()) {
return;
}
+ // Clear previous staged entries.
+ std::erase_if(chat_history_, [](const mojom::ConversationTurnPtr& turn) {
+ return turn->from_brave_search_SERP;
+ });
+
// Add the query & summary pairs to the conversation history and call
// OnHistoryUpdate to update UI.
for (const auto& entry : *entries) {
diff --git a/components/ai_chat/core/browser/conversation_handler.h b/components/ai_chat/core/browser/conversation_handler.h
index e5e7f4fa70b9..b8db86b65c4f 100644
--- a/components/ai_chat/core/browser/conversation_handler.h
+++ b/components/ai_chat/core/browser/conversation_handler.h
@@ -89,6 +89,9 @@ class ConversationHandler : public mojom::ConversationHandler,
// use it to fetch search query and summary from Brave search chatllm
// endpoint.
virtual void GetStagedEntriesFromContent(GetStagedEntriesCallback callback);
+ // Signifies whether the content has permission to open a conversation's UI
+ // within the browser.
+ virtual bool HasOpenAIChatPermission() const;
void GetTopSimilarityWithPromptTilContextLimit(
const std::string& prompt,
@@ -223,6 +226,10 @@ class ConversationHandler : public mojom::ConversationHandler,
void OnFaviconImageDataChanged();
void OnUserOptedIn();
+ // Some associated content may provide some conversation that the user wants
+ // to continue, e.g. Brave Search.
+ void MaybeFetchOrClearContentStagedConversation();
+
base::WeakPtr GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
@@ -244,6 +251,10 @@ class ConversationHandler : public mojom::ConversationHandler,
return associated_content_delegate_.get();
}
+ void SetRequestInProgressForTesting(bool in_progress) {
+ is_request_in_progress_ = in_progress;
+ }
+
protected:
// ModelService::Observer
void OnModelListUpdated() override;
@@ -261,6 +272,10 @@ class ConversationHandler : public mojom::ConversationHandler,
UpdateOrCreateLastAssistantEntry_NotDelta);
FRIEND_TEST_ALL_PREFIXES(ConversationHandlerUnitTest,
UpdateOrCreateLastAssistantEntry_NotDeltaWithSearch);
+ FRIEND_TEST_ALL_PREFIXES(ConversationHandlerUnitTest,
+ OnGetStagedEntriesFromContent);
+ FRIEND_TEST_ALL_PREFIXES(ConversationHandlerUnitTest,
+ OnGetStagedEntriesFromContent_FailedChecks);
FRIEND_TEST_ALL_PREFIXES(ConversationHandlerUnitTest, SelectedLanguage);
FRIEND_TEST_ALL_PREFIXES(PageContentRefineTest, LocalModelsUpdater);
FRIEND_TEST_ALL_PREFIXES(PageContentRefineTest, TextEmbedder);
@@ -285,9 +300,6 @@ class ConversationHandler : public mojom::ConversationHandler,
bool is_video,
std::string invalidation_token);
- // Some associated content may provide some conversation that the user wants
- // to continue, e.g. Brave Search.
- void MaybeFetchOrClearContentStagedConversation();
void OnGetStagedEntriesFromContent(
const std::optional>& entries);
diff --git a/components/ai_chat/core/browser/conversation_handler_unittest.cc b/components/ai_chat/core/browser/conversation_handler_unittest.cc
index 20377b76b5c2..975b1bb580d7 100644
--- a/components/ai_chat/core/browser/conversation_handler_unittest.cc
+++ b/components/ai_chat/core/browser/conversation_handler_unittest.cc
@@ -263,6 +263,32 @@ class ConversationHandlerUnitTest : public testing::Test {
});
}
+ // Pair of text and whether it's from Brave Search SERP
+ void SetupHistory(std::vector> entries) {
+ std::vector history;
+ for (size_t i = 0; i < entries.size(); i++) {
+ bool is_human = i % 2 == 0;
+
+ std::optional> events;
+ if (!is_human) {
+ events = std::vector();
+ events->push_back(mojom::ConversationEntryEvent::NewCompletionEvent(
+ mojom::CompletionEvent::New(entries[i].first)));
+ }
+
+ auto entry = mojom::ConversationTurn::New(
+ is_human ? mojom::CharacterType::HUMAN
+ : mojom::CharacterType::ASSISTANT,
+ is_human ? mojom::ActionType::QUERY : mojom::ActionType::RESPONSE,
+ mojom::ConversationTurnVisibility::VISIBLE,
+ entries[i].first /* text */, std::nullopt /* selected_text */,
+ std::move(events), base::Time::Now(), std::nullopt /* edits */,
+ entries[i].second /* from_brave_search_SERP */);
+ history.push_back(std::move(entry));
+ }
+ conversation_handler_->SetChatHistoryForTesting(std::move(history));
+ }
+
protected:
base::test::TaskEnvironment task_environment_;
std::unique_ptr ai_chat_service_;
@@ -1007,6 +1033,108 @@ TEST_F(ConversationHandlerUnitTest,
EXPECT_TRUE(conversation_handler_->GetConversationHistory().empty());
}
+TEST_F(
+ ConversationHandlerUnitTest,
+ MaybeFetchOrClearContentStagedConversation_FetchStagedEntriesWithHistory) {
+ NiceMock client(conversation_handler_.get());
+ ASSERT_TRUE(conversation_handler_->IsAnyClientConnected());
+
+ // MaybeFetchOrClearContentStagedConversation should clear old staged entries
+ // and fetch new ones.
+ EXPECT_CALL(*associated_content_, GetStagedEntriesFromContent).Times(1);
+ // One from SetupHistory and one from removing old entries and adding
+ // new entries in OnGetStagedEntriesFromContent.
+ EXPECT_CALL(client, OnConversationHistoryUpdate()).Times(2);
+
+ // Fill history with staged and non-staged entries.
+ SetupHistory({{"old query" /* text */, true /*from_brave_search_SERP */},
+ {"old summary", "true"},
+ {"normal query", false},
+ {"normal response", false}});
+
+ // Setting mock return values for GetStagedEntriesFromContent.
+ SetAssociatedContentStagedEntries(/*empty=*/false, /*multi=*/true);
+
+ conversation_handler_->MaybeFetchOrClearContentStagedConversation();
+ task_environment_.RunUntilIdle();
+
+ testing::Mock::VerifyAndClearExpectations(associated_content_.get());
+ testing::Mock::VerifyAndClearExpectations(&client);
+
+ auto& history = conversation_handler_->GetConversationHistory();
+ ASSERT_EQ(history.size(), 6u);
+ EXPECT_FALSE(history[0]->from_brave_search_SERP);
+ EXPECT_EQ(history[0]->text, "normal query");
+ EXPECT_FALSE(history[1]->from_brave_search_SERP);
+ EXPECT_EQ(history[1]->text, "normal response");
+ EXPECT_TRUE(history[2]->from_brave_search_SERP);
+ EXPECT_EQ(history[2]->text, "query");
+ EXPECT_TRUE(history[3]->from_brave_search_SERP);
+ EXPECT_EQ(history[3]->text, "summary");
+ EXPECT_TRUE(history[4]->from_brave_search_SERP);
+ EXPECT_EQ(history[4]->text, "query2");
+ EXPECT_TRUE(history[5]->from_brave_search_SERP);
+ EXPECT_EQ(history[5]->text, "summary2");
+}
+
+TEST_F(ConversationHandlerUnitTest,
+ OnGetStagedEntriesFromContent_FailedChecks) {
+ // No staged entries would be added if a request is in progress.
+ conversation_handler_->SetRequestInProgressForTesting(true);
+ std::vector entries = {{"query", "summary"},
+ {"query2", "summary2"}};
+ conversation_handler_->OnGetStagedEntriesFromContent(entries);
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(conversation_handler_->GetConversationHistory().size(), 0u);
+
+ // No staged entries if should_send_page_contents_ is false.
+ conversation_handler_->SetRequestInProgressForTesting(false);
+ conversation_handler_->SetShouldSendPageContents(false);
+ conversation_handler_->OnGetStagedEntriesFromContent(entries);
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(conversation_handler_->GetConversationHistory().size(), 0u);
+
+ // No staged entries if user opt-out.
+ conversation_handler_->SetShouldSendPageContents(true);
+ EmulateUserOptedOut();
+ conversation_handler_->OnGetStagedEntriesFromContent(entries);
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(conversation_handler_->GetConversationHistory().size(), 0u);
+}
+
+TEST_F(ConversationHandlerUnitTest, OnGetStagedEntriesFromContent) {
+ NiceMock client(conversation_handler_.get());
+ ASSERT_TRUE(conversation_handler_->IsAnyClientConnected());
+
+ EXPECT_CALL(client, OnConversationHistoryUpdate()).Times(2);
+ // Fill history with staged and non-staged entries.
+ SetupHistory({{"q1" /* text */, true /*from_brave_search_SERP */},
+ {"s1", "true"},
+ {"q2", false},
+ {"r1", false}});
+
+ std::vector entries = {{"query", "summary"},
+ {"query2", "summary2"}};
+ conversation_handler_->OnGetStagedEntriesFromContent(entries);
+ task_environment_.RunUntilIdle();
+ testing::Mock::VerifyAndClearExpectations(&client);
+
+ auto& history = conversation_handler_->GetConversationHistory();
+ ASSERT_EQ(history.size(), 6u);
+ EXPECT_FALSE(history[0]->from_brave_search_SERP);
+ EXPECT_EQ(history[0]->text, "q2");
+ EXPECT_FALSE(history[1]->from_brave_search_SERP);
+ EXPECT_EQ(history[1]->text, "r1");
+ EXPECT_TRUE(history[2]->from_brave_search_SERP);
+ EXPECT_EQ(history[2]->text, "query");
+ EXPECT_TRUE(history[3]->from_brave_search_SERP);
+ EXPECT_EQ(history[3]->text, "summary");
+ EXPECT_TRUE(history[4]->from_brave_search_SERP);
+ EXPECT_EQ(history[4]->text, "query2");
+ EXPECT_TRUE(history[5]->from_brave_search_SERP);
+ EXPECT_EQ(history[5]->text, "summary2");
+}
+
TEST_F(ConversationHandlerUnitTest_OptedOut,
MaybeFetchOrClearSearchQuerySummary_NotOptedIn) {
// Content will have staged entries, but we want to make sure that
@@ -1096,11 +1224,11 @@ TEST_F(ConversationHandlerUnitTest,
task_environment_.RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(associated_content_.get());
- // Verify that no re-fetch happens when new client connects
+ // Verify that fetch happens when another client connects.
client.Disconnect();
task_environment_.RunUntilIdle();
EXPECT_FALSE(conversation_handler_->IsAnyClientConnected());
- EXPECT_CALL(*associated_content_, GetStagedEntriesFromContent).Times(0);
+ EXPECT_CALL(*associated_content_, GetStagedEntriesFromContent).Times(1);
NiceMock client2(conversation_handler_.get());
task_environment_.RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(associated_content_.get());
diff --git a/components/ai_chat/core/browser/utils.cc b/components/ai_chat/core/browser/utils.cc
index 9df25cf759b5..1b1191aa6fe8 100644
--- a/components/ai_chat/core/browser/utils.cc
+++ b/components/ai_chat/core/browser/utils.cc
@@ -5,6 +5,10 @@
#include "brave/components/ai_chat/core/browser/utils.h"
+#include
+#include
+#include
+
#include "base/containers/fixed_flat_set.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
@@ -14,6 +18,7 @@
#include "base/time/time.h"
#include "brave/brave_domains/service_domains.h"
#include "brave/components/ai_chat/core/browser/constants.h"
+#include "brave/components/ai_chat/core/common/constants.h"
#include "brave/components/ai_chat/core/common/features.h"
#include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h"
#include "brave/components/ai_chat/core/common/pref_names.h"
diff --git a/components/ai_chat/core/common/BUILD.gn b/components/ai_chat/core/common/BUILD.gn
index a43344eac582..e923371bb8cd 100644
--- a/components/ai_chat/core/common/BUILD.gn
+++ b/components/ai_chat/core/common/BUILD.gn
@@ -12,23 +12,31 @@ component("common") {
defines = [ "IS_AI_CHAT_COMMON_IMPL" ]
sources = [
+ "constants.h",
"features.cc",
"features.h",
"pref_names.cc",
"pref_names.h",
+ "utils.cc",
+ "utils.h",
]
deps = [
"//base",
+ "//brave/brave_domains",
"//brave/components/ai_chat/core/common/buildflags:buildflags",
"//components/prefs",
+ "//url",
]
}
if (!is_ios) {
source_set("unit_tests") {
testonly = true
- sources = [ "pref_names_unittest.cc" ]
+ sources = [
+ "pref_names_unittest.cc",
+ "utils_unittest.cc",
+ ]
deps = [
"//base/test:test_support",
diff --git a/components/ai_chat/core/common/constants.h b/components/ai_chat/core/common/constants.h
new file mode 100644
index 000000000000..a3155a6088f4
--- /dev/null
+++ b/components/ai_chat/core/common/constants.h
@@ -0,0 +1,15 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef BRAVE_COMPONENTS_AI_CHAT_CORE_COMMON_CONSTANTS_H_
+#define BRAVE_COMPONENTS_AI_CHAT_CORE_COMMON_CONSTANTS_H_
+
+namespace ai_chat {
+
+inline constexpr char kBraveSearchURLPrefix[] = "search";
+
+} // namespace ai_chat
+
+#endif // BRAVE_COMPONENTS_AI_CHAT_CORE_COMMON_CONSTANTS_H_
diff --git a/components/ai_chat/core/common/features.cc b/components/ai_chat/core/common/features.cc
index 5d449bb0e369..e4f34ce73686 100644
--- a/components/ai_chat/core/common/features.cc
+++ b/components/ai_chat/core/common/features.cc
@@ -60,4 +60,16 @@ bool IsPageContentRefineEnabled() {
return base::FeatureList::IsEnabled(features::kPageContentRefine);
}
+BASE_FEATURE(kOpenAIChatFromBraveSearch,
+ "OpenAIChatFromBraveSearch",
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+ base::FEATURE_ENABLED_BY_DEFAULT);
+#else
+ base::FEATURE_DISABLED_BY_DEFAULT);
+#endif
+
+bool IsOpenAIChatFromBraveSearchEnabled() {
+ return base::FeatureList::IsEnabled(features::kOpenAIChatFromBraveSearch);
+}
+
} // namespace ai_chat::features
diff --git a/components/ai_chat/core/common/features.h b/components/ai_chat/core/common/features.h
index c12e9b74e529..ca454658d29b 100644
--- a/components/ai_chat/core/common/features.h
+++ b/components/ai_chat/core/common/features.h
@@ -45,6 +45,10 @@ COMPONENT_EXPORT(AI_CHAT_COMMON) bool IsContextMenuRewriteInPlaceEnabled();
COMPONENT_EXPORT(AI_CHAT_COMMON) BASE_DECLARE_FEATURE(kPageContentRefine);
COMPONENT_EXPORT(AI_CHAT_COMMON) bool IsPageContentRefineEnabled();
+COMPONENT_EXPORT(AI_CHAT_COMMON)
+BASE_DECLARE_FEATURE(kOpenAIChatFromBraveSearch);
+COMPONENT_EXPORT(AI_CHAT_COMMON) bool IsOpenAIChatFromBraveSearchEnabled();
+
} // namespace ai_chat::features
#endif // BRAVE_COMPONENTS_AI_CHAT_CORE_COMMON_FEATURES_H_
diff --git a/components/ai_chat/core/common/mojom/page_content_extractor.mojom b/components/ai_chat/core/common/mojom/page_content_extractor.mojom
index ad7ec91d38dc..66f982dd274a 100644
--- a/components/ai_chat/core/common/mojom/page_content_extractor.mojom
+++ b/components/ai_chat/core/common/mojom/page_content_extractor.mojom
@@ -32,6 +32,11 @@ interface PageContentExtractor {
// Get summarizer-key meta tag from Brave Search SERP if it exists.
// This should only be called when the last commited URL is Brave Search SERP.
GetSearchSummarizerKey() => (string? key);
+
+ // Get the nonce in the href URL and the nonce attribute value in the
+ // continue-with-leo HTML anchor element (open Leo button). These two
+ // nonces value must be the same, otherwise nullopt will be returned.
+ GetOpenAIChatButtonNonce() => (string? nonce);
};
// Allows the renderer to notify the browser process of meaningful changes to
diff --git a/components/ai_chat/core/common/utils.cc b/components/ai_chat/core/common/utils.cc
new file mode 100644
index 000000000000..36c2ea50b931
--- /dev/null
+++ b/components/ai_chat/core/common/utils.cc
@@ -0,0 +1,29 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "brave/components/ai_chat/core/common/utils.h"
+
+#include "brave/brave_domains/service_domains.h"
+#include "brave/components/ai_chat/core/common/constants.h"
+#include "url/gurl.h"
+#include "url/url_constants.h"
+
+namespace ai_chat {
+
+bool IsBraveSearchURL(const GURL& url) {
+ return url.is_valid() && url.SchemeIs(url::kHttpsScheme) &&
+ url.host_piece() ==
+ brave_domains::GetServicesDomain(kBraveSearchURLPrefix);
+}
+
+bool IsOpenAIChatButtonFromBraveSearchURL(const GURL& url) {
+ // Use search.brave.com in all cases because href on search site is
+ // hardcoded to search.brave.com for all environments.
+ return url.is_valid() && url.SchemeIs(url::kHttpsScheme) &&
+ url.host_piece() == "search.brave.com" && url.path_piece() == "/leo" &&
+ !url.ref_piece().empty();
+}
+
+} // namespace ai_chat
diff --git a/components/ai_chat/core/common/utils.h b/components/ai_chat/core/common/utils.h
new file mode 100644
index 000000000000..7ba37be18a50
--- /dev/null
+++ b/components/ai_chat/core/common/utils.h
@@ -0,0 +1,23 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef BRAVE_COMPONENTS_AI_CHAT_CORE_COMMON_UTILS_H_
+#define BRAVE_COMPONENTS_AI_CHAT_CORE_COMMON_UTILS_H_
+
+#include "base/component_export.h"
+
+class GURL;
+
+namespace ai_chat {
+
+COMPONENT_EXPORT(AI_CHAT_COMMON)
+bool IsBraveSearchURL(const GURL& url);
+
+COMPONENT_EXPORT(AI_CHAT_COMMON)
+bool IsOpenAIChatButtonFromBraveSearchURL(const GURL& url);
+
+} // namespace ai_chat
+
+#endif // BRAVE_COMPONENTS_AI_CHAT_CORE_COMMON_UTILS_H_
diff --git a/components/ai_chat/core/common/utils_unittest.cc b/components/ai_chat/core/common/utils_unittest.cc
new file mode 100644
index 000000000000..56099064a73e
--- /dev/null
+++ b/components/ai_chat/core/common/utils_unittest.cc
@@ -0,0 +1,35 @@
+/* Copyright (c) 2024 The Brave Authors. All rights reserved.
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "brave/components/ai_chat/core/common/utils.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace ai_chat {
+
+TEST(AIChatCommonUtilsUnitTest, IsBraveSearchURL) {
+ EXPECT_TRUE(IsBraveSearchURL(GURL("https://search.brave.com")));
+ EXPECT_FALSE(IsBraveSearchURL(GURL("http://search.brave.com")));
+ EXPECT_FALSE(IsBraveSearchURL(GURL("https://test.brave.com/")));
+ EXPECT_FALSE(IsBraveSearchURL(GURL("https://brave.com/")));
+ EXPECT_FALSE(IsBraveSearchURL(GURL()));
+}
+
+TEST(AIChatCommonUtilsUnitTest, IsOpenAIChatButtonFromBraveSearchURL) {
+ EXPECT_TRUE(IsOpenAIChatButtonFromBraveSearchURL(
+ GURL("https://search.brave.com/leo#5566")));
+ EXPECT_FALSE(IsOpenAIChatButtonFromBraveSearchURL(GURL()));
+ EXPECT_FALSE(
+ IsOpenAIChatButtonFromBraveSearchURL(GURL("https://search.brave.com")));
+ EXPECT_FALSE(IsOpenAIChatButtonFromBraveSearchURL(
+ GURL("https://search.brave.com/leo")));
+ EXPECT_FALSE(IsOpenAIChatButtonFromBraveSearchURL(
+ GURL("https://search.brave.com/leo#")));
+ EXPECT_FALSE(
+ IsOpenAIChatButtonFromBraveSearchURL(GURL("https://brave.com/leo#5566")));
+}
+
+} // namespace ai_chat
diff --git a/components/ai_chat/renderer/BUILD.gn b/components/ai_chat/renderer/BUILD.gn
index b214f86b3aeb..7394baf8dd02 100644
--- a/components/ai_chat/renderer/BUILD.gn
+++ b/components/ai_chat/renderer/BUILD.gn
@@ -22,6 +22,7 @@ static_library("renderer") {
deps = [
"//base",
+ "//brave/components/ai_chat/core/common",
"//brave/components/ai_chat/core/common/mojom",
"//content/public/renderer",
"//gin",
diff --git a/components/ai_chat/renderer/page_content_extractor.cc b/components/ai_chat/renderer/page_content_extractor.cc
index 9444d32cef2a..e5dde35a9a0c 100644
--- a/components/ai_chat/renderer/page_content_extractor.cc
+++ b/components/ai_chat/renderer/page_content_extractor.cc
@@ -21,10 +21,13 @@
#include "base/containers/fixed_flat_set.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
+#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
+#include "base/time/time.h"
#include "base/values.h"
#include "brave/components/ai_chat/core/common/mojom/page_content_extractor.mojom-shared.h"
#include "brave/components/ai_chat/core/common/mojom/page_content_extractor.mojom.h"
+#include "brave/components/ai_chat/core/common/utils.h"
#include "brave/components/ai_chat/renderer/page_text_distilling.h"
#include "brave/components/ai_chat/renderer/yt_util.h"
#include "content/public/renderer/render_frame.h"
@@ -318,4 +321,23 @@ void PageContentExtractor::GetSearchSummarizerKey(
std::move(callback).Run(element.GetAttribute("content").Utf8());
}
+void PageContentExtractor::GetOpenAIChatButtonNonce(
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback callback) {
+ auto element = render_frame()->GetWebFrame()->GetDocument().GetElementById(
+ "continue-with-leo");
+ if (element.IsNull() || !element.HasHTMLTagName("a")) {
+ std::move(callback).Run(std::nullopt);
+ return;
+ }
+
+ GURL url(element.GetAttribute("href").Utf8());
+ std::string nonce = element.GetAttribute("data-nonce").Utf8();
+ if (!IsOpenAIChatButtonFromBraveSearchURL(url) || nonce.empty() ||
+ url.ref_piece() != nonce) {
+ std::move(callback).Run(std::nullopt);
+ return;
+ }
+ std::move(callback).Run(nonce);
+}
+
} // namespace ai_chat
diff --git a/components/ai_chat/renderer/page_content_extractor.h b/components/ai_chat/renderer/page_content_extractor.h
index d56e8889249a..dda042ae0d9e 100644
--- a/components/ai_chat/renderer/page_content_extractor.h
+++ b/components/ai_chat/renderer/page_content_extractor.h
@@ -59,6 +59,9 @@ class PageContentExtractor
void GetSearchSummarizerKey(
mojom::PageContentExtractor::GetSearchSummarizerKeyCallback callback)
override;
+ void GetOpenAIChatButtonNonce(
+ mojom::PageContentExtractor::GetOpenAIChatButtonNonceCallback callback)
+ override;
// AIChatResourceSnifferThrottleDelegate
void OnInterceptedPageContentChanged(
diff --git a/components/permissions/contexts/brave_open_ai_chat_permission_context.cc b/components/permissions/contexts/brave_open_ai_chat_permission_context.cc
new file mode 100644
index 000000000000..5fa6211c5a35
--- /dev/null
+++ b/components/permissions/contexts/brave_open_ai_chat_permission_context.cc
@@ -0,0 +1,48 @@
+// Copyright (c) 2024 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/permissions/contexts/brave_open_ai_chat_permission_context.h"
+
+#include
+
+#include "brave/brave_domains/service_domains.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/permissions/permission_request_data.h"
+#include "third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
+#include "url/gurl.h"
+#include "url/url_constants.h"
+
+namespace permissions {
+
+BraveOpenAIChatPermissionContext::BraveOpenAIChatPermissionContext(
+ content::BrowserContext* browser_context)
+ : PermissionContextBase(browser_context,
+ ContentSettingsType::BRAVE_OPEN_AI_CHAT,
+ blink::mojom::PermissionsPolicyFeature::kNotFound) {
+}
+
+BraveOpenAIChatPermissionContext::~BraveOpenAIChatPermissionContext() = default;
+
+ContentSetting BraveOpenAIChatPermissionContext::GetPermissionStatusInternal(
+ content::RenderFrameHost* render_frame_host,
+ const GURL& requesting_origin,
+ const GURL& embedding_origin) const {
+ // Check if origin is https://search.brave.com.
+ if (!requesting_origin.SchemeIs(url::kHttpsScheme) ||
+ requesting_origin.host_piece() !=
+ brave_domains::GetServicesDomain("search")) {
+ return ContentSetting::CONTENT_SETTING_BLOCK;
+ }
+
+ return PermissionContextBase::GetPermissionStatusInternal(
+ render_frame_host, requesting_origin, embedding_origin);
+}
+
+bool BraveOpenAIChatPermissionContext::IsRestrictedToSecureOrigins() const {
+ return true;
+}
+
+} // namespace permissions
diff --git a/components/permissions/contexts/brave_open_ai_chat_permission_context.h b/components/permissions/contexts/brave_open_ai_chat_permission_context.h
new file mode 100644
index 000000000000..48915944fd55
--- /dev/null
+++ b/components/permissions/contexts/brave_open_ai_chat_permission_context.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2024 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#ifndef BRAVE_COMPONENTS_PERMISSIONS_CONTEXTS_BRAVE_OPEN_AI_CHAT_PERMISSION_CONTEXT_H_
+#define BRAVE_COMPONENTS_PERMISSIONS_CONTEXTS_BRAVE_OPEN_AI_CHAT_PERMISSION_CONTEXT_H_
+
+#include "components/permissions/permission_context_base.h"
+#include "content/public/browser/browser_context.h"
+
+namespace permissions {
+
+class BraveOpenAIChatPermissionContext : public PermissionContextBase {
+ public:
+ explicit BraveOpenAIChatPermissionContext(
+ content::BrowserContext* browser_context);
+ ~BraveOpenAIChatPermissionContext() override;
+
+ BraveOpenAIChatPermissionContext(const BraveOpenAIChatPermissionContext&) =
+ delete;
+ BraveOpenAIChatPermissionContext& operator=(
+ const BraveOpenAIChatPermissionContext&) = delete;
+
+ private:
+ // PermissionContextBase:
+ ContentSetting GetPermissionStatusInternal(
+ content::RenderFrameHost* render_frame_host,
+ const GURL& requesting_origin,
+ const GURL& embedding_origin) const override;
+ bool IsRestrictedToSecureOrigins() const override;
+};
+
+} // namespace permissions
+
+#endif // BRAVE_COMPONENTS_PERMISSIONS_CONTEXTS_BRAVE_OPEN_AI_CHAT_PERMISSION_CONTEXT_H_
diff --git a/components/permissions/sources.gni b/components/permissions/sources.gni
index 9d692b1d0a1d..889e225296c4 100644
--- a/components/permissions/sources.gni
+++ b/components/permissions/sources.gni
@@ -12,6 +12,8 @@ brave_components_permissions_sources = [
"//brave/components/permissions/contexts/brave_google_sign_in_permission_context.h",
"//brave/components/permissions/contexts/brave_localhost_permission_context.cc",
"//brave/components/permissions/contexts/brave_localhost_permission_context.h",
+ "//brave/components/permissions/contexts/brave_open_ai_chat_permission_context.cc",
+ "//brave/components/permissions/contexts/brave_open_ai_chat_permission_context.h",
"//brave/components/permissions/contexts/brave_wallet_permission_context.cc",
"//brave/components/permissions/contexts/brave_wallet_permission_context.h",
"//brave/components/permissions/permission_expiration_key.cc",
@@ -39,6 +41,7 @@ if (enable_widevine) {
brave_components_permissions_deps = [
"//base",
+ "//brave/brave_domains",
"//brave/components/brave_wallet/browser:permission_utils",
"//brave/components/brave_wallet/common:mojom",
"//brave/components/constants:constants",
diff --git a/components/resources/permissions_strings.grdp b/components/resources/permissions_strings.grdp
index fcdcaeabb25b..88923ee5a4e3 100644
--- a/components/resources/permissions_strings.grdp
+++ b/components/resources/permissions_strings.grdp
@@ -21,4 +21,10 @@
site permission
+
+ Allow opening Leo AI conversations in Brave?
+
+
+ Allow $1search.brave.com to open Leo AI conversations in Brave?
+
diff --git a/ios/browser/flags/about_flags.mm b/ios/browser/flags/about_flags.mm
index d30c20a840fb..8b86d6db6e4c 100644
--- a/ios/browser/flags/about_flags.mm
+++ b/ios/browser/flags/about_flags.mm
@@ -113,35 +113,33 @@
})
#if BUILDFLAG(ENABLE_AI_CHAT)
-#define BRAVE_AI_CHAT \
- EXPAND_FEATURE_ENTRIES({ \
- "brave-ai-chat", \
- "Brave AI Chat", \
- "Summarize articles and engage in conversation with AI", \
- flags_ui::kOsIos, \
- FEATURE_VALUE_TYPE(ai_chat::features::kAIChat), \
- })
-#define BRAVE_AI_CHAT_HISTORY \
- EXPAND_FEATURE_ENTRIES({ \
- "brave-ai-chat-history", \
- "Brave AI Chat History", \
- "Enables AI Chat History persistence and management", \
- flags_ui::kOsIos, \
- FEATURE_VALUE_TYPE(ai_chat::features::kAIChatHistory), \
- })
-#define BRAVE_AI_CHAT_PAGE_CONTENT_REFINE \
- EXPAND_FEATURE_ENTRIES({ \
- "brave-ai-chat-page-content-refine", \
- "Brave AI Chat Page Content Refine", \
- "Enable local text embedding for long page content in order to find " \
- "most relevant parts to the prompt within context limit.", \
- flags_ui::kOsIos, \
- FEATURE_VALUE_TYPE(ai_chat::features::kPageContentRefine), \
- })
+#define BRAVE_AI_CHAT_FEATURE_ENTRIES \
+ EXPAND_FEATURE_ENTRIES( \
+ { \
+ "brave-ai-chat", \
+ "Brave AI Chat", \
+ "Summarize articles and engage in conversation with AI", \
+ flags_ui::kOsIos, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kAIChat), \
+ }, \
+ { \
+ "brave-ai-chat-history", \
+ "Brave AI Chat History", \
+ "Enables AI Chat History persistence and management", \
+ flags_ui::kOsIos, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kAIChatHistory), \
+ }, \
+ { \
+ "brave-ai-chat-page-content-refine", \
+ "Brave AI Chat Page Content Refine", \
+ "Enable local text embedding for long page content in order to " \
+ "find " \
+ "most relevant parts to the prompt within context limit.", \
+ flags_ui::kOsIos, \
+ FEATURE_VALUE_TYPE(ai_chat::features::kPageContentRefine), \
+ })
#else
-#define BRAVE_AI_CHAT
-#define BRAVE_AI_CHAT_HISTORY
-#define BRAVE_AI_CHAT_PAGE_CONTENT_REFINE
+#define BRAVE_AI_CHAT_FEATURE_ENTRIES
#endif
#define BRAVE_PLAYLIST_FEATURE_ENTRIES \
@@ -242,8 +240,6 @@
BRAVE_SHIELDS_FEATURE_ENTRIES \
BRAVE_NATIVE_WALLET_FEATURE_ENTRIES \
BRAVE_SKU_SDK_FEATURE_ENTRIES \
- BRAVE_AI_CHAT \
- BRAVE_AI_CHAT_HISTORY \
- BRAVE_AI_CHAT_PAGE_CONTENT_REFINE \
+ BRAVE_AI_CHAT_FEATURE_ENTRIES \
BRAVE_PLAYLIST_FEATURE_ENTRIES \
LAST_BRAVE_FEATURE_ENTRIES_ITEM // Keep it as the last item.
diff --git a/patches/chrome-browser-resources-settings-site_settings-constants.ts.patch b/patches/chrome-browser-resources-settings-site_settings-constants.ts.patch
index f9b0bc8e41e9..4604536496d5 100644
--- a/patches/chrome-browser-resources-settings-site_settings-constants.ts.patch
+++ b/patches/chrome-browser-resources-settings-site_settings-constants.ts.patch
@@ -1,12 +1,12 @@
diff --git a/chrome/browser/resources/settings/site_settings/constants.ts b/chrome/browser/resources/settings/site_settings/constants.ts
-index 4ed920a8aabf9a853713935ffaa6653a8e1360b0..0c9e64bc05960754cdbcbc7bdcf118e292586b01 100644
+index 4ed920a8aabf9a853713935ffaa6653a8e1360b0..05d12ce52618e2b42f4d4c0fe59691bbffe297b3 100644
--- a/chrome/browser/resources/settings/site_settings/constants.ts
+++ b/chrome/browser/resources/settings/site_settings/constants.ts
@@ -67,6 +67,7 @@ export enum ContentSettingsTypes {
PDF_DOCUMENTS = 'pdfDocuments',
SITE_DATA = 'site-data',
OFFER_WRITING_HELP = 'offer-writing-help',
-+ AUTOPLAY = 'autoplay', ETHEREUM = 'ethereum', SOLANA = 'solana', BRAVE_SHIELDS = 'braveShields', GOOGLE_SIGN_IN = 'googleSignIn', LOCALHOST_ACCESS = 'localhostAccess',
++ AUTOPLAY = 'autoplay', ETHEREUM = 'ethereum', SOLANA = 'solana', BRAVE_SHIELDS = 'braveShields', GOOGLE_SIGN_IN = 'googleSignIn', LOCALHOST_ACCESS = 'localhostAccess', BRAVE_OPEN_AI_CHAT = 'braveOpenAIChat'
}
/**
diff --git a/patches/chrome-browser-resources-settings-site_settings-settings_category_default_radio_group.ts.patch b/patches/chrome-browser-resources-settings-site_settings-settings_category_default_radio_group.ts.patch
index 7ccb9a69cf87..2746347d724d 100644
--- a/patches/chrome-browser-resources-settings-site_settings-settings_category_default_radio_group.ts.patch
+++ b/patches/chrome-browser-resources-settings-site_settings-settings_category_default_radio_group.ts.patch
@@ -1,12 +1,12 @@
diff --git a/chrome/browser/resources/settings/site_settings/settings_category_default_radio_group.ts b/chrome/browser/resources/settings/site_settings/settings_category_default_radio_group.ts
-index f41407671c3aa79854cbf4c4adf5359278ea5f60..fa73ea59476b32d9feefe73c728f69c80669e848 100644
+index f41407671c3aa79854cbf4c4adf5359278ea5f60..fe1fe5d9527400c5a59143fafb3f17cc23a76ae8 100644
--- a/chrome/browser/resources/settings/site_settings/settings_category_default_radio_group.ts
+++ b/chrome/browser/resources/settings/site_settings/settings_category_default_radio_group.ts
@@ -171,6 +171,7 @@ export class SettingsCategoryDefaultRadioGroupElement extends
case ContentSettingsTypes.WEB_PRINTING:
// "Ask" vs "Blocked".
return ContentSetting.ASK;
-+ case ContentSettingsTypes.ETHEREUM: case ContentSettingsTypes.SOLANA: case ContentSettingsTypes.GOOGLE_SIGN_IN: case ContentSettingsTypes.LOCALHOST_ACCESS: return ContentSetting.ASK; case ContentSettingsTypes.AUTOPLAY: return ContentSetting.ALLOW;
++ case ContentSettingsTypes.ETHEREUM: case ContentSettingsTypes.SOLANA: case ContentSettingsTypes.GOOGLE_SIGN_IN: case ContentSettingsTypes.LOCALHOST_ACCESS: case ContentSettingsTypes.BRAVE_OPEN_AI_CHAT: return ContentSetting.ASK; case ContentSettingsTypes.AUTOPLAY: return ContentSetting.ALLOW;
default:
assertNotReached('Invalid category: ' + this.category);
}
diff --git a/test/data/leo/leo b/test/data/leo/leo
new file mode 100644
index 000000000000..0354eeb28ea6
--- /dev/null
+++ b/test/data/leo/leo
@@ -0,0 +1,9 @@
+
+
+
+ Leo test
+
+
+ I have spoken
+
+
diff --git a/test/data/leo/open_ai_chat_button.html b/test/data/leo/open_ai_chat_button.html
new file mode 100644
index 000000000000..4d6326aa59b4
--- /dev/null
+++ b/test/data/leo/open_ai_chat_button.html
@@ -0,0 +1,22 @@
+
+
+
+ Leo test
+
+
+ Open Leo
+ Open Leo
+
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+ Open Leo
+
+
diff --git a/test/data/leo/open_ai_chat_button_invalid.html b/test/data/leo/open_ai_chat_button_invalid.html
new file mode 100644
index 000000000000..469d3d84ac02
--- /dev/null
+++ b/test/data/leo/open_ai_chat_button_invalid.html
@@ -0,0 +1,9 @@
+
+
+
+ Leo test
+
+
+ Open Leo
+
+
diff --git a/test/data/leo/open_ai_chat_button_valid.html b/test/data/leo/open_ai_chat_button_valid.html
new file mode 100644
index 000000000000..8fb463d6a769
--- /dev/null
+++ b/test/data/leo/open_ai_chat_button_valid.html
@@ -0,0 +1,9 @@
+
+
+
+ Leo test
+
+
+ Open Leo
+
+