blob: dd687b1d6416bf2cb508d141ed07d0a0d661e31f [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 <cmath>
#include <memory>
#include <string>
#include <string_view>
#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/browser_process.h"
#include "chrome/browser/feedback/show_feedback_page.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/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/survey_config.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_hats_utils.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/content_extraction/content/browser/inner_text.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/model_execution_logging_wrappers.h"
#include "components/optimization_guide/core/model_quality/model_quality_log_entry.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_proto_util.h"
#include "components/optimization_guide/core/optimization_guide_util.h"
#include "components/optimization_guide/proto/features/compose.pb.h"
#include "components/optimization_guide/proto/model_quality_service.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/accessibility/ax_mode.h"
#include "ui/accessibility/ax_tree_update.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";
// TODO(crbug.com/40500621): Replace with p-link
const char kEnterpriseComposeLearnMorePageURL[] =
"https://support.google.com/chrome/a/answer/14443058";
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;
}
compose::ComposeRequestReason GetRequestReasonForInputMode(
compose::mojom::InputMode mode) {
switch (mode) {
case compose::mojom::InputMode::kElaborate:
return compose::ComposeRequestReason::kFirstRequestElaborateMode;
case compose::mojom::InputMode::kFormalize:
return compose::ComposeRequestReason::kFirstRequestFormalizeMode;
case compose::mojom::InputMode::kPolish:
return compose::ComposeRequestReason::kFirstRequestPolishMode;
case compose::mojom::InputMode::kUnset:
return compose::ComposeRequestReason::kFirstRequest;
}
}
bool WasRequestTriggeredFromModifier(compose::ComposeRequestReason reason) {
switch (reason) {
case compose::ComposeRequestReason::kRetryRequest:
case compose::ComposeRequestReason::kLengthShortenRequest:
case compose::ComposeRequestReason::kLengthElaborateRequest:
case compose::ComposeRequestReason::kToneCasualRequest:
case compose::ComposeRequestReason::kToneFormalRequest:
return true;
case compose::ComposeRequestReason::kUpdateRequest:
case compose::ComposeRequestReason::kFirstRequest:
case compose::ComposeRequestReason::kFirstRequestPolishMode:
case compose::ComposeRequestReason::kFirstRequestElaborateMode:
case compose::ComposeRequestReason::kFirstRequestFormalizeMode:
return false;
}
}
} // 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;
}
ComposeState(std::unique_ptr<optimization_guide::ModelQualityLogEntry>
modeling_log_entry,
compose::mojom::ComposeStatePtr mojo_state) {
modeling_log_entry_ = std::move(modeling_log_entry);
mojo_state_ = std::move(mojo_state);
}
~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() {
if (!modeling_log_entry_) {
return;
}
LogRequestFeedback();
optimization_guide::ModelQualityLogEntry::Upload(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_ = false;
};
ComposeSession::ComposeSession(
content::WebContents* web_contents,
optimization_guide::OptimizationGuideModelExecutor* executor,
optimization_guide::ModelQualityLogsUploaderService* model_quality_uploader,
base::Token session_id,
InnerTextProvider* inner_text,
autofill::FieldGlobalId node_id,
bool is_page_language_supported,
Observer* observer,
ComposeCallback callback)
: executor_(executor),
model_quality_uploader_(model_quality_uploader),
handler_receiver_(this),
web_contents_(web_contents),
observer_(observer),
collect_inner_text_(
base::FeatureList::IsEnabled(compose::features::kComposeInnerText)),
collect_ax_snapshot_(
base::FeatureList::IsEnabled(compose::features::kComposeAXSnapshot)),
inner_text_caller_(inner_text),
ukm_source_id_(web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId()),
node_id_(node_id),
is_page_language_supported_(is_page_language_supported),
session_id_(session_id),
weak_ptr_factory_(this) {
session_duration_ = std::make_unique<base::ElapsedTimer>();
callback_ = std::move(callback);
active_mojo_state_ = compose::mojom::ComposeState::New();
if (executor_) {
optimization_guide::SessionConfigParams config_params = {
.execution_mode = base::FeatureList::IsEnabled(
compose::features::kComposeAllowOnDeviceExecution)
? optimization_guide::SessionConfigParams::
ExecutionMode::kDefault
: optimization_guide::SessionConfigParams::
ExecutionMode::kServerOnly};
session_ = executor_->StartSession(
optimization_guide::ModelBasedCapabilityKey::kCompose, config_params);
}
}
base::optional_ref<ComposeState> ComposeSession::LastResponseState() {
if (history_.empty() || history_current_index_ >= history_.size() ||
!history_[history_current_index_]) {
return std::nullopt;
}
if (history_[history_current_index_]->is_user_edited()) {
// The state at `history_current_index_` is an edited result, so the last
// response state directly precedes it in `history_`.
CHECK_GT(history_current_index_, 0u);
return *history_[history_current_index_ - 1];
}
return *history_[history_current_index_];
}
base::optional_ref<ComposeState> ComposeSession::CurrentState(int offset) {
if (history_.empty() || history_current_index_ + offset >= history_.size() ||
!history_[history_current_index_ + offset]) {
return std::nullopt;
}
return *history_[history_current_index_ + offset];
}
ComposeSession::~ComposeSession() {
std::optional<compose::EvalLocation> eval_location =
compose::GetEvalLocationFromEvents(session_events_);
if (observer_) {
observer_->OnSessionComplete(node_id_, close_reason_, session_events_);
}
if (session_events_.fre_view_count > 0 &&
(!fre_complete_ || session_events_.fre_completed_in_session)) {
compose::LogComposeFirstRunSessionCloseReason(fre_close_reason_);
compose::LogComposeFirstRunSessionDialogShownCount(
fre_close_reason_, session_events_.fre_view_count);
if (!fre_complete_) {
compose::LogComposeSessionDuration(session_duration_->Elapsed(), ".FRE");
compose::LogComposeSessionEventCounts(std::nullopt, session_events_);
compose::LogComposeSessionCloseReason(
compose::ComposeSessionCloseReason::kEndedAtFre);
return;
}
}
if (session_events_.msbb_view_count > 0 &&
(!current_msbb_state_ || session_events_.msbb_enabled_in_session)) {
compose::LogComposeMSBBSessionDialogShownCount(
msbb_close_reason_, session_events_.msbb_view_count);
compose::LogComposeMSBBSessionCloseReason(msbb_close_reason_);
if (!current_msbb_state_) {
compose::LogComposeSessionDuration(session_duration_->Elapsed(), ".MSBB");
compose::LogComposeSessionEventCounts(std::nullopt, session_events_);
compose::ComposeSessionCloseReason session_close_reason =
(session_events_.fre_completed_in_session)
? compose::ComposeSessionCloseReason::kAckedFreEndedAtMsbb
: compose::ComposeSessionCloseReason::kEndedAtMsbb;
compose::LogComposeSessionCloseReason(session_close_reason);
return;
}
}
if (session_events_.compose_dialog_open_count < 1) {
// Do not report any further metrics if the dialog was never opened.
// 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::kAbandoned) {
base::RecordAction(
base::UserMetricsAction("Compose.EndedSession.EndedImplicitly"));
final_model_status_ =
optimization_guide::proto::FinalModelStatus::FINAL_MODEL_STATUS_FAILURE;
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 (most_recent_error_log_) {
// First set final status on most_recent_error_log.
most_recent_error_log_
->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_final_status(final_status_);
most_recent_error_log_
->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_final_model_status(final_model_status_);
optimization_guide::ModelQualityLogEntry::Upload(
std::move(most_recent_error_log_));
} else if (auto last_response_state = LastResponseState();
last_response_state.has_value()) {
if (auto* log_entry = last_response_state->modeling_log_entry()) {
log_entry->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_final_status(final_status_);
log_entry->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_final_model_status(final_model_status_);
last_response_state->UploadModelQualityLogs();
}
}
for (auto& state : history_) {
// Upload all saved states with a valid quality logs member (those tied to
// a ComposeResponse) and then clear all states.
state->UploadModelQualityLogs();
}
}
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/300974056): 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,
compose::mojom::InputMode mode,
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 = GetRequestReasonForInputMode(mode);
}
optimization_guide::proto::ComposeRequest request;
request.mutable_generate_params()->set_user_input(input);
optimization_guide::proto::ComposeUpfrontInputMode request_mode =
ComposeUpfrontInputMode(mode);
request.mutable_generate_params()->set_upfront_input_mode(request_mode);
MakeRequest(std::move(request), request_reason, is_input_edited);
}
void ComposeSession::Rewrite(compose::mojom::StyleModifier style) {
compose::ComposeRequestReason request_reason;
optimization_guide::proto::ComposeRequest request;
switch (style) {
case compose::mojom::StyleModifier::kFormal:
request.mutable_rewrite_params()->set_tone(
optimization_guide::proto::ComposeTone::COMPOSE_FORMAL);
session_events_.formal_count++;
request_reason = compose::ComposeRequestReason::kToneFormalRequest;
break;
case compose::mojom::StyleModifier::kCasual:
request.mutable_rewrite_params()->set_tone(
optimization_guide::proto::ComposeTone::COMPOSE_INFORMAL);
session_events_.casual_count++;
request_reason = compose::ComposeRequestReason::kToneCasualRequest;
break;
case compose::mojom::StyleModifier::kShorter:
request.mutable_rewrite_params()->set_length(
optimization_guide::proto::ComposeLength::COMPOSE_SHORTER);
session_events_.shorten_count++;
request_reason = compose::ComposeRequestReason::kLengthShortenRequest;
break;
case compose::mojom::StyleModifier::kLonger:
request.mutable_rewrite_params()->set_length(
optimization_guide::proto::ComposeLength::COMPOSE_LONGER);
session_events_.lengthen_count++;
request_reason = compose::ComposeRequestReason::kLengthElaborateRequest;
break;
case compose::mojom::StyleModifier::kUnset:
// TODO: kUnset is not reachable, but a `request_reason` must be set to
// satisfy the compiler
case compose::mojom::StyleModifier::kRetry:
request.mutable_rewrite_params()->set_regenerate(true);
session_events_.regenerate_count++;
request_reason = compose::ComposeRequestReason::kRetryRequest;
break;
}
request.mutable_rewrite_params()->set_previous_response(
CurrentState()->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) {
active_mojo_state_->has_pending_request = true;
active_mojo_state_->feedback =
compose::mojom::UserFeedback::kUserFeedbackUnspecified;
// Increase Compose count regardless of status of request.
++session_events_.compose_requests_count;
// 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;
}
// Prepare the compose call, which will be invoked when all required page
// metadata is collected.
continue_compose_ = base::BindOnce(
&ComposeSession::RequestWithSession, weak_ptr_factory_.GetWeakPtr(),
std::move(request), request_reason, is_input_edited);
// In case AX tree or page collection isn't required, we can run the
// continuation immediately. Note that going through this call ensures we
// populate the context object correctly.
TryContinueComposeWithContext();
}
bool ComposeSession::HasNecessaryPageContext() const {
return (!collect_inner_text_ || got_inner_text_) &&
(!collect_ax_snapshot_ || got_ax_snapshot_);
}
void ComposeSession::RequestWithSession(
const optimization_guide::proto::ComposeRequest& request,
compose::ComposeRequestReason request_reason,
bool is_input_edited) {
// 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, config.request_latency_timeout,
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);
optimization_guide::ModelExecutionSessionCallbackWithLogging callback =
base::BindRepeating(&ComposeSession::ModelExecutionCallback,
weak_ptr_factory_.GetWeakPtr(),
std::move(request_timer), request_id_, request_reason,
is_input_edited);
optimization_guide::ExecuteModelSessionWithLogging(session_.get(), request,
callback);
}
void ComposeSession::ComposeRequestTimeout(int id) {
request_timeouts_.erase(id);
compose::LogComposeRequestStatus(
is_page_language_supported_,
compose::mojom::ComposeStatus::kRequestTimeout);
active_mojo_state_->has_pending_request = false;
active_mojo_state_->response = compose::mojom::ComposeResponse::New();
active_mojo_state_->response->status =
compose::mojom::ComposeStatus::kRequestTimeout;
if (dialog_remote_.is_bound()) {
dialog_remote_->ResponseReceived(active_mojo_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,
std::unique_ptr<optimization_guide::proto::ComposeLoggingData>
logging_data) {
base::TimeDelta request_delta = request_timer.Elapsed();
std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry;
if (logging_data) {
// There is data to log, meaning this is a complete response.
log_entry = std::make_unique<optimization_guide::ModelQualityLogEntry>(
model_quality_uploader_->GetWeakPtr());
log_entry->log_ai_data_request()->mutable_compose()->MergeFrom(
*logging_data);
}
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(log_entry), request_delta,
was_input_edited);
compose::LogComposeRequestReason(eval_location, request_reason);
compose::LogComposeRequestStatus(
eval_location, is_page_language_supported_,
compose::mojom::ComposeStatus::kRequestTimeout);
return;
}
// A new request has been issued, ignore this one.
if (request_id != request_id_) {
SetQualityLogEntryUponError(std::move(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), std::move(log_entry));
}
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,
std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry) {
// Handle 'complete' results.
active_mojo_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 (!session_events_.session_contained_filtered_response &&
status == compose::mojom::ComposeStatus::kFiltered) {
session_events_.session_contained_filtered_response = true;
}
if (!session_events_.session_contained_any_error &&
status != compose::mojom::ComposeStatus::kOk) {
session_events_.session_contained_any_error = true;
}
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(log_entry), request_delta,
was_input_edited);
return;
}
CHECK(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(log_entry), request_delta,
was_input_edited);
return;
}
if (log_entry) {
log_entry->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_was_generated_via_edit(was_input_edited);
log_entry->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_started_with_proactive_nudge(
session_events_.started_with_proactive_nudge);
log_entry->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_request_latency_ms(request_delta.InMilliseconds());
optimization_guide::proto::Int128* token =
log_entry->log_ai_data_request()->mutable_compose()->mutable_quality()
->mutable_session_id();
token->set_high(session_id_.high());
token->set_low(session_id_.low());
// In the event that we are holding onto an error log upload it before it
// gets overwritten
if (most_recent_error_log_) {
optimization_guide::ModelQualityLogEntry::Upload(
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();
}
// Create a new ComposeState with the dialog's current mojo state and the log
// entry just received with the response.
std::unique_ptr<ComposeState> new_response_state =
std::make_unique<ComposeState>(std::move(log_entry),
active_mojo_state_.Clone());
// Update the new state's mojo state to reflect the new response.
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;
ui_response->provided_by_user = false;
// TODO(b/333944734): Remove undo_available and redo_available from
// ComposeState.
ui_response->undo_available = !history_.empty();
ui_response->redo_available = false;
new_response_state->mojo_state()->response = ui_response->Clone();
// Before adding the new state to history, mark redo available on the current
// state that will directly precede it.
if (auto current_state = CurrentState(); current_state.has_value()) {
current_state->mojo_state()->response->redo_available = true;
}
AddNewResponseToHistory(std::move(new_response_state));
// Update `active_mojo_state_` to match the new state
active_mojo_state_ = CurrentState()->mojo_state()->Clone();
if (dialog_remote_.is_bound()) {
dialog_remote_->ResponseReceived(active_mojo_state_->response->Clone());
}
// Log successful response status.
compose::LogComposeRequestStatus(is_page_language_supported_,
compose::mojom::ComposeStatus::kOk);
compose::LogComposeRequestStatus(eval_location, is_page_language_supported_,
compose::mojom::ComposeStatus::kOk);
compose::LogComposeRequestDuration(request_delta, eval_location,
/* is_ok */ true);
++session_events_.successful_requests_count;
}
void ComposeSession::AddNewResponseToHistory(
std::unique_ptr<ComposeState> new_state) {
// On a new response, all forward/redo states are cleared. Upload any
// associated quality logs first.
EraseForwardStatesInHistory();
history_.push_back(std::move(new_state));
history_current_index_ = history_.size() - 1;
}
void ComposeSession::EraseForwardStatesInHistory() {
for (size_t i = history_current_index_ + 1; i < history_.size(); i++) {
history_[i]->UploadModelQualityLogs();
}
if (history_.size() > history_current_index_ + 1) {
history_.erase(history_.begin() + history_current_index_ + 1,
history_.end());
}
}
void ComposeSession::ProcessError(
compose::EvalLocation eval_location,
compose::mojom::ComposeStatus error,
compose::ComposeRequestReason request_reason) {
compose::LogComposeRequestStatus(is_page_language_supported_, error);
compose::LogComposeRequestStatus(eval_location, is_page_language_supported_,
error);
++session_events_.failed_requests_count;
// Feedback can not be given for a request with an error so report now.
compose::LogComposeRequestFeedback(
eval_location, compose::ComposeRequestFeedback::kRequestError);
active_mojo_state_->has_pending_request = false;
active_mojo_state_->response = compose::mojom::ComposeResponse::New();
active_mojo_state_->response->status = error;
active_mojo_state_->response->triggered_from_modifier =
WasRequestTriggeredFromModifier(request_reason);
if (dialog_remote_.is_bound()) {
dialog_remote_->ResponseReceived(active_mojo_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_,
currently_has_selection_, active_mojo_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) {
active_mojo_state_->webui_state = webui_state;
}
void ComposeSession::AcceptComposeResult(
AcceptComposeResultCallback success_callback) {
if (callback_.is_null() || !active_mojo_state_->response ||
active_mojo_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(active_mojo_state_->response->result));
std::move(success_callback).Run(true);
}
void ComposeSession::RecoverFromErrorState(
RecoverFromErrorStateCallback callback) {
// Should only be called if there is a state to return to.
CHECK(CurrentState().has_value());
if (!CurrentState()->IsMojoValid()) {
// Gracefully fail if we find an invalid state.
std::move(callback).Run(nullptr);
return;
}
active_mojo_state_ = CurrentState()->mojo_state()->Clone();
std::move(callback).Run(active_mojo_state_->Clone());
}
void ComposeSession::Undo(UndoCallback callback) {
// Undo should only be called if a backwards saved state exists.
if (history_current_index_ < 1) {
std::move(callback).Run(nullptr);
return;
}
auto previous_state = CurrentState(-1);
if (!previous_state->IsMojoValid()) {
// Gracefully fail if we find an invalid state in the history.
std::move(callback).Run(nullptr);
return;
}
history_current_index_--;
// Only increase undo count if there are states to undo.
session_events_.undo_count += 1;
active_mojo_state_ = previous_state->mojo_state()->Clone();
std::move(callback).Run(active_mojo_state_->Clone());
}
void ComposeSession::Redo(RedoCallback callback) {
// Redo should only be called if a forward saved state exists.
if (history_current_index_ >= history_.size() - 1) {
std::move(callback).Run(nullptr);
return;
}
auto next_state = CurrentState(1);
if (!next_state->IsMojoValid()) {
// Gracefully fail if we find an invalid state in the history.
std::move(callback).Run(nullptr);
return;
}
history_current_index_++;
// Only increase redo count if there are states to redo.
session_events_.redo_count += 1;
active_mojo_state_ = next_state->mojo_state()->Clone();
std::move(callback).Run(active_mojo_state_->Clone());
}
void ComposeSession::OpenBugReportingLink() {
const char* url = kComposeBugReportURL;
if (auto last_response_state = LastResponseState();
last_response_state.has_value()) {
if (last_response_state->mojo_state() &&
last_response_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() {
if (base::FeatureList::IsEnabled(
compose::features::kEnableComposeProactiveNudge)) {
Browser* browser = chrome::FindBrowserWithTab(web_contents_);
CHECK(browser);
chrome::ShowSettingsSubPage(browser, chrome::kAiHelpMeWriteSubpage);
return;
}
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::OpenEnterpriseComposeLearnMorePage() {
web_contents_->OpenURL(
content::OpenURLParams(
GURL(kEnterpriseComposeLearnMorePageURL), 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 (auto last_response_state = LastResponseState();
last_response_state.has_value()) {
if (last_response_state->mojo_state() &&
last_response_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->ShouldFeatureBeCurrentlyAllowedForFeedback(
optimization_guide::proto::LogAiDataRequest::FeatureCase::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()),
feedback::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) {
auto last_response_state = LastResponseState();
if (!last_response_state.has_value() || !last_response_state->mojo_state()) {
// If there is no recent State there is nothing that we should be applying
// feedback to.
return;
}
// Save feedback to the last state associated with a valid response.
last_response_state->mojo_state()->feedback = feedback;
// Update `active_mojo_state_`, as it is returned by RequestInitialState()
// when resuming a saved session.
if (active_mojo_state_->response) {
active_mojo_state_->feedback = feedback;
}
optimization_guide::proto::UserFeedback user_feedback =
OptimizationFeedbackFromComposeFeedback(feedback);
// Apply feedback to the last saved state with a valid response.
optimization_guide::proto::ComposeQuality* quality =
last_response_state->modeling_log_entry()
->log_ai_data_request()
->mutable_compose()
->mutable_quality();
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 = last_response_state->modeling_log_entry()
->log_ai_data_request()
->compose()
.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,
EditResultCallback callback) {
// If there is no change in result text resulting from the edit, do nothing.
if (new_result == CurrentState()->mojo_state()->response->result) {
std::move(callback).Run(false);
return;
}
// Update the active state to reflect a new edit.
active_mojo_state_->response->result = new_result;
active_mojo_state_->response->undo_available = true;
active_mojo_state_->response->redo_available = false;
active_mojo_state_->response->provided_by_user = true;
active_mojo_state_->feedback =
compose::mojom::UserFeedback::kUserFeedbackUnspecified;
if (CurrentState()->is_user_edited()) {
// The current state being edited is an edit itself. In this case, update
// its result text instead of saving a new state.
EraseForwardStatesInHistory();
CurrentState()->mojo_state()->response->result = new_result;
} else {
// The current state being edited is a server response - save the edit as a
// new ComposeState.
CurrentState()->mojo_state()->response->redo_available = true;
// Add a new ComposeState to `history_` to represent the new result edit.
auto new_state =
std::make_unique<ComposeState>(nullptr, active_mojo_state_.Clone());
new_state->SetUserEdited(CurrentState()->mojo_state()->response->result);
AddNewResponseToHistory(std::move(new_state));
}
std::move(callback).Run(true);
session_events_.result_edit_count += 1;
}
void ComposeSession::InitializeWithText(std::string_view selected_text) {
// 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.
initial_input_ = std::string(selected_text);
session_events_.has_initial_text = !selected_text.empty();
MaybeRefreshPageContext(!initial_input_.empty());
}
void ComposeSession::MaybeRefreshPageContext(bool has_selection) {
// Update dialog state based on the current selection which can change while
// the dialog is hidden.
currently_has_selection_ = has_selection;
++session_events_.compose_dialog_open_count;
if (!fre_complete_) {
++session_events_.fre_view_count;
return;
}
if (!current_msbb_state_) {
++session_events_.msbb_view_count;
return;
}
// Session is initialized at the main dialog UI state.
++session_events_.compose_prompt_view_count;
RefreshInnerText();
RefreshAXSnapshot();
// We should only autocompose once per session
if (has_checked_autocompose_) {
return;
}
// Autocompose if it is enabled and there is a valid selection.
if (compose::GetComposeConfig().auto_submit_with_selection &&
IsValidComposePrompt(initial_input_)) {
Compose(initial_input_, compose::mojom::InputMode::kUnset, false);
}
has_checked_autocompose_ = true;
}
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::string trimmed_inner_text;
std::optional<uint64_t> node_offset;
if (result) {
const compose::Config& config = compose::GetComposeConfig();
inner_text = std::move(result->inner_text);
node_offset = result->node_offset;
if (node_offset.has_value()) {
trimmed_inner_text = compose::GetTrimmedPageText(
inner_text, config.trimmed_inner_text_max_chars, node_offset.value(),
config.trimmed_inner_text_header_length);
} else {
trimmed_inner_text =
inner_text.substr(0, config.trimmed_inner_text_max_chars);
}
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);
}
compose::LogComposeDialogInnerTextOffsetFound(node_offset.has_value());
}
if (!session_) {
return;
}
if (!page_metadata_) {
page_metadata_.emplace();
}
if (node_offset.has_value()) {
page_metadata_->set_page_inner_text_offset(node_offset.value());
}
page_metadata_->set_trimmed_page_inner_text(trimmed_inner_text);
page_metadata_->set_page_inner_text(std::move(inner_text));
TryContinueComposeWithContext();
}
void ComposeSession::UpdateAXSnapshotAndContinueComposeIfNecessary(
int request_id,
ui::AXTreeUpdate& update) {
if (current_ax_snapshot_request_id_ != request_id) {
return;
}
got_ax_snapshot_ = true;
if (!page_metadata_) {
page_metadata_.emplace();
}
optimization_guide::PopulateAXTreeUpdateProto(
update, page_metadata_->mutable_ax_tree_update());
TryContinueComposeWithContext();
}
void ComposeSession::TryContinueComposeWithContext() {
if (!HasNecessaryPageContext() || continue_compose_.is_null()) {
return;
}
if (!collect_inner_text_ && !collect_ax_snapshot_) {
// Make sure we populate the url and title even if we're not collecting
// other context information.
page_metadata_.emplace();
}
optimization_guide::proto::ComposeRequest request;
if (page_metadata_) {
page_metadata_->set_page_url(web_contents_->GetLastCommittedURL().spec());
page_metadata_->set_page_title(
base::UTF16ToUTF8(web_contents_->GetTitle()));
*request.mutable_page_metadata() = std::move(*page_metadata_);
page_metadata_.reset();
session_->AddContext(request);
}
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 here 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_.renderer_id.GetUnsafeValue(),
base::BindOnce(
&ComposeSession::UpdateInnerTextAndContinueComposeIfNecessary,
weak_ptr_factory_.GetWeakPtr(), current_inner_text_request_id_));
}
void ComposeSession::RefreshAXSnapshot() {
got_ax_snapshot_ = false;
if (!collect_ax_snapshot_) {
return;
}
++current_ax_snapshot_request_id_;
web_contents_->RequestAXTreeSnapshot(
base::BindOnce(
&ComposeSession::UpdateAXSnapshotAndContinueComposeIfNecessary,
weak_ptr_factory_.GetWeakPtr(), current_ax_snapshot_request_id_),
ui::kAXModeWebContentsOnly,
compose::GetComposeConfig().max_ax_node_count_for_page_context,
/*timeout=*/{},
content::WebContents::AXTreeSnapshotPolicy::kSameOriginDirectDescendants);
}
void ComposeSession::SetFirstRunCloseReason(
compose::ComposeFreOrMsbbSessionCloseReason close_reason) {
fre_close_reason_ = close_reason;
if (close_reason == compose::ComposeFreOrMsbbSessionCloseReason::
kAckedOrAcceptedWithoutInsert) {
if (current_msbb_state_) {
// The FRE dialog progresses directly to the main dialog.
session_events_.compose_prompt_view_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.
MaybeRefreshPageContext(currently_has_selection_);
}
void ComposeSession::SetMSBBCloseReason(
compose::ComposeFreOrMsbbSessionCloseReason close_reason) {
msbb_close_reason_ = close_reason;
}
void ComposeSession::SetCloseReason(
compose::ComposeSessionCloseReason close_reason) {
if (close_reason == compose::ComposeSessionCloseReason::kCloseButtonPressed &&
active_mojo_state_->has_pending_request) {
close_reason_ =
compose::ComposeSessionCloseReason::kCanceledBeforeResponseReceived;
} else {
close_reason_ = close_reason;
}
switch (close_reason) {
case compose::ComposeSessionCloseReason::kCloseButtonPressed:
case compose::ComposeSessionCloseReason::kCanceledBeforeResponseReceived:
final_status_ = optimization_guide::proto::FinalStatus::STATUS_ABANDONED;
final_model_status_ = optimization_guide::proto::FinalModelStatus::
FINAL_MODEL_STATUS_FAILURE;
session_events_.close_clicked = true;
break;
case compose::ComposeSessionCloseReason::kReplacedWithNewSession:
final_status_ = optimization_guide::proto::FinalStatus::STATUS_ABANDONED;
final_model_status_ = optimization_guide::proto::FinalModelStatus::
FINAL_MODEL_STATUS_FAILURE;
break;
case compose::ComposeSessionCloseReason::kExceededMaxDuration:
case compose::ComposeSessionCloseReason::kAbandoned:
final_status_ = optimization_guide::proto::FinalStatus::
STATUS_FINISHED_WITHOUT_INSERT;
final_model_status_ = optimization_guide::proto::FinalModelStatus::
FINAL_MODEL_STATUS_FAILURE;
break;
case compose::ComposeSessionCloseReason::kInsertedResponse:
final_status_ = optimization_guide::proto::FinalStatus::STATUS_INSERTED;
final_model_status_ = optimization_guide::proto::FinalModelStatus::
FINAL_MODEL_STATUS_SUCCESS;
session_events_.inserted_results = true;
if (CurrentState().has_value() && CurrentState()->is_user_edited()) {
session_events_.edited_result_inserted = true;
}
break;
case compose::ComposeSessionCloseReason::kEndedAtFre:
case compose::ComposeSessionCloseReason::kAckedFreEndedAtMsbb:
case compose::ComposeSessionCloseReason::kEndedAtMsbb:
// If the session ended during the FRE no need to set |final_status_|
break;
}
}
bool ComposeSession::HasExpired() {
return session_duration_->Elapsed() >
compose::GetComposeConfig().session_max_allowed_lifetime;
}
void ComposeSession::SetQualityLogEntryUponError(
std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry,
base::TimeDelta request_time,
bool was_input_edited) {
if (log_entry) {
log_entry->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->set_request_latency_ms(request_time.InMilliseconds());
optimization_guide::proto::Int128* token =
log_entry->log_ai_data_request()->mutable_compose()->mutable_quality()
->mutable_session_id();
token->set_high(session_id_.high());
token->set_low(session_id_.low());
log_entry->log_ai_data_request()
->mutable_compose()
->mutable_quality()
->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_) {
optimization_guide::ModelQualityLogEntry::Upload(
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::ComposeFreOrMsbbSessionCloseReason::
kAckedOrAcceptedWithoutInsert);
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;
}
void ComposeSession::LaunchHatsSurvey(
compose::ComposeSessionCloseReason close_reason) {
std::string trigger;
switch (close_reason) {
case compose::ComposeSessionCloseReason::kCloseButtonPressed:
if (!base::FeatureList::IsEnabled(
compose::features::kHappinessTrackingSurveysForComposeClose)) {
return;
}
trigger = kHatsSurveyTriggerComposeClose;
break;
case compose::ComposeSessionCloseReason::kInsertedResponse:
if (!base::FeatureList::IsEnabled(
compose::features::
kHappinessTrackingSurveysForComposeAcceptance)) {
return;
}
trigger = kHatsSurveyTriggerComposeAcceptance;
break;
default:
return;
}
HatsService* hats_service = HatsServiceFactory::GetForProfile(
Profile::FromBrowserContext(web_contents_->GetBrowserContext()),
/*create_if_necessary=*/true);
if (!hats_service) {
return;
}
// Determine if the user used any of the response modifiers.
bool response_modified =
session_events_.shorten_count > 0 || session_events_.lengthen_count > 0 ||
session_events_.formal_count > 0 || session_events_.casual_count > 0;
SurveyBitsData product_specific_bits_data = {
{compose::hats::HatsFields::kResponseModified, response_modified},
{compose::hats::HatsFields::kSessionContainedFilteredResponse,
session_events_.session_contained_filtered_response},
{compose::hats::HatsFields::kSessionContainedError,
session_events_.session_contained_any_error},
{compose::hats::HatsFields::kSessionBeganWithNudge,
session_events_.started_with_proactive_nudge}};
std::string url = web_contents_->GetLastCommittedURL().spec();
std::string session_id = session_id_.ToString();
SurveyStringData product_specific_string_data = {
{compose::hats::HatsFields::kSessionID, session_id},
{compose::hats::HatsFields::kURL, url},
{compose::hats::HatsFields::kLocale,
g_browser_process->GetApplicationLocale()}};
hats_service->LaunchSurveyForWebContents(trigger, web_contents_,
product_specific_bits_data,
product_specific_string_data);
}