From 2fc11a2dcfa754855c9d8c83602e18d279e6ab05 Mon Sep 17 00:00:00 2001 From: Pete Miller Date: Wed, 11 Dec 2024 22:42:20 -0800 Subject: [PATCH] AI Chat becomes a trusted WebUI with an UntrustedWebUI frame for LLM-generated responses (#26855) * AI Chat becomes a trusted WebUI with an UntrustedWebUI frame for LLM-generated responses - changes Leo Url to chrome://leo-ai - untrusted frame content Url is chrome-untrusted://leo-ai-conversation-entries They use the same JS build to optimize bundle size, but do not share the same allowed mojom interfaces. The untrusted frame is limited to UntrustedConversationHandler to send calls to the browser and UntrustedConversationUI to receive calls from the browser. --- browser/ai_chat/BUILD.gn | 3 + browser/ai_chat/ai_chat_throttle_unittest.cc | 33 ++- browser/ai_chat/ai_chat_urls.cc | 29 +++ browser/ai_chat/ai_chat_urls.h | 29 +++ .../ai_chat/android/ai_chat_utils_android.cc | 5 +- browser/brave_content_browser_client.cc | 5 + .../settings/brave_overrides/settings_menu.ts | 2 +- browser/resources/settings/brave_routes.ts | 2 +- browser/resources/settings/sources.gni | 1 + browser/ui/BUILD.gn | 4 +- browser/ui/brave_pages.cc | 2 +- .../side_panel/brave_side_panel_utils.cc | 5 +- browser/ui/webui/ai_chat/ai_chat_ui.cc | 88 +++---- browser/ui/webui/ai_chat/ai_chat_ui.h | 15 +- .../webui/ai_chat/ai_chat_ui_page_handler.cc | 17 +- .../webui/ai_chat/ai_chat_ui_page_handler.h | 4 +- .../ai_chat_untrusted_conversation_ui.cc | 215 ++++++++++++++++++ .../ai_chat_untrusted_conversation_ui.h | 53 +++++ .../chrome_autocomplete_provider_client.cc | 61 +++-- .../webui/chrome_untrusted_web_ui_configs.cc | 4 +- .../browser/ui/webui/chrome_web_ui_configs.cc | 6 + .../content/browser/ai_chat_tab_helper.cc | 1 + .../content/browser/ai_chat_throttle.cc | 30 ++- .../ai_chat/core/browser/ai_chat_service.h | 5 +- .../core/browser/associated_content_driver.cc | 6 + .../core/browser/associated_content_driver.h | 3 + .../core/browser/conversation_handler.cc | 118 +++++++--- .../core/browser/conversation_handler.h | 17 +- components/ai_chat/core/common/features.cc | 2 + components/ai_chat/core/common/features.h | 2 + components/ai_chat/core/common/mojom/BUILD.gn | 1 + .../ai_chat/core/common/mojom/ai_chat.mojom | 46 +++- .../core/common/mojom/untrusted_frame.mojom | 31 +++ .../ai_chat/resources/{page => }/BUILD.gn | 19 +- .../resources/ai_chat_ui_resources.grdp | 9 + components/ai_chat/resources/common/api.ts | 36 +++ .../components/action_type_label/index.tsx | 40 ++-- .../action_type_label/style.module.scss | 0 components/ai_chat/resources/common/mojom.ts | 7 + .../ai_chat/resources/common/useAPIState.ts | 39 ++++ .../ai_chat/resources/page/ai_chat_ui.html | 5 +- .../resources/page/ai_chat_ui_resources.grdp | 5 - .../ai_chat/resources/page/api/index.ts | 90 +++----- components/ai_chat/resources/page/chat_ui.tsx | 82 ++++++- .../context_menu_assistant/index.tsx | 201 ---------------- .../components/feature_button_menu/index.tsx | 7 +- .../page/components/feedback_form/index.tsx | 18 +- .../page/components/header/index.tsx | 3 +- .../page/components/input_box/index.tsx | 10 +- .../resources/page/components/main/index.tsx | 129 ++++++++--- .../page/components/main/style.module.scss | 35 ++- .../page/components/model_intro/index.tsx | 9 +- .../components/suggested_question/index.tsx | 42 +++- .../suggested_question/style.module.scss | 28 ++- .../components/tools_button_menu/index.tsx | 9 +- .../ai_chat/resources/page/model_utils.ts | 4 +- .../page/state/active_chat_context.tsx | 15 +- .../resources/page/state/ai_chat_context.tsx | 83 +++---- .../page/state/conversation_context.tsx | 79 +++---- .../resources/page/state/useSendFeedback.ts | 121 ++++++++++ .../page/stories/components_panel.tsx | 194 +++++++++------- .../story_utils/ConversationEntries.tsx | 24 ++ .../page/stories/{ => story_utils}/actions.ts | 12 +- .../page/stories/{ => story_utils}/locale.ts | 2 +- components/ai_chat/resources/page/styles.css | 7 + .../components/assistant_response/index.tsx | 13 +- .../assistant_response/style.module.scss | 0 .../components/code_block/index.tsx | 0 .../components/code_block/style.module.scss | 0 .../context_menu_assistant/index.tsx | 104 +++++++++ .../context_menu_assistant/style.module.scss | 0 .../components/conversation_entries/index.tsx | 129 +++-------- .../conversation_entries/style.module.scss | 19 -- .../components/copy_button/index.tsx | 0 .../components/copy_button/style.module.scss | 0 .../components/edit_button/index.tsx | 0 .../components/edit_button/style.module.scss | 0 .../components/edit_indicator/index.tsx | 0 .../edit_indicator/style.module.scss | 0 .../components/edit_input/index.tsx | 0 .../components/edit_input/style.module.scss | 0 .../components/markdown_renderer/index.tsx | 0 .../markdown_renderer/style.module.scss | 0 .../page_context_message}/long_page_info.tsx | 23 +- .../page_context_message/style.module.scss | 14 ++ .../components/quote/index.tsx | 0 .../components/quote/style.module.scss | 0 .../components/svg/caret.tsx | 0 .../untrusted_conversation_frame/styles.css | 20 ++ .../tsconfig.json | 9 + .../untrusted_conversation_context.tsx | 49 ++++ .../untrusted_conversation_frame.html | 27 +++ .../untrusted_conversation_frame.tsx | 29 +++ .../untrusted_conversation_frame_api.ts | 89 ++++++++ components/constants/webui_url_constants.h | 8 +- components/resources/BUILD.gn | 4 +- .../resources/brave_components_resources.grd | 2 +- .../AIChat/Components/AIChatView.swift | 8 +- .../AIChat/ModelView/AIChatViewModel.swift | 2 +- ios/browser/api/ai_chat/BUILD.gn | 12 +- ios/browser/api/ai_chat/ai_chat.h | 7 +- ios/browser/api/ai_chat/ai_chat.mm | 7 +- ios/browser/api/ai_chat/headers.gni | 1 + 103 files changed, 1941 insertions(+), 844 deletions(-) create mode 100644 browser/ai_chat/ai_chat_urls.cc create mode 100644 browser/ai_chat/ai_chat_urls.h create mode 100644 browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.cc create mode 100644 browser/ui/webui/ai_chat/ai_chat_untrusted_conversation_ui.h create mode 100644 components/ai_chat/core/common/mojom/untrusted_frame.mojom rename components/ai_chat/resources/{page => }/BUILD.gn (67%) create mode 100644 components/ai_chat/resources/ai_chat_ui_resources.grdp create mode 100644 components/ai_chat/resources/common/api.ts rename components/ai_chat/resources/{page => common}/components/action_type_label/index.tsx (81%) rename components/ai_chat/resources/{page => common}/components/action_type_label/style.module.scss (100%) create mode 100644 components/ai_chat/resources/common/mojom.ts create mode 100644 components/ai_chat/resources/common/useAPIState.ts delete mode 100644 components/ai_chat/resources/page/ai_chat_ui_resources.grdp delete mode 100644 components/ai_chat/resources/page/components/context_menu_assistant/index.tsx create mode 100644 components/ai_chat/resources/page/state/useSendFeedback.ts create mode 100644 components/ai_chat/resources/page/stories/story_utils/ConversationEntries.tsx rename components/ai_chat/resources/page/stories/{ => story_utils}/actions.ts (72%) rename components/ai_chat/resources/page/stories/{ => story_utils}/locale.ts (99%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/assistant_response/index.tsx (89%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/assistant_response/style.module.scss (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/code_block/index.tsx (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/code_block/style.module.scss (100%) create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/components/context_menu_assistant/index.tsx rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/context_menu_assistant/style.module.scss (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/conversation_entries/index.tsx (60%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/conversation_entries/style.module.scss (86%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/copy_button/index.tsx (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/copy_button/style.module.scss (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/edit_button/index.tsx (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/edit_button/style.module.scss (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/edit_indicator/index.tsx (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/edit_indicator/style.module.scss (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/edit_input/index.tsx (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/edit_input/style.module.scss (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/markdown_renderer/index.tsx (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/markdown_renderer/style.module.scss (100%) rename components/ai_chat/resources/{page/components/alerts => untrusted_conversation_frame/components/page_context_message}/long_page_info.tsx (58%) create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/components/page_context_message/style.module.scss rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/quote/index.tsx (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/quote/style.module.scss (100%) rename components/ai_chat/resources/{page => untrusted_conversation_frame}/components/svg/caret.tsx (100%) create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/styles.css create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/tsconfig.json create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/untrusted_conversation_context.tsx create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/untrusted_conversation_frame.html create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/untrusted_conversation_frame.tsx create mode 100644 components/ai_chat/resources/untrusted_conversation_frame/untrusted_conversation_frame_api.ts 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 { + //