@iblai/web-utils 1.11.12 → 1.12.0

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
@@ -11914,46 +11914,117 @@ const selectIsReasoning = (state) => state.chatSliceShared.currentStreamingMessa
11914
11914
  // NEW: Get streaming tool calls
11915
11915
  const selectStreamingToolCalls = (state) => state.chatSliceShared.currentStreamingMessage.toolCalls;
11916
11916
 
11917
+ const DEFAULT_IDLE_TIMEOUT_SECONDS = 120;
11918
+ // Minimum gap between idle-timer resets. Interaction events like mousemove fire
11919
+ // rapidly; there's no need to restart the (minutes-long) idle countdown more
11920
+ // than once per second.
11921
+ const ACTIVITY_THROTTLE_MS = 1000;
11922
+ // User interactions that count as activity and keep the user "present".
11923
+ const ACTIVITY_EVENTS = [
11924
+ "mousemove",
11925
+ "mousedown",
11926
+ "keydown",
11927
+ "wheel",
11928
+ "scroll",
11929
+ "touchstart",
11930
+ "pointerdown",
11931
+ ];
11917
11932
  class TimeTracker {
11918
11933
  constructor(config) {
11934
+ var _a, _b, _c;
11919
11935
  this.state = {
11920
11936
  currentUrl: "",
11921
- startTime: 0,
11922
11937
  intervalId: null,
11923
11938
  routeUnsubscribe: null,
11939
+ accumulatedMs: 0,
11940
+ activeSince: null,
11941
+ isVisible: true,
11942
+ isFocused: true,
11943
+ isIdle: false,
11944
+ idleTimerId: null,
11924
11945
  };
11946
+ // Timestamp of the last activity that reset the idle timer (for throttling).
11947
+ this.lastActivityAt = 0;
11948
+ // Bound listeners kept so they can be removed on destroy.
11949
+ this.handleActivity = () => this.onActivity();
11950
+ this.handleVisibilityChange = () => this.onVisibilityChange();
11951
+ this.handleWindowFocus = () => this.onWindowFocus();
11952
+ this.handleWindowBlur = () => this.onWindowBlur();
11925
11953
  this.config = config;
11954
+ this.idleTimeoutMs =
11955
+ ((_a = config.idleTimeoutSeconds) !== null && _a !== void 0 ? _a : DEFAULT_IDLE_TIMEOUT_SECONDS) * 1000;
11956
+ this.pauseOnHidden = (_b = config.pauseOnHidden) !== null && _b !== void 0 ? _b : true;
11957
+ this.pauseOnBlur = (_c = config.pauseOnBlur) !== null && _c !== void 0 ? _c : true;
11958
+ this.idleDetectionEnabled =
11959
+ this.idleTimeoutMs > 0 &&
11960
+ typeof window !== "undefined" &&
11961
+ typeof document !== "undefined";
11926
11962
  this.initialize();
11927
11963
  }
11928
11964
  initialize() {
11929
11965
  this.state.currentUrl = this.config.getCurrentUrl();
11966
+ // Seed activity signals from the current environment.
11967
+ if (this.pauseOnHidden && typeof document !== "undefined") {
11968
+ this.state.isVisible = !document.hidden;
11969
+ }
11970
+ this.state.isFocused = true;
11971
+ this.state.isIdle = false;
11972
+ this.attachActivityListeners();
11930
11973
  this.startTracking();
11974
+ this.startIdleTimer();
11931
11975
  if (this.config.onRouteChange) {
11932
11976
  this.state.routeUnsubscribe = this.config.onRouteChange(() => {
11933
11977
  this.handleRouteChange();
11934
11978
  });
11935
11979
  }
11936
11980
  }
11981
+ // --- Active time accounting -------------------------------------------------
11982
+ /** Whether time should currently be counted. */
11983
+ isActive() {
11984
+ return this.state.isVisible && this.state.isFocused && !this.state.isIdle;
11985
+ }
11986
+ /**
11987
+ * Reconcile the active window with the current activity signals: open a new
11988
+ * window when the user becomes active, or bank the elapsed time when they
11989
+ * become inactive.
11990
+ */
11991
+ syncActiveWindow() {
11992
+ const active = this.isActive();
11993
+ if (active && this.state.activeSince === null) {
11994
+ this.state.activeSince = Date.now();
11995
+ }
11996
+ else if (!active && this.state.activeSince !== null) {
11997
+ this.state.accumulatedMs += Date.now() - this.state.activeSince;
11998
+ this.state.activeSince = null;
11999
+ }
12000
+ }
12001
+ getTimeSpent() {
12002
+ let total = this.state.accumulatedMs;
12003
+ if (this.state.activeSince !== null) {
12004
+ total += Date.now() - this.state.activeSince;
12005
+ }
12006
+ return total;
12007
+ }
12008
+ resetTimer() {
12009
+ this.state.accumulatedMs = 0;
12010
+ this.state.activeSince = this.isActive() ? Date.now() : null;
12011
+ }
11937
12012
  startTracking() {
11938
- this.state.startTime = Date.now();
11939
- this.clearInterval();
12013
+ this.clearTrackingInterval();
12014
+ this.resetTimer();
12015
+ if (typeof window === "undefined")
12016
+ return;
11940
12017
  this.state.intervalId = window.setInterval(() => {
11941
12018
  this.sendTimeUpdate();
11942
12019
  this.resetTimer();
11943
12020
  }, this.config.intervalSeconds * 1000);
11944
12021
  }
11945
- clearInterval() {
12022
+ clearTrackingInterval() {
11946
12023
  if (this.state.intervalId !== null) {
11947
12024
  clearInterval(this.state.intervalId);
11948
12025
  this.state.intervalId = null;
11949
12026
  }
11950
12027
  }
11951
- resetTimer() {
11952
- this.state.startTime = Date.now();
11953
- }
11954
- getTimeSpent() {
11955
- return Date.now() - this.state.startTime;
11956
- }
11957
12028
  sendTimeUpdate() {
11958
12029
  const timeSpent = this.getTimeSpent();
11959
12030
  if (timeSpent > 0) {
@@ -11968,16 +12039,116 @@ class TimeTracker {
11968
12039
  this.resetTimer();
11969
12040
  }
11970
12041
  }
12042
+ // --- Activity / inactivity detection ---------------------------------------
12043
+ attachActivityListeners() {
12044
+ if (typeof window !== "undefined" && window.addEventListener) {
12045
+ if (this.idleDetectionEnabled) {
12046
+ for (const event of ACTIVITY_EVENTS) {
12047
+ window.addEventListener(event, this.handleActivity, {
12048
+ passive: true,
12049
+ });
12050
+ }
12051
+ }
12052
+ if (this.pauseOnBlur) {
12053
+ window.addEventListener("focus", this.handleWindowFocus);
12054
+ window.addEventListener("blur", this.handleWindowBlur);
12055
+ }
12056
+ }
12057
+ if (this.pauseOnHidden &&
12058
+ typeof document !== "undefined" &&
12059
+ document.addEventListener) {
12060
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
12061
+ }
12062
+ }
12063
+ detachActivityListeners() {
12064
+ if (typeof window !== "undefined" && window.removeEventListener) {
12065
+ for (const event of ACTIVITY_EVENTS) {
12066
+ window.removeEventListener(event, this.handleActivity);
12067
+ }
12068
+ window.removeEventListener("focus", this.handleWindowFocus);
12069
+ window.removeEventListener("blur", this.handleWindowBlur);
12070
+ }
12071
+ if (typeof document !== "undefined" && document.removeEventListener) {
12072
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
12073
+ }
12074
+ }
12075
+ startIdleTimer() {
12076
+ if (!this.idleDetectionEnabled)
12077
+ return;
12078
+ this.clearIdleTimer();
12079
+ this.lastActivityAt = Date.now();
12080
+ this.state.idleTimerId = window.setTimeout(() => {
12081
+ this.markIdle();
12082
+ }, this.idleTimeoutMs);
12083
+ }
12084
+ clearIdleTimer() {
12085
+ if (this.state.idleTimerId !== null) {
12086
+ clearTimeout(this.state.idleTimerId);
12087
+ this.state.idleTimerId = null;
12088
+ }
12089
+ }
12090
+ markIdle() {
12091
+ if (this.state.isIdle)
12092
+ return;
12093
+ this.state.isIdle = true;
12094
+ this.syncActiveWindow();
12095
+ }
12096
+ onActivity() {
12097
+ // Returning from idle must always restart the countdown immediately.
12098
+ if (this.state.isIdle) {
12099
+ this.state.isIdle = false;
12100
+ this.syncActiveWindow();
12101
+ this.startIdleTimer();
12102
+ return;
12103
+ }
12104
+ // Otherwise throttle: no need to reset a minutes-long timer on every
12105
+ // mousemove.
12106
+ if (Date.now() - this.lastActivityAt < ACTIVITY_THROTTLE_MS)
12107
+ return;
12108
+ this.startIdleTimer();
12109
+ }
12110
+ onVisibilityChange() {
12111
+ if (typeof document === "undefined")
12112
+ return;
12113
+ this.state.isVisible = !document.hidden;
12114
+ if (this.state.isVisible) {
12115
+ // Returning to the tab counts as being present again.
12116
+ this.state.isIdle = false;
12117
+ this.startIdleTimer();
12118
+ }
12119
+ this.syncActiveWindow();
12120
+ }
12121
+ onWindowFocus() {
12122
+ this.state.isFocused = true;
12123
+ this.state.isIdle = false;
12124
+ this.startIdleTimer();
12125
+ this.syncActiveWindow();
12126
+ }
12127
+ onWindowBlur() {
12128
+ this.state.isFocused = false;
12129
+ this.syncActiveWindow();
12130
+ }
12131
+ // --- Public API -------------------------------------------------------------
11971
12132
  pause() {
11972
12133
  this.sendTimeUpdate();
11973
- this.clearInterval();
12134
+ this.clearTrackingInterval();
12135
+ this.clearIdleTimer();
12136
+ // Stop accounting until resumed.
12137
+ this.state.accumulatedMs = 0;
12138
+ this.state.activeSince = null;
11974
12139
  }
11975
12140
  resume() {
12141
+ // Treat resume as a fresh, present start so a stale idle flag can't keep
12142
+ // the tracker from counting.
12143
+ this.state.isIdle = false;
11976
12144
  this.startTracking();
12145
+ this.startIdleTimer();
11977
12146
  }
11978
12147
  destroy() {
11979
12148
  this.sendTimeUpdate();
11980
- this.clearInterval();
12149
+ this.clearTrackingInterval();
12150
+ this.clearIdleTimer();
12151
+ this.detachActivityListeners();
11981
12152
  if (this.state.routeUnsubscribe) {
11982
12153
  this.state.routeUnsubscribe();
11983
12154
  this.state.routeUnsubscribe = null;
@@ -11989,6 +12160,10 @@ class TimeTracker {
11989
12160
  getTimeSpentSinceLastReset() {
11990
12161
  return this.getTimeSpent();
11991
12162
  }
12163
+ /** Whether the user is currently considered active (present & interacting). */
12164
+ getIsActive() {
12165
+ return this.isActive();
12166
+ }
11992
12167
  }
11993
12168
 
11994
12169
  function useTimeTracker(config) {