begeniux 0.1.0 → 0.2.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.cjs CHANGED
@@ -28,67 +28,93 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
30
  // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ AdaptationEngine: () => AdaptationEngine,
33
34
  BeGenProvider: () => BeGenProvider,
34
- BeGenSurface: () => BeGenSurface,
35
- PERSONAS: () => PERSONAS,
36
- createGeminiClassifier: () => createGeminiClassifier,
37
- createHeuristicClassifier: () => createHeuristicClassifier,
35
+ createHttpAdapter: () => createHttpAdapter,
36
+ snapshotVisibleSelectors: () => snapshotVisibleSelectors,
38
37
  useBeGenContext: () => useBeGenContext,
39
38
  useBehaviorTracker: () => useBehaviorTracker
40
39
  });
41
- module.exports = __toCommonJS(index_exports);
40
+ module.exports = __toCommonJS(src_exports);
42
41
 
43
42
  // src/BeGenProvider.tsx
44
43
  var React = __toESM(require("react"), 1);
45
- var import_jsx_runtime = require("react/jsx-runtime");
46
- var BeGenContext = React.createContext(null);
47
- function BeGenProvider({ children }) {
48
- const [variant, setVariant] = React.useState("neutral");
49
- const [directive, setDirective] = React.useState(null);
50
- const [summary, setSummary] = React.useState(null);
51
- const value = React.useMemo(
52
- () => ({ variant, directive, summary, setVariant, setDirective, setSummary }),
53
- [variant, directive, summary]
54
- );
55
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BeGenContext.Provider, { value, children });
56
- }
57
-
58
- // src/BeGenSurface.tsx
59
- var React2 = __toESM(require("react"), 1);
60
44
 
61
45
  // src/useBehaviorTracker.ts
62
46
  var import_react = require("react");
63
- var RECENT_EVENTS_CAP = 5;
47
+ var RECENT_EVENTS_CAP = 10;
64
48
  var HOVER_MIN_MS = 200;
49
+ var RAGE_CLICK_WINDOW_MS = 1e3;
50
+ var RAGE_CLICK_THRESHOLD = 3;
51
+ var INPUT_THROTTLE_MS = 1e3;
65
52
  function targetLabel(el) {
66
53
  if (!(el instanceof HTMLElement)) return "unknown";
67
- return el.dataset.begenId || el.getAttribute("data-product-id") || el.getAttribute("aria-label") || el.tagName.toLowerCase();
54
+ const begenId = el.getAttribute("data-begen-id");
55
+ if (begenId) return `[data-begen-id="${begenId}"]`;
56
+ if (el.id) return `#${el.id}`;
57
+ const testId = el.getAttribute("data-testid");
58
+ if (testId) return `[data-testid="${testId}"]`;
59
+ return el.tagName.toLowerCase();
68
60
  }
69
- function computeSummary(buffer, bufferSize, pageContext) {
61
+ function computeSummary(buffer, bufferSize, pageContext, custom) {
70
62
  const now = Date.now();
71
63
  const windowStart = now - 6e4;
72
64
  let clicksLastMin = 0;
65
+ let rageClicks = 0;
73
66
  let dwellSum = 0;
74
67
  let dwellCount = 0;
75
68
  let maxScroll = 0;
69
+ let formInteractions = 0;
70
+ let errorsSeen = 0;
76
71
  const hoverTargets = /* @__PURE__ */ new Set();
72
+ let viewportW = typeof window !== "undefined" ? window.innerWidth || 0 : 0;
73
+ let viewportH = typeof window !== "undefined" ? window.innerHeight || 0 : 0;
77
74
  for (const ev of buffer) {
78
- if (ev.kind === "click" && ev.t >= windowStart) clicksLastMin += 1;
79
- if (ev.kind === "dwell") {
80
- dwellSum += ev.durationMs;
81
- dwellCount += 1;
75
+ switch (ev.kind) {
76
+ case "click":
77
+ if (ev.t >= windowStart) clicksLastMin += 1;
78
+ break;
79
+ case "rage-click":
80
+ rageClicks += 1;
81
+ break;
82
+ case "dwell":
83
+ dwellSum += ev.durationMs;
84
+ dwellCount += 1;
85
+ break;
86
+ case "scroll":
87
+ if (ev.depth > maxScroll) maxScroll = ev.depth;
88
+ break;
89
+ case "hover":
90
+ hoverTargets.add(ev.target);
91
+ break;
92
+ case "input":
93
+ case "submit":
94
+ formInteractions += 1;
95
+ break;
96
+ case "error":
97
+ errorsSeen += 1;
98
+ break;
99
+ case "viewport-change":
100
+ viewportW = ev.width;
101
+ viewportH = ev.height;
102
+ break;
103
+ default:
104
+ break;
82
105
  }
83
- if (ev.kind === "scroll" && ev.depth > maxScroll) maxScroll = ev.depth;
84
- if (ev.kind === "hover") hoverTargets.add(ev.target);
85
106
  }
86
107
  return {
87
108
  clicks_per_min: clicksLastMin,
109
+ rage_clicks: rageClicks,
88
110
  avg_dwell_ms: dwellCount === 0 ? 0 : Math.round(dwellSum / dwellCount),
89
111
  scroll_depth: Math.max(0, Math.min(1, maxScroll)),
90
112
  hover_count: hoverTargets.size,
113
+ form_interactions: formInteractions,
114
+ errors_seen: errorsSeen,
91
115
  events_seen: Math.min(buffer.length, bufferSize),
116
+ viewport: { width: viewportW, height: viewportH },
117
+ custom,
92
118
  page_context: pageContext
93
119
  };
94
120
  }
@@ -100,13 +126,19 @@ function useBehaviorTracker(opts) {
100
126
  bufferSize = 50,
101
127
  onFlush,
102
128
  pageContext,
103
- seedTrace
129
+ seedTrace,
130
+ customListeners
104
131
  } = opts;
105
132
  const bufferRef = (0, import_react.useRef)([]);
106
133
  const sinceFlushRef = (0, import_react.useRef)(0);
107
134
  const lastFlushAtRef = (0, import_react.useRef)(Date.now());
108
135
  const hoverStartRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
136
+ const focusStartRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
137
+ const lastInputAtRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
138
+ const recentClicksRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
109
139
  const scrollRafRef = (0, import_react.useRef)(null);
140
+ const resizeRafRef = (0, import_react.useRef)(null);
141
+ const customSlotRef = (0, import_react.useRef)({});
110
142
  const onFlushRef = (0, import_react.useRef)(onFlush);
111
143
  const pageContextRef = (0, import_react.useRef)(pageContext);
112
144
  (0, import_react.useEffect)(() => {
@@ -123,7 +155,9 @@ function useBehaviorTracker(opts) {
123
155
  sinceFlushRef.current += 1;
124
156
  setRecentEvents((prev) => {
125
157
  const next = [...prev, ev];
126
- if (next.length > RECENT_EVENTS_CAP) next.splice(0, next.length - RECENT_EVENTS_CAP);
158
+ if (next.length > RECENT_EVENTS_CAP) {
159
+ next.splice(0, next.length - RECENT_EVENTS_CAP);
160
+ }
127
161
  return next;
128
162
  });
129
163
  if (sinceFlushRef.current >= flushEveryEvents) {
@@ -131,31 +165,58 @@ function useBehaviorTracker(opts) {
131
165
  }
132
166
  };
133
167
  const flush = () => {
134
- const summary = computeSummary(bufferRef.current, bufferSize, pageContextRef.current);
168
+ const summary = computeSummary(
169
+ bufferRef.current,
170
+ bufferSize,
171
+ pageContextRef.current,
172
+ customSlotRef.current
173
+ );
135
174
  sinceFlushRef.current = 0;
136
175
  lastFlushAtRef.current = Date.now();
137
176
  onFlushRef.current(summary);
138
177
  };
139
178
  (0, import_react.useEffect)(() => {
140
- if (seedTrace && seedTrace.length > 0) {
141
- const buf = bufferRef.current;
142
- const maxT = seedTrace.reduce((m, e) => e.t > m ? e.t : m, -Infinity);
143
- const now = Date.now();
144
- const offset = Number.isFinite(maxT) ? now - maxT : 0;
145
- const rebased = seedTrace.map((e) => ({ ...e, t: e.t + offset }));
146
- buf.push(...rebased);
147
- if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);
148
- const summary = computeSummary(buf, bufferSize, pageContextRef.current);
149
- sinceFlushRef.current = 0;
150
- lastFlushAtRef.current = Date.now();
151
- onFlushRef.current(summary);
152
- }
179
+ if (!seedTrace || seedTrace.length === 0) return;
180
+ const buf = bufferRef.current;
181
+ const maxT = seedTrace.reduce((m, e) => e.t > m ? e.t : m, -Infinity);
182
+ const now = Date.now();
183
+ const offset = Number.isFinite(maxT) ? now - maxT : 0;
184
+ const rebased = seedTrace.map((e) => ({ ...e, t: e.t + offset }));
185
+ buf.push(...rebased);
186
+ if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);
187
+ const summary = computeSummary(
188
+ buf,
189
+ bufferSize,
190
+ pageContextRef.current,
191
+ customSlotRef.current
192
+ );
193
+ sinceFlushRef.current = 0;
194
+ lastFlushAtRef.current = Date.now();
195
+ onFlushRef.current(summary);
153
196
  }, []);
154
197
  (0, import_react.useEffect)(() => {
155
- const el = containerRef.current;
198
+ if (typeof window === "undefined" || typeof document === "undefined") return;
199
+ const el = containerRef?.current ?? (typeof document !== "undefined" ? document.body : null);
156
200
  if (!el) return;
157
201
  const onClick = (e) => {
158
- pushEvent({ kind: "click", target: targetLabel(e.target), t: Date.now() });
202
+ const label = targetLabel(e.target);
203
+ const t = Date.now();
204
+ pushEvent({ kind: "click", target: label, t });
205
+ const recent = recentClicksRef.current.get(label) ?? [];
206
+ const filtered = recent.filter(
207
+ (then) => t - then < RAGE_CLICK_WINDOW_MS
208
+ );
209
+ filtered.push(t);
210
+ recentClicksRef.current.set(label, filtered);
211
+ if (filtered.length >= RAGE_CLICK_THRESHOLD) {
212
+ pushEvent({
213
+ kind: "rage-click",
214
+ target: label,
215
+ count: filtered.length,
216
+ t
217
+ });
218
+ recentClicksRef.current.set(label, []);
219
+ }
159
220
  };
160
221
  const onMouseOver = (e) => {
161
222
  const label = targetLabel(e.target);
@@ -174,11 +235,46 @@ function useBehaviorTracker(opts) {
174
235
  pushEvent({ kind: "hover", target: label, durationMs, t });
175
236
  pushEvent({ kind: "dwell", target: label, durationMs, t });
176
237
  };
238
+ const onFocusIn = (e) => {
239
+ const label = targetLabel(e.target);
240
+ focusStartRef.current.set(label, Date.now());
241
+ pushEvent({ kind: "focus", target: label, t: Date.now() });
242
+ };
243
+ const onFocusOut = (e) => {
244
+ const label = targetLabel(e.target);
245
+ const started = focusStartRef.current.get(label);
246
+ if (started == null) return;
247
+ focusStartRef.current.delete(label);
248
+ const durationMs = Date.now() - started;
249
+ if (durationMs < HOVER_MIN_MS) return;
250
+ pushEvent({
251
+ kind: "blur",
252
+ target: label,
253
+ durationMs,
254
+ t: Date.now()
255
+ });
256
+ };
257
+ const onInput = (e) => {
258
+ const target = e.target;
259
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target instanceof HTMLElement && target.isContentEditable)) {
260
+ return;
261
+ }
262
+ const label = targetLabel(target);
263
+ const now = Date.now();
264
+ const last = lastInputAtRef.current.get(label) ?? 0;
265
+ if (now - last < INPUT_THROTTLE_MS) return;
266
+ lastInputAtRef.current.set(label, now);
267
+ pushEvent({ kind: "input", target: label, t: now });
268
+ };
269
+ const onSubmit = (e) => {
270
+ const label = targetLabel(e.target);
271
+ pushEvent({ kind: "submit", target: label, t: Date.now() });
272
+ };
177
273
  const computeScrollDepth = () => {
178
274
  const rect = el.getBoundingClientRect();
179
- const viewportH = window.innerHeight || 1;
275
+ const viewportHCalc = window.innerHeight || 1;
180
276
  const elementH = el.scrollHeight || rect.height || 1;
181
- const visibleBottom = Math.min(rect.bottom, viewportH);
277
+ const visibleBottom = Math.min(rect.bottom, viewportHCalc);
182
278
  const scrolledPast = Math.max(0, visibleBottom - rect.top);
183
279
  return Math.max(0, Math.min(1, scrolledPast / elementH));
184
280
  };
@@ -186,13 +282,60 @@ function useBehaviorTracker(opts) {
186
282
  if (scrollRafRef.current != null) return;
187
283
  scrollRafRef.current = requestAnimationFrame(() => {
188
284
  scrollRafRef.current = null;
189
- pushEvent({ kind: "scroll", depth: computeScrollDepth(), t: Date.now() });
285
+ pushEvent({
286
+ kind: "scroll",
287
+ depth: computeScrollDepth(),
288
+ t: Date.now()
289
+ });
290
+ });
291
+ };
292
+ const onResize = () => {
293
+ if (resizeRafRef.current != null) return;
294
+ resizeRafRef.current = requestAnimationFrame(() => {
295
+ resizeRafRef.current = null;
296
+ pushEvent({
297
+ kind: "viewport-change",
298
+ width: window.innerWidth,
299
+ height: window.innerHeight,
300
+ t: Date.now()
301
+ });
302
+ });
303
+ };
304
+ const onError = (e) => {
305
+ pushEvent({
306
+ kind: "error",
307
+ message: String(e.message ?? "unknown"),
308
+ t: Date.now()
309
+ });
310
+ };
311
+ const onUnhandledRejection = (e) => {
312
+ pushEvent({
313
+ kind: "error",
314
+ message: `unhandled-rejection: ${String(e.reason ?? "unknown")}`,
315
+ t: Date.now()
190
316
  });
191
317
  };
192
318
  el.addEventListener("click", onClick);
193
319
  el.addEventListener("mouseover", onMouseOver);
194
320
  el.addEventListener("mouseout", onMouseOut);
321
+ el.addEventListener("focusin", onFocusIn);
322
+ el.addEventListener("focusout", onFocusOut);
323
+ el.addEventListener("input", onInput);
324
+ el.addEventListener("submit", onSubmit);
195
325
  window.addEventListener("scroll", onScroll, { passive: true });
326
+ window.addEventListener("resize", onResize);
327
+ window.addEventListener("error", onError);
328
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
329
+ const cleanups = [];
330
+ if (customListeners) {
331
+ for (const listener of customListeners) {
332
+ try {
333
+ const cleanup = listener.attach(el, pushEvent);
334
+ cleanups.push(cleanup);
335
+ } catch {
336
+ }
337
+ }
338
+ }
196
339
  const interval = window.setInterval(() => {
197
340
  if (Date.now() - lastFlushAtRef.current >= flushAfterMs) {
198
341
  flush();
@@ -202,321 +345,514 @@ function useBehaviorTracker(opts) {
202
345
  el.removeEventListener("click", onClick);
203
346
  el.removeEventListener("mouseover", onMouseOver);
204
347
  el.removeEventListener("mouseout", onMouseOut);
348
+ el.removeEventListener("focusin", onFocusIn);
349
+ el.removeEventListener("focusout", onFocusOut);
350
+ el.removeEventListener("input", onInput);
351
+ el.removeEventListener("submit", onSubmit);
205
352
  window.removeEventListener("scroll", onScroll);
353
+ window.removeEventListener("resize", onResize);
354
+ window.removeEventListener("error", onError);
355
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
206
356
  window.clearInterval(interval);
207
- if (scrollRafRef.current != null) cancelAnimationFrame(scrollRafRef.current);
357
+ if (scrollRafRef.current != null) {
358
+ cancelAnimationFrame(scrollRafRef.current);
359
+ }
360
+ if (resizeRafRef.current != null) {
361
+ cancelAnimationFrame(resizeRafRef.current);
362
+ }
363
+ for (const cleanup of cleanups) {
364
+ try {
365
+ cleanup();
366
+ } catch {
367
+ }
368
+ }
208
369
  };
209
370
  }, [containerRef, flushAfterMs, flushEveryEvents, bufferSize]);
210
371
  return { recentEvents };
211
372
  }
212
373
 
213
- // src/personas.ts
214
- function buildDecisive() {
215
- const events = [];
216
- const productIds = ["p-101", "p-102", "p-103", "p-104", "p-105", "p-106"];
217
- let t = 0;
218
- for (let i = 0; i < 12; i++) {
219
- t += 1500;
220
- events.push({ kind: "click", target: productIds[i % productIds.length], t });
374
+ // src/AdaptationEngine.ts
375
+ var STRUCTURAL_PROPERTY_DENY = /* @__PURE__ */ new Set([
376
+ "display",
377
+ "position",
378
+ "visibility",
379
+ "float",
380
+ "clear",
381
+ "z-index",
382
+ "overflow",
383
+ "overflow-x",
384
+ "overflow-y",
385
+ "transform-origin"
386
+ ]);
387
+ var AdaptationEngine = class {
388
+ constructor(opts) {
389
+ this.revertLog = [];
390
+ this.appliedSnapshot = [];
391
+ this.root = opts.root;
392
+ this.scope = opts.scope ?? {};
393
+ this.onEvent = opts.onEvent;
221
394
  }
222
- for (let i = 0; i < 6; i++) {
223
- t += 800;
224
- events.push({
225
- kind: "dwell",
226
- target: productIds[i % productIds.length],
227
- durationMs: 600 + i * 80,
228
- t
229
- });
395
+ /** Apply a plan. Reverts the previous plan first so mutations don't accumulate. */
396
+ apply(plan) {
397
+ this.revertAll();
398
+ for (const adaptation of plan.adaptations) {
399
+ this.applyOne(adaptation);
400
+ }
230
401
  }
231
- for (let i = 0; i < 6; i++) {
232
- t += 1200;
233
- events.push({
234
- kind: "scroll",
235
- depth: Math.min(0.95, 0.4 + i * 0.1),
236
- t
237
- });
402
+ /** Roll back every adaptation applied since the last revertAll. */
403
+ revertAll() {
404
+ if (this.revertLog.length === 0) return;
405
+ const count = this.revertLog.length;
406
+ for (let i = this.revertLog.length - 1; i >= 0; i--) {
407
+ try {
408
+ this.revertLog[i]();
409
+ } catch {
410
+ }
411
+ }
412
+ this.revertLog = [];
413
+ this.appliedSnapshot = [];
414
+ this.onEvent?.({ kind: "reverted", count });
415
+ }
416
+ /** Read-only view of currently applied adaptations (for telemetry). */
417
+ getApplied() {
418
+ return this.appliedSnapshot;
238
419
  }
239
- for (let i = 0; i < 2; i++) {
240
- t += 600;
241
- events.push({
242
- kind: "hover",
243
- target: productIds[i],
244
- durationMs: 250,
245
- t
420
+ // ── Internals ──────────────────────────────────────────────────────
421
+ applyOne(adaptation) {
422
+ const elements = this.resolveTargets(adaptation);
423
+ if (elements === null) return;
424
+ if (elements.length === 0) {
425
+ this.onEvent?.({ kind: "skipped", adaptation, reason: "no-match" });
426
+ return;
427
+ }
428
+ if (adaptation.kind === "set-style" && STRUCTURAL_PROPERTY_DENY.has(adaptation.property.toLowerCase())) {
429
+ this.onEvent?.({
430
+ kind: "skipped",
431
+ adaptation,
432
+ reason: `structural-property-denied:${adaptation.property}`
433
+ });
434
+ return;
435
+ }
436
+ for (const el of elements) {
437
+ const revert = this.applyToElement(el, adaptation);
438
+ if (revert) this.revertLog.push(revert);
439
+ }
440
+ this.appliedSnapshot.push(adaptation);
441
+ this.onEvent?.({
442
+ kind: "applied",
443
+ adaptation,
444
+ matched: elements.length
246
445
  });
247
446
  }
248
- return events;
447
+ resolveTargets(adaptation) {
448
+ const sel = adaptation.selector;
449
+ if (!sel || typeof sel !== "string") {
450
+ this.onEvent?.({
451
+ kind: "skipped",
452
+ adaptation,
453
+ reason: "invalid-selector"
454
+ });
455
+ return null;
456
+ }
457
+ if (this.scope.deny?.some((d) => this.selectorMatchesPattern(sel, d))) {
458
+ this.onEvent?.({ kind: "skipped", adaptation, reason: "scope-deny" });
459
+ return null;
460
+ }
461
+ if (this.scope.allow && !this.scope.allow.some((a) => this.selectorMatchesPattern(sel, a))) {
462
+ this.onEvent?.({
463
+ kind: "skipped",
464
+ adaptation,
465
+ reason: "scope-not-in-allow"
466
+ });
467
+ return null;
468
+ }
469
+ let nodes;
470
+ try {
471
+ if (sel === ":root" || sel === "html") {
472
+ return [document.documentElement];
473
+ }
474
+ nodes = this.root.querySelectorAll(sel);
475
+ } catch {
476
+ this.onEvent?.({
477
+ kind: "skipped",
478
+ adaptation,
479
+ reason: "selector-syntax-error"
480
+ });
481
+ return null;
482
+ }
483
+ return Array.from(nodes).filter(
484
+ (n) => n instanceof HTMLElement
485
+ );
486
+ }
487
+ selectorMatchesPattern(selector, pattern) {
488
+ if (selector === pattern) return true;
489
+ return selector.includes(pattern);
490
+ }
491
+ applyToElement(el, adaptation) {
492
+ switch (adaptation.kind) {
493
+ case "set-css-var": {
494
+ const prev = el.style.getPropertyValue(adaptation.name);
495
+ const prevPriority = el.style.getPropertyPriority(adaptation.name);
496
+ el.style.setProperty(adaptation.name, adaptation.value);
497
+ return () => {
498
+ if (prev) el.style.setProperty(adaptation.name, prev, prevPriority);
499
+ else el.style.removeProperty(adaptation.name);
500
+ };
501
+ }
502
+ case "add-class": {
503
+ if (el.classList.contains(adaptation.className)) return null;
504
+ el.classList.add(adaptation.className);
505
+ return () => el.classList.remove(adaptation.className);
506
+ }
507
+ case "remove-class": {
508
+ if (!el.classList.contains(adaptation.className)) return null;
509
+ el.classList.remove(adaptation.className);
510
+ return () => el.classList.add(adaptation.className);
511
+ }
512
+ case "set-style": {
513
+ const prop = adaptation.property;
514
+ const prev = el.style.getPropertyValue(prop);
515
+ const prevPriority = el.style.getPropertyPriority(prop);
516
+ el.style.setProperty(prop, adaptation.value);
517
+ return () => {
518
+ if (prev) el.style.setProperty(prop, prev, prevPriority);
519
+ else el.style.removeProperty(prop);
520
+ };
521
+ }
522
+ case "set-attribute": {
523
+ const had = el.hasAttribute(adaptation.name);
524
+ const prev = el.getAttribute(adaptation.name);
525
+ el.setAttribute(adaptation.name, adaptation.value);
526
+ return () => {
527
+ if (had && prev !== null) el.setAttribute(adaptation.name, prev);
528
+ else el.removeAttribute(adaptation.name);
529
+ };
530
+ }
531
+ case "set-aria-label": {
532
+ const had = el.hasAttribute("aria-label");
533
+ const prev = el.getAttribute("aria-label");
534
+ el.setAttribute("aria-label", adaptation.value);
535
+ return () => {
536
+ if (had && prev !== null) el.setAttribute("aria-label", prev);
537
+ else el.removeAttribute("aria-label");
538
+ };
539
+ }
540
+ default: {
541
+ const _exhaustive = adaptation;
542
+ void _exhaustive;
543
+ return null;
544
+ }
545
+ }
546
+ }
547
+ };
548
+
549
+ // src/domSnapshot.ts
550
+ function snapshotVisibleSelectors(root, opts = {}) {
551
+ if (typeof window === "undefined" || typeof document === "undefined") {
552
+ return [];
553
+ }
554
+ const max = opts.maxSelectors ?? 50;
555
+ const seen = /* @__PURE__ */ new Set();
556
+ const out = [];
557
+ const viewportH = window.innerHeight || 1;
558
+ const viewportW = window.innerWidth || 1;
559
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
560
+ acceptNode: (node2) => {
561
+ if (!(node2 instanceof HTMLElement)) return NodeFilter.FILTER_SKIP;
562
+ const rect = node2.getBoundingClientRect();
563
+ if (rect.width === 0 || rect.height === 0) return NodeFilter.FILTER_SKIP;
564
+ if (rect.bottom < 0 || rect.top > viewportH) return NodeFilter.FILTER_SKIP;
565
+ if (rect.right < 0 || rect.left > viewportW) return NodeFilter.FILTER_SKIP;
566
+ return NodeFilter.FILTER_ACCEPT;
567
+ }
568
+ });
569
+ let node = walker.nextNode();
570
+ while (node && out.length < max) {
571
+ if (node instanceof HTMLElement) {
572
+ const sel = pickSelector(node);
573
+ if (sel && !seen.has(sel) && passesScope(sel, opts.scope)) {
574
+ seen.add(sel);
575
+ out.push(sel);
576
+ }
577
+ }
578
+ node = walker.nextNode();
579
+ }
580
+ return out;
249
581
  }
250
- function buildDeliberate() {
251
- const events = [];
252
- const productIds = ["p-201", "p-202", "p-203", "p-204", "p-205", "p-206"];
253
- let t = 0;
254
- for (let i = 0; i < 6; i++) {
255
- t += 5500;
256
- const target = productIds[i];
257
- events.push({ kind: "hover", target, durationMs: 1800 + i * 200, t });
258
- t += 200;
259
- events.push({
260
- kind: "dwell",
261
- target,
262
- durationMs: 5e3 + i * 400,
263
- t
264
- });
582
+ function pickSelector(el) {
583
+ if (el.id) return `#${cssEscape(el.id)}`;
584
+ const begenId = el.getAttribute("data-begen-id");
585
+ if (begenId) return `[data-begen-id="${attrEscape(begenId)}"]`;
586
+ const testId = el.getAttribute("data-testid");
587
+ if (testId) return `[data-testid="${attrEscape(testId)}"]`;
588
+ const role = el.getAttribute("role");
589
+ if (role) return `${el.tagName.toLowerCase()}[role="${attrEscape(role)}"]`;
590
+ const semantic = SEMANTIC_TAGS.has(el.tagName.toLowerCase());
591
+ if (semantic) {
592
+ const aria = el.getAttribute("aria-label");
593
+ if (aria) {
594
+ return `${el.tagName.toLowerCase()}[aria-label="${attrEscape(aria)}"]`;
595
+ }
596
+ return el.tagName.toLowerCase();
265
597
  }
266
- for (let i = 0; i < 3; i++) {
267
- t += 4e3;
268
- events.push({ kind: "click", target: productIds[i], t });
598
+ const cls = pickStableClass(el);
599
+ if (cls) return `.${cssEscape(cls)}`;
600
+ return null;
601
+ }
602
+ var SEMANTIC_TAGS = /* @__PURE__ */ new Set([
603
+ "header",
604
+ "main",
605
+ "nav",
606
+ "footer",
607
+ "aside",
608
+ "article",
609
+ "section",
610
+ "form",
611
+ "dialog"
612
+ ]);
613
+ function pickStableClass(el) {
614
+ for (const cls of el.classList) {
615
+ if (cls.includes(":")) continue;
616
+ if (/^[a-z]+-[a-z0-9-]+$/.test(cls)) continue;
617
+ if (cls.length < 3) continue;
618
+ return cls;
269
619
  }
270
- for (let i = 0; i < 5; i++) {
271
- t += 1500;
272
- events.push({
273
- kind: "scroll",
274
- depth: Math.min(0.6, 0.2 + i * 0.08),
275
- t
276
- });
620
+ return null;
621
+ }
622
+ function passesScope(selector, scope) {
623
+ if (!scope) return true;
624
+ if (scope.deny?.some((d) => selector === d || selector.includes(d))) {
625
+ return false;
277
626
  }
278
- return events;
627
+ if (scope.allow && !scope.allow.some((a) => selector === a || selector.includes(a))) {
628
+ return false;
629
+ }
630
+ return true;
279
631
  }
280
- var PERSONAS = {
281
- get decisive() {
282
- return buildDecisive();
283
- },
284
- get deliberate() {
285
- return buildDeliberate();
632
+ function cssEscape(s) {
633
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
634
+ return CSS.escape(s);
286
635
  }
287
- };
636
+ return s.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
637
+ }
638
+ function attrEscape(s) {
639
+ return s.replace(/"/g, '\\"');
640
+ }
288
641
 
289
- // src/BeGenSurface.tsx
290
- var import_jsx_runtime2 = require("react/jsx-runtime");
291
- function BeGenSurface(props) {
642
+ // src/BeGenProvider.tsx
643
+ var import_jsx_runtime = require("react/jsx-runtime");
644
+ var BeGenContext = React.createContext(null);
645
+ function useBeGenContext() {
646
+ const ctx = React.useContext(BeGenContext);
647
+ if (!ctx) {
648
+ throw new Error("useBeGenContext must be used inside <BeGenProvider>");
649
+ }
650
+ return ctx;
651
+ }
652
+ function BeGenProvider(props) {
292
653
  const {
293
- variants,
294
- classify,
295
- variantProps,
654
+ designSystem,
296
655
  pageContext,
297
- seedPersona,
298
- rateLimitMs = 4e3,
299
- className,
300
- style
656
+ classify,
657
+ scope,
658
+ rateLimitMs = 5e3,
659
+ triggerEveryEvents = 5,
660
+ flushAfterMs = 5e3,
661
+ flushEveryEvents = 10,
662
+ bufferSize = 50,
663
+ customListeners,
664
+ containerRef,
665
+ seedTrace,
666
+ children
301
667
  } = props;
302
- const containerRef = React2.useRef(null);
303
- const ctx = React2.useContext(BeGenContext);
304
- const [currentVariant, setCurrentVariant] = React2.useState("neutral");
305
- const [lastDirective, setLastDirective] = React2.useState(null);
306
- const [lastSummary, setLastSummary] = React2.useState(null);
307
- const lastClassifyAtRef = React2.useRef(0);
308
- const inFlightRef = React2.useRef(false);
309
- const classifyRef = React2.useRef(classify);
310
- React2.useEffect(() => {
668
+ const engineRef = React.useRef(null);
669
+ const [summary, setSummary] = React.useState(null);
670
+ const [lastPlan, setLastPlan] = React.useState(null);
671
+ const [appliedAdaptations, setAppliedAdaptations] = React.useState([]);
672
+ const lastClassifyAtRef = React.useRef(0);
673
+ const inFlightRef = React.useRef(false);
674
+ const eventsSinceClassifyRef = React.useRef(0);
675
+ const lastPlanHashRef = React.useRef(null);
676
+ const classifyRef = React.useRef(classify);
677
+ React.useEffect(() => {
311
678
  classifyRef.current = classify;
312
679
  }, [classify]);
313
- const ctxRef = React2.useRef(ctx);
314
- React2.useEffect(() => {
315
- ctxRef.current = ctx;
316
- }, [ctx]);
317
- const handleFlush = React2.useCallback(async (summary) => {
318
- setLastSummary(summary);
319
- ctxRef.current?.setSummary(summary);
320
- const now = Date.now();
321
- if (inFlightRef.current) return;
322
- if (now - lastClassifyAtRef.current < rateLimitMs) return;
323
- lastClassifyAtRef.current = now;
324
- inFlightRef.current = true;
325
- try {
326
- const directive = await classifyRef.current(summary);
327
- setLastDirective(directive);
328
- setCurrentVariant(directive.variant);
329
- ctxRef.current?.setDirective(directive);
330
- ctxRef.current?.setVariant(directive.variant);
331
- } catch {
332
- } finally {
333
- inFlightRef.current = false;
334
- }
335
- }, [rateLimitMs]);
336
- const seedTrace = seedPersona ? PERSONAS[seedPersona] : void 0;
680
+ const designSystemRef = React.useRef(designSystem);
681
+ React.useEffect(() => {
682
+ designSystemRef.current = designSystem;
683
+ }, [designSystem]);
684
+ const pageContextRef = React.useRef(pageContext);
685
+ React.useEffect(() => {
686
+ pageContextRef.current = pageContext;
687
+ }, [pageContext]);
688
+ const scopeRef = React.useRef(scope);
689
+ React.useEffect(() => {
690
+ scopeRef.current = scope;
691
+ }, [scope]);
692
+ React.useEffect(() => {
693
+ if (typeof document === "undefined") return;
694
+ const root = containerRef?.current ?? document.body;
695
+ if (!root) return;
696
+ engineRef.current = new AdaptationEngine({
697
+ root,
698
+ scope: scopeRef.current,
699
+ onEvent: () => {
700
+ if (engineRef.current) {
701
+ setAppliedAdaptations(engineRef.current.getApplied().slice());
702
+ }
703
+ }
704
+ });
705
+ return () => {
706
+ engineRef.current?.revertAll();
707
+ engineRef.current = null;
708
+ };
709
+ }, []);
710
+ const handleFlush = React.useCallback(
711
+ async (s) => {
712
+ setSummary(s);
713
+ eventsSinceClassifyRef.current += 1;
714
+ const fn = classifyRef.current;
715
+ if (!fn) return;
716
+ if (inFlightRef.current) return;
717
+ const now = Date.now();
718
+ if (now - lastClassifyAtRef.current < rateLimitMs) return;
719
+ if (eventsSinceClassifyRef.current < triggerEveryEvents) return;
720
+ lastClassifyAtRef.current = now;
721
+ eventsSinceClassifyRef.current = 0;
722
+ inFlightRef.current = true;
723
+ try {
724
+ const root = containerRef?.current ?? (typeof document !== "undefined" ? document.body : null) ?? null;
725
+ const visibleSelectors = root ? snapshotVisibleSelectors(root, { scope: scopeRef.current }) : [];
726
+ const route = s.page_context?.route ?? "/";
727
+ const input = {
728
+ summary: s,
729
+ designSystem: designSystemRef.current,
730
+ dom: { visibleSelectors, route }
731
+ };
732
+ const plan = await fn(input);
733
+ if (!plan || !Array.isArray(plan.adaptations)) return;
734
+ const hash = hashPlan(plan);
735
+ if (hash === lastPlanHashRef.current) return;
736
+ lastPlanHashRef.current = hash;
737
+ setLastPlan(plan);
738
+ engineRef.current?.apply(plan);
739
+ } catch {
740
+ } finally {
741
+ inFlightRef.current = false;
742
+ }
743
+ },
744
+ [rateLimitMs, triggerEveryEvents, containerRef]
745
+ );
337
746
  useBehaviorTracker({
338
747
  containerRef,
339
748
  onFlush: handleFlush,
340
749
  pageContext,
341
- seedTrace
750
+ seedTrace,
751
+ customListeners,
752
+ flushAfterMs,
753
+ flushEveryEvents,
754
+ bufferSize
342
755
  });
343
- const ActiveComponent = variants[currentVariant] ?? variants.neutral;
344
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
345
- "div",
346
- {
347
- ref: containerRef,
348
- className,
349
- style,
350
- "data-begen-surface": true,
351
- "data-begen-variant": currentVariant,
352
- children: [
353
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
354
- "div",
355
- {
356
- style: {
357
- opacity: 1,
358
- transition: "opacity 200ms ease",
359
- animation: "begen-fade-in 200ms ease"
360
- },
361
- children: ActiveComponent ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ActiveComponent, { ...variantProps ?? {} }) : null
362
- },
363
- currentVariant
364
- ),
365
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BeGenSurfaceStyles, {}),
366
- lastDirective && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
367
- "span",
368
- {
369
- style: { display: "none" },
370
- "data-begen-confidence": lastDirective.confidence,
371
- "data-begen-reasoning": lastDirective.reasoning,
372
- "data-begen-events": lastSummary?.events_seen ?? 0
373
- }
374
- )
375
- ]
376
- }
377
- );
378
- }
379
- var STYLE_INJECTED = "__begen_styles__";
380
- function BeGenSurfaceStyles() {
381
- React2.useEffect(() => {
382
- if (typeof document === "undefined") return;
383
- if (document[STYLE_INJECTED]) return;
384
- const tag = document.createElement("style");
385
- tag.setAttribute("data-begen-styles", "");
386
- tag.textContent = `@keyframes begen-fade-in { from { opacity: 0 } to { opacity: 1 } }`;
387
- document.head.appendChild(tag);
388
- document[STYLE_INJECTED] = true;
756
+ const recentEventsRef = React.useRef([]);
757
+ const applyPlan = React.useCallback((plan) => {
758
+ if (!plan || !Array.isArray(plan.adaptations)) return;
759
+ const hash = hashPlan(plan);
760
+ if (hash === lastPlanHashRef.current) return;
761
+ lastPlanHashRef.current = hash;
762
+ setLastPlan(plan);
763
+ engineRef.current?.apply(plan);
389
764
  }, []);
390
- return null;
765
+ const getDesignSystem = React.useCallback(
766
+ () => designSystemRef.current,
767
+ []
768
+ );
769
+ const value = React.useMemo(
770
+ () => ({
771
+ summary,
772
+ lastPlan,
773
+ appliedAdaptations,
774
+ recentEvents: recentEventsRef.current,
775
+ applyPlan,
776
+ getDesignSystem
777
+ }),
778
+ [summary, lastPlan, appliedAdaptations, applyPlan, getDesignSystem]
779
+ );
780
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BeGenContext.Provider, { value, children });
391
781
  }
392
-
393
- // src/useBeGenContext.ts
394
- var React3 = __toESM(require("react"), 1);
395
- function useBeGenContext() {
396
- const ctx = React3.useContext(BeGenContext);
397
- if (!ctx) {
398
- throw new Error("useBeGenContext must be used inside <BeGenProvider>");
399
- }
400
- return ctx;
782
+ function hashPlan(plan) {
783
+ const parts = plan.adaptations.map((a) => {
784
+ switch (a.kind) {
785
+ case "set-css-var":
786
+ return `v|${a.selector}|${a.name}|${a.value}`;
787
+ case "add-class":
788
+ return `+|${a.selector}|${a.className}`;
789
+ case "remove-class":
790
+ return `-|${a.selector}|${a.className}`;
791
+ case "set-style":
792
+ return `s|${a.selector}|${a.property}|${a.value}`;
793
+ case "set-attribute":
794
+ return `a|${a.selector}|${a.name}|${a.value}`;
795
+ case "set-aria-label":
796
+ return `l|${a.selector}|${a.value}`;
797
+ }
798
+ });
799
+ return parts.join("\n");
401
800
  }
402
801
 
403
- // src/classifier/gemini.ts
404
- var CLASSIFIER_SYSTEM_PROMPT = `
405
- You classify e-commerce shopper behavior into UI variants.
406
-
407
- You receive a JSON object describing a user's recent interaction pattern on
408
- a product listing page. Decide which UI variant best serves them right now.
409
-
410
- Variants:
411
- - "decisive": user knows what they want; minimize friction. Dense grid,
412
- prominent prices, fast paths to cart, no recommendations.
413
- Signals: high clicks/min, low dwell, high scroll depth, few hovers.
414
- - "deliberate": user is researching; help them compare. Larger cards,
415
- reviews surfaced inline, "people also viewed", expandable detail.
416
- Signals: low clicks/min, high dwell, hovers across multiple products.
417
- - "neutral": insufficient signal yet, or pattern is mixed. Baseline grid.
418
- Default for first ~10 events.
419
-
420
- Examples:
421
- Input: {"clicks_per_min":14,"avg_dwell_ms":820,"scroll_depth":0.91,"hover_count":2,"events_seen":18}
422
- Output: {"variant":"decisive","confidence":0.86,"reasoning":"Fast clicks, low dwell, deep scroll \u2014 purposeful navigation."}
423
-
424
- Input: {"clicks_per_min":3,"avg_dwell_ms":7400,"scroll_depth":0.42,"hover_count":4,"events_seen":22}
425
- Output: {"variant":"deliberate","confidence":0.81,"reasoning":"Slow pace, long dwell, multi-product hover \u2014 comparing options."}
426
-
427
- Input: {"clicks_per_min":5,"avg_dwell_ms":2100,"scroll_depth":0.55,"hover_count":2,"events_seen":7}
428
- Output: {"variant":"neutral","confidence":0.6,"reasoning":"Not enough events yet to commit to a mode."}
429
-
430
- Return ONLY a JSON object. No prose, no markdown.
431
- `.trim();
432
- var VALID_VARIANTS = /* @__PURE__ */ new Set(["decisive", "deliberate", "neutral"]);
433
- var FALLBACK = {
434
- variant: "neutral",
802
+ // src/adapters/http.ts
803
+ var FALLBACK_PLAN = {
804
+ adaptations: [],
435
805
  confidence: 0,
436
- reasoning: "Classifier error."
806
+ reasoning: "HTTP adapter error."
437
807
  };
438
- function createGeminiClassifier(opts) {
439
- const { apiKey, model = "gemini-2.0-flash", endpoint, fetchImpl } = opts;
440
- const url = endpoint ?? `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`;
808
+ function createHttpAdapter(opts) {
809
+ const {
810
+ url,
811
+ headers,
812
+ fetchImpl,
813
+ timeoutMs = 8e3,
814
+ bodyTransform,
815
+ responseTransform
816
+ } = opts;
441
817
  const fetcher = fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
442
- return async (summary) => {
443
- if (!fetcher) return FALLBACK;
818
+ return async (input) => {
819
+ if (!fetcher) return FALLBACK_PLAN;
820
+ const ac = new AbortController();
821
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
444
822
  try {
445
- const res = await fetcher(`${url}?key=${encodeURIComponent(apiKey)}`, {
823
+ const body = bodyTransform ? bodyTransform(input) : input;
824
+ const res = await fetcher(url, {
446
825
  method: "POST",
447
- headers: { "content-type": "application/json" },
448
- body: JSON.stringify({
449
- systemInstruction: { parts: [{ text: CLASSIFIER_SYSTEM_PROMPT }] },
450
- contents: [
451
- {
452
- role: "user",
453
- parts: [{ text: JSON.stringify(summary) }]
454
- }
455
- ],
456
- generationConfig: {
457
- responseMimeType: "application/json",
458
- temperature: 0.2
459
- }
460
- })
826
+ headers: { "content-type": "application/json", ...headers },
827
+ body: JSON.stringify(body),
828
+ signal: ac.signal
461
829
  });
462
- if (!res.ok) return FALLBACK;
463
- const data = await res.json();
464
- const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
465
- if (!text) return FALLBACK;
466
- const parsed = JSON.parse(text);
467
- if (!parsed || typeof parsed !== "object" || typeof parsed.variant !== "string" || !VALID_VARIANTS.has(parsed.variant) || typeof parsed.reasoning !== "string") {
468
- return FALLBACK;
469
- }
470
- const confidence = typeof parsed.confidence === "number" && Number.isFinite(parsed.confidence) ? Math.max(0, Math.min(1, parsed.confidence)) : 0.5;
471
- return {
472
- variant: parsed.variant,
473
- confidence,
474
- reasoning: parsed.reasoning
475
- };
830
+ if (!res.ok) return FALLBACK_PLAN;
831
+ const raw = await res.json();
832
+ const plan = responseTransform ? responseTransform(raw) : raw;
833
+ if (!isValidPlan(plan)) return FALLBACK_PLAN;
834
+ return plan;
476
835
  } catch {
477
- return FALLBACK;
836
+ return FALLBACK_PLAN;
837
+ } finally {
838
+ clearTimeout(timer);
478
839
  }
479
840
  };
480
841
  }
481
-
482
- // src/classifier/heuristic.ts
483
- function createHeuristicClassifier() {
484
- return async (summary) => {
485
- if (summary.events_seen < 10) {
486
- return {
487
- variant: "neutral",
488
- confidence: 0.5,
489
- reasoning: "Insufficient events."
490
- };
491
- }
492
- if (summary.clicks_per_min > 8 && summary.avg_dwell_ms < 2e3) {
493
- return {
494
- variant: "decisive",
495
- confidence: 0.8,
496
- reasoning: "Fast pace, low dwell."
497
- };
498
- }
499
- if (summary.avg_dwell_ms > 4e3 && summary.hover_count > 2) {
500
- return {
501
- variant: "deliberate",
502
- confidence: 0.8,
503
- reasoning: "Slow pace, multi-hover."
504
- };
505
- }
506
- return {
507
- variant: "neutral",
508
- confidence: 0.6,
509
- reasoning: "Mixed signals."
510
- };
511
- };
842
+ function isValidPlan(p) {
843
+ if (!p || typeof p !== "object") return false;
844
+ const plan = p;
845
+ if (!Array.isArray(plan.adaptations)) return false;
846
+ if (typeof plan.confidence !== "number") return false;
847
+ if (typeof plan.reasoning !== "string") return false;
848
+ return true;
512
849
  }
513
850
  // Annotate the CommonJS export names for ESM import in node:
514
851
  0 && (module.exports = {
852
+ AdaptationEngine,
515
853
  BeGenProvider,
516
- BeGenSurface,
517
- PERSONAS,
518
- createGeminiClassifier,
519
- createHeuristicClassifier,
854
+ createHttpAdapter,
855
+ snapshotVisibleSelectors,
520
856
  useBeGenContext,
521
857
  useBehaviorTracker
522
858
  });