blob: 71bb0889ed35462b550bed11985c3207a95fe4bf [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/intelligence/bwg/model/bwg_session_handler.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "ios/chrome/browser/intelligence/bwg/metrics/bwg_metrics.h"
#import "ios/chrome/browser/intelligence/bwg/model/bwg_session_delegate.h"
#import "ios/chrome/browser/intelligence/bwg/model/bwg_tab_helper.h"
#import "ios/chrome/browser/intelligence/features/features.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/bwg_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
namespace {
IOSGeminiFirstPromptSubmissionMethod ConvertBWGInputTypeToHistogramEnum(
BWGInputType input_type) {
switch (input_type) {
case BWGInputTypeText:
return IOSGeminiFirstPromptSubmissionMethod::kText;
case BWGInputTypeSummarize:
return IOSGeminiFirstPromptSubmissionMethod::kSummarize;
case BWGInputTypeCheckThisSite:
return IOSGeminiFirstPromptSubmissionMethod::kCheckThisSite;
case BWGInputTypeFindRelatedSites:
return IOSGeminiFirstPromptSubmissionMethod::kFindRelatedSites;
case BWGInputTypeAskAboutPage:
return IOSGeminiFirstPromptSubmissionMethod::kAskAboutPage;
case BWGInputTypeCreateFaq:
return IOSGeminiFirstPromptSubmissionMethod::kCreateFaq;
case BWGInputTypeZeroStateModelSuggestion:
return IOSGeminiFirstPromptSubmissionMethod::kZeroStateSuggestions;
case BWGInputTypeWhatCanGeminiDo:
return IOSGeminiFirstPromptSubmissionMethod::kWhatCanGeminiDo;
case BWGInputTypeUnknown:
default:
return IOSGeminiFirstPromptSubmissionMethod::kUnknown;
}
}
} // namespace
@implementation BWGSessionHandler {
// The associated WebStateList.
raw_ptr<WebStateList> _webStateList;
// Session start time for duration tracking.
base::TimeTicks _sessionStartTime;
// Tracks if user has received the first response in current session.
BOOL _hasReceivedFirstResponse;
// Tracks if user has sent their first prompt in current session.
BOOL _hasSubmittedFirstPrompt;
base::TimeTicks _lastPromptSentTime;
BOOL _lastPromptHadPageContext;
BOOL _waitingForResponse;
// Track prompts per session.
int _totalPromptsInSession;
}
- (instancetype)initWithWebStateList:(WebStateList*)webStateList {
self = [super init];
if (self) {
_webStateList = webStateList;
}
return self;
}
#pragma mark - BWGSessionDelegate
- (void)newSessionCreatedWithClientID:(NSString*)clientID
serverID:(NSString*)serverID {
[self updateSessionWithClientID:clientID serverID:serverID];
}
- (void)UIDidAppearWithClientID:(NSString*)clientID
serverID:(NSString*)serverID {
[self updateSessionWithClientID:clientID serverID:serverID];
[self setSessionActive:YES clientID:clientID];
// Start session timer.
_sessionStartTime = base::TimeTicks::Now();
// Reset first response flag for new session.
_hasReceivedFirstResponse = NO;
// Reset first prompt flag for new session.
_hasSubmittedFirstPrompt = NO;
if (IsGeminiCrossTabEnabled()) {
[self dismissOtherActiveSessionsUsingClientID:clientID];
}
// Reset prompt counters for new session.
_totalPromptsInSession = 0;
}
- (void)UIDidDisappearWithClientID:(NSString*)clientID
serverID:(NSString*)serverID {
[_BWGHandler dismissBWGFlowWithCompletion:nil];
[self setSessionActive:NO clientID:clientID];
web::WebState* webState = [self webStateWithClientID:clientID];
if (!webState) {
return;
}
// Get the BWGTabHelper from the WebState.
BwgTabHelper* BWGTabHelper = BwgTabHelper::FromWebState(webState);
// WebState should always be valid as long as the tab is open.
if (!BWGTabHelper) {
// Early exit if no valid tab helper is found.
return;
}
bool isFirstSession = BWGTabHelper->GetIsFirstRun();
BWGTabHelper->SetIsFirstRun(false);
// Record session duration.
if (!_sessionStartTime.is_null()) {
base::TimeDelta session_duration =
base::TimeTicks::Now() - _sessionStartTime;
// Determine session type.
IOSGeminiSessionType session_type;
if (_hasSubmittedFirstPrompt) {
session_type = IOSGeminiSessionType::kWithPrompt;
} else {
session_type = IOSGeminiSessionType::kAbandoned;
}
RecordBWGSessionLengthByType(session_duration, isFirstSession,
session_type);
RecordBWGSessionTime(session_duration);
_sessionStartTime = base::TimeTicks();
}
// Reset latency tracking on session end.
_waitingForResponse = NO;
_lastPromptSentTime = base::TimeTicks();
// TODO(crbug.com/435649967): log # of times users dismissed the floaty before
// receiving a response.
// Record prompt counts for the session.
RecordSessionPromptCount(_totalPromptsInSession);
RecordSessionFirstPrompt(_hasSubmittedFirstPrompt);
}
- (void)responseReceivedWithClientID:(NSString*)clientID
serverID:(NSString*)serverID {
[self updateSessionWithClientID:clientID serverID:serverID];
// Calculate and record response latency.
if (_waitingForResponse && !_lastPromptSentTime.is_null()) {
base::TimeDelta latency = base::TimeTicks::Now() - _lastPromptSentTime;
RecordResponseLatency(latency, _lastPromptHadPageContext);
// Reset latency tracking.
_waitingForResponse = NO;
_lastPromptSentTime = base::TimeTicks();
}
if (!_hasReceivedFirstResponse) {
_hasReceivedFirstResponse = YES;
RecordFirstResponseReceived();
}
// Track all responses for conversation engagement.
RecordBWGResponseReceived();
}
- (void)didTapBWGSettingsButton {
[self.settingsHandler showBWGSettings];
}
- (void)didSendQueryWithInputType:(BWGInputType)inputType
pageContextAttached:(BOOL)pageContextAttached {
_totalPromptsInSession++;
// Record user action for prompt sent.
RecordBWGPromptSent();
// Check if this is the user's first prompt.
if (!_hasSubmittedFirstPrompt) {
_hasSubmittedFirstPrompt = YES;
IOSGeminiFirstPromptSubmissionMethod method =
ConvertBWGInputTypeToHistogramEnum(inputType);
RecordFirstPromptSubmission(method);
}
// Track context attachment for all prompts.
RecordPromptContextAttachment(pageContextAttached);
// Start latency tracking.
_lastPromptSentTime = base::TimeTicks::Now();
_lastPromptHadPageContext = pageContextAttached;
_waitingForResponse = YES;
}
// Called when a new chat button is tapped.
- (void)didTapNewChatButtonWithSessionID:(NSString*)sessionID
conversationID:(NSString*)conversationID {
web::WebState* webState = [self webStateWithClientID:sessionID];
if (!webState) {
return;
}
BwgTabHelper* BWGTabHelper = BwgTabHelper::FromWebState(webState);
BWGTabHelper->DeleteBwgSessionInStorage();
// Record the new chat metric.
RecordBWGNewChatButtonTapped();
}
#pragma mark - Private
// Finds the web state with the given client ID as unique identifier.
- (web::WebState*)webStateWithClientID:(NSString*)clientID {
for (int i = 0; i < _webStateList->count(); i++) {
web::WebState* webState = _webStateList->GetWebStateAt(i);
NSString* webStateUniqueID = base::SysUTF8ToNSString(
base::NumberToString(webState->GetUniqueIdentifier().identifier()));
if ([webStateUniqueID isEqualToString:clientID]) {
return webState;
}
}
return nil;
}
// Updates the session state in storage with the given client ID and server ID.
- (void)updateSessionWithClientID:(NSString*)clientID
serverID:(NSString*)serverID {
web::WebState* webState = [self webStateWithClientID:clientID];
if (!webState) {
return;
}
BwgTabHelper* BWGTabHelper = BwgTabHelper::FromWebState(webState);
BWGTabHelper->CreateOrUpdateBwgSessionInStorage(
base::SysNSStringToUTF8(serverID));
}
// Sets the session's active state for the given client ID.
- (void)setSessionActive:(BOOL)active clientID:(NSString*)clientID {
web::WebState* webState = [self webStateWithClientID:clientID];
if (!webState) {
return;
}
BwgTabHelper* BWGTabHelper = BwgTabHelper::FromWebState(webState);
BWGTabHelper->SetBwgUiShowing(active);
}
// Sets all BWG sessions inactive other than for the WebState matching
// `clientID`.
- (void)dismissOtherActiveSessionsUsingClientID:(NSString*)clientID {
// TODO(crbug.com/437338434): Keep track of last known active instance to not
// have to iterate over all WebStates.
for (int i = 0; i < _webStateList->count(); i++) {
web::WebState* webState = _webStateList->GetWebStateAt(i);
NSString* webStateUniqueID = base::SysUTF8ToNSString(
base::NumberToString(webState->GetUniqueIdentifier().identifier()));
if (!webState->IsRealized() ||
[webStateUniqueID isEqualToString:clientID]) {
continue;
}
BwgTabHelper* BWGTabHelper = BwgTabHelper::FromWebState(webState);
BWGTabHelper->DeactivateBWGSession();
}
}
@end