@iblai/web-utils 1.11.12 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,6 +27,103 @@ function _interopNamespaceDefault(e) {
27
27
 
28
28
  var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
29
29
 
30
+ const MENTOR_CHAT_DOCUMENTS_EXTENSIONS = [
31
+ // Media types
32
+ "video/*",
33
+ "audio/*",
34
+ "image/*",
35
+ // Document types
36
+ ".docx",
37
+ ".xlsx",
38
+ ".pptx",
39
+ ".csv",
40
+ ".md",
41
+ ".txt",
42
+ ".pdf",
43
+ ];
44
+ const ANONYMOUS_USERNAME = "anonymous";
45
+ const DEFAULT_DISCLAIMER_CONTENT = `By accessing and using the [ibl.ai](http://ibl.ai/) MentorAI platform (“Platform”), you agree to the following terms. The Platform provides AI-powered mentorship and educational support. You understand that AI responses may contain inaccuracies, and you are responsible for independently verifying any information before relying on it for academic, professional, or personal decisions. Do not input confidential, sensitive, or personally identifiable information of yourself or others.
46
+ All content, code, and data processed through the Platform remain the property of their respective owners. [ibl.ai](http://ibl.ai/) retains all rights to the Platform’s underlying technology and intellectual property. You are granted a limited, non-exclusive, revocable license to use the Platform for personal or institutional learning purposes only.
47
+ Your use must comply with applicable laws and institutional policies. Misuse—including attempting to bypass security, copy source code, or use the service for unlawful activity—may result in suspension or termination of access. Continued use constitutes acceptance of these terms.`;
48
+ const MAX_INITIAL_WEBSOCKET_CONNECTION_ATTEMPTS = 3;
49
+ const LOCAL_STORAGE_KEYS = {
50
+ CURRENT_TENANT: "current_tenant",
51
+ TENANTS: "tenants",
52
+ REDIRECT_TO: "redirect-to",
53
+ AUTH_TOKEN: "axd_token",
54
+ TOKEN_EXPIRY: "axd_token_expires",
55
+ USER_DATA: "userData",
56
+ USER_TENANTS: "tenants",
57
+ VISITING_TENANT: "visiting_tenant",
58
+ SESSION_ID: "session_id",
59
+ EDX_TOKEN_KEY: "edx_jwt_token",
60
+ DM_TOKEN_KEY: "dm_token",
61
+ DM_TOKEN_EXPIRES: "dm_token_expires",
62
+ DM_TOKEN_EXPIRY: "dm_token_expires",
63
+ AXD_TOKEN_KEY: "axd_token",
64
+ DEFAULT_TENANT: "tenant",
65
+ MODEL_DOWNLOAD_STATE: "model_download_state",
66
+ MODEL_DOWNLOAD_PROMPT_DISMISSED: "model_download_prompt_dismissed",
67
+ };
68
+ const TOOLS = {
69
+ WEB_SEARCH: "web-search",
70
+ WIKIPEDIA_SEARCH: "wikipedia-search",
71
+ COURSE_CREATION: "course-creation",
72
+ MCP: "mcp",
73
+ IMAGE_GENERATION: "image-generation",
74
+ IBL_PATHWAY_DOCUMENTS_RETRIEVER: "ibl-pathway-documents-retriever",
75
+ TRAINED_DOCUMENTS: "trained-documents",
76
+ IBL_SESSION_DOCUMENTS_RETRIEVER: "ibl-session-documents-retriever",
77
+ PLAYWRIGHT_BROWSER: "playwright-browser",
78
+ HUMAN_SUPPORT: "human-support",
79
+ CODE_INTERPRETER: "code-interpreter",
80
+ DEEP_RESEARCH: "deep-research",
81
+ STUDY_MODE: "study-mode",
82
+ MEMORY: "memory",
83
+ SCREEN_SHARE: "screen-share",
84
+ POWERPOINT: "powerpoint",
85
+ PROMPT: "prompt",
86
+ QUIZ: "quiz",
87
+ RUBRIC: "rubric",
88
+ RESOURCE: "resource",
89
+ LESSON_PLAN: "lesson-plan",
90
+ SYLLABUS: "syllabus",
91
+ CANVAS: "canvas",
92
+ GOOGLE_SLIDES: "google-slides",
93
+ GOOGLE_DOCUMENT: "google-docs",
94
+ };
95
+ // Maps WebSocket tool_call type names to human-readable UI labels
96
+ const TOOL_NAME_MAP = {
97
+ web_search_call: "Searching the web",
98
+ vector_search: "Searching knowledge base",
99
+ calculator: "Calculating",
100
+ file_reader: "Reading file",
101
+ code_executor: "Running code",
102
+ wikipedia: "Searching Wikipedia",
103
+ };
104
+ const REQUIRED_ACTIONS_FOR_GROUPS = {
105
+ NOTIFICATIONS: "Ibl.Notifications/Notification/action",
106
+ };
107
+ const MENTOR_VISIBILITY_VALUES = {
108
+ ADMINISTRATORS: "viewable_by_tenant_admins",
109
+ STUDENTS: "viewable_by_tenant_students",
110
+ ANYONE: "viewable_by_anyone",
111
+ };
112
+ const MENTOR_VISIBILITY = [
113
+ {
114
+ label: "Administrators",
115
+ value: MENTOR_VISIBILITY_VALUES.ADMINISTRATORS,
116
+ },
117
+ {
118
+ label: "Users",
119
+ value: MENTOR_VISIBILITY_VALUES.STUDENTS,
120
+ },
121
+ {
122
+ label: "Anyone",
123
+ value: MENTOR_VISIBILITY_VALUES.ANYONE,
124
+ },
125
+ ];
126
+
30
127
  /**
31
128
  * Authentication and Authorization Utilities
32
129
  *
@@ -63,6 +160,61 @@ function deleteCookie(name, path, domain) {
63
160
  const domainValue = domain ? `domain=${domain};` : "";
64
161
  document.cookie = cookieValue + expires + pathValue + domainValue;
65
162
  }
163
+ /* Reads a JWT's `exp` claim and reports whether it is in the past. A token that
164
+ * cannot be decoded (or is malformed) is treated as expired; a token with no
165
+ * `exp` claim is treated as non-expiring (mirrors the axd "no expiry" case).
166
+ */
167
+ function isJwtExpired(token) {
168
+ try {
169
+ const payload = token.split(".")[1];
170
+ if (!payload)
171
+ return true;
172
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
173
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
174
+ const claims = JSON.parse(atob(padded));
175
+ if (typeof claims.exp !== "number")
176
+ return false;
177
+ return claims.exp * 1000 <= Date.now();
178
+ }
179
+ catch (_a) {
180
+ return true;
181
+ }
182
+ }
183
+ function hasNonExpiredAuthToken() {
184
+ // The edx JWT is stored alongside the axd token at SSO login; a valid session
185
+ // requires it to be present and unexpired too, so re-auth is triggered when
186
+ // it is missing or its `exp` has passed.
187
+ const edxToken = window.localStorage.getItem(LOCAL_STORAGE_KEYS.EDX_TOKEN_KEY);
188
+ if (!edxToken) {
189
+ console.log("################### [hasNonExpiredAuthToken] edx_jwt_token is not defined", edxToken);
190
+ return false;
191
+ }
192
+ if (isJwtExpired(edxToken)) {
193
+ console.log("################### [hasNonExpiredAuthToken] edx_jwt_token is expired");
194
+ return false;
195
+ }
196
+ const token = window.localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
197
+ if (!token) {
198
+ console.log("################### [hasNonExpiredAuthToken] axd token is not defined", token);
199
+ return false;
200
+ }
201
+ const tokenExpiry = window.localStorage.getItem(LOCAL_STORAGE_KEYS.TOKEN_EXPIRY);
202
+ if (!tokenExpiry) {
203
+ console.log("################### [hasNonExpiredAuthToken] axd token expiry is not defined", tokenExpiry);
204
+ return true;
205
+ }
206
+ const expiryDate = new Date(tokenExpiry);
207
+ if (isNaN(expiryDate.getTime())) {
208
+ console.log("################### [hasNonExpiredAuthToken] axd token expiry date", expiryDate);
209
+ return false;
210
+ }
211
+ const currentDate = new Date();
212
+ if (expiryDate <= currentDate) {
213
+ console.log("################### [hasNonExpiredAuthToken] axd token expiry date is less than current date ", expiryDate, currentDate);
214
+ return false;
215
+ }
216
+ return true;
217
+ }
66
218
  /**
67
219
  * Get all possible domain parts for cookie deletion
68
220
  * @param domain - The domain to split
@@ -210,7 +362,7 @@ async function redirectToAuthSpa(options) {
210
362
  logoutTimestamp: "ibl_logout_timestamp",
211
363
  loginTimestamp: "ibl_login_timestamp",
212
364
  tenantSwitching: "ibl_tenant_switching",
213
- }, hasNonExpiredAuthToken, isOffline, preserveTokenKey, authRedirectProxy, isNativeApp, scheme = "iblai-mentor", forceRedirect = false, } = options;
365
+ }, isOffline, preserveTokenKey, authRedirectProxy, isNativeApp, scheme = "iblai-mentor", forceRedirect = false, } = options;
214
366
  console.log("[redirectToAuthSpa] starting redirect to auth spa", redirectTo, platformKey, logout, saveRedirect, forceRedirect);
215
367
  // Skip if a tenant switch is already in progress
216
368
  if (!forceRedirect &&
@@ -1285,103 +1437,6 @@ const advancedTabs = Object.keys(advancedTabsProperties).map((tab) => {
1285
1437
  });
1286
1438
  const translatePrompt = (language) => `Please translate the summarized content into ${language}`;
1287
1439
 
1288
- const MENTOR_CHAT_DOCUMENTS_EXTENSIONS = [
1289
- // Media types
1290
- "video/*",
1291
- "audio/*",
1292
- "image/*",
1293
- // Document types
1294
- ".docx",
1295
- ".xlsx",
1296
- ".pptx",
1297
- ".csv",
1298
- ".md",
1299
- ".txt",
1300
- ".pdf",
1301
- ];
1302
- const ANONYMOUS_USERNAME = "anonymous";
1303
- const DEFAULT_DISCLAIMER_CONTENT = `By accessing and using the [ibl.ai](http://ibl.ai/) MentorAI platform (“Platform”), you agree to the following terms. The Platform provides AI-powered mentorship and educational support. You understand that AI responses may contain inaccuracies, and you are responsible for independently verifying any information before relying on it for academic, professional, or personal decisions. Do not input confidential, sensitive, or personally identifiable information of yourself or others.
1304
- All content, code, and data processed through the Platform remain the property of their respective owners. [ibl.ai](http://ibl.ai/) retains all rights to the Platform’s underlying technology and intellectual property. You are granted a limited, non-exclusive, revocable license to use the Platform for personal or institutional learning purposes only.
1305
- Your use must comply with applicable laws and institutional policies. Misuse—including attempting to bypass security, copy source code, or use the service for unlawful activity—may result in suspension or termination of access. Continued use constitutes acceptance of these terms.`;
1306
- const MAX_INITIAL_WEBSOCKET_CONNECTION_ATTEMPTS = 3;
1307
- const LOCAL_STORAGE_KEYS = {
1308
- CURRENT_TENANT: "current_tenant",
1309
- TENANTS: "tenants",
1310
- REDIRECT_TO: "redirect-to",
1311
- AUTH_TOKEN: "axd_token",
1312
- TOKEN_EXPIRY: "axd_token_expires",
1313
- USER_DATA: "userData",
1314
- USER_TENANTS: "tenants",
1315
- VISITING_TENANT: "visiting_tenant",
1316
- SESSION_ID: "session_id",
1317
- EDX_TOKEN_KEY: "edx_jwt_token",
1318
- DM_TOKEN_KEY: "dm_token",
1319
- DM_TOKEN_EXPIRES: "dm_token_expires",
1320
- DM_TOKEN_EXPIRY: "dm_token_expires",
1321
- AXD_TOKEN_KEY: "axd_token",
1322
- DEFAULT_TENANT: "tenant",
1323
- MODEL_DOWNLOAD_STATE: "model_download_state",
1324
- MODEL_DOWNLOAD_PROMPT_DISMISSED: "model_download_prompt_dismissed",
1325
- };
1326
- const TOOLS = {
1327
- WEB_SEARCH: "web-search",
1328
- WIKIPEDIA_SEARCH: "wikipedia-search",
1329
- COURSE_CREATION: "course-creation",
1330
- MCP: "mcp",
1331
- IMAGE_GENERATION: "image-generation",
1332
- IBL_PATHWAY_DOCUMENTS_RETRIEVER: "ibl-pathway-documents-retriever",
1333
- TRAINED_DOCUMENTS: "trained-documents",
1334
- IBL_SESSION_DOCUMENTS_RETRIEVER: "ibl-session-documents-retriever",
1335
- PLAYWRIGHT_BROWSER: "playwright-browser",
1336
- HUMAN_SUPPORT: "human-support",
1337
- CODE_INTERPRETER: "code-interpreter",
1338
- DEEP_RESEARCH: "deep-research",
1339
- STUDY_MODE: "study-mode",
1340
- MEMORY: "memory",
1341
- SCREEN_SHARE: "screen-share",
1342
- POWERPOINT: "powerpoint",
1343
- PROMPT: "prompt",
1344
- QUIZ: "quiz",
1345
- RUBRIC: "rubric",
1346
- RESOURCE: "resource",
1347
- LESSON_PLAN: "lesson-plan",
1348
- SYLLABUS: "syllabus",
1349
- CANVAS: "canvas",
1350
- GOOGLE_SLIDES: "google-slides",
1351
- GOOGLE_DOCUMENT: "google-docs",
1352
- };
1353
- // Maps WebSocket tool_call type names to human-readable UI labels
1354
- const TOOL_NAME_MAP = {
1355
- web_search_call: "Searching the web",
1356
- vector_search: "Searching knowledge base",
1357
- calculator: "Calculating",
1358
- file_reader: "Reading file",
1359
- code_executor: "Running code",
1360
- wikipedia: "Searching Wikipedia",
1361
- };
1362
- const REQUIRED_ACTIONS_FOR_GROUPS = {
1363
- NOTIFICATIONS: "Ibl.Notifications/Notification/action",
1364
- };
1365
- const MENTOR_VISIBILITY_VALUES = {
1366
- ADMINISTRATORS: "viewable_by_tenant_admins",
1367
- STUDENTS: "viewable_by_tenant_students",
1368
- ANYONE: "viewable_by_anyone",
1369
- };
1370
- const MENTOR_VISIBILITY = [
1371
- {
1372
- label: "Administrators",
1373
- value: MENTOR_VISIBILITY_VALUES.ADMINISTRATORS,
1374
- },
1375
- {
1376
- label: "Users",
1377
- value: MENTOR_VISIBILITY_VALUES.STUDENTS,
1378
- },
1379
- {
1380
- label: "Anyone",
1381
- value: MENTOR_VISIBILITY_VALUES.ANYONE,
1382
- },
1383
- ];
1384
-
1385
1440
  /**
1386
1441
  * Reads the cached user_nicename from the LOCAL_STORAGE_KEYS.USER_DATA blob.
1387
1442
  */
@@ -9197,29 +9252,6 @@ async function syncAuthToCookies(storageService) {
9197
9252
  console.error("[syncAuthToCookies] Error syncing to cookies:", error);
9198
9253
  }
9199
9254
  }
9200
- function hasNonExpiredAuthToken() {
9201
- const token = window.localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
9202
- if (!token) {
9203
- console.log("################### [hasNonExpiredAuthToken] axd token is not defined", token);
9204
- return false;
9205
- }
9206
- const tokenExpiry = window.localStorage.getItem(LOCAL_STORAGE_KEYS.TOKEN_EXPIRY);
9207
- if (!tokenExpiry) {
9208
- console.log("################### [hasNonExpiredAuthToken] axd token expiry is not defined", tokenExpiry);
9209
- return true;
9210
- }
9211
- const expiryDate = new Date(tokenExpiry);
9212
- if (isNaN(expiryDate.getTime())) {
9213
- console.log("################### [hasNonExpiredAuthToken] axd token expiry date", expiryDate);
9214
- return false;
9215
- }
9216
- const currentDate = new Date();
9217
- if (expiryDate <= currentDate) {
9218
- console.log("################### [hasNonExpiredAuthToken] axd token expiry date is less than current date ", expiryDate, currentDate);
9219
- return false;
9220
- }
9221
- return true;
9222
- }
9223
9255
  /**
9224
9256
  * Clear current tenant cookie only (web only)
9225
9257
  * Should be called before tenant switching
@@ -11914,46 +11946,117 @@ const selectIsReasoning = (state) => state.chatSliceShared.currentStreamingMessa
11914
11946
  // NEW: Get streaming tool calls
11915
11947
  const selectStreamingToolCalls = (state) => state.chatSliceShared.currentStreamingMessage.toolCalls;
11916
11948
 
11949
+ const DEFAULT_IDLE_TIMEOUT_SECONDS = 120;
11950
+ // Minimum gap between idle-timer resets. Interaction events like mousemove fire
11951
+ // rapidly; there's no need to restart the (minutes-long) idle countdown more
11952
+ // than once per second.
11953
+ const ACTIVITY_THROTTLE_MS = 1000;
11954
+ // User interactions that count as activity and keep the user "present".
11955
+ const ACTIVITY_EVENTS = [
11956
+ "mousemove",
11957
+ "mousedown",
11958
+ "keydown",
11959
+ "wheel",
11960
+ "scroll",
11961
+ "touchstart",
11962
+ "pointerdown",
11963
+ ];
11917
11964
  class TimeTracker {
11918
11965
  constructor(config) {
11966
+ var _a, _b, _c;
11919
11967
  this.state = {
11920
11968
  currentUrl: "",
11921
- startTime: 0,
11922
11969
  intervalId: null,
11923
11970
  routeUnsubscribe: null,
11971
+ accumulatedMs: 0,
11972
+ activeSince: null,
11973
+ isVisible: true,
11974
+ isFocused: true,
11975
+ isIdle: false,
11976
+ idleTimerId: null,
11924
11977
  };
11978
+ // Timestamp of the last activity that reset the idle timer (for throttling).
11979
+ this.lastActivityAt = 0;
11980
+ // Bound listeners kept so they can be removed on destroy.
11981
+ this.handleActivity = () => this.onActivity();
11982
+ this.handleVisibilityChange = () => this.onVisibilityChange();
11983
+ this.handleWindowFocus = () => this.onWindowFocus();
11984
+ this.handleWindowBlur = () => this.onWindowBlur();
11925
11985
  this.config = config;
11986
+ this.idleTimeoutMs =
11987
+ ((_a = config.idleTimeoutSeconds) !== null && _a !== void 0 ? _a : DEFAULT_IDLE_TIMEOUT_SECONDS) * 1000;
11988
+ this.pauseOnHidden = (_b = config.pauseOnHidden) !== null && _b !== void 0 ? _b : true;
11989
+ this.pauseOnBlur = (_c = config.pauseOnBlur) !== null && _c !== void 0 ? _c : true;
11990
+ this.idleDetectionEnabled =
11991
+ this.idleTimeoutMs > 0 &&
11992
+ typeof window !== "undefined" &&
11993
+ typeof document !== "undefined";
11926
11994
  this.initialize();
11927
11995
  }
11928
11996
  initialize() {
11929
11997
  this.state.currentUrl = this.config.getCurrentUrl();
11998
+ // Seed activity signals from the current environment.
11999
+ if (this.pauseOnHidden && typeof document !== "undefined") {
12000
+ this.state.isVisible = !document.hidden;
12001
+ }
12002
+ this.state.isFocused = true;
12003
+ this.state.isIdle = false;
12004
+ this.attachActivityListeners();
11930
12005
  this.startTracking();
12006
+ this.startIdleTimer();
11931
12007
  if (this.config.onRouteChange) {
11932
12008
  this.state.routeUnsubscribe = this.config.onRouteChange(() => {
11933
12009
  this.handleRouteChange();
11934
12010
  });
11935
12011
  }
11936
12012
  }
12013
+ // --- Active time accounting -------------------------------------------------
12014
+ /** Whether time should currently be counted. */
12015
+ isActive() {
12016
+ return this.state.isVisible && this.state.isFocused && !this.state.isIdle;
12017
+ }
12018
+ /**
12019
+ * Reconcile the active window with the current activity signals: open a new
12020
+ * window when the user becomes active, or bank the elapsed time when they
12021
+ * become inactive.
12022
+ */
12023
+ syncActiveWindow() {
12024
+ const active = this.isActive();
12025
+ if (active && this.state.activeSince === null) {
12026
+ this.state.activeSince = Date.now();
12027
+ }
12028
+ else if (!active && this.state.activeSince !== null) {
12029
+ this.state.accumulatedMs += Date.now() - this.state.activeSince;
12030
+ this.state.activeSince = null;
12031
+ }
12032
+ }
12033
+ getTimeSpent() {
12034
+ let total = this.state.accumulatedMs;
12035
+ if (this.state.activeSince !== null) {
12036
+ total += Date.now() - this.state.activeSince;
12037
+ }
12038
+ return total;
12039
+ }
12040
+ resetTimer() {
12041
+ this.state.accumulatedMs = 0;
12042
+ this.state.activeSince = this.isActive() ? Date.now() : null;
12043
+ }
11937
12044
  startTracking() {
11938
- this.state.startTime = Date.now();
11939
- this.clearInterval();
12045
+ this.clearTrackingInterval();
12046
+ this.resetTimer();
12047
+ if (typeof window === "undefined")
12048
+ return;
11940
12049
  this.state.intervalId = window.setInterval(() => {
11941
12050
  this.sendTimeUpdate();
11942
12051
  this.resetTimer();
11943
12052
  }, this.config.intervalSeconds * 1000);
11944
12053
  }
11945
- clearInterval() {
12054
+ clearTrackingInterval() {
11946
12055
  if (this.state.intervalId !== null) {
11947
12056
  clearInterval(this.state.intervalId);
11948
12057
  this.state.intervalId = null;
11949
12058
  }
11950
12059
  }
11951
- resetTimer() {
11952
- this.state.startTime = Date.now();
11953
- }
11954
- getTimeSpent() {
11955
- return Date.now() - this.state.startTime;
11956
- }
11957
12060
  sendTimeUpdate() {
11958
12061
  const timeSpent = this.getTimeSpent();
11959
12062
  if (timeSpent > 0) {
@@ -11968,16 +12071,116 @@ class TimeTracker {
11968
12071
  this.resetTimer();
11969
12072
  }
11970
12073
  }
12074
+ // --- Activity / inactivity detection ---------------------------------------
12075
+ attachActivityListeners() {
12076
+ if (typeof window !== "undefined" && window.addEventListener) {
12077
+ if (this.idleDetectionEnabled) {
12078
+ for (const event of ACTIVITY_EVENTS) {
12079
+ window.addEventListener(event, this.handleActivity, {
12080
+ passive: true,
12081
+ });
12082
+ }
12083
+ }
12084
+ if (this.pauseOnBlur) {
12085
+ window.addEventListener("focus", this.handleWindowFocus);
12086
+ window.addEventListener("blur", this.handleWindowBlur);
12087
+ }
12088
+ }
12089
+ if (this.pauseOnHidden &&
12090
+ typeof document !== "undefined" &&
12091
+ document.addEventListener) {
12092
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
12093
+ }
12094
+ }
12095
+ detachActivityListeners() {
12096
+ if (typeof window !== "undefined" && window.removeEventListener) {
12097
+ for (const event of ACTIVITY_EVENTS) {
12098
+ window.removeEventListener(event, this.handleActivity);
12099
+ }
12100
+ window.removeEventListener("focus", this.handleWindowFocus);
12101
+ window.removeEventListener("blur", this.handleWindowBlur);
12102
+ }
12103
+ if (typeof document !== "undefined" && document.removeEventListener) {
12104
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
12105
+ }
12106
+ }
12107
+ startIdleTimer() {
12108
+ if (!this.idleDetectionEnabled)
12109
+ return;
12110
+ this.clearIdleTimer();
12111
+ this.lastActivityAt = Date.now();
12112
+ this.state.idleTimerId = window.setTimeout(() => {
12113
+ this.markIdle();
12114
+ }, this.idleTimeoutMs);
12115
+ }
12116
+ clearIdleTimer() {
12117
+ if (this.state.idleTimerId !== null) {
12118
+ clearTimeout(this.state.idleTimerId);
12119
+ this.state.idleTimerId = null;
12120
+ }
12121
+ }
12122
+ markIdle() {
12123
+ if (this.state.isIdle)
12124
+ return;
12125
+ this.state.isIdle = true;
12126
+ this.syncActiveWindow();
12127
+ }
12128
+ onActivity() {
12129
+ // Returning from idle must always restart the countdown immediately.
12130
+ if (this.state.isIdle) {
12131
+ this.state.isIdle = false;
12132
+ this.syncActiveWindow();
12133
+ this.startIdleTimer();
12134
+ return;
12135
+ }
12136
+ // Otherwise throttle: no need to reset a minutes-long timer on every
12137
+ // mousemove.
12138
+ if (Date.now() - this.lastActivityAt < ACTIVITY_THROTTLE_MS)
12139
+ return;
12140
+ this.startIdleTimer();
12141
+ }
12142
+ onVisibilityChange() {
12143
+ if (typeof document === "undefined")
12144
+ return;
12145
+ this.state.isVisible = !document.hidden;
12146
+ if (this.state.isVisible) {
12147
+ // Returning to the tab counts as being present again.
12148
+ this.state.isIdle = false;
12149
+ this.startIdleTimer();
12150
+ }
12151
+ this.syncActiveWindow();
12152
+ }
12153
+ onWindowFocus() {
12154
+ this.state.isFocused = true;
12155
+ this.state.isIdle = false;
12156
+ this.startIdleTimer();
12157
+ this.syncActiveWindow();
12158
+ }
12159
+ onWindowBlur() {
12160
+ this.state.isFocused = false;
12161
+ this.syncActiveWindow();
12162
+ }
12163
+ // --- Public API -------------------------------------------------------------
11971
12164
  pause() {
11972
12165
  this.sendTimeUpdate();
11973
- this.clearInterval();
12166
+ this.clearTrackingInterval();
12167
+ this.clearIdleTimer();
12168
+ // Stop accounting until resumed.
12169
+ this.state.accumulatedMs = 0;
12170
+ this.state.activeSince = null;
11974
12171
  }
11975
12172
  resume() {
12173
+ // Treat resume as a fresh, present start so a stale idle flag can't keep
12174
+ // the tracker from counting.
12175
+ this.state.isIdle = false;
11976
12176
  this.startTracking();
12177
+ this.startIdleTimer();
11977
12178
  }
11978
12179
  destroy() {
11979
12180
  this.sendTimeUpdate();
11980
- this.clearInterval();
12181
+ this.clearTrackingInterval();
12182
+ this.clearIdleTimer();
12183
+ this.detachActivityListeners();
11981
12184
  if (this.state.routeUnsubscribe) {
11982
12185
  this.state.routeUnsubscribe();
11983
12186
  this.state.routeUnsubscribe = null;
@@ -11989,6 +12192,10 @@ class TimeTracker {
11989
12192
  getTimeSpentSinceLastReset() {
11990
12193
  return this.getTimeSpent();
11991
12194
  }
12195
+ /** Whether the user is currently considered active (present & interacting). */
12196
+ getIsActive() {
12197
+ return this.isActive();
12198
+ }
11992
12199
  }
11993
12200
 
11994
12201
  function useTimeTracker(config) {
@@ -19077,6 +19284,7 @@ exports.isExpo = isExpo;
19077
19284
  exports.isFileAccepted = isFileAccepted;
19078
19285
  exports.isInIframe = isInIframe;
19079
19286
  exports.isJSON = isJSON;
19287
+ exports.isJwtExpired = isJwtExpired;
19080
19288
  exports.isLoggedIn = isLoggedIn;
19081
19289
  exports.isNode = isNode;
19082
19290
  exports.isReactNative = isReactNative;