blob: 40cd78a29d18dd0f9bc0b2c5b8aff14cd116c8d7 [file] [log] [blame]
// Copyright 2023 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/compose/compose_session.h"
#include <memory>
#include <string>
#include <utility>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "chrome/browser/content_extraction/inner_text.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/common/compose/type_conversions.h"
#include "chrome/common/webui_url_constants.h"
#include "components/autofill/core/common/form_field_data.h"
#include "components/compose/core/browser/compose_features.h"
#include "components/compose/core/browser/compose_manager_impl.h"
#include "components/compose/core/browser/compose_metrics.h"
#include "components/compose/core/browser/compose_utils.h"
#include "components/compose/core/browser/config.h"
#include "components/optimization_guide/core/model_execution/feature_keys.h"
#include "components/optimization_guide/core/model_execution/optimization_guide_model_execution_error.h"
#include "components/optimization_guide/core/model_quality/feature_type_map.h"
#include "components/optimization_guide/core/optimization_guide_features.h"
#include "components/optimization_guide/core/optimization_guide_model_executor.h"
#include "components/optimization_guide/core/optimization_guide_util.h"
#include "components/optimization_guide/proto/features/compose.pb.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/web_contents_user_data.h"
#include "content/public/common/referrer.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/gfx/geometry/rect_f.h"
namespace {
bool IsValidComposePrompt(const std::string& prompt) {
const compose::Config& config = compose::GetComposeConfig();
if (prompt.length() > config.input_max_chars) {
return false;
}
return compose::IsWordCountWithinBounds(prompt, config.input_min_words,
config.input_max_words);
}
const char kComposeBugReportURL[] = "https://goto.google.com/ccbrfd";
const char kOnDeviceComposeBugReportURL[] = "https://goto.google.com/ccbrfdod";
const char kComposeLearnMorePageURL[] =
"https://support.google.com/chrome?p=help_me_write";
const char kComposeFeedbackSurveyURL[] = "https://goto.google.com/ccfsfd";
const char kSignInPageURL[] = "https://accounts.google.com";
const char kOnDeviceComposeFeedbackSurveyURL[] =
"https://goto.google.com/ccfsfdod";
compose::EvalLocation GetEvalLocation(
const optimization_guide::OptimizationGuideModelStreamingExecutionResult&
result) {
return result.provided_by_on_device ? compose::EvalLocation::kOnDevice
: compose::EvalLocation::kServer;
}
} // namespace
// The state of a compose session. This currently includes the model quality log
// entry, and the mojo based compose state.
class ComposeState {
public:
ComposeState() {
modeling_log_entry_ = nullptr;
mojo_state_ = nullptr;
is_user_edited_ = false;
original_response_ = "";
}
ComposeState(std::unique_ptr<optimization_guide::ModelQualityLogEntry>
modeling_log_entry,
compose::mojom::ComposeStatePtr mojo_state,
bool is_user_edited,
std::string original_response) {
modeling_log_entry_ = std::move(modeling_log_entry);
mojo_state_ = std::move(mojo_state);
is_user_edited_ = is_user_edited;
original_response_ = original_response;
}
~ComposeState() = default;
bool IsMojoValid() {
return (mojo_state_ && mojo_state_->response &&
mojo_state_->response->status ==
compose::mojom::ComposeStatus::kOk &&
mojo_state_->response->result != "");
}
optimization_guide::ModelQualityLogEntry* modeling_log_entry() {
return modeling_log_entry_.get();
}
std::unique_ptr<optimization_guide::ModelQualityLogEntry>
TakeModelingLogEntry() {
auto to_return = std::move(modeling_log_entry_);
return to_return;
}
void SetModelingLogEntry(
std::unique_ptr<optimization_guide::ModelQualityLogEntry>
modeling_log_entry) {
modeling_log_entry_ = std::move(modeling_log_entry);
}
compose::mojom::ComposeState* mojo_state() { return mojo_state_.get(); }
compose::mojom::ComposeStatePtr TakeMojoState() {
auto to_return = std::move(mojo_state_);
return to_return;
}
bool is_user_edited() { return is_user_edited_; }
std::string original_response() { return original_response_; }
void SetUserEdited(std::string original_response) {
original_response_ = original_response;
is_user_edited_ = true;
}
void UnsetUserEdited() {
original_response_ = "";
is_user_edited_ = false;
}
void SetMojoState(compose::mojom::ComposeStatePtr mojo_state) {
mojo_state_ = std::move(mojo_state);
}
void UploadModelQualityLogs(
raw_ptr<optimization_guide::ModelQualityLogsUploader> logs_uploader) {
if (!logs_uploader || !modeling_log_entry_) {
return;
}
LogRequestFeedback();
logs_uploader->UploadModelQualityLogs(TakeModelingLogEntry());
}
void LogRequestFeedback() {
if (!mojo_state_ || !mojo_state_->response) {
// No request or modeling information so nothing to report.
return;
}
if (mojo_state_->response->status != compose::mojom::ComposeStatus::kOk) {
// Request Feedback was already reported when error was received.
return;
}
compose::EvalLocation eval_location =
mojo_state_->response->on_device_evaluation_used
? compose::EvalLocation::kOnDevice
: compose::EvalLocation::kServer;
compose::ComposeRequestFeedback feedback;
switch (mojo_state_->feedback) {
case compose::mojom::UserFeedback::kUserFeedbackPositive:
feedback = compose::ComposeRequestFeedback::kPositiveFeedback;
break;
case compose::mojom::UserFeedback::kUserFeedbackNegative:
feedback = compose::ComposeRequestFeedback::kNegativeFeedback;
break;
case compose::mojom::UserFeedback::kUserFeedbackUnspecified:
feedback = compose::ComposeRequestFeedback::kNoFeedback;
break;
}
compose::LogComposeRequestFeedback(eval_location, feedback);
}
private:
std::unique_ptr<optimization_guide::ModelQualityLogEntry> modeling_log_entry_;
compose::mojom::ComposeStatePtr mojo_state_;
std::string original_response_;
bool is_user_edited_;
};
ComposeSession::ComposeSession(
content::WebContents* web_contents,
optimization_guide::OptimizationGuideModelExecutor* executor,
optimization_guide::ModelQualityLogsUploader* model_quality_logs_uploader,
base::Token session_id,
InnerTextProvider* inner_text,
autofill::FieldRendererId node_id,
ComposeCallback callback)
: executor_(executor),
handler_receiver_(this),
current_msbb_state_(false),
msbb_initially_off_(false),
msbb_close_reason_(
compose::ComposeMSBBSessionCloseReason::kMSBBEndedImplicitly),
fre_close_reason_(
compose::ComposeFirstRunSessionCloseReason::kEndedImplicitly),
close_reason_(compose::ComposeSessionCloseReason::kEndedImplicitly),
final_status_(optimization_guide::proto::FinalStatus::STATUS_UNSPECIFIED),
web_contents_(web_contents),
collect_inner_text_(
base::FeatureList::IsEnabled(compose::features::kComposeInnerText)),
inner_text_caller_(inner_text),
ukm_source_id_(web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId()),
node_id_(node_id),
model_quality_logs_uploader_(model_quality_logs_uploader),
session_id_(session_id),
weak_ptr_factory_(this) {
session_duration_ = std::make_unique<base::ElapsedTimer>();
callback_ = std::move(callback);
current_state_ = compose::mojom::ComposeState::New();
most_recent_ok_state_ = std::make_unique<ComposeState>();
if (executor_) {
session_ = executor_->StartSession(
optimization_guide::ModelBasedCapabilityKey::kCompose,
/*config_params=*/std::nullopt);
}
}
ComposeSession::~ComposeSession() {
std::optional<compose::EvalLocation> eval_location =
compose::GetEvalLocationFromEvents(session_events_);
if (session_events_.fre_dialog_shown_count > 0 &&
(!fre_complete_ || session_events_.fre_completed_in_session)) {
compose::LogComposeFirstRunSessionCloseReason(fre_close_reason_);
compose::LogComposeFirstRunSessionDialogShownCount(
fre_close_reason_, session_events_.fre_dialog_shown_count);
if (!fre_complete_) {
compose::LogComposeSessionDuration(session_duration_->Elapsed(), ".FRE");
return;
}
}
if (session_events_.msbb_dialog_shown_count > 0 &&
(!current_msbb_state_ || session_events_.msbb_enabled_in_session)) {
compose::LogComposeMSBBSessionDialogShownCount(
msbb_close_reason_, session_events_.msbb_dialog_shown_count);
compose::LogComposeMSBBSessionCloseReason(msbb_close_reason_);
if (!current_msbb_state_) {
compose::LogComposeSessionDuration(session_duration_->Elapsed(), ".MSBB");
return;
}
}
if (session_events_.dialog_shown_count < 1) {
// Do not report any further metrics if the dialog was never shown.
// This is mostly like because the session was the debug session but
// could occur if the tab closes while Compose is opening.
return;
}
if (session_events_.inserted_results) {
compose::LogComposeSessionDuration(session_duration_->Elapsed(),
".Inserted", eval_location);
} else {
compose::LogComposeSessionDuration(session_duration_->Elapsed(), ".Ignored",
eval_location);
}
if (close_reason_ == compose::ComposeSessionCloseReason::kEndedImplicitly) {
base::RecordAction(
base::UserMetricsAction("Compose.EndedSession.EndedImplicitly"));
final_status_ =
optimization_guide::proto::FinalStatus::STATUS_FINISHED_WITHOUT_INSERT;
}
LogComposeSessionCloseMetrics(close_reason_, session_events_);
LogComposeSessionCloseUkmMetrics(ukm_source_id_, session_events_);
// Quality log would automatically be uploaded on the destruction of
// a modeling_log_entry. However in order to more easily test the quality
// uploads we are calling upload directly here.
if (!model_quality_logs_uploader_) {
// Can not upload any logs so exit early.
return;
}
if (most_recent_error_log_) {
// First set final status on most_recent_error_log
most_recent_error_log_
->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->set_final_status(final_status_);
model_quality_logs_uploader_->UploadModelQualityLogs(
std::move(most_recent_error_log_));
} else if (most_recent_ok_state_->modeling_log_entry()) {
// First set final status on most_recent_ok_state_.
most_recent_ok_state_->modeling_log_entry()
->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->set_final_status(final_status_);
most_recent_ok_state_->UploadModelQualityLogs(model_quality_logs_uploader_);
}
// Explicitly upload the rest of the undo stack.
while (!undo_states_.empty()) {
undo_states_.top()->UploadModelQualityLogs(model_quality_logs_uploader_);
undo_states_.pop();
}
}
void ComposeSession::Bind(
mojo::PendingReceiver<compose::mojom::ComposeSessionUntrustedPageHandler>
handler,
mojo::PendingRemote<compose::mojom::ComposeUntrustedDialog> dialog) {
handler_receiver_.reset();
handler_receiver_.Bind(std::move(handler));
dialog_remote_.reset();
dialog_remote_.Bind(std::move(dialog));
}
// TODO(b/f3213db859d47): Add histogram test for Sessions triggering CancelEdit.
void ComposeSession::LogCancelEdit() {
session_events_.did_click_cancel_on_edit = true;
}
// ComposeSessionUntrustedPageHandler
void ComposeSession::Compose(const std::string& input, bool is_input_edited) {
compose::ComposeRequestReason request_reason;
if (is_input_edited) {
session_events_.update_input_count += 1;
request_reason = compose::ComposeRequestReason::kUpdateRequest;
} else {
base::RecordAction(
base::UserMetricsAction("Compose.ComposeRequest.CreateClicked"));
request_reason = compose::ComposeRequestReason::kFirstRequest;
}
optimization_guide::proto::ComposeRequest request;
request.mutable_generate_params()->set_user_input(input);
MakeRequest(std::move(request), request_reason, is_input_edited);
}
void ComposeSession::Rewrite(compose::mojom::StyleModifiersPtr style) {
compose::ComposeRequestReason request_reason;
optimization_guide::proto::ComposeRequest request;
if (style && style->is_tone()) {
request.mutable_rewrite_params()->set_tone(
optimization_guide::proto::ComposeTone(style->get_tone()));
if (style->get_tone() == compose::mojom::Tone::kFormal) {
session_events_.formal_count++;
request_reason = compose::ComposeRequestReason::kToneFormalRequest;
} else {
session_events_.casual_count++;
request_reason = compose::ComposeRequestReason::kToneCasualRequest;
}
} else if (style && style->is_length()) {
request.mutable_rewrite_params()->set_length(
optimization_guide::proto::ComposeLength(style->get_length()));
if (style->get_length() == compose::mojom::Length::kLonger) {
session_events_.lengthen_count++;
request_reason = compose::ComposeRequestReason::kLengthElaborateRequest;
} else {
session_events_.shorten_count++;
request_reason = compose::ComposeRequestReason::kLengthShortenRequest;
}
} else {
request.mutable_rewrite_params()->set_regenerate(true);
session_events_.regenerate_count++;
request_reason = compose::ComposeRequestReason::kRetryRequest;
}
request.mutable_rewrite_params()->set_previous_response(
most_recent_ok_state_->mojo_state()->response->result);
MakeRequest(std::move(request), request_reason, false);
}
// TODO(b/300974056): Add histogram test for Sessions triggering EditInput.
void ComposeSession::LogEditInput() {
session_events_.did_click_edit = true;
}
void ComposeSession::MakeRequest(
optimization_guide::proto::ComposeRequest request,
compose::ComposeRequestReason request_reason,
bool is_input_edited) {
current_state_->has_pending_request = true;
current_state_->feedback =
compose::mojom::UserFeedback::kUserFeedbackUnspecified;
// TODO(b/300974056): Move this to the overall feature-enabled check.
if (!session_ ||
!base::FeatureList::IsEnabled(
optimization_guide::features::kOptimizationGuideModelExecution)) {
ProcessError(compose::EvalLocation::kServer,
compose::mojom::ComposeStatus::kMisconfiguration,
request_reason);
return;
}
// Increase compose count regardless of status of request.
session_events_.compose_count += 1;
if (!collect_inner_text_ || got_inner_text_) {
RequestWithSession(std::move(request), request_reason, is_input_edited);
} else {
// Prepare the compose call, which will be invoked when inner text
// extraction is completed.
continue_compose_ = base::BindOnce(
&ComposeSession::RequestWithSession, weak_ptr_factory_.GetWeakPtr(),
std::move(request), request_reason, is_input_edited);
}
}
void ComposeSession::RequestWithSession(
const optimization_guide::proto::ComposeRequest& request,
compose::ComposeRequestReason request_reason,
bool is_input_edited) {
if (!collect_inner_text_) {
// Make sure context is added for sessions with no inner text.
AddPageContentToSession("", std::nullopt);
}
// Add timeout for high latency Compose requests.
const compose::Config& config = compose::GetComposeConfig();
base::ElapsedTimer request_timer;
request_id_++;
auto timeout = std::make_unique<base::OneShotTimer>();
timeout->Start(FROM_HERE,
base::Seconds(config.request_latency_timeout_seconds),
base::BindOnce(&ComposeSession::ComposeRequestTimeout,
base::Unretained(this), request_id_));
request_timeouts_.emplace(request_id_, std::move(timeout));
// Record the eval_location independent request metrics before model
// execution in case request fails.
compose::LogComposeRequestReason(request_reason);
session_->ExecuteModel(
request, base::BindRepeating(&ComposeSession::ModelExecutionCallback,
weak_ptr_factory_.GetWeakPtr(),
std::move(request_timer), request_id_,
request_reason, is_input_edited));
}
void ComposeSession::ComposeRequestTimeout(int id) {
request_timeouts_.erase(id);
compose::LogComposeRequestStatus(
compose::mojom::ComposeStatus::kRequestTimeout);
current_state_->has_pending_request = false;
current_state_->response = compose::mojom::ComposeResponse::New();
current_state_->response->status =
compose::mojom::ComposeStatus::kRequestTimeout;
if (dialog_remote_.is_bound()) {
dialog_remote_->ResponseReceived(current_state_->response->Clone());
}
}
void ComposeSession::ModelExecutionCallback(
const base::ElapsedTimer& request_timer,
int request_id,
compose::ComposeRequestReason request_reason,
bool was_input_edited,
optimization_guide::OptimizationGuideModelStreamingExecutionResult result) {
base::TimeDelta request_delta = request_timer.Elapsed();
compose::EvalLocation eval_location = GetEvalLocation(result);
// Presence of the timer with the corresponding `request_id` indicates that
// the request has not timed out - process the response. Otherwise ignore the
// response.
if (auto iter = request_timeouts_.find(request_id);
iter != request_timeouts_.end()) {
iter->second->Stop();
// If a partial response was received, then this callback may be reused.
// Only remove the associated timer if the response is complete.
if (result.response.has_value() && result.response->is_complete) {
request_timeouts_.erase(request_id);
}
} else {
SetQualityLogEntryUponError(std::move(result.log_entry), request_delta,
was_input_edited);
compose::LogComposeRequestReason(eval_location, request_reason);
compose::LogComposeRequestStatus(
eval_location, compose::mojom::ComposeStatus::kRequestTimeout);
return;
}
// A new request has been issued, ignore this one.
if (request_id != request_id_) {
SetQualityLogEntryUponError(std::move(result.log_entry), request_delta,
was_input_edited);
compose::LogComposeRequestReason(eval_location, request_reason);
return;
}
if (result.response.has_value() && !result.response->is_complete) {
ModelExecutionProgress(std::move(result.response).value());
return;
}
ModelExecutionComplete(request_delta, request_reason, was_input_edited,
std::move(result));
}
void ComposeSession::ModelExecutionProgress(
optimization_guide::StreamingResponse result) {
CHECK(base::FeatureList::IsEnabled(
optimization_guide::features::kOptimizationGuideOnDeviceModel));
if (!base::FeatureList::IsEnabled(
compose::features::kComposeTextOutputAnimation)) {
return;
}
if (!dialog_remote_.is_bound()) {
return;
}
auto response = optimization_guide::ParsedAnyMetadata<
optimization_guide::proto::ComposeResponse>(result.response);
if (!response) {
DLOG(ERROR) << "Failed to parse partial compose response";
return;
}
auto partial_ui_response = compose::mojom::PartialComposeResponse::New();
partial_ui_response->result = response->output();
dialog_remote_->PartialResponseReceived(std::move(partial_ui_response));
}
void ComposeSession::ModelExecutionComplete(
base::TimeDelta request_delta,
compose::ComposeRequestReason request_reason,
bool was_input_edited,
optimization_guide::OptimizationGuideModelStreamingExecutionResult result) {
// Handle 'complete' results.
current_state_->has_pending_request = false;
compose::EvalLocation eval_location = GetEvalLocation(result);
if (eval_location == compose::EvalLocation::kOnDevice) {
++session_events_.on_device_responses;
} else {
++session_events_.server_responses;
}
compose::LogComposeRequestReason(eval_location, request_reason);
compose::mojom::ComposeStatus status =
ComposeStatusFromOptimizationGuideResult(result);
if (status != compose::mojom::ComposeStatus::kOk) {
compose::LogComposeRequestDuration(request_delta, eval_location,
/* is_ok */ false);
if (content::GetNetworkConnectionTracker()->IsOffline()) {
ProcessError(eval_location, compose::mojom::ComposeStatus::kOffline,
request_reason);
} else {
ProcessError(eval_location, status, request_reason);
}
SetQualityLogEntryUponError(std::move(result.log_entry), request_delta,
was_input_edited);
return;
}
DCHECK(result.response->is_complete);
auto response = optimization_guide::ParsedAnyMetadata<
optimization_guide::proto::ComposeResponse>(result.response->response);
if (!response) {
compose::LogComposeRequestDuration(request_delta, eval_location,
/* is_ok */ false);
ProcessError(eval_location, compose::mojom::ComposeStatus::kNoResponse,
request_reason);
SetQualityLogEntryUponError(std::move(result.log_entry), request_delta,
was_input_edited);
return;
}
auto ui_response = compose::mojom::ComposeResponse::New();
ui_response->status = compose::mojom::ComposeStatus::kOk;
ui_response->result = response->output();
ui_response->on_device_evaluation_used = result.provided_by_on_device;
current_state_->response = ui_response->Clone();
// Log successful response status.
compose::LogComposeRequestStatus(compose::mojom::ComposeStatus::kOk);
compose::LogComposeRequestStatus(eval_location,
compose::mojom::ComposeStatus::kOk);
compose::LogComposeRequestDuration(request_delta, eval_location,
/* is_ok */ true);
SaveMostRecentOkStateToUndoStack();
current_state_->response->undo_available = !undo_states_.empty();
most_recent_ok_state_->SetMojoState(current_state_->Clone());
most_recent_ok_state_->UnsetUserEdited();
ui_response->undo_available = !undo_states_.empty();
if (dialog_remote_.is_bound()) {
dialog_remote_->ResponseReceived(std::move(ui_response));
}
if (result.log_entry) {
result.log_entry->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->set_was_generated_via_edit(was_input_edited);
result.log_entry->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->set_request_latency_ms(request_delta.InMilliseconds());
optimization_guide::proto::Int128* token =
result.log_entry
->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->mutable_session_id();
token->set_high(session_id_.high());
token->set_low(session_id_.low());
most_recent_ok_state_->SetModelingLogEntry(std::move(result.log_entry));
// In the event that we are holding onto an error log upload it before it
// gets overwritten
if (most_recent_error_log_ && model_quality_logs_uploader_) {
model_quality_logs_uploader_->UploadModelQualityLogs(
std::move(most_recent_error_log_));
}
// if we have a valid most recent state we no longer need an error state.
most_recent_error_log_.reset();
}
}
void ComposeSession::ProcessError(
compose::EvalLocation eval_location,
compose::mojom::ComposeStatus error,
compose::ComposeRequestReason request_reason) {
compose::LogComposeRequestStatus(error);
compose::LogComposeRequestStatus(eval_location, error);
// Feedback can not be given for a request with an error so report now.
compose::LogComposeRequestFeedback(
eval_location, compose::ComposeRequestFeedback::kRequestError);
current_state_->has_pending_request = false;
current_state_->response = compose::mojom::ComposeResponse::New();
current_state_->response->status = error;
current_state_->response->triggered_from_modifier =
request_reason != compose::ComposeRequestReason::kFirstRequest &&
request_reason != compose::ComposeRequestReason::kUpdateRequest;
if (dialog_remote_.is_bound()) {
dialog_remote_->ResponseReceived(current_state_->response->Clone());
}
}
void ComposeSession::RequestInitialState(RequestInitialStateCallback callback) {
auto compose_config = compose::GetComposeConfig();
std::move(callback).Run(compose::mojom::OpenMetadata::New(
fre_complete_, current_msbb_state_, initial_input_, text_selected_,
current_state_->Clone(),
compose::mojom::ConfigurableParams::New(compose_config.input_min_words,
compose_config.input_max_words,
compose_config.input_max_chars)));
}
void ComposeSession::SaveWebUIState(const std::string& webui_state) {
current_state_->webui_state = webui_state;
}
void ComposeSession::AcceptComposeResult(
AcceptComposeResultCallback success_callback) {
if (callback_.is_null() || !current_state_->response ||
current_state_->response->status != compose::mojom::ComposeStatus::kOk) {
// Guard against invoking twice before the UI is able to disconnect.
std::move(success_callback).Run(false);
return;
}
std::move(callback_).Run(base::UTF8ToUTF16(current_state_->response->result));
std::move(success_callback).Run(true);
}
void ComposeSession::RevertToMostRecentOkState(
RevertToMostRecentOkStateCallback callback) {
if (!most_recent_ok_state_->IsMojoValid()) {
// Gracefully fail if we find an invalid state.
std::move(callback).Run(nullptr);
return;
}
current_state_ = most_recent_ok_state_->mojo_state()->Clone();
std::move(callback).Run(most_recent_ok_state_->mojo_state()->Clone());
}
void ComposeSession::Undo(UndoCallback callback) {
if (undo_states_.empty()) {
std::move(callback).Run(nullptr);
return;
}
// Only increase undo count if there are states to undo.
session_events_.undo_count += 1;
std::unique_ptr<ComposeState> undo_state = std::move(undo_states_.top());
undo_states_.pop();
// Upload the most recent modeling quality log entry before overwriting it
// with state from undo.
most_recent_ok_state_->UploadModelQualityLogs(model_quality_logs_uploader_);
if (!undo_state->IsMojoValid()) {
// Gracefully fail if we find an invalid state on the undo stack.
std::move(callback).Run(nullptr);
return;
}
// State returns to the last undo_state.
current_state_ = undo_state->mojo_state()->Clone();
std::move(callback).Run(undo_state->mojo_state()->Clone());
// set recent state to the last undo modeling entry and last mojo state.
most_recent_ok_state_->SetMojoState(undo_state->TakeMojoState());
most_recent_ok_state_->SetModelingLogEntry(
undo_state->TakeModelingLogEntry());
}
void ComposeSession::OpenBugReportingLink() {
const char* url = kComposeBugReportURL;
if (most_recent_ok_state_ && most_recent_ok_state_->mojo_state() &&
most_recent_ok_state_->mojo_state()
->response->on_device_evaluation_used) {
url = kOnDeviceComposeBugReportURL;
}
web_contents_->OpenURL(
content::OpenURLParams(GURL(url), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK,
/* is_renderer_initiated= */ false),
/*navigation_handle_callback=*/{});
}
void ComposeSession::OpenComposeLearnMorePage() {
web_contents_->OpenURL(
content::OpenURLParams(
GURL(kComposeLearnMorePageURL), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB, ui::PAGE_TRANSITION_LINK,
/* is_renderer_initiated= */ false),
/*navigation_handle_callback=*/{});
}
void ComposeSession::OpenFeedbackSurveyLink() {
const char* url = kComposeFeedbackSurveyURL;
if (most_recent_ok_state_ && most_recent_ok_state_->mojo_state() &&
most_recent_ok_state_->mojo_state()
->response->on_device_evaluation_used) {
url = kOnDeviceComposeFeedbackSurveyURL;
}
web_contents_->OpenURL(
content::OpenURLParams(GURL(url), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK,
/* is_renderer_initiated= */ false),
/*navigation_handle_callback=*/{});
}
void ComposeSession::OpenSignInPage() {
web_contents_->OpenURL(
content::OpenURLParams(GURL(kSignInPageURL), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK,
/* is_renderer_initiated= */ false),
/*navigation_handle_callback=*/{});
}
bool ComposeSession::CanShowFeedbackPage() {
if (skip_feedback_ui_for_testing_) {
return false;
}
OptimizationGuideKeyedService* opt_guide_keyed_service =
OptimizationGuideKeyedServiceFactory::GetForProfile(
Profile::FromBrowserContext(web_contents_->GetBrowserContext()));
if (!opt_guide_keyed_service ||
!opt_guide_keyed_service->ShouldFeatureBeCurrentlyAllowedForLogging(
optimization_guide::UserVisibleFeatureKey::kCompose)) {
return false;
}
return true;
}
void ComposeSession::OpenFeedbackPage(std::string feedback_id) {
base::Value::Dict feedback_metadata;
feedback_metadata.Set("log_id", feedback_id);
chrome::ShowFeedbackPage(
web_contents_->GetLastCommittedURL(),
Profile::FromBrowserContext(web_contents_->GetBrowserContext()),
chrome::kFeedbackSourceAI,
/*description_template=*/std::string(),
/*description_placeholder_text=*/
l10n_util::GetStringUTF8(IDS_COMPOSE_FEEDBACK_PLACEHOLDER),
/*category_tag=*/"compose",
/*extra_diagnostics=*/std::string(),
/*autofill_metadata=*/base::Value::Dict(), std::move(feedback_metadata));
}
void ComposeSession::SetUserFeedback(compose::mojom::UserFeedback feedback) {
if (!most_recent_ok_state_->mojo_state()) {
// If there is no recent State there is nothing that we should be applying
// feedback to.
return;
}
// Add to most_recent_ok_state_ in case of undos.
most_recent_ok_state_->mojo_state()->feedback = feedback;
// Add to current_state_ in case of coming back to a saved state, as
// RequestInitialState() returns current_state_.
if (current_state_->response) {
current_state_->feedback = feedback;
}
optimization_guide::proto::UserFeedback user_feedback =
OptimizationFeedbackFromComposeFeedback(feedback);
if (most_recent_ok_state_->modeling_log_entry()) {
optimization_guide::proto::ComposeQuality* quality =
most_recent_ok_state_->modeling_log_entry()
->quality_data<optimization_guide::ComposeFeatureTypeMap>();
if (quality) {
quality->set_user_feedback(user_feedback);
}
if (feedback == compose::mojom::UserFeedback::kUserFeedbackNegative) {
session_events_.has_thumbs_down = true;
if (CanShowFeedbackPage()) {
// Open the Feedback Page for a thumbs down using current request log.
std::string feedback_id = most_recent_ok_state_->modeling_log_entry()
->log_ai_data_request()
->model_execution_info()
.execution_id();
OpenFeedbackPage(feedback_id);
}
} else if (feedback ==
compose::mojom::UserFeedback::kUserFeedbackPositive) {
session_events_.has_thumbs_up = true;
}
}
}
void ComposeSession::EditResult(const std::string& new_result) {
// Nothing has changed. Ignore.
if (new_result == most_recent_ok_state_->mojo_state()->response->result) {
return;
}
// If user undid edits, then pop from stack and return.
if (most_recent_ok_state_->is_user_edited() &&
most_recent_ok_state_->original_response() == new_result) {
current_state_->response->result = new_result;
most_recent_ok_state_->UnsetUserEdited();
most_recent_ok_state_->SetModelingLogEntry(
undo_states_.top()->TakeModelingLogEntry());
most_recent_ok_state_->SetMojoState(undo_states_.top()->TakeMojoState());
current_state_->response->undo_available =
most_recent_ok_state_->mojo_state()->response->undo_available;
undo_states_.pop();
return;
}
// Save the state if it hasn't been edited.
if (!most_recent_ok_state_->is_user_edited()) {
SaveMostRecentOkStateToUndoStack();
// Restore the states that were moved away as part of the undo save with
// copies.
most_recent_ok_state_->SetMojoState(current_state_.Clone());
// Set the user edited field in the restored ok state.
most_recent_ok_state_->SetUserEdited(
most_recent_ok_state_->mojo_state()->response->result);
// Update the undo_available field to show that there is an undo state.
most_recent_ok_state_->mojo_state()->response->undo_available = true;
current_state_->response->undo_available = true;
}
// Update result to be the edited result.
most_recent_ok_state_->mojo_state()->response->result = new_result;
current_state_->response->result = new_result;
}
void ComposeSession::InitializeWithText(const std::optional<std::string>& text,
const bool text_selected) {
// In some cases (FRE not shown, MSBB not accepted), we wait to extract the
// inner text until all conditions are met to enable the feature. However, if
// we want to extract the inner text content later, we still need to store the
// selected text.
text_selected_ = text_selected;
if (text.has_value()) {
initial_input_ = text.value();
session_events_.has_initial_text = true;
}
if (!fre_complete_) {
session_events_.fre_dialog_shown_count += 1;
return;
}
if (!current_msbb_state_) {
session_events_.msbb_dialog_shown_count += 1;
return;
}
// Session is initialized at the main dialog UI state.
session_events_.dialog_shown_count += 1;
RefreshInnerText();
// If no text provided (even an empty string), then we are reopening without
// calling compose again, or updating the input text, so skip autocompose.
if (text.has_value() && IsValidComposePrompt(initial_input_) &&
compose::GetComposeConfig().auto_submit_with_selection) {
Compose(initial_input_, false);
}
}
void ComposeSession::SaveMostRecentOkStateToUndoStack() {
if (!most_recent_ok_state_->IsMojoValid()) {
// This occurs when processing the first ok response of a session - no
// previous ok state exists and so there is nothing to save to the undo
// stack.
return;
}
undo_states_.push(std::make_unique<ComposeState>(
most_recent_ok_state_->TakeModelingLogEntry(),
most_recent_ok_state_->TakeMojoState(),
most_recent_ok_state_->is_user_edited(),
most_recent_ok_state_->original_response()));
}
void ComposeSession::AddPageContentToSession(
std::string inner_text,
std::optional<uint64_t> node_offset) {
if (!session_) {
return;
}
optimization_guide::proto::ComposePageMetadata page_metadata;
page_metadata.set_page_url(web_contents_->GetLastCommittedURL().spec());
page_metadata.set_page_title(base::UTF16ToUTF8(web_contents_->GetTitle()));
page_metadata.set_page_inner_text(std::move(inner_text));
if (node_offset.has_value()) {
page_metadata.set_page_inner_text_offset(node_offset.value());
}
optimization_guide::proto::ComposeRequest request;
*request.mutable_page_metadata() = std::move(page_metadata);
session_->AddContext(request);
}
void ComposeSession::UpdateInnerTextAndContinueComposeIfNecessary(
int request_id,
std::unique_ptr<content_extraction::InnerTextResult> result) {
if (request_id != current_inner_text_request_id_) {
// If this condition is hit, it means there are multiple requests for
// inner-text in flight. Early out so that we always use the most recent
// request.
return;
}
got_inner_text_ = true;
std::string inner_text;
std::optional<uint64_t> node_offset;
if (result) {
const compose::Config& config = compose::GetComposeConfig();
inner_text = std::move(result->inner_text);
compose::LogComposeDialogInnerTextSize(inner_text.size());
if (inner_text.size() > config.inner_text_max_bytes) {
compose::LogComposeDialogInnerTextShortenedBy(
inner_text.size() - config.inner_text_max_bytes);
inner_text.erase(config.inner_text_max_bytes);
}
node_offset = result->node_offset;
compose::LogComposeDialogInnerTextOffsetFound(node_offset.has_value());
}
AddPageContentToSession(std::move(inner_text), node_offset);
if (!continue_compose_.is_null()) {
std::move(continue_compose_).Run();
}
}
void ComposeSession::RefreshInnerText() {
got_inner_text_ = false;
if (!collect_inner_text_) {
return;
}
++current_inner_text_request_id_;
inner_text_caller_->GetInnerText(
*web_contents_->GetPrimaryMainFrame(),
// This unsafeValue call is acceptable ehre because node_id is a
// FieldRendererId which while being an U64 type is based one the int
// DOMid which we are querying here.
node_id_.GetUnsafeValue(),
base::BindOnce(
&ComposeSession::UpdateInnerTextAndContinueComposeIfNecessary,
weak_ptr_factory_.GetWeakPtr(), current_inner_text_request_id_));
}
void ComposeSession::SetFirstRunCloseReason(
compose::ComposeFirstRunSessionCloseReason close_reason) {
fre_close_reason_ = close_reason;
if (close_reason == compose::ComposeFirstRunSessionCloseReason::
kFirstRunDisclaimerAcknowledgedWithoutInsert) {
if (current_msbb_state_) {
// The FRE dialog progresses directly to the main dialog.
session_events_.dialog_shown_count = 1;
base::RecordAction(
base::UserMetricsAction("Compose.DialogSeen.MainDialog"));
} else {
base::RecordAction(
base::UserMetricsAction("Compose.DialogSeen.FirstRunMSBB"));
}
}
}
void ComposeSession::SetFirstRunCompleted() {
session_events_.fre_completed_in_session = true;
fre_complete_ = true;
// Start inner text capture which was skipped until FRE was complete.
InitializeWithText(std::make_optional(initial_input_), text_selected_);
}
void ComposeSession::SetMSBBCloseReason(
compose::ComposeMSBBSessionCloseReason close_reason) {
msbb_close_reason_ = close_reason;
}
void ComposeSession::SetCloseReason(
compose::ComposeSessionCloseReason close_reason) {
if (close_reason == compose::ComposeSessionCloseReason::kCloseButtonPressed &&
current_state_->has_pending_request) {
close_reason_ =
compose::ComposeSessionCloseReason::kCanceledBeforeResponseReceived;
} else {
close_reason_ = close_reason;
}
switch (close_reason) {
case compose::ComposeSessionCloseReason::kCloseButtonPressed:
case compose::ComposeSessionCloseReason::kNewSessionWithSelectedText:
case compose::ComposeSessionCloseReason::kCanceledBeforeResponseReceived:
final_status_ = optimization_guide::proto::FinalStatus::STATUS_ABANDONED;
session_events_.close_clicked = true;
break;
case compose::ComposeSessionCloseReason::kEndedImplicitly:
final_status_ = optimization_guide::proto::FinalStatus::
STATUS_FINISHED_WITHOUT_INSERT;
break;
case compose::ComposeSessionCloseReason::kAcceptedSuggestion:
final_status_ = optimization_guide::proto::FinalStatus::STATUS_INSERTED;
session_events_.inserted_results = true;
break;
}
}
void ComposeSession::SetQualityLogEntryUponError(
std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry,
base::TimeDelta request_time,
bool was_input_edited) {
if (log_entry) {
log_entry->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->set_request_latency_ms(request_time.InMilliseconds());
optimization_guide::proto::Int128* token =
log_entry->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->mutable_session_id();
token->set_high(session_id_.high());
token->set_low(session_id_.low());
log_entry->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->set_was_generated_via_edit(was_input_edited);
// In the event that we are holding onto an error log upload it before it
// gets overwritten
if (most_recent_error_log_ && model_quality_logs_uploader_) {
model_quality_logs_uploader_->UploadModelQualityLogs(
std::move(most_recent_error_log_));
}
most_recent_error_log_ = std::move(log_entry);
}
}
void ComposeSession::set_current_msbb_state(bool msbb_enabled) {
current_msbb_state_ = msbb_enabled;
if (!msbb_enabled) {
msbb_initially_off_ = true;
} else if (msbb_initially_off_) {
session_events_.msbb_enabled_in_session = true;
SetMSBBCloseReason(
compose::ComposeMSBBSessionCloseReason::kMSBBAcceptedWithoutInsert);
base::RecordAction(
base::UserMetricsAction("Compose.DialogSeen.MainDialog"));
// Reset this initial state so that this block is not re-executed on every
// subsequent dialog open.
msbb_initially_off_ = false;
}
}
void ComposeSession::SetSkipFeedbackUiForTesting(bool allowed) {
skip_feedback_ui_for_testing_ = allowed;
}