| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/sparky/sparky_manager_impl.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/constants/ash_switches.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/system/mahi/mahi_panel_widget.h" |
| #include "ash/system/mahi/mahi_ui_controller.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/location.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "base/values.h" |
| #include "chrome/browser/ash/crosapi/crosapi_ash.h" |
| #include "chrome/browser/ash/crosapi/crosapi_manager.h" |
| #include "chrome/browser/ash/mahi/mahi_browser_delegate_ash.h" |
| #include "chrome/browser/ash/sparky/sparky_delegate_impl.h" |
| #include "chromeos/ash/components/sparky/system_info_delegate_impl.h" |
| #include "chromeos/components/mahi/public/cpp/mahi_manager.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "chromeos/strings/grit/chromeos_strings.h" |
| #include "components/manta/features.h" |
| #include "components/manta/manta_service.h" |
| #include "components/manta/proto/sparky.pb.h" |
| #include "components/manta/sparky/sparky_context.h" |
| #include "components/manta/sparky/sparky_util.h" |
| #include "components/prefs/pref_service.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/views/widget/unique_widget_ptr.h" |
| |
| namespace { |
| |
| using chromeos::MahiResponseStatus; |
| using crosapi::mojom::MahiContextMenuActionType; |
| constexpr int kMaxConsecutiveTurns = 20; |
| constexpr base::TimeDelta kWaitBeforeAdditionalCall = base::Seconds(2); |
| |
| ash::MahiBrowserDelegateAsh* GetMahiBrowserDelgateAsh() { |
| auto* mahi_browser_delegate_ash = crosapi::CrosapiManager::Get() |
| ->crosapi_ash() |
| ->mahi_browser_delegate_ash(); |
| CHECK(mahi_browser_delegate_ash); |
| return mahi_browser_delegate_ash; |
| } |
| |
| } // namespace |
| namespace ash { |
| |
| SparkyManagerImpl::SparkyManagerImpl(Profile* profile, |
| manta::MantaService* manta_service) |
| : profile_(profile), |
| sparky_provider_(manta_service->CreateSparkyProvider( |
| std::make_unique<SparkyDelegateImpl>(profile), |
| std::make_unique<sparky::SystemInfoDelegateImpl>())), |
| timer_(std::make_unique<base::OneShotTimer>()) { |
| CHECK(manta::features::IsMantaServiceEnabled()); |
| } |
| |
| SparkyManagerImpl::~SparkyManagerImpl() = default; |
| |
| std::u16string SparkyManagerImpl::GetContentTitle() { |
| return u""; |
| } |
| |
| gfx::ImageSkia SparkyManagerImpl::GetContentIcon() { |
| return gfx::ImageSkia(); |
| } |
| |
| GURL SparkyManagerImpl::GetContentUrl() { |
| return current_page_info_->url; |
| } |
| |
| void SparkyManagerImpl::GetSummary(MahiSummaryCallback callback) { |
| GetMahiBrowserDelgateAsh()->GetContentFromClient( |
| current_page_info_->client_id, current_page_info_->page_id, |
| base::BindOnce(&SparkyManagerImpl::OnGetPageContentForSummary, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void SparkyManagerImpl::GetOutlines(MahiOutlinesCallback callback) { |
| std::vector<chromeos::MahiOutline> outlines; |
| std::move(callback).Run(outlines, MahiResponseStatus::kUnknownError); |
| } |
| |
| void SparkyManagerImpl::GoToOutlineContent(int outline_id) {} |
| |
| void SparkyManagerImpl::AnswerQuestionRepeating( |
| const std::u16string& question, |
| bool current_panel_content, |
| MahiAnswerQuestionCallbackRepeating callback) { |
| if (current_panel_content) { |
| // Add the current question to the dialog. |
| dialog_turns_.emplace_back(base::UTF16ToUTF8(question), manta::Role::kUser); |
| |
| auto sparky_context = std::make_unique<manta::SparkyContext>( |
| dialog_turns_, base::UTF16ToUTF8(current_panel_content_->page_content)); |
| sparky_context->server_url = ash::switches::ObtainSparkyServerUrl(); |
| sparky_context->page_url = current_page_info_->url.spec(); |
| sparky_context->files = sparky_provider_->GetFilesSummary(); |
| |
| RequestProviderWithQuestion(std::move(sparky_context), std::move(callback)); |
| return; |
| } |
| |
| GetMahiBrowserDelgateAsh()->GetContentFromClient( |
| current_page_info_->client_id, current_page_info_->page_id, |
| base::BindOnce(&SparkyManagerImpl::OnGetPageContentForQA, |
| weak_ptr_factory_.GetWeakPtr(), question, |
| std::move(callback))); |
| } |
| |
| void SparkyManagerImpl::GetSuggestedQuestion( |
| MahiGetSuggestedQuestionCallback callback) {} |
| |
| void SparkyManagerImpl::SetCurrentFocusedPageInfo( |
| crosapi::mojom::MahiPageInfoPtr info) { |
| GURL url_before_update = current_page_info_->url; |
| current_page_info_ = std::move(info); |
| bool did_url_change = |
| !url_before_update.EqualsIgnoringRef(current_page_info_->url); |
| |
| bool available = |
| current_page_info_->IsDistillable.value_or(false) && did_url_change; |
| NotifyRefreshAvailability(/*available=*/available); |
| } |
| |
| void SparkyManagerImpl::OnContextMenuClicked( |
| crosapi::mojom::MahiContextMenuRequestPtr context_menu_request) { |
| switch (context_menu_request->action_type) { |
| case MahiContextMenuActionType::kSummary: |
| case MahiContextMenuActionType::kOutline: |
| // TODO(b/318565610): Update the behaviour of kOutline. |
| ui_controller_.OpenMahiPanel( |
| context_menu_request->display_id, |
| context_menu_request->mahi_menu_bounds.has_value() |
| ? context_menu_request->mahi_menu_bounds.value() |
| : gfx::Rect()); |
| return; |
| case MahiContextMenuActionType::kQA: |
| ui_controller_.OpenMahiPanel( |
| context_menu_request->display_id, |
| context_menu_request->mahi_menu_bounds.has_value() |
| ? context_menu_request->mahi_menu_bounds.value() |
| : gfx::Rect()); |
| |
| // Ask question. |
| if (!context_menu_request->question) { |
| return; |
| } |
| |
| // When the user sends a question from the context menu, we treat it as |
| // the start of a new journey, so we set `current_panel_content` false. |
| ui_controller_.SendQuestion(context_menu_request->question.value(), |
| /*current_panel_content=*/false, |
| MahiUiController::QuestionSource::kMenuView); |
| return; |
| case MahiContextMenuActionType::kSettings: |
| // TODO(b/318565610): Update the behaviour of kSettings |
| return; |
| case MahiContextMenuActionType::kNone: |
| return; |
| } |
| } |
| |
| bool SparkyManagerImpl::IsEnabled() { |
| // TODO (b/333479467): Update with new pref for this feature. |
| return chromeos::features::IsSparkyEnabled() && |
| ash::switches::IsSparkySecretKeyMatched() && |
| Shell::Get()->session_controller()->GetActivePrefService()->GetBoolean( |
| ash::prefs::kHmrEnabled); |
| } |
| |
| void SparkyManagerImpl::SetMediaAppPDFFocused() {} |
| |
| void SparkyManagerImpl::NotifyRefreshAvailability(bool available) { |
| if (ui_controller_.IsMahiPanelOpen()) { |
| ui_controller_.NotifyRefreshAvailabilityChanged(available); |
| } |
| } |
| |
| void SparkyManagerImpl::OnGetPageContentForSummary( |
| MahiSummaryCallback callback, |
| crosapi::mojom::MahiPageContentPtr mahi_content_ptr) { |
| if (!mahi_content_ptr) { |
| std::move(callback).Run(u"summary text", |
| MahiResponseStatus::kContentExtractionError); |
| return; |
| } |
| |
| // Assign current panel content and clear the current panel QA |
| current_panel_content_ = std::move(mahi_content_ptr); |
| |
| latest_response_status_ = MahiResponseStatus::kUnknownError; |
| std::move(callback).Run(u"Couldn't get summary", latest_response_status_); |
| return; |
| } |
| |
| void SparkyManagerImpl::RequestProviderWithQuestion( |
| std::unique_ptr<manta::SparkyContext> sparky_context, |
| MahiAnswerQuestionCallbackRepeating callback) { |
| sparky_provider_->QuestionAndAnswer( |
| std::move(sparky_context), |
| base::BindOnce(&SparkyManagerImpl::OnSparkyProviderQAResponse, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void SparkyManagerImpl::OnSparkyProviderQAResponse( |
| MahiAnswerQuestionCallbackRepeating callback, |
| manta::MantaStatus status, |
| manta::DialogTurn* latest_turn) { |
| // Currently the history of dialogs will only refresh if the user closes the |
| // UI and then reopens it again. |
| // TODO (b/352651459): Add a refresh button to reset the dialog. |
| |
| if (status.status_code != manta::MantaStatusCode::kOk) { |
| latest_response_status_ = MahiResponseStatus::kUnknownError; |
| std::move(callback).Run(std::nullopt, latest_response_status_); |
| return; |
| } |
| |
| if (latest_turn) { |
| latest_response_status_ = MahiResponseStatus::kSuccess; |
| callback.Run(base::UTF8ToUTF16(latest_turn->message), |
| latest_response_status_); |
| |
| dialog_turns_.emplace_back(std::move(*latest_turn)); |
| |
| auto sparky_context = std::make_unique<manta::SparkyContext>( |
| dialog_turns_, base::UTF16ToUTF8(current_panel_content_->page_content)); |
| sparky_context->server_url = ash::switches::ObtainSparkyServerUrl(); |
| sparky_context->page_url = current_page_info_->url.spec(); |
| sparky_context->files = sparky_provider_->GetFilesSummary(); |
| CheckTurnLimit(); |
| |
| // If the latest action is not the final action from the server, then an |
| // additional request is made to the server. The last action must be of type |
| // kAllDone to prevent an additional call. |
| if (!latest_turn->actions.empty() && |
| (latest_turn->actions.back().type != manta::ActionType::kAllDone || |
| !latest_turn->actions.back().all_done)) { |
| timer_->Start( |
| FROM_HERE, kWaitBeforeAdditionalCall, |
| base::BindOnce(&SparkyManagerImpl::RequestProviderWithQuestion, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(sparky_context), callback)); |
| } |
| |
| } else { |
| latest_response_status_ = MahiResponseStatus::kCantFindOutputData; |
| std::move(callback).Run(std::nullopt, latest_response_status_); |
| } |
| } |
| |
| void SparkyManagerImpl::CheckTurnLimit() { |
| // If the size of the dialog does not exceed the turn limit then return. |
| if (dialog_turns_.size() < kMaxConsecutiveTurns) { |
| return; |
| } |
| // If the last action is already set to not made an additional server call |
| // then return. |
| if (dialog_turns_.back().actions.empty() || |
| dialog_turns_.back().actions.back().type != manta::ActionType::kAllDone || |
| dialog_turns_.back().actions.back().all_done == true) { |
| return; |
| } |
| // Iterate through the last n turns if any of them are from the user then |
| // return as the turn limit has not yet been reached. |
| for (int position = 1; position < kMaxConsecutiveTurns; ++position) { |
| auto turn = dialog_turns_.at(dialog_turns_.size() - kMaxConsecutiveTurns); |
| if (turn.role == manta::Role::kUser) { |
| return; |
| } |
| } |
| // Assign the last action as all done to prevent any additional calls to the |
| // server. |
| dialog_turns_.back().actions.back().all_done = true; |
| } |
| |
| void SparkyManagerImpl::OnGetPageContentForQA( |
| const std::u16string& question, |
| MahiAnswerQuestionCallbackRepeating callback, |
| crosapi::mojom::MahiPageContentPtr mahi_content_ptr) { |
| if (!mahi_content_ptr) { |
| std::move(callback).Run(std::nullopt, |
| MahiResponseStatus::kContentExtractionError); |
| return; |
| } |
| |
| // Assign current panel content and clear the current panel QA |
| current_panel_content_ = std::move(mahi_content_ptr); |
| |
| // Add the current question to the dialog. |
| dialog_turns_.emplace_back(base::UTF16ToUTF8(question), manta::Role::kUser); |
| |
| auto sparky_context = std::make_unique<manta::SparkyContext>( |
| dialog_turns_, base::UTF16ToUTF8(current_panel_content_->page_content)); |
| sparky_context->server_url = ash::switches::ObtainSparkyServerUrl(); |
| sparky_context->page_url = current_page_info_->url.spec(); |
| sparky_context->files = sparky_provider_->GetFilesSummary(); |
| |
| RequestProviderWithQuestion(std::move(sparky_context), std::move(callback)); |
| } |
| |
| // This function will never be called as Sparky uses a repeating callback to |
| // respond to the question rather than a once callback. |
| void SparkyManagerImpl::AnswerQuestion(const std::u16string& question, |
| bool current_panel_content, |
| MahiAnswerQuestionCallback callback) {} |
| |
| // Sparky allows for multi consecutive responses back from the server to |
| // complete the task requested by the user. |
| bool SparkyManagerImpl::AllowRepeatingAnswers() { |
| return true; |
| } |
| |
| void SparkyManagerImpl::OpenFeedbackDialog() {} |
| |
| } // namespace ash |