// 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