@sentientui/react 0.4.0 → 0.6.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/README.md CHANGED
@@ -134,6 +134,8 @@ Accepts the same `apiKey`, `context`, `consent`, `ssrFallback`, `onAssignment`,
134
134
  | `id` | `string` | Unique component identifier within your project. |
135
135
  | `variants` | `Record<string, ReactNode>` | Map of variant ID → content. Any two or more keys; the bandit explores them all. |
136
136
  | `goal` | `string \| GoalConfig` | Conversion goal. A string is a click-goal label; an object is an explicit `GoalConfig`. |
137
+ | `agentDataByVariant` | `Record<string, unknown>` *(optional)* | Structured data keyed by variant ID that AI agents can consume via `GET /v1/agent/layout`. Only the assigned variant's entry is sent to the server. Preferred over `agentData`. |
138
+ | `agentData` | `unknown` *(optional, deprecated)* | Deprecated in favour of `agentDataByVariant`. Single value stored once regardless of which variant is shown; kept for backward compatibility. |
137
139
  | `clientOnly` | `boolean` | Render nothing on the server; resolve on the client only. Use for cookie-dependent slots. |
138
140
 
139
141
  #### Goal types
@@ -159,9 +161,21 @@ goal={{
159
161
  { type: 'click' },
160
162
  ],
161
163
  }}
164
+
165
+ // Weighted composite — each step fires immediately as it completes, with a fractional reward.
166
+ // Use for multi-step funnels where partial completion still signals quality.
167
+ // Steps are independent: step 2 can fire before step 1.
168
+ goal={{
169
+ type: 'weighted_composite',
170
+ steps: [
171
+ { goal: { type: 'scroll_depth', threshold: 0.5 }, name: 'viewed_pricing', weight: 0.2 },
172
+ { goal: { type: 'click' }, name: 'clicked_cta', weight: 0.4 },
173
+ { goal: { type: 'form_submit' }, name: 'signed_up', weight: 1.0 },
174
+ ],
175
+ }}
162
176
  ```
163
177
 
164
- Each goal fires at most once per variant mount.
178
+ Each goal fires at most once per variant mount. `WeightedCompositeGoal` fires each step's reward independently; `CompositeGoal` waits for all sub-goals and fires reward `1.0` once.
165
179
 
166
180
  ### `<AdaptiveText>`
167
181
 
package/dist/index.d.cts CHANGED
@@ -88,7 +88,16 @@ type CompositeGoal = {
88
88
  type: 'composite';
89
89
  all: GoalConfig[];
90
90
  };
91
- type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;
91
+ type WeightedStep = {
92
+ goal: GoalConfig;
93
+ name: string;
94
+ weight: number;
95
+ };
96
+ type WeightedCompositeGoal = {
97
+ type: 'weighted_composite';
98
+ steps: WeightedStep[];
99
+ };
100
+ type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal | WeightedCompositeGoal;
92
101
  type AdaptiveProps = {
93
102
  id: string;
94
103
  variants: Record<string, ReactNode>;
@@ -99,7 +108,15 @@ type AdaptiveProps = {
99
108
  * a hydration mismatch. Tradeoff: minor CLS on first paint.
100
109
  */
101
110
  clientOnly?: boolean;
102
- /** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */
111
+ /**
112
+ * Variant-specific structured data for AI agent consumption via /sentient.json and
113
+ * GET /v1/agent/layout. Keyed by variant ID — only the assigned variant's entry is stored,
114
+ * so agents see only the content currently being served to visitors.
115
+ *
116
+ * Prefer this over `agentData` when variants have meaningfully different content.
117
+ */
118
+ agentDataByVariant?: Record<string, unknown>;
119
+ /** @deprecated Use agentDataByVariant for variant-specific content. Stored as-is for the assigned variant. */
103
120
  agentData?: unknown;
104
121
  };
105
122
  declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
@@ -134,7 +151,7 @@ type AssignmentState = {
134
151
  * on the next render. Subsequent paints read synchronously from cache —
135
152
  * no flicker, no loading state after first paint.
136
153
  */
137
- declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown): AssignmentState;
154
+ declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown, agentDataByVariant?: Record<string, unknown>): AssignmentState;
138
155
 
139
156
  /** Per-component weights store with isolated subscriptions. */
140
157
  type VariantWeight = {
@@ -163,4 +180,4 @@ declare function update(componentId: string, weights: ComponentWeights): void;
163
180
  */
164
181
  declare function getWeights(componentId: string): ComponentWeights | null;
165
182
 
166
- export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, AdaptiveText, type AdaptiveTextProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useLayoutOrder, useSentient };
183
+ export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, AdaptiveText, type AdaptiveTextProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, type WeightedCompositeGoal, type WeightedStep, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useLayoutOrder, useSentient };
package/dist/index.d.ts CHANGED
@@ -88,7 +88,16 @@ type CompositeGoal = {
88
88
  type: 'composite';
89
89
  all: GoalConfig[];
90
90
  };
91
- type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;
91
+ type WeightedStep = {
92
+ goal: GoalConfig;
93
+ name: string;
94
+ weight: number;
95
+ };
96
+ type WeightedCompositeGoal = {
97
+ type: 'weighted_composite';
98
+ steps: WeightedStep[];
99
+ };
100
+ type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal | WeightedCompositeGoal;
92
101
  type AdaptiveProps = {
93
102
  id: string;
94
103
  variants: Record<string, ReactNode>;
@@ -99,7 +108,15 @@ type AdaptiveProps = {
99
108
  * a hydration mismatch. Tradeoff: minor CLS on first paint.
100
109
  */
101
110
  clientOnly?: boolean;
102
- /** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */
111
+ /**
112
+ * Variant-specific structured data for AI agent consumption via /sentient.json and
113
+ * GET /v1/agent/layout. Keyed by variant ID — only the assigned variant's entry is stored,
114
+ * so agents see only the content currently being served to visitors.
115
+ *
116
+ * Prefer this over `agentData` when variants have meaningfully different content.
117
+ */
118
+ agentDataByVariant?: Record<string, unknown>;
119
+ /** @deprecated Use agentDataByVariant for variant-specific content. Stored as-is for the assigned variant. */
103
120
  agentData?: unknown;
104
121
  };
105
122
  declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
@@ -134,7 +151,7 @@ type AssignmentState = {
134
151
  * on the next render. Subsequent paints read synchronously from cache —
135
152
  * no flicker, no loading state after first paint.
136
153
  */
137
- declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown): AssignmentState;
154
+ declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown, agentDataByVariant?: Record<string, unknown>): AssignmentState;
138
155
 
139
156
  /** Per-component weights store with isolated subscriptions. */
140
157
  type VariantWeight = {
@@ -163,4 +180,4 @@ declare function update(componentId: string, weights: ComponentWeights): void;
163
180
  */
164
181
  declare function getWeights(componentId: string): ComponentWeights | null;
165
182
 
166
- export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, AdaptiveText, type AdaptiveTextProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useLayoutOrder, useSentient };
183
+ export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, AdaptiveText, type AdaptiveTextProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, type WeightedCompositeGoal, type WeightedStep, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useLayoutOrder, useSentient };
package/dist/index.js CHANGED
@@ -1,516 +1,3 @@
1
1
  'use client';
2
- "use strict";
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // src/index.ts
22
- var src_exports = {};
23
- __export(src_exports, {
24
- Adaptive: () => Adaptive,
25
- AdaptiveProvider: () => AdaptiveProvider,
26
- AdaptiveText: () => AdaptiveText,
27
- detectSegment: () => import_core2.deriveSessionSegment,
28
- getWeights: () => getWeights,
29
- subscribeWeights: () => subscribe,
30
- updateWeights: () => update,
31
- useAssignment: () => useAssignment,
32
- useInitialAssignments: () => useInitialAssignments,
33
- useLayoutOrder: () => useLayoutOrder,
34
- useSentient: () => useSentient
35
- });
36
- module.exports = __toCommonJS(src_exports);
37
-
38
- // src/provider.tsx
39
- var import_react = require("react");
40
- var import_core = require("@sentientui/core");
41
- var import_jsx_runtime = require("react/jsx-runtime");
42
- var AdaptiveContext = (0, import_react.createContext)({
43
- client: null,
44
- apiKey: "",
45
- initialAssignments: {},
46
- sessionSegment: "desktop:direct",
47
- ssrFallback: "first",
48
- onAssignment: void 0,
49
- initialLayoutOrder: null
50
- });
51
- function AdaptiveProvider(props) {
52
- var _a, _b, _c, _d;
53
- const [client, setClient] = (0, import_react.useState)(null);
54
- (0, import_react.useEffect)(() => {
55
- if (props.consent === false && !props.preConsentBehavior) {
56
- setClient((prev) => {
57
- prev == null ? void 0 : prev.destroy();
58
- return null;
59
- });
60
- return;
61
- }
62
- const c = (0, import_core.init)({
63
- apiKey: props.apiKey,
64
- context: props.context,
65
- debug: props.debug,
66
- initialAssignments: props.initialAssignments,
67
- sessionSegment: props.sessionSegment,
68
- consent: props.consent,
69
- preConsentBehavior: props.preConsentBehavior
70
- });
71
- setClient(c);
72
- return () => {
73
- c.destroy();
74
- };
75
- }, [props.consent]);
76
- const ssrFallback = (_a = props.ssrFallback) != null ? _a : "first";
77
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
78
- AdaptiveContext.Provider,
79
- {
80
- value: {
81
- client,
82
- apiKey: props.apiKey,
83
- initialAssignments: (_b = props.initialAssignments) != null ? _b : {},
84
- sessionSegment: (_c = props.sessionSegment) != null ? _c : "desktop:direct",
85
- ssrFallback,
86
- onAssignment: props.onAssignment,
87
- initialLayoutOrder: (_d = props.initialLayoutOrder) != null ? _d : null
88
- },
89
- children: props.children
90
- }
91
- );
92
- }
93
- function useSentient() {
94
- return (0, import_react.useContext)(AdaptiveContext).client;
95
- }
96
- function useAdaptiveApiKey() {
97
- return (0, import_react.useContext)(AdaptiveContext).apiKey;
98
- }
99
- function useInitialAssignments() {
100
- return (0, import_react.useContext)(AdaptiveContext).initialAssignments;
101
- }
102
- function useSessionSegment() {
103
- return (0, import_react.useContext)(AdaptiveContext).sessionSegment;
104
- }
105
- function useSsrFallback() {
106
- return (0, import_react.useContext)(AdaptiveContext).ssrFallback;
107
- }
108
- function useOnAssignment() {
109
- return (0, import_react.useContext)(AdaptiveContext).onAssignment;
110
- }
111
- function useLayoutOrder() {
112
- return (0, import_react.useContext)(AdaptiveContext).initialLayoutOrder;
113
- }
114
-
115
- // src/adaptive.tsx
116
- var import_react3 = require("react");
117
-
118
- // src/use-assignment.ts
119
- var import_react2 = require("react");
120
-
121
- // src/weights-store.ts
122
- var store = /* @__PURE__ */ new Map();
123
- var listeners = /* @__PURE__ */ new Map();
124
- function subscribe(componentId, cb) {
125
- let set = listeners.get(componentId);
126
- if (!set) {
127
- set = /* @__PURE__ */ new Set();
128
- listeners.set(componentId, set);
129
- }
130
- set.add(cb);
131
- return () => {
132
- set.delete(cb);
133
- if (set.size === 0) listeners.delete(componentId);
134
- };
135
- }
136
- function update(componentId, weights) {
137
- store.set(componentId, weights);
138
- const set = listeners.get(componentId);
139
- if (!set) return;
140
- for (const cb of set) {
141
- try {
142
- cb(weights);
143
- } catch (e) {
144
- }
145
- }
146
- }
147
- function getWeights(componentId) {
148
- var _a;
149
- return (_a = store.get(componentId)) != null ? _a : null;
150
- }
151
-
152
- // src/use-assignment.ts
153
- function getDevOverride(componentId) {
154
- var _a;
155
- if (typeof window === "undefined") return null;
156
- const global = (_a = window.__sentient_overrides) == null ? void 0 : _a[componentId];
157
- if (global) return global;
158
- try {
159
- const params = new URLSearchParams(window.location.search);
160
- for (const raw of params.getAll("sentient_variant")) {
161
- const sep = raw.indexOf(":");
162
- if (sep === -1) continue;
163
- if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);
164
- }
165
- } catch (e) {
166
- }
167
- return null;
168
- }
169
- function pickFromWeights(weights, variantIds) {
170
- var _a;
171
- let best = null;
172
- for (const v of weights.variants) {
173
- if (!variantIds.includes(v.variantId)) continue;
174
- if (!best || v.avgReward > best.avgReward) {
175
- best = { variantId: v.variantId, avgReward: v.avgReward };
176
- }
177
- }
178
- return (_a = best == null ? void 0 : best.variantId) != null ? _a : null;
179
- }
180
- function useAssignment(componentId, variantIds, agentData) {
181
- const initialAssignments = useInitialAssignments();
182
- const ssrFallback = useSsrFallback();
183
- const client = useSentient();
184
- const segment = useSessionSegment();
185
- const onAssignment = useOnAssignment();
186
- const assignmentReportedRef = (0, import_react2.useRef)(null);
187
- const devOverride = getDevOverride(componentId);
188
- const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;
189
- if (overrideVariant) {
190
- console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);
191
- }
192
- const initial = (() => {
193
- var _a, _b;
194
- if (overrideVariant) {
195
- return { variantId: overrideVariant, content: null, isLoading: false };
196
- }
197
- if (!client) {
198
- const preloaded = initialAssignments[componentId];
199
- if (preloaded && variantIds.includes(preloaded)) {
200
- return { variantId: preloaded, content: null, isLoading: false };
201
- }
202
- if (ssrFallback === "first" && variantIds.length > 0) {
203
- return { variantId: variantIds[0], content: null, isLoading: false };
204
- }
205
- return { variantId: null, content: null, isLoading: true };
206
- }
207
- const cached = client.getAssignment(componentId, segment);
208
- if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
209
- return { variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false };
210
- }
211
- const weights = getWeights(componentId);
212
- if (weights) {
213
- const chosen = pickFromWeights(weights, variantIds);
214
- if (chosen) return { variantId: chosen, content: null, isLoading: false };
215
- }
216
- return { variantId: (_b = variantIds[0]) != null ? _b : null, content: null, isLoading: false };
217
- })();
218
- const [state, setState] = (0, import_react2.useState)(initial);
219
- const reportAssignment = (variantId) => {
220
- if (!onAssignment) return;
221
- if (assignmentReportedRef.current === variantId) return;
222
- assignmentReportedRef.current = variantId;
223
- onAssignment(componentId, variantId);
224
- };
225
- (0, import_react2.useEffect)(() => {
226
- var _a;
227
- if (overrideVariant) return;
228
- if (!client) return;
229
- const cached = client.getAssignment(componentId, segment);
230
- if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
231
- setState({ variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false });
232
- reportAssignment(cached.variantId);
233
- return;
234
- }
235
- setState((prev) => {
236
- var _a2;
237
- return prev.variantId ? prev : { variantId: (_a2 = variantIds[0]) != null ? _a2 : null, content: null, isLoading: false };
238
- });
239
- }, [overrideVariant, client, componentId, segment]);
240
- (0, import_react2.useEffect)(() => {
241
- if (overrideVariant) return;
242
- if (!client) return;
243
- const cached = client.getAssignment(componentId, segment);
244
- if (cached && variantIds.includes(cached.variantId)) return;
245
- let cancelled = false;
246
- void client.assign(componentId, variantIds, agentData).then((result) => {
247
- var _a;
248
- if (cancelled) return;
249
- if (!result) return;
250
- if (!variantIds.includes(result.variantId) && !result.content) return;
251
- setState({ variantId: result.variantId, content: (_a = result.content) != null ? _a : null, isLoading: false });
252
- reportAssignment(result.variantId);
253
- });
254
- return () => {
255
- cancelled = true;
256
- };
257
- }, [overrideVariant, client, componentId, segment]);
258
- (0, import_react2.useEffect)(() => {
259
- if (overrideVariant) return;
260
- if (!client) return;
261
- return subscribe(componentId, (weights) => {
262
- var _a;
263
- const cached = client.getAssignment(componentId, segment);
264
- if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
265
- setState({ variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false });
266
- return;
267
- }
268
- const chosen = pickFromWeights(weights, variantIds);
269
- if (chosen) setState({ variantId: chosen, content: null, isLoading: false });
270
- });
271
- }, [overrideVariant, client, componentId, segment]);
272
- if (overrideVariant) {
273
- return { variantId: overrideVariant, content: null, isLoading: false };
274
- }
275
- return state;
276
- }
277
-
278
- // src/adaptive.tsx
279
- var import_jsx_runtime2 = require("react/jsx-runtime");
280
- function normalize(goal) {
281
- if (typeof goal === "string") return { type: "click" };
282
- return goal;
283
- }
284
- function isClickableTarget(el) {
285
- if (!(el instanceof Element)) return false;
286
- const tag = el.tagName.toLowerCase();
287
- if (tag === "a" || tag === "button") return true;
288
- const role = el.getAttribute("role");
289
- return role === "button";
290
- }
291
- function findClickable(start, container) {
292
- let cursor = start;
293
- while (cursor && cursor !== container) {
294
- if (isClickableTarget(cursor)) return true;
295
- cursor = cursor.parentElement;
296
- }
297
- return false;
298
- }
299
- function AdaptiveImpl(props) {
300
- var _a, _b;
301
- const client = useSentient();
302
- const apiKey = useAdaptiveApiKey();
303
- const variantIds = (0, import_react3.useMemo)(() => Object.keys(props.variants), [props.variants]);
304
- const { variantId, content } = useAssignment(props.id, variantIds, props.agentData);
305
- const containerRef = (0, import_react3.useRef)(null);
306
- const [mounted, setMounted] = (0, import_react3.useState)(false);
307
- (0, import_react3.useEffect)(() => {
308
- setMounted(true);
309
- }, []);
310
- const goalFiredRef = (0, import_react3.useRef)(false);
311
- const assignTrackedRef = (0, import_react3.useRef)(null);
312
- const goal = (0, import_react3.useMemo)(() => normalize(props.goal), [props.goal]);
313
- const goalLabel = typeof props.goal === "string" ? props.goal : goal.type;
314
- (0, import_react3.useEffect)(() => {
315
- var _a2, _b2;
316
- if (!client || !variantId || !apiKey) return;
317
- if (assignTrackedRef.current === variantId) return;
318
- const rawHtml = (_b2 = (_a2 = containerRef.current) == null ? void 0 : _a2.innerHTML) != null ? _b2 : "";
319
- if (!rawHtml && !mounted) return;
320
- assignTrackedRef.current = variantId;
321
- client.track({
322
- projectId: apiKey,
323
- componentId: props.id,
324
- variantId,
325
- eventType: "variant_assigned",
326
- payload: rawHtml ? { previewHtml: rawHtml.slice(0, 3e4) } : {}
327
- });
328
- }, [client, variantId, apiKey, props.id, mounted]);
329
- (0, import_react3.useEffect)(() => {
330
- goalFiredRef.current = false;
331
- }, [variantId]);
332
- (0, import_react3.useEffect)(() => {
333
- if (!client || !variantId) return;
334
- const node = containerRef.current;
335
- if (!node) return;
336
- let timerId = null;
337
- let hoverStart = 0;
338
- const onEnter = () => {
339
- hoverStart = Date.now();
340
- timerId = setTimeout(() => {
341
- client.track({
342
- projectId: apiKey,
343
- componentId: props.id,
344
- variantId,
345
- eventType: "cursor_signal",
346
- payload: { hoverDuration: Date.now() - hoverStart }
347
- });
348
- timerId = null;
349
- }, 800);
350
- };
351
- const onLeave = () => {
352
- if (timerId !== null) {
353
- clearTimeout(timerId);
354
- timerId = null;
355
- }
356
- };
357
- node.addEventListener("mouseenter", onEnter);
358
- node.addEventListener("mouseleave", onLeave);
359
- return () => {
360
- node.removeEventListener("mouseenter", onEnter);
361
- node.removeEventListener("mouseleave", onLeave);
362
- if (timerId !== null) clearTimeout(timerId);
363
- };
364
- }, [client, variantId, apiKey, props.id]);
365
- (0, import_react3.useEffect)(() => {
366
- if (!client || !variantId) return;
367
- const node = containerRef.current;
368
- if (!node) return;
369
- const fireGoal = () => {
370
- if (goalFiredRef.current) return;
371
- goalFiredRef.current = true;
372
- client.track({
373
- projectId: apiKey,
374
- componentId: props.id,
375
- variantId,
376
- eventType: "goal_achieved",
377
- goalType: goalLabel,
378
- payload: { reward: 1 }
379
- });
380
- };
381
- const subgoals = goal.type === "composite" ? goal.all : [goal];
382
- const remaining = new Set(subgoals.map((_, i) => i));
383
- const checkComposite = (idx) => {
384
- remaining.delete(idx);
385
- if (remaining.size === 0) fireGoal();
386
- };
387
- const cleanups = [];
388
- subgoals.forEach((sub, idx) => {
389
- if (sub.type === "click") {
390
- const onClick = (e) => {
391
- const target = e.target;
392
- if (!(target instanceof Element)) return;
393
- if (!findClickable(target, node)) return;
394
- if (goal.type === "composite") checkComposite(idx);
395
- else fireGoal();
396
- };
397
- node.addEventListener("click", onClick);
398
- cleanups.push(() => node.removeEventListener("click", onClick));
399
- return;
400
- }
401
- if (sub.type === "form_submit") {
402
- const onSubmit = (e) => {
403
- if (!(e.target instanceof HTMLFormElement)) return;
404
- if (!node.contains(e.target)) return;
405
- if (goal.type === "composite") checkComposite(idx);
406
- else fireGoal();
407
- };
408
- node.addEventListener("submit", onSubmit);
409
- cleanups.push(() => node.removeEventListener("submit", onSubmit));
410
- return;
411
- }
412
- if (sub.type === "scroll_depth") {
413
- const threshold = Math.max(0, Math.min(1, sub.threshold));
414
- const io = new IntersectionObserver(
415
- (entries) => {
416
- for (const entry of entries) {
417
- if (entry.intersectionRatio >= threshold) {
418
- if (goal.type === "composite") checkComposite(idx);
419
- else fireGoal();
420
- io.disconnect();
421
- break;
422
- }
423
- }
424
- },
425
- { threshold: [threshold] }
426
- );
427
- io.observe(node);
428
- cleanups.push(() => io.disconnect());
429
- return;
430
- }
431
- });
432
- return () => {
433
- for (const c of cleanups) c();
434
- };
435
- }, [client, variantId, apiKey, props.id, goal, goalLabel]);
436
- if (props.clientOnly && (!mounted || !client)) return null;
437
- if (!variantId) return null;
438
- const jsxContent = (_a = props.variants[variantId]) != null ? _a : null;
439
- const managedContent = jsxContent === null ? content : null;
440
- if (((_b = process == null ? void 0 : process.env) == null ? void 0 : _b.NODE_ENV) !== "production" && jsxContent === null && managedContent === null) {
441
- console.warn(
442
- `[sentient] <Adaptive id="${props.id}"> was assigned variant "${variantId}" but no matching key exists in props.variants. If this is a dashboard-managed text variant, use <AdaptiveText id="${props.id}"> instead.`
443
- );
444
- }
445
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: containerRef, "data-sentient-id": props.id, "data-sentient-variant": variantId, children: jsxContent != null ? jsxContent : managedContent });
446
- }
447
- var Adaptive = (0, import_react3.memo)(AdaptiveImpl, (prev, next) => {
448
- if (prev.id !== next.id) return false;
449
- if (prev.goal !== next.goal) return false;
450
- if (prev.variants === next.variants) return true;
451
- const prevKeys = Object.keys(prev.variants);
452
- const nextKeys = Object.keys(next.variants);
453
- if (prevKeys.length !== nextKeys.length) return false;
454
- return prevKeys.every((k) => k in next.variants);
455
- });
456
-
457
- // src/adaptive-text.tsx
458
- var import_react4 = require("react");
459
- var import_jsx_runtime3 = require("react/jsx-runtime");
460
- function AdaptiveText({
461
- id,
462
- default: defaultText,
463
- component: Tag = "span",
464
- className
465
- }) {
466
- const client = useSentient();
467
- const apiKey = useAdaptiveApiKey();
468
- const onAssignment = useOnAssignment();
469
- const [text, setText] = (0, import_react4.useState)(null);
470
- const [variantId, setVariantId] = (0, import_react4.useState)(null);
471
- const trackedRef = (0, import_react4.useRef)(null);
472
- (0, import_react4.useEffect)(() => {
473
- if (!client) return;
474
- let cancelled = false;
475
- void client.assign(id).then((result) => {
476
- if (cancelled || !result) return;
477
- setVariantId(result.variantId);
478
- if (result.content) setText(result.content);
479
- });
480
- return () => {
481
- cancelled = true;
482
- };
483
- }, [client, id]);
484
- (0, import_react4.useEffect)(() => {
485
- if (!client || !variantId || !apiKey) return;
486
- if (trackedRef.current === variantId) return;
487
- trackedRef.current = variantId;
488
- client.track({
489
- projectId: apiKey,
490
- componentId: id,
491
- variantId,
492
- eventType: "variant_assigned",
493
- payload: {}
494
- });
495
- onAssignment == null ? void 0 : onAssignment(id, variantId);
496
- }, [client, variantId, apiKey, id, onAssignment]);
497
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Tag, { className, children: text != null ? text : defaultText });
498
- }
499
-
500
- // src/segment.ts
501
- var import_core2 = require("@sentientui/core");
502
- // Annotate the CommonJS export names for ESM import in node:
503
- 0 && (module.exports = {
504
- Adaptive,
505
- AdaptiveProvider,
506
- AdaptiveText,
507
- detectSegment,
508
- getWeights,
509
- subscribeWeights,
510
- updateWeights,
511
- useAssignment,
512
- useInitialAssignments,
513
- useLayoutOrder,
514
- useSentient
515
- });
2
+ "use strict";var K=Object.defineProperty;var me=Object.getOwnPropertyDescriptor;var ve=Object.getOwnPropertyNames,U=Object.getOwnPropertySymbols;var Q=Object.prototype.hasOwnProperty,pe=Object.prototype.propertyIsEnumerable;var q=(e,t,n)=>t in e?K(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Y=(e,t)=>{for(var n in t||(t={}))Q.call(t,n)&&q(e,n,t[n]);if(U)for(var n of U(t))pe.call(t,n)&&q(e,n,t[n]);return e};var ye=(e,t)=>{for(var n in t)K(e,n,{get:t[n],enumerable:!0})},he=(e,t,n,l)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of ve(t))!Q.call(e,i)&&i!==n&&K(e,i,{get:()=>t[i],enumerable:!(l=me(t,i))||l.enumerable});return e};var Ae=e=>he(K({},"__esModule",{value:!0}),e);var Le={};ye(Le,{Adaptive:()=>ce,AdaptiveProvider:()=>Z,AdaptiveText:()=>de,detectSegment:()=>J.deriveSessionSegment,getWeights:()=>B,subscribeWeights:()=>$,updateWeights:()=>se,useAssignment:()=>X,useInitialAssignments:()=>N,useLayoutOrder:()=>ne,useSentient:()=>I});module.exports=Ae(Le);var m=require("react"),G=require("@sentientui/core"),ie=require("react/jsx-runtime");function Se(){var e,t;if(typeof window=="undefined")return"desktop:direct";try{let n=(0,G.detectDeviceClass)((e=navigator.userAgent)!=null?e:""),l=(0,G.detectTrafficSource)((t=document.referrer)!=null?t:"",window.location.origin);return`${n}:${l}`}catch(n){return"desktop:direct"}}var L=(0,m.createContext)({client:null,apiKey:"",initialAssignments:{},sessionSegment:"desktop:direct",ssrFallback:"first",onAssignment:void 0,initialLayoutOrder:null});function Z(e){var o;let[t,n]=(0,m.useState)(null),[l]=(0,m.useState)(()=>{var c;return(c=e.sessionSegment)!=null?c:Se()});(0,m.useEffect)(()=>{if(e.consent===!1&&!e.preConsentBehavior){n(y=>(y==null||y.destroy(),null));return}let c=(0,G.init)({apiKey:e.apiKey,context:e.context,debug:e.debug,initialAssignments:e.initialAssignments,sessionSegment:l,consent:e.consent,preConsentBehavior:e.preConsentBehavior});return n(c),()=>{c.destroy()}},[e.consent]);let i=(o=e.ssrFallback)!=null?o:"first",p=(0,m.useMemo)(()=>{var c,y;return{client:t,apiKey:e.apiKey,initialAssignments:(c=e.initialAssignments)!=null?c:{},sessionSegment:l,ssrFallback:i,onAssignment:e.onAssignment,initialLayoutOrder:(y=e.initialLayoutOrder)!=null?y:null}},[t,e.apiKey,e.initialAssignments,l,i,e.onAssignment,e.initialLayoutOrder]);return(0,ie.jsx)(L.Provider,{value:p,children:e.children})}function I(){return(0,m.useContext)(L).client}function V(){return(0,m.useContext)(L).apiKey}function N(){return(0,m.useContext)(L).initialAssignments}function ee(){return(0,m.useContext)(L).sessionSegment}function te(){return(0,m.useContext)(L).ssrFallback}function j(){return(0,m.useContext)(L).onAssignment}function ne(){return(0,m.useContext)(L).initialLayoutOrder}var g=require("react"),le=require("@sentientui/core");var R=require("react");var re=new Map,H=new Map;function $(e,t){let n=H.get(e);return n||(n=new Set,H.set(e,n)),n.add(t),()=>{n.delete(t),n.size===0&&H.delete(e)}}function se(e,t){re.set(e,t);let n=H.get(e);if(n)for(let l of n)try{l(t)}catch(i){}}function B(e){var t;return(t=re.get(e))!=null?t:null}function be(e){var n;if(typeof window=="undefined")return null;let t=(n=window.__sentient_overrides)==null?void 0:n[e];if(t)return t;try{let l=new URLSearchParams(window.location.search);for(let i of l.getAll("sentient_variant")){let p=i.indexOf(":");if(p!==-1&&i.slice(0,p)===e)return i.slice(p+1)}}catch(l){}return null}function oe(e,t){var l;let n=null;for(let i of e.variants)t.includes(i.variantId)&&(!n||i.avgReward>n.avgReward)&&(n={variantId:i.variantId,avgReward:i.avgReward});return(l=n==null?void 0:n.variantId)!=null?l:null}function X(e,t,n,l){let i=N(),p=te(),o=I(),c=ee(),y=j(),h=(0,R.useRef)(null),x=be(e),a=x&&t.includes(x)?x:null;a&&console.info(`[sentient] override active: ${e} -> ${a}`);let T=(()=>{var s,d;if(a)return{variantId:a,content:null,isLoading:!1};if(!o){let f=i[e];return f&&t.includes(f)?{variantId:f,content:null,isLoading:!1}:p==="first"&&t.length>0?{variantId:t[0],content:null,isLoading:!1}:{variantId:null,content:null,isLoading:!0}}let u=o.getAssignment(e,c);if(u&&(t.includes(u.variantId)||u.content))return{variantId:u.variantId,content:(s=u.content)!=null?s:null,isLoading:!1};let r=B(e);if(r){let f=oe(r,t);if(f)return{variantId:f,content:null,isLoading:!1}}return{variantId:(d=t[0])!=null?d:null,content:null,isLoading:!1}})(),[A,w]=(0,R.useState)(T),P=u=>{y&&h.current!==u&&(h.current=u,y(e,u))};return(0,R.useEffect)(()=>{var r;if(a||!o)return;let u=o.getAssignment(e,c);if(u&&(t.includes(u.variantId)||u.content)){w({variantId:u.variantId,content:(r=u.content)!=null?r:null,isLoading:!1}),P(u.variantId);return}w(s=>{var d;return s.variantId?s:{variantId:(d=t[0])!=null?d:null,content:null,isLoading:!1}})},[a,o,e,c]),(0,R.useEffect)(()=>{if(a||!o)return;let u=o.getAssignment(e,c);if(u&&t.includes(u.variantId))return;let r=!1;return o.assign(e,t,n,l).then(s=>{var d;r||s&&(!t.includes(s.variantId)&&!s.content||(w({variantId:s.variantId,content:(d=s.content)!=null?d:null,isLoading:!1}),P(s.variantId)))}),()=>{r=!0}},[a,o,e,c]),(0,R.useEffect)(()=>{if(!a&&o)return $(e,u=>{var d;let r=o.getAssignment(e,c);if(r&&(t.includes(r.variantId)||r.content)){w({variantId:r.variantId,content:(d=r.content)!=null?d:null,isLoading:!1});return}let s=oe(u,t);s&&w({variantId:s,content:null,isLoading:!1})})},[a,o,e,c]),a?{variantId:a,content:null,isLoading:!1}:A}var ue=require("react/jsx-runtime");function ke(e){return typeof e=="string"?{type:"click"}:e}var xe=3e4;function we(e,t){try{let n=`_snt_pv_${e}_${t}`;return sessionStorage.getItem(n)?!1:(sessionStorage.setItem(n,"1"),!0)}catch(n){return!0}}function Ce(e){if(!(e instanceof Element))return!1;let t=e.tagName.toLowerCase();return t==="a"||t==="button"?!0:e.getAttribute("role")==="button"}function ae(e,t){let n=e;for(;n&&n!==t;){if(Ce(n))return!0;n=n.parentElement}return!1}function Ee(e){var P,u;let t=I(),n=V(),l=(0,g.useMemo)(()=>Object.keys(e.variants),[e.variants]),{variantId:i,content:p}=X(e.id,l,e.agentData,e.agentDataByVariant),o=(0,g.useRef)(null),[c,y]=(0,g.useState)(!1);(0,g.useEffect)(()=>{y(!0)},[]);let h=(0,g.useRef)(!1),x=(0,g.useRef)(null),a=(0,g.useMemo)(()=>ke(e.goal),[e.goal]),T=typeof e.goal=="string"?e.goal:a.type;if((0,g.useEffect)(()=>{var d,f;if(!t||!i||!n||x.current===i)return;let r=(f=(d=o.current)==null?void 0:d.innerHTML)!=null?f:"";if(!r&&!c)return;x.current=i;let s=r!==""&&we(e.id,i);t.track({projectId:n,componentId:e.id,variantId:i,eventType:"variant_assigned",payload:s?{previewHtml:r.slice(0,xe)}:{}})},[t,i,n,e.id,c]),(0,g.useEffect)(()=>{h.current=!1},[i]),(0,g.useEffect)(()=>{if(!t||!i)return;let r=o.current;if(!r)return;let s=null,d=0,f=()=>{d=Date.now(),s=setTimeout(()=>{t.track({projectId:n,componentId:e.id,variantId:i,eventType:"cursor_signal",payload:{hoverDuration:Date.now()-d}}),s=null},800)},W=()=>{s!==null&&(clearTimeout(s),s=null)};return r.addEventListener("mouseenter",f),r.addEventListener("mouseleave",W),()=>{r.removeEventListener("mouseenter",f),r.removeEventListener("mouseleave",W),s!==null&&clearTimeout(s)}},[t,i,n,e.id]),(0,g.useEffect)(()=>{if(!t||!i)return;let r=o.current;if(!r)return;let s=Date.now();return(0,le.attachMicroSignalDetectors)((d,f={})=>{t.track({projectId:n,componentId:e.id,variantId:i,eventType:"micro_signal",payload:Y({signalType:d},f)})},r,s)},[t,i,n,e.id]),(0,g.useEffect)(()=>{if(!t||!i)return;let r=o.current;if(!r)return;if(a.type==="weighted_composite"){let S=new Set,k=[];return a.steps.forEach(({goal:v,name:b,weight:O},F)=>{let z=()=>{S.has(F)||(S.add(F),t.track({projectId:n,componentId:e.id,variantId:i,eventType:"goal_achieved",goalType:b,payload:{reward:O}}),t.goal(b,{},O,F))};if(v.type==="click"){let C=E=>{let M=E.target;M instanceof Element&&ae(M,r)&&z()};r.addEventListener("click",C),k.push(()=>r.removeEventListener("click",C));return}if(v.type==="form_submit"){let C=E=>{E.target instanceof HTMLFormElement&&r.contains(E.target)&&z()};r.addEventListener("submit",C),k.push(()=>r.removeEventListener("submit",C));return}if(v.type==="scroll_depth"){let C=Math.max(0,Math.min(1,v.threshold)),E=new IntersectionObserver(M=>{for(let fe of M)if(fe.intersectionRatio>=C){z(),E.disconnect();break}},{threshold:[C]});E.observe(r),k.push(()=>E.disconnect())}}),()=>{for(let v of k)v()}}let s=()=>{h.current||(h.current=!0,t.track({projectId:n,componentId:e.id,variantId:i,eventType:"goal_achieved",goalType:T,payload:{reward:1}}))},d=a.type==="composite"?a.all:[a],f=new Set(d.map((S,k)=>k)),W=S=>{f.delete(S),f.size===0&&s()},D=[];return d.forEach((S,k)=>{if(S.type==="click"){let v=b=>{let O=b.target;O instanceof Element&&ae(O,r)&&(a.type==="composite"?W(k):s())};r.addEventListener("click",v),D.push(()=>r.removeEventListener("click",v));return}if(S.type==="form_submit"){let v=b=>{b.target instanceof HTMLFormElement&&r.contains(b.target)&&(a.type==="composite"?W(k):s())};r.addEventListener("submit",v),D.push(()=>r.removeEventListener("submit",v));return}if(S.type==="scroll_depth"){let v=Math.max(0,Math.min(1,S.threshold)),b=new IntersectionObserver(O=>{for(let F of O)if(F.intersectionRatio>=v){a.type==="composite"?W(k):s(),b.disconnect();break}},{threshold:[v]});b.observe(r),D.push(()=>b.disconnect());return}}),()=>{for(let S of D)S()}},[t,i,n,e.id,a,T]),e.clientOnly&&(!c||!t)||!i)return null;let A=(P=e.variants[i])!=null?P:null,w=A===null?p:null;return((u=process==null?void 0:process.env)==null?void 0:u.NODE_ENV)!=="production"&&A===null&&w===null&&console.warn(`[sentient] <Adaptive id="${e.id}"> was assigned variant "${i}" but no matching key exists in props.variants. If this is a dashboard-managed text variant, use <AdaptiveText id="${e.id}"> instead.`),(0,ue.jsx)("div",{ref:o,"data-sentient-id":e.id,"data-sentient-variant":i,children:A!=null?A:w})}var ce=(0,g.memo)(Ee,(e,t)=>{if(e.id!==t.id||e.goal!==t.goal)return!1;if(e.variants===t.variants)return!0;let n=Object.keys(e.variants),l=Object.keys(t.variants);return n.length!==l.length?!1:n.every(i=>i in t.variants)});var _=require("react");var ge=require("react/jsx-runtime");function de({id:e,default:t,component:n="span",className:l}){let i=I(),p=V(),o=j(),[c,y]=(0,_.useState)(null),[h,x]=(0,_.useState)(null),a=(0,_.useRef)(null);return(0,_.useEffect)(()=>{if(!i)return;let T=!1;return i.assign(e).then(A=>{T||!A||(x(A.variantId),A.content&&y(A.content))}),()=>{T=!0}},[i,e]),(0,_.useEffect)(()=>{!i||!h||!p||a.current!==h&&(a.current=h,i.track({projectId:p,componentId:e,variantId:h,eventType:"variant_assigned",payload:{}}),o==null||o(e,h))},[i,h,p,e,o]),(0,ge.jsx)(n,{className:l,children:c!=null?c:t})}var J=require("@sentientui/core");0&&(module.exports={Adaptive,AdaptiveProvider,AdaptiveText,detectSegment,getWeights,subscribeWeights,updateWeights,useAssignment,useInitialAssignments,useLayoutOrder,useSentient});
516
3
  //# sourceMappingURL=index.js.map