diff --git a/browser/ai_chat/BUILD.gn b/browser/ai_chat/BUILD.gn index d6cfffee0c94..f9af82632195 100644 --- a/browser/ai_chat/BUILD.gn +++ b/browser/ai_chat/BUILD.gn @@ -11,6 +11,8 @@ static_library("ai_chat") { "ai_chat_service_factory.h", "ai_chat_settings_helper.cc", "ai_chat_settings_helper.h", + "ai_chat_urls.cc", + "ai_chat_urls.h", "ai_chat_utils.cc", "ai_chat_utils.h", ] @@ -24,6 +26,7 @@ static_library("ai_chat") { "//brave/components/ai_chat/core/browser", "//brave/components/ai_chat/core/common", "//brave/components/ai_chat/core/common/mojom", + "//brave/components/constants", "//brave/components/resources:strings_grit", "//brave/net/base:utils", "//chrome/browser:browser_process", diff --git a/browser/ai_chat/ai_chat_throttle_unittest.cc b/browser/ai_chat/ai_chat_throttle_unittest.cc index 9a931cceee4e..4cf514656885 100644 --- a/browser/ai_chat/ai_chat_throttle_unittest.cc +++ b/browser/ai_chat/ai_chat_throttle_unittest.cc @@ -3,11 +3,13 @@ * 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_throttle.h" + #include #include "base/test/scoped_feature_list.h" -#include "brave/components/ai_chat/content/browser/ai_chat_throttle.h" #include "brave/components/ai_chat/core/common/features.h" +#include "brave/components/constants/webui_url_constants.h" #include "chrome/test/base/testing_browser_process.h" #include "chrome/test/base/testing_profile.h" #include "chrome/test/base/testing_profile_manager.h" @@ -20,7 +22,9 @@ namespace ai_chat { namespace { + constexpr char kTestProfileName[] = "TestProfile"; + } // namespace class AiChatThrottleUnitTest : public testing::Test, @@ -73,7 +77,7 @@ INSTANTIATE_TEST_SUITE_P( TEST_P(AiChatThrottleUnitTest, CancelNavigationFromTab) { content::MockNavigationHandle test_handle(web_contents()); - test_handle.set_url(GURL("chrome-untrusted://chat")); + test_handle.set_url(GURL(kAIChatUIURL)); #if BUILDFLAG(IS_ANDROID) ui::PageTransition transition = ui::PageTransitionFromInt( @@ -98,10 +102,33 @@ TEST_P(AiChatThrottleUnitTest, CancelNavigationFromTab) { } } +TEST_P(AiChatThrottleUnitTest, CancelNavigationToFrame) { + content::MockNavigationHandle test_handle(web_contents()); + + test_handle.set_url(GURL(kAIChatUntrustedConversationUIURL)); + +#if BUILDFLAG(IS_ANDROID) + ui::PageTransition transition = ui::PageTransitionFromInt( + ui::PageTransition::PAGE_TRANSITION_FROM_ADDRESS_BAR); +#else + ui::PageTransition transition = ui::PageTransitionFromInt( + ui::PageTransition::PAGE_TRANSITION_FROM_ADDRESS_BAR | + ui::PageTransition::PAGE_TRANSITION_TYPED); +#endif + + test_handle.set_page_transition(transition); + + std::unique_ptr throttle = + AiChatThrottle::MaybeCreateThrottleFor(&test_handle); + + EXPECT_EQ(content::NavigationThrottle::CANCEL_AND_IGNORE, + throttle->WillStartRequest().action()); +} + TEST_P(AiChatThrottleUnitTest, AllowNavigationFromPanel) { content::MockNavigationHandle test_handle(web_contents()); - test_handle.set_url(GURL("chrome-untrusted://chat")); + test_handle.set_url(GURL(kAIChatUIURL)); #if BUILDFLAG(IS_ANDROID) ui::PageTransition transition = diff --git a/browser/ai_chat/ai_chat_urls.cc b/browser/ai_chat/ai_chat_urls.cc new file mode 100644 index 000000000000..ed9b4e66399c --- /dev/null +++ b/browser/ai_chat/ai_chat_urls.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/browser/ai_chat/ai_chat_urls.h" + +#include + +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "brave/components/constants/webui_url_constants.h" +#include "url/gurl.h" + +namespace ai_chat { + +GURL TabAssociatedConversationUrl() { + return GURL(base::StrCat({kAIChatUIURL, "tab"})); +} + +GURL ConversationUrl(std::string_view conversation_uuid) { + return GURL(base::StrCat({kAIChatUIURL, conversation_uuid})); +} + +std::string_view ConversationUUIDFromURL(const GURL& url) { + return base::TrimString(url.path_piece(), "/", base::TrimPositions::TRIM_ALL); +} + +} // namespace ai_chat diff --git a/browser/ai_chat/ai_chat_urls.h b/browser/ai_chat/ai_chat_urls.h new file mode 100644 index 000000000000..1cf4c970e34e --- /dev/null +++ b/browser/ai_chat/ai_chat_urls.h @@ -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/. + +#ifndef BRAVE_BROWSER_AI_CHAT_AI_CHAT_URLS_H_ +#define BRAVE_BROWSER_AI_CHAT_AI_CHAT_URLS_H_ + +#include + +#include "url/gurl.h" + +namespace ai_chat { + +// UI that will open a conversation associated with the active Tab in the same +// browser window. The conversation will change when that Tab navigates. +GURL TabAssociatedConversationUrl(); + +// UI that will open to a specific conversation. The conversation will not +// change upon any navigation. +GURL ConversationUrl(std::string_view conversation_uuid); + +// Extracts the conversation UUID from a conversation URL or a conversation +// entries iframe +std::string_view ConversationUUIDFromURL(const GURL& url); + +} // namespace ai_chat + +#endif // BRAVE_BROWSER_AI_CHAT_AI_CHAT_URLS_H_ diff --git a/browser/ai_chat/android/ai_chat_utils_android.cc b/browser/ai_chat/android/ai_chat_utils_android.cc index 880fde1bef6d..cea9c5b95ecb 100644 --- a/browser/ai_chat/android/ai_chat_utils_android.cc +++ b/browser/ai_chat/android/ai_chat_utils_android.cc @@ -7,6 +7,7 @@ #include "base/android/jni_string.h" #include "base/time/time.h" #include "brave/browser/ai_chat/ai_chat_service_factory.h" +#include "brave/browser/ai_chat/ai_chat_urls.h" #include "brave/build/android/jni_headers/BraveLeoUtils_jni.h" #include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h" #include "brave/components/ai_chat/core/browser/ai_chat_service.h" @@ -55,7 +56,7 @@ static void JNI_BraveLeoUtils_OpenLeoQuery( conversation->SubmitHumanConversationEntry(std::move(turn)); content::OpenURLParams params( - GURL(base::StrCat({kChatUIURL, conversation->get_conversation_uuid()})), + ConversationUrl(conversation->get_conversation_uuid()), content::Referrer(), WindowOpenDisposition::CURRENT_TAB, ui::PAGE_TRANSITION_FROM_API, false); web_contents->OpenURL(params, {}); @@ -77,7 +78,7 @@ static void JNI_BraveLeoUtils_OpenLeoUrlForTab( chat_tab_helper->GetContentId(), chat_tab_helper->GetWeakPtr()); content::OpenURLParams params( - GURL(base::StrCat({kChatUIURL, conversation->get_conversation_uuid()})), + ConversationUrl(conversation->get_conversation_uuid()), content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB, ui::PAGE_TRANSITION_FROM_API, false); web_contents->OpenURL(params, {}); diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 1201e8621f35..1b59ee88e47c 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -39,6 +39,7 @@ #include "brave/browser/ui/brave_ui_features.h" #include "brave/browser/ui/webui/ads_internals/ads_internals_ui.h" #include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h" +#include "brave/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.h" #include "brave/browser/ui/webui/brave_rewards/rewards_page_ui.h" #include "brave/browser/ui/webui/skus_internals_ui.h" #include "brave/browser/url_sanitizer/url_sanitizer_service_factory.h" @@ -50,6 +51,7 @@ #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" +#include "brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom.h" #include "brave/components/ai_rewriter/common/buildflags/buildflags.h" #include "brave/components/body_sniffer/body_sniffer_throttle.h" #include "brave/components/brave_federated/features.h" @@ -622,6 +624,9 @@ void BraveContentBrowserClient::RegisterWebUIInterfaceBrokers( registry.ForWebUI() .Add() .Add(); + registry.ForWebUI() + .Add() + .Add(); } #if BUILDFLAG(ENABLE_AI_REWRITER) diff --git a/browser/resources/settings/brave_overrides/settings_menu.ts b/browser/resources/settings/brave_overrides/settings_menu.ts index 215e787ca80b..d2e3f5d37c25 100644 --- a/browser/resources/settings/brave_overrides/settings_menu.ts +++ b/browser/resources/settings/brave_overrides/settings_menu.ts @@ -266,7 +266,7 @@ RegisterPolymerTemplateModifications({ // Add leo item const leoAssistantEl = createMenuElement( loadTimeData.getString('leoAssistant'), - '/leo-assistant', + '/leo-ai', 'product-brave-leo', 'leoAssistant', ) diff --git a/browser/resources/settings/brave_routes.ts b/browser/resources/settings/brave_routes.ts index bce4d58943d9..93d1dd16f96e 100644 --- a/browser/resources/settings/brave_routes.ts +++ b/browser/resources/settings/brave_routes.ts @@ -53,7 +53,7 @@ export default function addBraveRoutes(r: Partial) { if (pageVisibility.leoAssistant) { r.BRAVE_LEO_ASSISTANT = - r.BASIC.createSection('/leo-assistant', 'leoAssistant') + r.BASIC.createSection('/leo-ai', 'leoAssistant') } if (pageVisibility.content) { r.BRAVE_CONTENT = r.BASIC.createSection('/braveContent', 'content') diff --git a/browser/resources/settings/sources.gni b/browser/resources/settings/sources.gni index e452ae2096c3..41f3df19e0df 100644 --- a/browser/resources/settings/sources.gni +++ b/browser/resources/settings/sources.gni @@ -180,6 +180,7 @@ brave_settings_ts_extra_deps = brave_settings_mojo_files = [ "$root_gen_dir/brave/components/ai_chat/core/common/mojom/settings_helper.mojom-webui.ts", "$root_gen_dir/brave/components/ai_chat/core/common/mojom/ai_chat.mojom-webui.ts", + "$root_gen_dir/brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom-webui.ts", ] brave_settings_mojo_files_deps = diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 1212cb615578..e9ef106a20d8 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -71,6 +71,8 @@ source_set("ui") { "webui/ai_chat/ai_chat_ui.h", "webui/ai_chat/ai_chat_ui_page_handler.cc", "webui/ai_chat/ai_chat_ui_page_handler.h", + "webui/ai_chat/ai_chat_untrusted_conversation_ui.cc", + "webui/ai_chat/ai_chat_untrusted_conversation_ui.h", "webui/brave_adblock_internals_ui.cc", "webui/brave_adblock_internals_ui.h", "webui/brave_adblock_ui.cc", @@ -772,8 +774,8 @@ source_set("ui") { "//brave/components/ai_chat/core/browser", "//brave/components/ai_chat/core/common", "//brave/components/ai_chat/core/common/mojom", + "//brave/components/ai_chat/resources", "//brave/components/ai_chat/resources/custom_site_distiller_scripts:generated_resources", - "//brave/components/ai_chat/resources/page:generated_resources", "//brave/components/ai_rewriter/common/buildflags", "//brave/components/brave_adaptive_captcha", "//brave/components/brave_adblock_ui:generated_resources", diff --git a/browser/ui/brave_pages.cc b/browser/ui/brave_pages.cc index 5f75232d46c1..c8d2e523ae9d 100644 --- a/browser/ui/brave_pages.cc +++ b/browser/ui/brave_pages.cc @@ -50,7 +50,7 @@ void ShowFullpageChat(Browser* browser) { if (!ai_chat::features::IsAIChatHistoryEnabled()) { return; } - ShowSingletonTabOverwritingNTP(browser, GURL(kChatUIURL)); + ShowSingletonTabOverwritingNTP(browser, GURL(kAIChatUIURL)); } void ShowWebcompatReporter(Browser* browser) { diff --git a/browser/ui/views/side_panel/brave_side_panel_utils.cc b/browser/ui/views/side_panel/brave_side_panel_utils.cc index dac6ba0d3406..9be0e0c85032 100644 --- a/browser/ui/views/side_panel/brave_side_panel_utils.cc +++ b/browser/ui/views/side_panel/brave_side_panel_utils.cc @@ -3,10 +3,9 @@ * 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 "base/strings/strcat.h" +#include "brave/browser/ai_chat/ai_chat_urls.h" #include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h" #include "brave/components/ai_chat/core/browser/utils.h" -#include "brave/components/constants/webui_url_constants.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/views/side_panel/side_panel_registry.h" #include "chrome/browser/ui/views/side_panel/side_panel_web_ui_view.h" @@ -29,7 +28,7 @@ std::unique_ptr CreateAIChatSidePanelWebView( auto web_view = std::make_unique>( scope, base::RepeatingClosure(), base::RepeatingClosure(), std::make_unique>( - GURL(base::StrCat({kChatUIURL, "tab"})), profile.get(), + ai_chat::TabAssociatedConversationUrl(), profile.get(), IDS_SIDEBAR_CHAT_SUMMARIZER_ITEM_TITLE, /*esc_closes_ui=*/false)); web_view->ShowUI(); diff --git a/browser/ui/webui/ai_chat/ai_chat_ui.cc b/browser/ui/webui/ai_chat/ai_chat_ui.cc index 81a401297444..0c3d40894c54 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui.cc +++ b/browser/ui/webui/ai_chat/ai_chat_ui.cc @@ -17,18 +17,22 @@ #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" -#include "brave/components/ai_chat/resources/page/grit/ai_chat_ui_generated_map.h" +#include "brave/components/ai_chat/resources/grit/ai_chat_ui_generated_map.h" #include "brave/components/constants/webui_url_constants.h" #include "brave/components/l10n/common/localization_util.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/tabs/tab_model.h" +#include "chrome/browser/ui/webui/favicon_source.h" #include "chrome/browser/ui/webui/webui_util.h" +#include "components/favicon_base/favicon_url_parser.h" #include "components/grit/brave_components_resources.h" #include "components/prefs/pref_service.h" #include "components/user_prefs/user_prefs.h" #include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui_controller.h" #include "content/public/browser/web_ui_data_source.h" #include "content/public/common/url_constants.h" +#include "ui/webui/mojo_web_ui_controller.h" #if !BUILDFLAG(IS_ANDROID) #include "chrome/browser/ui/browser.h" @@ -58,51 +62,51 @@ content::WebContents* GetActiveWebContents(content::BrowserContext* context) { #endif AIChatUI::AIChatUI(content::WebUI* web_ui) - : ui::UntrustedWebUIController(web_ui), - profile_(Profile::FromWebUI(web_ui)) { + : ui::MojoWebUIController(web_ui), profile_(Profile::FromWebUI(web_ui)) { DCHECK(profile_); DCHECK(profile_->IsRegularProfile()); // Create a URLDataSource and add resources. - content::WebUIDataSource* untrusted_source = - content::WebUIDataSource::CreateAndAdd( - web_ui->GetWebContents()->GetBrowserContext(), kChatUIURL); + content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd( + web_ui->GetWebContents()->GetBrowserContext(), kAIChatUIHost); - webui::SetupWebUIDataSource(untrusted_source, kAiChatUiGenerated, - IDR_CHAT_UI_HTML); + webui::SetupWebUIDataSource(source, kAiChatUiGenerated, IDR_AI_CHAT_UI_HTML); - untrusted_source->AddResourcePath("styles.css", IDR_CHAT_UI_CSS); + source->AddResourcePath("styles.css", IDR_AI_CHAT_UI_CSS); for (const auto& str : ai_chat::GetLocalizedStrings()) { - untrusted_source->AddString( - str.name, brave_l10n::GetLocalizedResourceUTF16String(str.id)); + source->AddString(str.name, + brave_l10n::GetLocalizedResourceUTF16String(str.id)); } -#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS) - constexpr bool kIsMobile = true; -#else - constexpr bool kIsMobile = false; -#endif + constexpr bool kIsMobile = BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS); + source->AddBoolean("isMobile", kIsMobile); + source->AddBoolean("isHistoryEnabled", + ai_chat::features::IsAIChatHistoryEnabled()); - untrusted_source->AddBoolean("isMobile", kIsMobile); - untrusted_source->AddBoolean("isHistoryEnabled", - ai_chat::features::IsAIChatHistoryEnabled()); - - untrusted_source->OverrideContentSecurityPolicy( + web_ui->AddRequestableScheme(content::kChromeUIUntrustedScheme); + source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::ScriptSrc, - "script-src 'self' chrome-untrusted://resources;"); - untrusted_source->OverrideContentSecurityPolicy( + "script-src 'self' chrome://resources;"); + source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::StyleSrc, - "style-src 'self' 'unsafe-inline' chrome-untrusted://resources;"); - untrusted_source->OverrideContentSecurityPolicy( + "style-src 'self' 'unsafe-inline' chrome://resources;"); + source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::ImgSrc, - "img-src 'self' blob: chrome-untrusted://resources;"); - untrusted_source->OverrideContentSecurityPolicy( + "img-src 'self' blob: chrome://resources chrome://favicon2;"); + source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::FontSrc, - "font-src 'self' data: chrome-untrusted://resources;"); + "font-src 'self' chrome://resources;"); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ChildSrc, + base::StringPrintf("child-src %s;", kAIChatUntrustedConversationUIURL)); - untrusted_source->OverrideContentSecurityPolicy( + source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::TrustedTypes, "trusted-types default;"); + + content::URLDataSource::Add( + profile_, std::make_unique( + profile_, chrome::FaviconUrlFormat::kFavicon2)); } AIChatUI::~AIChatUI() = default; @@ -148,28 +152,34 @@ void AIChatUI::BindInterface( std::move(receiver)); } -bool UntrustedChatUIConfig::IsWebUIEnabled( - content::BrowserContext* browser_context) { +void AIChatUI::BindInterface( + mojo::PendingReceiver + parent_ui_frame_receiver) { + CHECK(page_handler_); + page_handler_->BindParentUIFrameFromChildFrame( + std::move(parent_ui_frame_receiver)); +} + +bool AIChatUIConfig::IsWebUIEnabled(content::BrowserContext* browser_context) { return ai_chat::IsAIChatEnabled( user_prefs::UserPrefs::Get(browser_context)) && Profile::FromBrowserContext(browser_context)->IsRegularProfile(); } #if BUILDFLAG(IS_ANDROID) -std::unique_ptr -UntrustedChatUIConfig::CreateWebUIController(content::WebUI* web_ui, - const GURL& url) { +std::unique_ptr AIChatUIConfig::CreateWebUIController( + content::WebUI* web_ui, + const GURL& url) { return std::make_unique(web_ui); } #endif // #if BUILDFLAG(IS_ANDROID) #if !BUILDFLAG(IS_ANDROID) -UntrustedChatUIConfig::UntrustedChatUIConfig() - : DefaultTopChromeWebUIConfig(content::kChromeUIUntrustedScheme, - kChatUIHost) {} +AIChatUIConfig::AIChatUIConfig() + : DefaultTopChromeWebUIConfig(content::kChromeUIScheme, kAIChatUIHost) {} #else -UntrustedChatUIConfig::UntrustedChatUIConfig() - : WebUIConfig(content::kChromeUIUntrustedScheme, kChatUIHost) {} +AIChatUIConfig::AIChatUIConfig() + : WebUIConfig(content::kChromeUIScheme, kAIChatUIHost) {} #endif // #if !BUILDFLAG(IS_ANDROID) WEB_UI_CONTROLLER_TYPE_IMPL(AIChatUI) diff --git a/browser/ui/webui/ai_chat/ai_chat_ui.h b/browser/ui/webui/ai_chat/ai_chat_ui.h index 3dfab0af575b..67c26ed7403a 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui.h +++ b/browser/ui/webui/ai_chat/ai_chat_ui.h @@ -9,6 +9,7 @@ #include #include +#include "brave/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h" #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h" #include "chrome/browser/ui/webui/top_chrome/top_chrome_web_ui_controller.h" #include "content/public/browser/web_ui_controller.h" @@ -27,7 +28,7 @@ class BrowserContext; class Profile; -class AIChatUI : public ui::UntrustedWebUIController { +class AIChatUI : public ui::MojoWebUIController { public: explicit AIChatUI(content::WebUI* web_ui); AIChatUI(const AIChatUI&) = delete; @@ -37,6 +38,8 @@ class AIChatUI : public ui::UntrustedWebUIController { void BindInterface( mojo::PendingReceiver receiver); void BindInterface(mojo::PendingReceiver receiver); + void BindInterface(mojo::PendingReceiver + parent_ui_frame_receiver); // Set by WebUIContentsWrapperT. TopChromeWebUIController provides default // implementation for this but we don't use it. @@ -48,7 +51,7 @@ class AIChatUI : public ui::UntrustedWebUIController { static constexpr std::string GetWebUIName() { return "AIChatPanel"; } private: - std::unique_ptr page_handler_; + std::unique_ptr page_handler_; base::WeakPtr embedder_; raw_ptr profile_ = nullptr; @@ -57,13 +60,13 @@ class AIChatUI : public ui::UntrustedWebUIController { }; #if !BUILDFLAG(IS_ANDROID) -class UntrustedChatUIConfig : public DefaultTopChromeWebUIConfig { +class AIChatUIConfig : public DefaultTopChromeWebUIConfig { #else -class UntrustedChatUIConfig : public content::WebUIConfig { +class AIChatUIConfig : public content::WebUIConfig { #endif // #if !BUILDFLAG(IS_ANDROID) public: - UntrustedChatUIConfig(); - ~UntrustedChatUIConfig() override = default; + AIChatUIConfig(); + ~AIChatUIConfig() override = default; bool IsWebUIEnabled(content::BrowserContext* browser_context) override; diff --git a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc index 8bafa8354685..9a9585d93109 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc +++ b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc @@ -11,6 +11,7 @@ #include #include "brave/browser/ai_chat/ai_chat_service_factory.h" +#include "brave/browser/ai_chat/ai_chat_urls.h" #include "brave/browser/ui/side_panel/ai_chat/ai_chat_side_panel_utils.h" #include "brave/components/ai_chat/core/browser/ai_chat_service.h" #include "brave/components/ai_chat/core/browser/constants.h" @@ -38,8 +39,7 @@ namespace { constexpr uint32_t kDesiredFaviconSizePixels = 32; constexpr char kURLRefreshPremiumSession[] = "https://account.brave.com/?intent=recover&product=leo"; -constexpr char kURLLearnMoreBraveSearchLeo[] = - "https://support.brave.com/hc/en-us/categories/20990938292237-Brave-Leo"; + #if !BUILDFLAG(IS_ANDROID) constexpr char kURLGoPremium[] = "https://account.brave.com/account/?intent=checkout&product=leo"; @@ -96,7 +96,7 @@ void AIChatUIPageHandler::OpenAIChatSettings() { (active_chat_tab_helper_) ? active_chat_tab_helper_->web_contents() : owner_web_contents_.get(); #if !BUILDFLAG(IS_ANDROID) - const GURL url("brave://settings/leo-assistant"); + const GURL url("brave://settings/leo-ai"); if (auto* browser = chrome::FindBrowserWithTab(contents_to_navigate)) { ShowSingletonTab(browser, url); } else { @@ -116,7 +116,7 @@ void AIChatUIPageHandler::OpenConversationFullPage( CHECK(active_chat_tab_helper_); active_chat_tab_helper_->web_contents()->OpenURL( { - GURL(kChatUIURL).Resolve(conversation_uuid), + ConversationUrl(conversation_uuid), content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB, ui::PAGE_TRANSITION_TYPED, @@ -172,10 +172,6 @@ void AIChatUIPageHandler::ManagePremium() { #endif } -void AIChatUIPageHandler::OpenLearnMoreAboutBraveSearchWithLeo() { - OpenURL(GURL(kURLLearnMoreBraveSearchLeo)); -} - void AIChatUIPageHandler::OpenModelSupportUrl() { OpenURL(GURL(kLeoModelSupportUrl)); } @@ -264,6 +260,11 @@ void AIChatUIPageHandler::GetFaviconImageData( weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } +void AIChatUIPageHandler::BindParentUIFrameFromChildFrame( + mojo::PendingReceiver receiver) { + chat_ui_->OnChildFrameBound(std::move(receiver)); +} + void AIChatUIPageHandler::GetFaviconImageDataForAssociatedContent( GetFaviconImageDataCallback callback, mojom::SiteInfoPtr content_info, diff --git a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h index 4dc28747bba5..81e4f6dfc4ed 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h +++ b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h @@ -47,7 +47,6 @@ class AIChatUIPageHandler : public mojom::AIChatUIHandler, void OpenAIChatSettings() override; void OpenConversationFullPage(const std::string& conversation_uuid) override; void OpenURL(const GURL& url) override; - void OpenLearnMoreAboutBraveSearchWithLeo() override; void OpenModelSupportUrl() override; void GoPremium() override; void RefreshPremiumSession() override; @@ -67,6 +66,9 @@ class AIChatUIPageHandler : public mojom::AIChatUIHandler, void GetFaviconImageData(const std::string& conversation_id, GetFaviconImageDataCallback callback) override; + void BindParentUIFrameFromChildFrame( + mojo::PendingReceiver receiver); + private: class ChatContextObserver : public content::WebContentsObserver { public: diff --git a/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.cc b/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.cc new file mode 100644 index 000000000000..adaa05832dd5 --- /dev/null +++ b/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.cc @@ -0,0 +1,215 @@ +// 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/webui/ai_chat/ai_chat_untrusted_conversation_ui.h" + +#include +#include + +#include "base/strings/escape.h" +#include "brave/browser/ai_chat/ai_chat_service_factory.h" +#include "brave/browser/ai_chat/ai_chat_urls.h" +#include "brave/browser/ui/side_panel/ai_chat/ai_chat_side_panel_utils.h" +#include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h" +#include "brave/components/ai_chat/core/browser/ai_chat_service.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/common/mojom/ai_chat.mojom.h" +#include "brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom.h" +#include "brave/components/ai_chat/resources/grit/ai_chat_ui_generated_map.h" +#include "brave/components/constants/webui_url_constants.h" +#include "brave/components/l10n/common/localization_util.h" +#include "chrome/browser/ui/webui/webui_util.h" +#include "components/grit/brave_components_resources.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/common/url_constants.h" + +#if BUILDFLAG(IS_ANDROID) +#include "brave/browser/ui/android/ai_chat/brave_leo_settings_launcher_helper.h" +#else +#include "chrome/browser/ui/browser.h" +#endif + +namespace { +constexpr char kURLLearnMoreBraveSearchLeo[] = + "https://support.brave.com/hc/en-us/categories/20990938292237-Brave-Leo"; + +// Implments the interface to calls from the UI to the browser +class UIHandler : public ai_chat::mojom::UntrustedUIHandler { + public: + UIHandler(content::WebUI* web_ui, + mojo::PendingReceiver receiver) + : web_ui_(web_ui), receiver_(this, std::move(receiver)) {} + UIHandler(const UIHandler&) = delete; + UIHandler& operator=(const UIHandler&) = delete; + + ~UIHandler() override = default; + + // ai_chat::mojom::UntrustedConversationUIHandler + void OpenLearnMoreAboutBraveSearchWithLeo() override { + if (!web_ui_->GetRenderFrameHost()->HasTransientUserActivation()) { + return; + } + OpenURL(GURL(kURLLearnMoreBraveSearchLeo)); + } + + void OpenSearchURL(const std::string& search_query) override { + if (!web_ui_->GetRenderFrameHost()->HasTransientUserActivation()) { + return; + } + OpenURL(GURL("https://search.brave.com/search?q=" + + base::EscapeQueryParamValue(search_query, true))); + } + + void BindParentPage(mojo::PendingReceiver + parent_ui_frame_receiver) override { + // Route the receiver to the parent frame + auto* rfh = web_ui_->GetWebContents()->GetPrimaryMainFrame(); + if (!rfh) { + return; + } + + // We should not be embedded on a non-WebUI page + CHECK(rfh->GetWebUI()); + + AIChatUI* ai_chat_ui_controller = + rfh->GetWebUI()->GetController()->GetAs(); + // We should not be embedded on any non AIChatUI page + CHECK(ai_chat_ui_controller); + + ai_chat_ui_controller->BindInterface(std::move(parent_ui_frame_receiver)); + } + + private: + void OpenURL(GURL url) { + if (!url.SchemeIs(url::kHttpsScheme)) { + return; + } + +#if !BUILDFLAG(IS_ANDROID) + Browser* browser = + ai_chat::GetBrowserForWebContents(web_ui_->GetWebContents()); + browser->OpenURL( + {url, content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui::PAGE_TRANSITION_LINK, false}, + /*navigation_handle_callback=*/{}); +#else + // We handle open link different on Android as we need to close the chat + // window because it's always full screen + ai_chat::OpenURL(url.spec()); +#endif + } + + raw_ptr web_ui_ = nullptr; + mojo::Receiver receiver_; +}; + +} // namespace + +bool AIChatUntrustedConversationUIConfig::IsWebUIEnabled( + content::BrowserContext* browser_context) { + // Only enabled if we have a valid service + return (ai_chat::AIChatServiceFactory::GetForBrowserContext( + browser_context) != nullptr); +} + +std::unique_ptr +AIChatUntrustedConversationUIConfig::CreateWebUIController( + content::WebUI* web_ui, + const GURL& url) { + return std::make_unique(web_ui); +} + +AIChatUntrustedConversationUIConfig::AIChatUntrustedConversationUIConfig() + : WebUIConfig(content::kChromeUIUntrustedScheme, + kAIChatUntrustedConversationUIHost) {} + +AIChatUntrustedConversationUIConfig::~AIChatUntrustedConversationUIConfig() = + default; + +AIChatUntrustedConversationUI::AIChatUntrustedConversationUI( + content::WebUI* web_ui) + : ui::MojoWebUIController(web_ui) { + // Create a URLDataSource and add resources. + content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd( + web_ui->GetWebContents()->GetBrowserContext(), + kAIChatUntrustedConversationUIURL); + webui::SetupWebUIDataSource(source, kAiChatUiGenerated, + IDR_AI_CHAT_UNTRUSTED_CONVERSATION_UI_HTML); + + for (const auto& str : ai_chat::GetLocalizedStrings()) { + source->AddString(str.name, + brave_l10n::GetLocalizedResourceUTF16String(str.id)); + } + + constexpr bool kIsMobile = BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS); + source->AddBoolean("isMobile", kIsMobile); + + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ScriptSrc, + "script-src 'self' chrome-untrusted://resources;"); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::StyleSrc, + "style-src 'self' 'unsafe-inline' chrome-untrusted://resources;"); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ImgSrc, + "img-src 'self' blob: chrome-untrusted://resources;"); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FontSrc, + "font-src 'self' chrome-untrusted://resources;"); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FrameAncestors, + base::StringPrintf("frame-ancestors %s;", kAIChatUIURL)); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::TrustedTypes, "trusted-types default;"); +} + +AIChatUntrustedConversationUI::~AIChatUntrustedConversationUI() = default; + +void AIChatUntrustedConversationUI::BindInterface( + mojo::PendingReceiver receiver) { + ui_handler_ = std::make_unique(web_ui(), std::move(receiver)); +} + +void AIChatUntrustedConversationUI::BindInterface( + mojo::PendingReceiver + receiver) { + // Get conversation from URL + std::string_view conversation_uuid = ai_chat::ConversationUUIDFromURL( + web_ui()->GetRenderFrameHost()->GetLastCommittedURL()); + DVLOG(2) << "Binding conversation frame for conversation uuid:" + << conversation_uuid; + if (conversation_uuid.empty()) { + return; + } + + ai_chat::AIChatService* service = + ai_chat::AIChatServiceFactory::GetForBrowserContext( + web_ui()->GetWebContents()->GetBrowserContext()); + + if (!service) { + return; + } + + service->GetConversation( + conversation_uuid, + base::BindOnce( + [](mojo::PendingReceiver + receiver, + ai_chat::ConversationHandler* conversation_handler) { + if (!conversation_handler) { + DVLOG(0) << "Failed to get conversation handler for conversation " + "entries frame"; + return; + } + conversation_handler->Bind(std::move(receiver)); + }, + std::move(receiver))); +} + +WEB_UI_CONTROLLER_TYPE_IMPL(AIChatUntrustedConversationUI) diff --git a/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.h b/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.h new file mode 100644 index 000000000000..b633d4968375 --- /dev/null +++ b/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.h @@ -0,0 +1,53 @@ +// 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_WEBUI_AI_CHAT_AI_CHAT_UNTRUSTED_CONVERSATION_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_AI_CHAT_AI_CHAT_UNTRUSTED_CONVERSATION_UI_H_ + +#include + +#include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom-forward.h" +#include "brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom-forward.h" +#include "content/public/browser/webui_config.h" +#include "ui/webui/mojo_web_ui_controller.h" + +// Determines in which context the untrusted conversation UI should be enabled +class AIChatUntrustedConversationUIConfig : public content::WebUIConfig { + public: + AIChatUntrustedConversationUIConfig(); + ~AIChatUntrustedConversationUIConfig() override; + + // content::WebUIConfig: + bool IsWebUIEnabled(content::BrowserContext* browser_context) override; + std::unique_ptr CreateWebUIController( + content::WebUI* web_ui, + const GURL& url) override; +}; + +// This Untrusted WebUI hosts the UI to display conversation entries, including +// ones generated by an LLM. It should not be granted more permissions than +// required to display the conversation entries. Anything requiring access +// to browser features should be done in the trusted UI. +class AIChatUntrustedConversationUI : public ui::MojoWebUIController { + public: + explicit AIChatUntrustedConversationUI(content::WebUI* web_ui); + AIChatUntrustedConversationUI(const AIChatUntrustedConversationUI&) = delete; + AIChatUntrustedConversationUI& operator=( + const AIChatUntrustedConversationUI&) = delete; + ~AIChatUntrustedConversationUI() override; + + void BindInterface( + mojo::PendingReceiver receiver); + void BindInterface( + mojo::PendingReceiver + receiver); + + private: + std::unique_ptr ui_handler_; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +#endif // BRAVE_BROWSER_UI_WEBUI_AI_CHAT_AI_CHAT_UNTRUSTED_CONVERSATION_UI_H_ diff --git a/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc index e4b18a05f36c..a1ba9fb5acec 100644 --- a/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc +++ b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc @@ -6,13 +6,19 @@ #include "src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc" #include "brave/browser/ai_chat/ai_chat_service_factory.h" +#include "brave/browser/ai_chat/ai_chat_urls.h" #include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h" #include "brave/components/ai_chat/core/browser/ai_chat_metrics.h" #include "brave/components/ai_chat/core/browser/ai_chat_service.h" +#include "brave/components/ai_chat/core/browser/conversation_handler.h" +#include "brave/components/ai_chat/core/common/features.h" #include "brave/components/ai_chat/core/common/pref_names.h" #include "brave/components/commander/common/buildflags/buildflags.h" #include "build/build_config.h" #include "chrome/browser/profiles/profile.h" +#include "content/public/browser/web_contents.h" +#include "ui/base/page_transition_types.h" +#include "ui/base/window_open_disposition.h" #if !BUILDFLAG(IS_ANDROID) #include "brave/browser/brave_browser_process.h" @@ -57,25 +63,42 @@ void ChromeAutocompleteProviderClient::OpenLeo(const std::u16string& query) { return; } - auto* chat_tab_helper = ai_chat::AIChatTabHelper::FromWebContents( - browser->tab_strip_model()->GetActiveWebContents()); - DCHECK(chat_tab_helper); - - auto* conversation_handler = - ai_chat_service->GetOrCreateConversationHandlerForContent( - chat_tab_helper->GetContentId(), chat_tab_helper->GetWeakPtr()); - CHECK(conversation_handler); - - // Before trying to activate the panel, unlink page content if needed. - // This needs to be called before activating the panel to check against the - // current state. - conversation_handler->MaybeUnlinkAssociatedContent(); - - // Activate the panel. - auto* sidebar_controller = - static_cast(browser)->sidebar_controller(); - sidebar_controller->ActivatePanelItem( - sidebar::SidebarItem::BuiltInItemType::kChatUI); + ai_chat::ConversationHandler* conversation_handler; + + if (ai_chat_service->IsAIChatHistoryEnabled() && + ai_chat::features::kOmniboxOpensFullPage.Get()) { + conversation_handler = ai_chat_service->CreateConversation(); + browser->OpenURL({ai_chat::ConversationUrl( + conversation_handler->get_conversation_uuid()), + content::Referrer(), WindowOpenDisposition::CURRENT_TAB, + ui::PageTransition::PAGE_TRANSITION_GENERATED, false}, + {}); + } else { + auto* chat_tab_helper = ai_chat::AIChatTabHelper::FromWebContents( + browser->tab_strip_model()->GetActiveWebContents()); + DCHECK(chat_tab_helper); + conversation_handler = + ai_chat_service->GetOrCreateConversationHandlerForContent( + chat_tab_helper->GetContentId(), chat_tab_helper->GetWeakPtr()); + if (!conversation_handler) { + return; + } + + // Before trying to activate the panel, unlink page content if needed. + // This needs to be called before activating the panel to check against the + // current state. + conversation_handler->MaybeUnlinkAssociatedContent(); + + // Activate the panel. + auto* sidebar_controller = + static_cast(browser)->sidebar_controller(); + sidebar_controller->ActivatePanelItem( + sidebar::SidebarItem::BuiltInItemType::kChatUI); + } + + if (!conversation_handler) { + return; + } // Send the query to the AIChat's backend. ai_chat::mojom::ConversationTurnPtr turn = diff --git a/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc b/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc index 6c2a2a6824be..b0b5a6593d8b 100644 --- a/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc +++ b/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc @@ -6,7 +6,7 @@ #include "chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.h" #include "base/feature_list.h" -#include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h" +#include "brave/browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.h" #include "brave/browser/ui/webui/brave_wallet/ledger/ledger_ui.h" #include "brave/browser/ui/webui/brave_wallet/line_chart/line_chart_ui.h" #include "brave/browser/ui/webui/brave_wallet/market/market_ui.h" @@ -68,6 +68,6 @@ void RegisterChromeUntrustedWebUIConfigs() { if (ai_chat::features::IsAIChatEnabled()) { content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( - std::make_unique()); + std::make_unique()); } } diff --git a/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc b/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc index d590d2ed34e7..91e43134b001 100644 --- a/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc +++ b/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc @@ -5,6 +5,8 @@ #include "chrome/browser/ui/webui/chrome_web_ui_configs.h" +#include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h" +#include "brave/components/ai_chat/core/common/features.h" #include "content/public/browser/webui_config_map.h" #define RegisterChromeWebUIConfigs RegisterChromeWebUIConfigs_ChromiumImpl @@ -79,4 +81,8 @@ void RegisterChromeWebUIConfigs() { #endif // !BUILDFLAG(IS_ANDROID) map.AddWebUIConfig(std::make_unique()); map.AddWebUIConfig(std::make_unique()); + + if (ai_chat::features::IsAIChatEnabled()) { + map.AddWebUIConfig(std::make_unique()); + } } 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 29cfd54ed3d6..d79992c0e213 100644 --- a/components/ai_chat/content/browser/ai_chat_tab_helper.cc +++ b/components/ai_chat/content/browser/ai_chat_tab_helper.cc @@ -207,6 +207,7 @@ void AIChatTabHelper::TitleWasSet(content::NavigationEntry* entry) { << " title=" << entry->GetTitle(); MaybeSameDocumentIsNewPage(); previous_page_title_ = GetPageTitle(); + OnTitleChanged(); } void AIChatTabHelper::InnerWebContentsAttached( diff --git a/components/ai_chat/content/browser/ai_chat_throttle.cc b/components/ai_chat/content/browser/ai_chat_throttle.cc index 6b8bdea40412..d0cef44790f5 100644 --- a/components/ai_chat/content/browser/ai_chat_throttle.cc +++ b/components/ai_chat/content/browser/ai_chat_throttle.cc @@ -25,27 +25,37 @@ namespace ai_chat { // static std::unique_ptr AiChatThrottle::MaybeCreateThrottleFor( content::NavigationHandle* navigation_handle) { - // The AI Chat WebUI won't be enabled if the feature is disabled + // The throttle's only purpose is to deny navigation in a Tab. + + // The AI Chat WebUI won't be enabled if the feature or policy is disabled + // (this is not checking a user preference). if (!ai_chat::IsAIChatEnabled(user_prefs::UserPrefs::Get( navigation_handle->GetWebContents()->GetBrowserContext()))) { return nullptr; } - // We don't need this throttle if the full-page feature is enabled via proxy - // of the AIChatHistory feature flag. - if (features::IsAIChatHistoryEnabled()) { + const GURL& url = navigation_handle->GetURL(); + + bool is_main_page_url = url.SchemeIs(content::kChromeUIScheme) && + url.host_piece() == kAIChatUIHost; + + // We allow main page navigation only if the full-page feature is enabled + // via the AIChatHistory feature flag. + if (is_main_page_url && features::IsAIChatHistoryEnabled()) { return nullptr; } - // We need this throttle to work only for chrome-untrusted://chat page - if (!navigation_handle->GetURL().SchemeIs( - content::kChromeUIUntrustedScheme) || - navigation_handle->GetURL().host_piece() != kChatUIHost) { + bool is_ai_chat_frame = + url.SchemeIs(content::kChromeUIUntrustedScheme) && + url.host_piece() == kAIChatUntrustedConversationUIHost; + + // We need this throttle to work only for AI Chat related URLs + if (!is_main_page_url && !is_ai_chat_frame) { return nullptr; } - // Purpose of this throttle is to forbid loading of chrome-untrusted://chat - // in tab. + // Purpose of this throttle is to forbid loading of chrome://leo-ai and + // related urls in tab. // Parameters check is made different for Android and Desktop because // there are different flags: // --------+---------------------------------+------------------------------ diff --git a/components/ai_chat/core/browser/ai_chat_service.h b/components/ai_chat/core/browser/ai_chat_service.h index e81f7cc4f697..80913c0985ba 100644 --- a/components/ai_chat/core/browser/ai_chat_service.h +++ b/components/ai_chat/core/browser/ai_chat_service.h @@ -161,6 +161,9 @@ class AIChatService : public KeyedService, bool HasUserOptedIn(); bool IsPremiumStatus(); + // Whether the feature and user preference for history storage is enabled + bool IsAIChatHistoryEnabled(); + std::unique_ptr GetDefaultAIEngine(); AIChatCredentialManager* GetCredentialManagerForTesting() { @@ -215,8 +218,6 @@ class AIChatService : public KeyedService, mojom::ServiceStatePtr BuildState(); void OnStateChanged(); - bool IsAIChatHistoryEnabled(); - raw_ptr model_service_; raw_ptr profile_prefs_; raw_ptr ai_chat_metrics_; diff --git a/components/ai_chat/core/browser/associated_content_driver.cc b/components/ai_chat/core/browser/associated_content_driver.cc index 8d54b9b4bcd9..fd8a51c3e47e 100644 --- a/components/ai_chat/core/browser/associated_content_driver.cc +++ b/components/ai_chat/core/browser/associated_content_driver.cc @@ -273,6 +273,12 @@ void AssociatedContentDriver::OnFaviconImageDataChanged() { } } +void AssociatedContentDriver::OnTitleChanged() { + for (auto& conversation : associated_conversations_) { + conversation->OnAssociatedContentTitleChanged(); + } +} + void AssociatedContentDriver::OnNewPage(int64_t navigation_id) { // This instance will now be used for different content so existing // conversations need to be disassociated. diff --git a/components/ai_chat/core/browser/associated_content_driver.h b/components/ai_chat/core/browser/associated_content_driver.h index 9f95dc73462a..34ea298fc362 100644 --- a/components/ai_chat/core/browser/associated_content_driver.h +++ b/components/ai_chat/core/browser/associated_content_driver.h @@ -110,6 +110,9 @@ class AssociatedContentDriver // Implementer should call this when the favicon for the content changes void OnFaviconImageDataChanged(); + // Implementer should call this when the title is updated + void OnTitleChanged(); + // Implementer should call this when the content is updated in a way that // will not be detected by the on-demand techniques used by GetPageContent. // For example for sites where GetPageContent does not read the live DOM but diff --git a/components/ai_chat/core/browser/conversation_handler.cc b/components/ai_chat/core/browser/conversation_handler.cc index 62bf21009328..898b40671860 100644 --- a/components/ai_chat/core/browser/conversation_handler.cc +++ b/components/ai_chat/core/browser/conversation_handler.cc @@ -51,7 +51,6 @@ #include "brave/components/ai_chat/core/browser/types.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/mojom/ai_chat.mojom-forward.h" #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h" #include "brave/components/api_request_helper/api_request_helper.h" #include "components/grit/brave_components_strings.h" @@ -269,6 +268,20 @@ void ConversationHandler::Bind( Bind(std::move(conversation_ui_handler)); } +void ConversationHandler::Bind( + mojo::PendingReceiver receiver) { + untrusted_receivers_.Add(this, std::move(receiver)); +} + +void ConversationHandler::BindUntrustedConversationUI( + mojo::PendingRemote + untrusted_conversation_ui_handler, + BindUntrustedConversationUICallback callback) { + untrusted_conversation_ui_handlers_.Add( + std::move(untrusted_conversation_ui_handler)); + std::move(callback).Run(GetStateForConversationEntries()); +} + void ConversationHandler::OnConversationMetadataUpdated() { // Pass the updated data to archive content if (archive_content_) { @@ -508,8 +521,9 @@ void ConversationHandler::GetState(GetStateCallback callback) { } void ConversationHandler::RateMessage(bool is_liked, - uint32_t turn_id, + const std::string& turn_uuid, RateMessageCallback callback) { + DVLOG(2) << __func__ << ": " << is_liked << ", " << turn_uuid; auto& model = GetCurrentModel(); // We only allow Leo models to be rated. @@ -517,33 +531,39 @@ void ConversationHandler::RateMessage(bool is_liked, const std::vector& history = chat_history_; - // TODO(petemill): Something more robust than relying on message index, - // and probably a message uuid. - uint32_t current_turn_id = turn_id + 1; - - if (current_turn_id <= history.size()) { - base::span history_slice = - base::span(history).first(current_turn_id); - - feedback_api_->SendRating( - is_liked, ai_chat_service_->IsPremiumStatus(), history_slice, - model.options->get_leo_model_options()->name, selected_language_, - base::BindOnce( - [](RateMessageCallback callback, APIRequestResult result) { - if (result.Is2XXResponseCode() && result.value_body().is_dict()) { - std::string id = - *result.value_body().GetDict().FindString("id"); - std::move(callback).Run(id); - return; - } - std::move(callback).Run(std::nullopt); - }, - std::move(callback))); + auto entry_it = + base::ranges::find(history, turn_uuid, &mojom::ConversationTurn::uuid); + if (entry_it == history.end()) { + std::move(callback).Run(std::nullopt); return; } - std::move(callback).Run(std::nullopt); + const size_t count = std::distance(history.begin(), entry_it) + 1; + + base::span history_slice = + base::span(history).first(count); + + feedback_api_->SendRating( + is_liked, ai_chat_service_->IsPremiumStatus(), history_slice, + model.options->get_leo_model_options()->name, selected_language_, + base::BindOnce( + [](RateMessageCallback callback, APIRequestResult result) { + if (result.Is2XXResponseCode() && result.value_body().is_dict()) { + const std::string* id_result = + result.value_body().GetDict().FindString("id"); + if (id_result) { + std::move(callback).Run(*id_result); + } else { + DLOG(ERROR) << "Failed to get rating ID"; + std::move(callback).Run(std::nullopt); + } + return; + } + DLOG(ERROR) << "Failed to send rating: " << result.response_code(); + std::move(callback).Run(std::nullopt); + }, + std::move(callback))); } void ConversationHandler::SendFeedback(const std::string& category, @@ -551,13 +571,15 @@ void ConversationHandler::SendFeedback(const std::string& category, const std::string& rating_id, bool send_hostname, SendFeedbackCallback callback) { + DVLOG(2) << __func__ << ": " << rating_id << ", " << send_hostname << ", " + << category << ", " << feedback; auto on_complete = base::BindOnce( [](SendFeedbackCallback callback, APIRequestResult result) { if (result.Is2XXResponseCode()) { std::move(callback).Run(true); return; } - + DLOG(ERROR) << "Failed to send feedback: " << result.response_code(); std::move(callback).Run(false); }, std::move(callback)); @@ -1049,6 +1071,10 @@ void ConversationHandler::OnFaviconImageDataChanged() { } } +void ConversationHandler::OnAssociatedContentTitleChanged() { + OnAssociatedContentInfoChanged(); +} + void ConversationHandler::OnUserOptedIn() { MaybePopPendingRequests(); MaybeFetchOrClearContentStagedConversation(); @@ -1508,6 +1534,7 @@ void ConversationHandler::OnModelDataChanged() { [](auto& model) { return model.Clone(); }); client->OnModelDataChanged(model_key_, std::move(models_copy)); } + OnStateForConversationEntriesChanged(); } void ConversationHandler::OnHistoryUpdate() { @@ -1516,6 +1543,9 @@ void ConversationHandler::OnHistoryUpdate() { for (auto& client : conversation_ui_handlers_) { client->OnConversationHistoryUpdate(); } + for (auto& client : untrusted_conversation_ui_handlers_) { + client->OnConversationHistoryUpdate(); + } } void ConversationHandler::OnConversationEntryRemoved( @@ -1611,12 +1641,37 @@ void ConversationHandler::BuildAssociatedContentInfo() { } } +mojom::ConversationEntriesStatePtr +ConversationHandler::GetStateForConversationEntries() { + auto& model = GetCurrentModel(); + bool is_leo_model = model.options->is_leo_model_options(); + + mojom::ConversationEntriesStatePtr entries_state = + mojom::ConversationEntriesState::New(); + entries_state->is_generating = IsRequestInProgress(); + entries_state->is_content_refined = is_content_refined_; + entries_state->is_leo_model = is_leo_model; + entries_state->content_used_percentage = + metadata_->associated_content->is_content_association_possible + ? std::make_optional( + metadata_->associated_content->content_used_percentage) + : std::nullopt; + // Can't submit if not a premium user and the model is premium-only + entries_state->can_submit_user_entries = + !IsRequestInProgress() && + (ai_chat_service_->IsPremiumStatus() || !is_leo_model || + model.options->get_leo_model_options()->access != + mojom::ModelAccess::PREMIUM); + return entries_state; +} + void ConversationHandler::OnAssociatedContentInfoChanged() { BuildAssociatedContentInfo(); for (auto& client : conversation_ui_handlers_) { client->OnAssociatedContentInfoChanged( metadata_->associated_content->Clone(), should_send_page_contents_); } + OnStateForConversationEntriesChanged(); } void ConversationHandler::OnClientConnectionChanged() { @@ -1648,6 +1703,9 @@ void ConversationHandler::OnAssociatedContentFaviconImageDataChanged() { for (auto& client : conversation_ui_handlers_) { client->OnFaviconImageDataChanged(); } + for (auto& client : untrusted_conversation_ui_handlers_) { + client->OnFaviconImageDataChanged(); + } } void ConversationHandler::OnSuggestedQuestionsChanged() { @@ -1662,6 +1720,7 @@ void ConversationHandler::OnSuggestedQuestionsChanged() { } void ConversationHandler::OnAPIRequestInProgressChanged() { + OnStateForConversationEntriesChanged(); for (auto& client : conversation_ui_handlers_) { client->OnAPIRequestInProgress(is_request_in_progress_); } @@ -1670,6 +1729,13 @@ void ConversationHandler::OnAPIRequestInProgressChanged() { } } +void ConversationHandler::OnStateForConversationEntriesChanged() { + auto entries_state = GetStateForConversationEntries(); + for (auto& client : untrusted_conversation_ui_handlers_) { + client->OnEntriesUIStateChanged(entries_state->Clone()); + } +} + } // namespace ai_chat #undef STARTER_PROMPT diff --git a/components/ai_chat/core/browser/conversation_handler.h b/components/ai_chat/core/browser/conversation_handler.h index 8036f52fdee0..8dfcd8a9d99b 100644 --- a/components/ai_chat/core/browser/conversation_handler.h +++ b/components/ai_chat/core/browser/conversation_handler.h @@ -33,6 +33,7 @@ #include "brave/components/ai_chat/core/browser/model_service.h" #include "brave/components/ai_chat/core/browser/text_embedder.h" #include "brave/components/ai_chat/core/browser/types.h" +#include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom-forward.h" #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom-shared.h" #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h" #include "mojo/public/cpp/bindings/pending_receiver.h" @@ -63,6 +64,7 @@ class AIChatCredentialManager; // messages to the conversation engine, handling the responses, and owning // the in-memory conversation history. class ConversationHandler : public mojom::ConversationHandler, + public mojom::UntrustedConversationHandler, public ModelService::Observer { public: // |invalidation_token| is an optional parameter that will be passed back on @@ -197,6 +199,12 @@ class ConversationHandler : public mojom::ConversationHandler, void Bind(mojo::PendingRemote conversation_ui_handler); void Bind(mojo::PendingReceiver receiver, mojo::PendingRemote conversation_ui_handler); + void Bind( + mojo::PendingReceiver receiver); + void BindUntrustedConversationUI( + mojo::PendingRemote + untrusted_conversation_ui_handler, + BindUntrustedConversationUICallback callback) override; void AddObserver(Observer* observer); void RemoveObserver(Observer* observer); @@ -229,7 +237,7 @@ class ConversationHandler : public mojom::ConversationHandler, void GetState(GetStateCallback callback) override; void GetConversationHistory(GetConversationHistoryCallback callback) override; void RateMessage(bool is_liked, - uint32_t turn_id, + const std::string& turn_uuid, RateMessageCallback callback) override; void SendFeedback(const std::string& category, const std::string& feedback, @@ -269,6 +277,7 @@ class ConversationHandler : public mojom::ConversationHandler, void AddSubmitSelectedTextError(const std::string& selected_text, mojom::ActionType action_type, mojom::APIError error); + void OnAssociatedContentTitleChanged(); void OnFaviconImageDataChanged(); void OnUserOptedIn(); @@ -345,6 +354,7 @@ class ConversationHandler : public mojom::ConversationHandler, void InitEngine(); void BuildAssociatedContentInfo(); + mojom::ConversationEntriesStatePtr GetStateForConversationEntries(); bool IsContentAssociationPossible(); int GetContentUsedPercentage(); void AddToConversationHistory(mojom::ConversationTurnPtr turn); @@ -402,6 +412,7 @@ class ConversationHandler : public mojom::ConversationHandler, void OnSelectedLanguageChanged(const std::string& selected_language); void OnAssociatedContentFaviconImageDataChanged(); void OnAPIRequestInProgressChanged(); + void OnStateForConversationEntriesChanged(); base::WeakPtr associated_content_delegate_; std::unique_ptr archive_content_; @@ -462,8 +473,10 @@ class ConversationHandler : public mojom::ConversationHandler, base::ObserverList observers_; mojo::ReceiverSet receivers_; - // TODO(petemill): Rename to ConversationUIHandler + mojo::ReceiverSet untrusted_receivers_; mojo::RemoteSet conversation_ui_handlers_; + mojo::RemoteSet + untrusted_conversation_ui_handlers_; base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/components/ai_chat/core/common/features.cc b/components/ai_chat/core/common/features.cc index 1b82c2f8fb30..fd7a18b2d532 100644 --- a/components/ai_chat/core/common/features.cc +++ b/components/ai_chat/core/common/features.cc @@ -25,6 +25,8 @@ const base::FeatureParam kFreemiumAvailable(&kAIChat, "is_freemium_available", true); const base::FeatureParam kAIChatSSE{&kAIChat, "ai_chat_sse", true}; +const base::FeatureParam kOmniboxOpensFullPage{ + &kAIChat, "omnibox_opens_full_page", true}; const base::FeatureParam kConversationAPIEnabled{ &kAIChat, "conversation_api", true}; const base::FeatureParam kAITemperature{&kAIChat, "temperature", 0.2}; diff --git a/components/ai_chat/core/common/features.h b/components/ai_chat/core/common/features.h index d77ace4194ce..8850a41c54ec 100644 --- a/components/ai_chat/core/common/features.h +++ b/components/ai_chat/core/common/features.h @@ -28,6 +28,8 @@ extern const base::FeatureParam kFreemiumAvailable; COMPONENT_EXPORT(AI_CHAT_COMMON) extern const base::FeatureParam kAIChatSSE; COMPONENT_EXPORT(AI_CHAT_COMMON) +extern const base::FeatureParam kOmniboxOpensFullPage; +COMPONENT_EXPORT(AI_CHAT_COMMON) extern const base::FeatureParam kConversationAPIEnabled; COMPONENT_EXPORT(AI_CHAT_COMMON) extern const base::FeatureParam kAITemperature; diff --git a/components/ai_chat/core/common/mojom/BUILD.gn b/components/ai_chat/core/common/mojom/BUILD.gn index b3e5bd6535d4..646ac80617bc 100644 --- a/components/ai_chat/core/common/mojom/BUILD.gn +++ b/components/ai_chat/core/common/mojom/BUILD.gn @@ -17,6 +17,7 @@ mojom_component("mojom") { "ai_chat.mojom", "page_content_extractor.mojom", "settings_helper.mojom", + "untrusted_frame.mojom", ] deps = [ diff --git a/components/ai_chat/core/common/mojom/ai_chat.mojom b/components/ai_chat/core/common/mojom/ai_chat.mojom index a10a1be1713c..b06c21f291c9 100644 --- a/components/ai_chat/core/common/mojom/ai_chat.mojom +++ b/components/ai_chat/core/common/mojom/ai_chat.mojom @@ -5,9 +5,9 @@ module ai_chat.mojom; +import "brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom"; import "mojo/public/mojom/base/time.mojom"; import "url/mojom/url.mojom"; -import "mojo/public/mojom/base/time.mojom"; enum CharacterType { HUMAN, @@ -360,7 +360,7 @@ interface AIChatUIHandler { OpenConversationFullPage(string conversation_uuid); OpenURL(url.mojom.Url url); - OpenLearnMoreAboutBraveSearchWithLeo(); + OpenModelSupportUrl(); GoPremium(); RefreshPremiumSession(); @@ -392,11 +392,12 @@ interface AIChatUIHandler { array? favicon_image_data); }; -// UI-side handler for whole AI Chat UI +// UI-side handler for messages from the browser WebUI interface ChatUI { // Notifies that the default conversation for the // panel has changed. e.g. Tab navigation with AIChat open in sidebar. OnNewDefaultConversation(); + OnChildFrameBound(pending_receiver receiver); }; struct ConversationState { @@ -413,6 +414,22 @@ struct ConversationState { // `OnConversationHistoryUpdate` is more intelligent (see TOOD in definition). }; +// State required to show the conversations entries UI block +struct ConversationEntriesState { + // Whether an answer generation is in progress + bool is_generating; + // Whether the current model is a built-in Leo model + bool is_leo_model; + // How much of the content has been used by the AI engine, percentage,or null + // if no content is associated. + uint32? content_used_percentage; + // Whether the content has been refined + bool is_content_refined; + // Whether the UI should represent that the user cannot submit new messages + // or edits to the conversation. + bool can_submit_user_entries; +}; + // Browser-side handler for a Conversation interface ConversationHandler { GetState() => (ConversationState conversation_state); @@ -458,7 +475,7 @@ interface ConversationHandler { // Send a user-rating for a chat // message. |turn_id| is the index of the message in the // specified conversation. - RateMessage(bool is_liked, uint32 turn_id) + RateMessage(bool is_liked, string turn_uuid) => (string? rating_id); SendFeedback( string category, @@ -466,9 +483,28 @@ interface ConversationHandler { string rating_id, bool send_hostname) => (bool is_success); }; -interface ConversationEntriesHandler { +// Browser-side handler for a Conversation's UI responsible for displaying +// untrusted content (e.g. content generated by the AI engine). +interface UntrustedConversationHandler { + BindUntrustedConversationUI( + pending_remote untrusted_ui) + => (ConversationEntriesState conversation_entries_state); + // Get all visible history entries, including in-progress responses GetConversationHistory() => (array conversation_history); + + ModifyConversation(uint32 turn_index, string new_text); +}; + +// Untrusted-UI-side handler for a Conversation, responsible for displaying +// content generated by the AI engine. +interface UntrustedConversationUI { + // TODO(petemill): Provide single entry that's been updated so that we don't + // need to fetch (and clone) all conversation entries each time text is added + // to the most recent entry. + OnConversationHistoryUpdate(); + OnEntriesUIStateChanged(ConversationEntriesState state); + OnFaviconImageDataChanged(); }; interface ConversationUI { diff --git a/components/ai_chat/core/common/mojom/untrusted_frame.mojom b/components/ai_chat/core/common/mojom/untrusted_frame.mojom new file mode 100644 index 000000000000..264d5e2c3c2c --- /dev/null +++ b/components/ai_chat/core/common/mojom/untrusted_frame.mojom @@ -0,0 +1,31 @@ +// 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/. + +module ai_chat.mojom; + +// Interfaces for communication between the untrusted content frame and both +// the Browser and the parent trusted UI frame. + +// Trusted WebUI-side handler for messages from the untrusted child frame +interface ParentUIFrame { + //