910e62b5创建于 1月15日历史提交
// 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/multimodal_message.h"
#include "components/optimization_guide/core/model_execution/optimization_guide_model_execution_error.h"
#include "components/optimization_guide/core/model_execution/remote_model_executor.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_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::OptimizationGuideModelExecutionResult& result) {
  return 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::RemoteModelExecutor* 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();
}

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 (!executor_ ||
      !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::ModelExecutionCallbackWithLogging callback =
      base::BindOnce(&ComposeSession::ModelExecutionCallback,
                     weak_ptr_factory_.GetWeakPtr(), std::move(request_timer),
                     request_id_, request_reason, is_input_edited);
  auto merged_request = request_context_.Merge(request);
  optimization_guide::ExecuteModelWithLogging(
      executor_, optimization_guide::ModelBasedCapabilityKey::kCompose,
      static_cast<const optimization_guide::proto::ComposeRequest&>(
          merged_request.BuildProtoMessage()),
      std::nullopt, std::move(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::OptimizationGuideModelExecutionResult 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();
    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;
  }

  ModelExecutionComplete(request_delta, request_reason, was_input_edited,
                         std::move(result), std::move(log_entry));
}

void ComposeSession::ModelExecutionComplete(
    base::TimeDelta request_delta,
    compose::ComposeRequestReason request_reason,
    bool was_input_edited,
    optimization_guide::OptimizationGuideModelExecutionResult 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;
  }

  auto response = optimization_guide::ParsedAnyMetadata<
      optimization_guide::proto::ComposeResponse>(result.response.value());

  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 = false;
  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 (!executor_) {
    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();

    request_context_ = optimization_guide::MultimodalMessage(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);
}