@sentientui/react 0.3.0 → 0.3.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.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +51 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -41
- package/dist/index.mjs.map +1 -1
- package/dist/next/adaptive-root-client.js +2 -2
- package/dist/next/adaptive-root-client.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -112,6 +112,8 @@ declare global {
|
|
|
112
112
|
}
|
|
113
113
|
type AssignmentState = {
|
|
114
114
|
variantId: string | null;
|
|
115
|
+
/** Populated when the assigned variant is a dashboard-managed text variant. */
|
|
116
|
+
content: string | null;
|
|
115
117
|
isLoading: boolean;
|
|
116
118
|
};
|
|
117
119
|
/**
|
|
@@ -119,7 +121,7 @@ type AssignmentState = {
|
|
|
119
121
|
*
|
|
120
122
|
* First render reads the local SDK cache; if empty, falls back to a
|
|
121
123
|
* deterministic default and asynchronously calls `/v1/assign`. The server
|
|
122
|
-
* picks the actual variant via
|
|
124
|
+
* picks the actual variant via Thompson Sampling and the result replaces the fallback
|
|
123
125
|
* on the next render. Subsequent paints read synchronously from cache —
|
|
124
126
|
* no flicker, no loading state after first paint.
|
|
125
127
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -112,6 +112,8 @@ declare global {
|
|
|
112
112
|
}
|
|
113
113
|
type AssignmentState = {
|
|
114
114
|
variantId: string | null;
|
|
115
|
+
/** Populated when the assigned variant is a dashboard-managed text variant. */
|
|
116
|
+
content: string | null;
|
|
115
117
|
isLoading: boolean;
|
|
116
118
|
};
|
|
117
119
|
/**
|
|
@@ -119,7 +121,7 @@ type AssignmentState = {
|
|
|
119
121
|
*
|
|
120
122
|
* First render reads the local SDK cache; if empty, falls back to a
|
|
121
123
|
* deterministic default and asynchronously calls `/v1/assign`. The server
|
|
122
|
-
* picks the actual variant via
|
|
124
|
+
* picks the actual variant via Thompson Sampling and the result replaces the fallback
|
|
123
125
|
* on the next render. Subsequent paints read synchronously from cache —
|
|
124
126
|
* no flicker, no loading state after first paint.
|
|
125
127
|
*/
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ var import_core = require("@sentientui/core");
|
|
|
41
41
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
42
42
|
var AdaptiveContext = (0, import_react.createContext)({
|
|
43
43
|
client: null,
|
|
44
|
-
|
|
44
|
+
apiKey: "",
|
|
45
45
|
initialAssignments: {},
|
|
46
46
|
sessionSegment: "desktop:direct",
|
|
47
47
|
ssrFallback: "first",
|
|
@@ -78,7 +78,7 @@ function AdaptiveProvider(props) {
|
|
|
78
78
|
{
|
|
79
79
|
value: {
|
|
80
80
|
client,
|
|
81
|
-
|
|
81
|
+
apiKey: props.apiKey,
|
|
82
82
|
initialAssignments: (_b = props.initialAssignments) != null ? _b : {},
|
|
83
83
|
sessionSegment: (_c = props.sessionSegment) != null ? _c : "desktop:direct",
|
|
84
84
|
ssrFallback,
|
|
@@ -92,8 +92,8 @@ function AdaptiveProvider(props) {
|
|
|
92
92
|
function useSentient() {
|
|
93
93
|
return (0, import_react.useContext)(AdaptiveContext).client;
|
|
94
94
|
}
|
|
95
|
-
function
|
|
96
|
-
return (0, import_react.useContext)(AdaptiveContext).
|
|
95
|
+
function useAdaptiveApiKey() {
|
|
96
|
+
return (0, import_react.useContext)(AdaptiveContext).apiKey;
|
|
97
97
|
}
|
|
98
98
|
function useInitialAssignments() {
|
|
99
99
|
return (0, import_react.useContext)(AdaptiveContext).initialAssignments;
|
|
@@ -189,30 +189,30 @@ function useAssignment(componentId, variantIds) {
|
|
|
189
189
|
console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);
|
|
190
190
|
}
|
|
191
191
|
const initial = (() => {
|
|
192
|
-
var _a;
|
|
192
|
+
var _a, _b;
|
|
193
193
|
if (overrideVariant) {
|
|
194
|
-
return { variantId: overrideVariant, isLoading: false };
|
|
194
|
+
return { variantId: overrideVariant, content: null, isLoading: false };
|
|
195
195
|
}
|
|
196
196
|
if (!client) {
|
|
197
197
|
const preloaded = initialAssignments[componentId];
|
|
198
198
|
if (preloaded && variantIds.includes(preloaded)) {
|
|
199
|
-
return { variantId: preloaded, isLoading: false };
|
|
199
|
+
return { variantId: preloaded, content: null, isLoading: false };
|
|
200
200
|
}
|
|
201
201
|
if (ssrFallback === "first" && variantIds.length > 0) {
|
|
202
|
-
return { variantId: variantIds[0], isLoading: false };
|
|
202
|
+
return { variantId: variantIds[0], content: null, isLoading: false };
|
|
203
203
|
}
|
|
204
|
-
return { variantId: null, isLoading: true };
|
|
204
|
+
return { variantId: null, content: null, isLoading: true };
|
|
205
205
|
}
|
|
206
206
|
const cached = client.getAssignment(componentId, segment);
|
|
207
|
-
if (cached && variantIds.includes(cached.variantId)) {
|
|
208
|
-
return { variantId: cached.variantId, isLoading: false };
|
|
207
|
+
if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
|
|
208
|
+
return { variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false };
|
|
209
209
|
}
|
|
210
210
|
const weights = getWeights(componentId);
|
|
211
211
|
if (weights) {
|
|
212
212
|
const chosen = pickFromWeights(weights, variantIds);
|
|
213
|
-
if (chosen) return { variantId: chosen, isLoading: false };
|
|
213
|
+
if (chosen) return { variantId: chosen, content: null, isLoading: false };
|
|
214
214
|
}
|
|
215
|
-
return { variantId: (
|
|
215
|
+
return { variantId: (_b = variantIds[0]) != null ? _b : null, content: null, isLoading: false };
|
|
216
216
|
})();
|
|
217
217
|
const [state, setState] = (0, import_react2.useState)(initial);
|
|
218
218
|
const reportAssignment = (variantId) => {
|
|
@@ -222,17 +222,18 @@ function useAssignment(componentId, variantIds) {
|
|
|
222
222
|
onAssignment(componentId, variantId);
|
|
223
223
|
};
|
|
224
224
|
(0, import_react2.useEffect)(() => {
|
|
225
|
+
var _a;
|
|
225
226
|
if (overrideVariant) return;
|
|
226
227
|
if (!client) return;
|
|
227
228
|
const cached = client.getAssignment(componentId, segment);
|
|
228
|
-
if (cached && variantIds.includes(cached.variantId)) {
|
|
229
|
-
setState({ variantId: cached.variantId, isLoading: false });
|
|
229
|
+
if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
|
|
230
|
+
setState({ variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false });
|
|
230
231
|
reportAssignment(cached.variantId);
|
|
231
232
|
return;
|
|
232
233
|
}
|
|
233
234
|
setState((prev) => {
|
|
234
|
-
var
|
|
235
|
-
return prev.variantId ? prev : { variantId: (
|
|
235
|
+
var _a2;
|
|
236
|
+
return prev.variantId ? prev : { variantId: (_a2 = variantIds[0]) != null ? _a2 : null, content: null, isLoading: false };
|
|
236
237
|
});
|
|
237
238
|
}, [overrideVariant, client, componentId, segment]);
|
|
238
239
|
(0, import_react2.useEffect)(() => {
|
|
@@ -242,9 +243,11 @@ function useAssignment(componentId, variantIds) {
|
|
|
242
243
|
if (cached && variantIds.includes(cached.variantId)) return;
|
|
243
244
|
let cancelled = false;
|
|
244
245
|
void client.assign(componentId, variantIds).then((result) => {
|
|
246
|
+
var _a;
|
|
245
247
|
if (cancelled) return;
|
|
246
|
-
if (!result
|
|
247
|
-
|
|
248
|
+
if (!result) return;
|
|
249
|
+
if (!variantIds.includes(result.variantId) && !result.content) return;
|
|
250
|
+
setState({ variantId: result.variantId, content: (_a = result.content) != null ? _a : null, isLoading: false });
|
|
248
251
|
reportAssignment(result.variantId);
|
|
249
252
|
});
|
|
250
253
|
return () => {
|
|
@@ -255,17 +258,18 @@ function useAssignment(componentId, variantIds) {
|
|
|
255
258
|
if (overrideVariant) return;
|
|
256
259
|
if (!client) return;
|
|
257
260
|
return subscribe(componentId, (weights) => {
|
|
261
|
+
var _a;
|
|
258
262
|
const cached = client.getAssignment(componentId, segment);
|
|
259
|
-
if (cached && variantIds.includes(cached.variantId)) {
|
|
260
|
-
setState({ variantId: cached.variantId, isLoading: false });
|
|
263
|
+
if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
|
|
264
|
+
setState({ variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false });
|
|
261
265
|
return;
|
|
262
266
|
}
|
|
263
267
|
const chosen = pickFromWeights(weights, variantIds);
|
|
264
|
-
if (chosen) setState({ variantId: chosen, isLoading: false });
|
|
268
|
+
if (chosen) setState({ variantId: chosen, content: null, isLoading: false });
|
|
265
269
|
});
|
|
266
270
|
}, [overrideVariant, client, componentId, segment]);
|
|
267
271
|
if (overrideVariant) {
|
|
268
|
-
return { variantId: overrideVariant, isLoading: false };
|
|
272
|
+
return { variantId: overrideVariant, content: null, isLoading: false };
|
|
269
273
|
}
|
|
270
274
|
return state;
|
|
271
275
|
}
|
|
@@ -292,11 +296,11 @@ function findClickable(start, container) {
|
|
|
292
296
|
return false;
|
|
293
297
|
}
|
|
294
298
|
function AdaptiveImpl(props) {
|
|
295
|
-
var _a;
|
|
299
|
+
var _a, _b;
|
|
296
300
|
const client = useSentient();
|
|
297
|
-
const
|
|
301
|
+
const apiKey = useAdaptiveApiKey();
|
|
298
302
|
const variantIds = (0, import_react3.useMemo)(() => Object.keys(props.variants), [props.variants]);
|
|
299
|
-
const { variantId } = useAssignment(props.id, variantIds);
|
|
303
|
+
const { variantId, content } = useAssignment(props.id, variantIds);
|
|
300
304
|
const containerRef = (0, import_react3.useRef)(null);
|
|
301
305
|
const [mounted, setMounted] = (0, import_react3.useState)(false);
|
|
302
306
|
(0, import_react3.useEffect)(() => {
|
|
@@ -307,20 +311,20 @@ function AdaptiveImpl(props) {
|
|
|
307
311
|
const goal = (0, import_react3.useMemo)(() => normalize(props.goal), [props.goal]);
|
|
308
312
|
const goalLabel = typeof props.goal === "string" ? props.goal : goal.type;
|
|
309
313
|
(0, import_react3.useEffect)(() => {
|
|
310
|
-
var _a2,
|
|
311
|
-
if (!client || !variantId || !
|
|
314
|
+
var _a2, _b2;
|
|
315
|
+
if (!client || !variantId || !apiKey) return;
|
|
312
316
|
if (assignTrackedRef.current === variantId) return;
|
|
313
|
-
const rawHtml = (
|
|
317
|
+
const rawHtml = (_b2 = (_a2 = containerRef.current) == null ? void 0 : _a2.innerHTML) != null ? _b2 : "";
|
|
314
318
|
if (!rawHtml && !mounted) return;
|
|
315
319
|
assignTrackedRef.current = variantId;
|
|
316
320
|
client.track({
|
|
317
|
-
projectId,
|
|
321
|
+
projectId: apiKey,
|
|
318
322
|
componentId: props.id,
|
|
319
323
|
variantId,
|
|
320
324
|
eventType: "variant_assigned",
|
|
321
325
|
payload: rawHtml ? { previewHtml: rawHtml.slice(0, 3e4) } : {}
|
|
322
326
|
});
|
|
323
|
-
}, [client, variantId,
|
|
327
|
+
}, [client, variantId, apiKey, props.id, mounted]);
|
|
324
328
|
(0, import_react3.useEffect)(() => {
|
|
325
329
|
goalFiredRef.current = false;
|
|
326
330
|
}, [variantId]);
|
|
@@ -334,7 +338,7 @@ function AdaptiveImpl(props) {
|
|
|
334
338
|
hoverStart = Date.now();
|
|
335
339
|
timerId = setTimeout(() => {
|
|
336
340
|
client.track({
|
|
337
|
-
projectId,
|
|
341
|
+
projectId: apiKey,
|
|
338
342
|
componentId: props.id,
|
|
339
343
|
variantId,
|
|
340
344
|
eventType: "cursor_signal",
|
|
@@ -356,7 +360,7 @@ function AdaptiveImpl(props) {
|
|
|
356
360
|
node.removeEventListener("mouseleave", onLeave);
|
|
357
361
|
if (timerId !== null) clearTimeout(timerId);
|
|
358
362
|
};
|
|
359
|
-
}, [client, variantId,
|
|
363
|
+
}, [client, variantId, apiKey, props.id]);
|
|
360
364
|
(0, import_react3.useEffect)(() => {
|
|
361
365
|
if (!client || !variantId) return;
|
|
362
366
|
const node = containerRef.current;
|
|
@@ -365,7 +369,7 @@ function AdaptiveImpl(props) {
|
|
|
365
369
|
if (goalFiredRef.current) return;
|
|
366
370
|
goalFiredRef.current = true;
|
|
367
371
|
client.track({
|
|
368
|
-
projectId,
|
|
372
|
+
projectId: apiKey,
|
|
369
373
|
componentId: props.id,
|
|
370
374
|
variantId,
|
|
371
375
|
eventType: "goal_achieved",
|
|
@@ -427,11 +431,17 @@ function AdaptiveImpl(props) {
|
|
|
427
431
|
return () => {
|
|
428
432
|
for (const c of cleanups) c();
|
|
429
433
|
};
|
|
430
|
-
}, [client, variantId,
|
|
434
|
+
}, [client, variantId, apiKey, props.id, goal, goalLabel]);
|
|
431
435
|
if (props.clientOnly && (!mounted || !client)) return null;
|
|
432
436
|
if (!variantId) return null;
|
|
433
|
-
const
|
|
434
|
-
|
|
437
|
+
const jsxContent = (_a = props.variants[variantId]) != null ? _a : null;
|
|
438
|
+
const managedContent = jsxContent === null ? content : null;
|
|
439
|
+
if (((_b = process == null ? void 0 : process.env) == null ? void 0 : _b.NODE_ENV) !== "production" && jsxContent === null && managedContent === null) {
|
|
440
|
+
console.warn(
|
|
441
|
+
`[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.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: containerRef, "data-sentient-id": props.id, "data-sentient-variant": variantId, children: jsxContent != null ? jsxContent : managedContent });
|
|
435
445
|
}
|
|
436
446
|
var Adaptive = (0, import_react3.memo)(AdaptiveImpl, (prev, next) => {
|
|
437
447
|
if (prev.id !== next.id) return false;
|
|
@@ -453,7 +463,7 @@ function AdaptiveText({
|
|
|
453
463
|
className
|
|
454
464
|
}) {
|
|
455
465
|
const client = useSentient();
|
|
456
|
-
const
|
|
466
|
+
const apiKey = useAdaptiveApiKey();
|
|
457
467
|
const onAssignment = useOnAssignment();
|
|
458
468
|
const [text, setText] = (0, import_react4.useState)(null);
|
|
459
469
|
const [variantId, setVariantId] = (0, import_react4.useState)(null);
|
|
@@ -471,18 +481,18 @@ function AdaptiveText({
|
|
|
471
481
|
};
|
|
472
482
|
}, [client, id]);
|
|
473
483
|
(0, import_react4.useEffect)(() => {
|
|
474
|
-
if (!client || !variantId || !
|
|
484
|
+
if (!client || !variantId || !apiKey) return;
|
|
475
485
|
if (trackedRef.current === variantId) return;
|
|
476
486
|
trackedRef.current = variantId;
|
|
477
487
|
client.track({
|
|
478
|
-
projectId,
|
|
488
|
+
projectId: apiKey,
|
|
479
489
|
componentId: id,
|
|
480
490
|
variantId,
|
|
481
491
|
eventType: "variant_assigned",
|
|
482
492
|
payload: {}
|
|
483
493
|
});
|
|
484
494
|
onAssignment == null ? void 0 : onAssignment(id, variantId);
|
|
485
|
-
}, [client, variantId,
|
|
495
|
+
}, [client, variantId, apiKey, id, onAssignment]);
|
|
486
496
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Tag, { className, children: text != null ? text : defaultText });
|
|
487
497
|
}
|
|
488
498
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/adaptive.tsx","../src/use-assignment.ts","../src/weights-store.ts","../src/adaptive-text.tsx","../src/segment.ts"],"sourcesContent":["export { AdaptiveProvider, useSentient, useInitialAssignments, useLayoutOrder } from './provider.js';\nexport type { AdaptiveProviderProps, SsrFallback } from './provider.js';\n\nexport { Adaptive } from './adaptive.js';\nexport type {\n AdaptiveProps,\n GoalConfig,\n ClickGoal,\n ScrollDepthGoal,\n FormSubmitGoal,\n CompositeGoal,\n} from './adaptive.js';\n\nexport { AdaptiveText } from './adaptive-text.js';\nexport type { AdaptiveTextProps } from './adaptive-text.js';\n\nexport { useAssignment } from './use-assignment.js';\nexport type { AssignmentState } from './use-assignment.js';\n\nexport {\n subscribe as subscribeWeights,\n update as updateWeights,\n getWeights,\n} from './weights-store.js';\nexport type { ComponentWeights, VariantWeight } from './weights-store.js';\n\nexport { detectSegment } from './segment.js';\n","'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveProjectId, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, projectId, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, projectId, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, projectId, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n const content = props.variants[variantId] ?? null;\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {content}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via ε-greedy and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], isLoading: false };\n }\n return { variantId: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n return { variantId: cached.variantId, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n // If the API is unreachable or returns null, the fallback set above stays.\n if (!result || !variantIds.includes(result.variantId)) return;\n setState({ variantId: result.variantId, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { AssignResult } from '@sentientui/core';\nimport { useSentient, useAdaptiveProjectId, useOnAssignment } from './provider.js';\n\nexport type AdaptiveTextProps = {\n id: string;\n default: string;\n component?: keyof JSX.IntrinsicElements;\n className?: string;\n};\n\nexport function AdaptiveText({\n id,\n default: defaultText,\n component: Tag = 'span',\n className,\n}: AdaptiveTextProps) {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const onAssignment = useOnAssignment();\n const [text, setText] = useState<string | null>(null);\n const [variantId, setVariantId] = useState<string | null>(null);\n const trackedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n void client.assign(id).then((result: AssignResult | null) => {\n if (cancelled || !result) return;\n setVariantId(result.variantId);\n if (result.content) setText(result.content);\n });\n return () => {\n cancelled = true;\n };\n }, [client, id]);\n\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (trackedRef.current === variantId) return;\n trackedRef.current = variantId;\n client.track({\n projectId,\n componentId: id,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n onAssignment?.(id, variantId);\n }, [client, variantId, projectId, id, onAssignment]);\n\n return <Tag className={className}>{text ?? defaultText}</Tag>;\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","_a","import_react","import_jsx_runtime","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/adaptive.tsx","../src/use-assignment.ts","../src/weights-store.ts","../src/adaptive-text.tsx","../src/segment.ts"],"sourcesContent":["export { AdaptiveProvider, useSentient, useInitialAssignments, useLayoutOrder } from './provider.js';\nexport type { AdaptiveProviderProps, SsrFallback } from './provider.js';\n\nexport { Adaptive } from './adaptive.js';\nexport type {\n AdaptiveProps,\n GoalConfig,\n ClickGoal,\n ScrollDepthGoal,\n FormSubmitGoal,\n CompositeGoal,\n} from './adaptive.js';\n\nexport { AdaptiveText } from './adaptive-text.js';\nexport type { AdaptiveTextProps } from './adaptive-text.js';\n\nexport { useAssignment } from './use-assignment.js';\nexport type { AssignmentState } from './use-assignment.js';\n\nexport {\n subscribe as subscribeWeights,\n update as updateWeights,\n getWeights,\n} from './weights-store.js';\nexport type { ComponentWeights, VariantWeight } from './weights-store.js';\n\nexport { detectSegment } from './segment.js';\n","'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n // The publishable API key (pk_…). Historically named `projectId` because the\n // API uses it as the project identifier on the wire, but it is the API key,\n // not the project UUID. Field renamed for clarity.\n apiKey: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n apiKey: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n apiKey: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: publishable API key carried alongside the client. */\nexport function useAdaptiveApiKey(): string {\n return useContext(AdaptiveContext).apiKey;\n}\n\n/**\n * @deprecated Renamed to `useAdaptiveApiKey`. Kept as an alias so external\n * imports from earlier versions of this package keep working. Will be removed\n * in 0.4.0.\n */\nlet warnedProjectIdAlias = false;\nexport function useAdaptiveProjectId(): string {\n if (\n !warnedProjectIdAlias &&\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV !== 'production'\n ) {\n warnedProjectIdAlias = true;\n // eslint-disable-next-line no-console\n console.warn(\n '[@sentientui/react] useAdaptiveProjectId is deprecated; use useAdaptiveApiKey. Will be removed in 0.4.0.',\n );\n }\n return useAdaptiveApiKey();\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveApiKey, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const apiKey = useAdaptiveApiKey();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId, content } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !apiKey) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId: apiKey,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, apiKey, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId: apiKey,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, apiKey, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId: apiKey,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, apiKey, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n\n const jsxContent = props.variants[variantId] ?? null;\n const managedContent = jsxContent === null ? content : null;\n\n if (process?.env?.NODE_ENV !== 'production' && jsxContent === null && managedContent === null) {\n console.warn(\n `[sentient] <Adaptive id=\"${props.id}\"> was assigned variant \"${variantId}\" but no matching key exists in props.variants.` +\n ` If this is a dashboard-managed text variant, use <AdaptiveText id=\"${props.id}\"> instead.`,\n );\n }\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {jsxContent ?? managedContent}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n /** Populated when the assigned variant is a dashboard-managed text variant. */\n content: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via Thompson Sampling and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, content: null, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, content: null, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], content: null, isLoading: false };\n }\n return { variantId: null, content: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n // Allow cached managed variants (content present) even if not in variantIds.\n if (cached && (variantIds.includes(cached.variantId) || cached.content)) {\n return { variantId: cached.variantId, content: cached.content ?? null, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, content: null, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, content: null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && (variantIds.includes(cached.variantId) || cached.content)) {\n setState({ variantId: cached.variantId, content: cached.content ?? null, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, content: null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n if (!result) return;\n // Allow the result if it's a known code variant OR a managed text variant (has content).\n if (!variantIds.includes(result.variantId) && !result.content) return;\n setState({ variantId: result.variantId, content: result.content ?? null, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && (variantIds.includes(cached.variantId) || cached.content)) {\n setState({ variantId: cached.variantId, content: cached.content ?? null, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, content: null, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, content: null, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { AssignResult } from '@sentientui/core';\nimport { useSentient, useAdaptiveApiKey, useOnAssignment } from './provider.js';\n\nexport type AdaptiveTextProps = {\n id: string;\n default: string;\n component?: keyof JSX.IntrinsicElements;\n className?: string;\n};\n\nexport function AdaptiveText({\n id,\n default: defaultText,\n component: Tag = 'span',\n className,\n}: AdaptiveTextProps) {\n const client = useSentient();\n const apiKey = useAdaptiveApiKey();\n const onAssignment = useOnAssignment();\n const [text, setText] = useState<string | null>(null);\n const [variantId, setVariantId] = useState<string | null>(null);\n const trackedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n void client.assign(id).then((result: AssignResult | null) => {\n if (cancelled || !result) return;\n setVariantId(result.variantId);\n if (result.content) setText(result.content);\n });\n return () => {\n cancelled = true;\n };\n }, [client, id]);\n\n useEffect(() => {\n if (!client || !variantId || !apiKey) return;\n if (trackedRef.current === variantId) return;\n trackedRef.current = variantId;\n client.track({\n projectId: apiKey,\n componentId: id,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n onAssignment?.(id, variantId);\n }, [client, variantId, apiKey, id, onAssignment]);\n\n return <Tag className={className}>{text ?? defaultText}</Tag>;\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":"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","names":["import_react","import_react","_a","import_jsx_runtime","_a","_b","import_react","import_jsx_runtime","import_core"]}
|
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { init } from "@sentientui/core";
|
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
var AdaptiveContext = createContext({
|
|
8
8
|
client: null,
|
|
9
|
-
|
|
9
|
+
apiKey: "",
|
|
10
10
|
initialAssignments: {},
|
|
11
11
|
sessionSegment: "desktop:direct",
|
|
12
12
|
ssrFallback: "first",
|
|
@@ -43,7 +43,7 @@ function AdaptiveProvider(props) {
|
|
|
43
43
|
{
|
|
44
44
|
value: {
|
|
45
45
|
client,
|
|
46
|
-
|
|
46
|
+
apiKey: props.apiKey,
|
|
47
47
|
initialAssignments: (_b = props.initialAssignments) != null ? _b : {},
|
|
48
48
|
sessionSegment: (_c = props.sessionSegment) != null ? _c : "desktop:direct",
|
|
49
49
|
ssrFallback,
|
|
@@ -57,8 +57,8 @@ function AdaptiveProvider(props) {
|
|
|
57
57
|
function useSentient() {
|
|
58
58
|
return useContext(AdaptiveContext).client;
|
|
59
59
|
}
|
|
60
|
-
function
|
|
61
|
-
return useContext(AdaptiveContext).
|
|
60
|
+
function useAdaptiveApiKey() {
|
|
61
|
+
return useContext(AdaptiveContext).apiKey;
|
|
62
62
|
}
|
|
63
63
|
function useInitialAssignments() {
|
|
64
64
|
return useContext(AdaptiveContext).initialAssignments;
|
|
@@ -154,30 +154,30 @@ function useAssignment(componentId, variantIds) {
|
|
|
154
154
|
console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);
|
|
155
155
|
}
|
|
156
156
|
const initial = (() => {
|
|
157
|
-
var _a;
|
|
157
|
+
var _a, _b;
|
|
158
158
|
if (overrideVariant) {
|
|
159
|
-
return { variantId: overrideVariant, isLoading: false };
|
|
159
|
+
return { variantId: overrideVariant, content: null, isLoading: false };
|
|
160
160
|
}
|
|
161
161
|
if (!client) {
|
|
162
162
|
const preloaded = initialAssignments[componentId];
|
|
163
163
|
if (preloaded && variantIds.includes(preloaded)) {
|
|
164
|
-
return { variantId: preloaded, isLoading: false };
|
|
164
|
+
return { variantId: preloaded, content: null, isLoading: false };
|
|
165
165
|
}
|
|
166
166
|
if (ssrFallback === "first" && variantIds.length > 0) {
|
|
167
|
-
return { variantId: variantIds[0], isLoading: false };
|
|
167
|
+
return { variantId: variantIds[0], content: null, isLoading: false };
|
|
168
168
|
}
|
|
169
|
-
return { variantId: null, isLoading: true };
|
|
169
|
+
return { variantId: null, content: null, isLoading: true };
|
|
170
170
|
}
|
|
171
171
|
const cached = client.getAssignment(componentId, segment);
|
|
172
|
-
if (cached && variantIds.includes(cached.variantId)) {
|
|
173
|
-
return { variantId: cached.variantId, isLoading: false };
|
|
172
|
+
if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
|
|
173
|
+
return { variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false };
|
|
174
174
|
}
|
|
175
175
|
const weights = getWeights(componentId);
|
|
176
176
|
if (weights) {
|
|
177
177
|
const chosen = pickFromWeights(weights, variantIds);
|
|
178
|
-
if (chosen) return { variantId: chosen, isLoading: false };
|
|
178
|
+
if (chosen) return { variantId: chosen, content: null, isLoading: false };
|
|
179
179
|
}
|
|
180
|
-
return { variantId: (
|
|
180
|
+
return { variantId: (_b = variantIds[0]) != null ? _b : null, content: null, isLoading: false };
|
|
181
181
|
})();
|
|
182
182
|
const [state, setState] = useState2(initial);
|
|
183
183
|
const reportAssignment = (variantId) => {
|
|
@@ -187,17 +187,18 @@ function useAssignment(componentId, variantIds) {
|
|
|
187
187
|
onAssignment(componentId, variantId);
|
|
188
188
|
};
|
|
189
189
|
useEffect2(() => {
|
|
190
|
+
var _a;
|
|
190
191
|
if (overrideVariant) return;
|
|
191
192
|
if (!client) return;
|
|
192
193
|
const cached = client.getAssignment(componentId, segment);
|
|
193
|
-
if (cached && variantIds.includes(cached.variantId)) {
|
|
194
|
-
setState({ variantId: cached.variantId, isLoading: false });
|
|
194
|
+
if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
|
|
195
|
+
setState({ variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false });
|
|
195
196
|
reportAssignment(cached.variantId);
|
|
196
197
|
return;
|
|
197
198
|
}
|
|
198
199
|
setState((prev) => {
|
|
199
|
-
var
|
|
200
|
-
return prev.variantId ? prev : { variantId: (
|
|
200
|
+
var _a2;
|
|
201
|
+
return prev.variantId ? prev : { variantId: (_a2 = variantIds[0]) != null ? _a2 : null, content: null, isLoading: false };
|
|
201
202
|
});
|
|
202
203
|
}, [overrideVariant, client, componentId, segment]);
|
|
203
204
|
useEffect2(() => {
|
|
@@ -207,9 +208,11 @@ function useAssignment(componentId, variantIds) {
|
|
|
207
208
|
if (cached && variantIds.includes(cached.variantId)) return;
|
|
208
209
|
let cancelled = false;
|
|
209
210
|
void client.assign(componentId, variantIds).then((result) => {
|
|
211
|
+
var _a;
|
|
210
212
|
if (cancelled) return;
|
|
211
|
-
if (!result
|
|
212
|
-
|
|
213
|
+
if (!result) return;
|
|
214
|
+
if (!variantIds.includes(result.variantId) && !result.content) return;
|
|
215
|
+
setState({ variantId: result.variantId, content: (_a = result.content) != null ? _a : null, isLoading: false });
|
|
213
216
|
reportAssignment(result.variantId);
|
|
214
217
|
});
|
|
215
218
|
return () => {
|
|
@@ -220,17 +223,18 @@ function useAssignment(componentId, variantIds) {
|
|
|
220
223
|
if (overrideVariant) return;
|
|
221
224
|
if (!client) return;
|
|
222
225
|
return subscribe(componentId, (weights) => {
|
|
226
|
+
var _a;
|
|
223
227
|
const cached = client.getAssignment(componentId, segment);
|
|
224
|
-
if (cached && variantIds.includes(cached.variantId)) {
|
|
225
|
-
setState({ variantId: cached.variantId, isLoading: false });
|
|
228
|
+
if (cached && (variantIds.includes(cached.variantId) || cached.content)) {
|
|
229
|
+
setState({ variantId: cached.variantId, content: (_a = cached.content) != null ? _a : null, isLoading: false });
|
|
226
230
|
return;
|
|
227
231
|
}
|
|
228
232
|
const chosen = pickFromWeights(weights, variantIds);
|
|
229
|
-
if (chosen) setState({ variantId: chosen, isLoading: false });
|
|
233
|
+
if (chosen) setState({ variantId: chosen, content: null, isLoading: false });
|
|
230
234
|
});
|
|
231
235
|
}, [overrideVariant, client, componentId, segment]);
|
|
232
236
|
if (overrideVariant) {
|
|
233
|
-
return { variantId: overrideVariant, isLoading: false };
|
|
237
|
+
return { variantId: overrideVariant, content: null, isLoading: false };
|
|
234
238
|
}
|
|
235
239
|
return state;
|
|
236
240
|
}
|
|
@@ -257,11 +261,11 @@ function findClickable(start, container) {
|
|
|
257
261
|
return false;
|
|
258
262
|
}
|
|
259
263
|
function AdaptiveImpl(props) {
|
|
260
|
-
var _a;
|
|
264
|
+
var _a, _b;
|
|
261
265
|
const client = useSentient();
|
|
262
|
-
const
|
|
266
|
+
const apiKey = useAdaptiveApiKey();
|
|
263
267
|
const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);
|
|
264
|
-
const { variantId } = useAssignment(props.id, variantIds);
|
|
268
|
+
const { variantId, content } = useAssignment(props.id, variantIds);
|
|
265
269
|
const containerRef = useRef2(null);
|
|
266
270
|
const [mounted, setMounted] = useState3(false);
|
|
267
271
|
useEffect3(() => {
|
|
@@ -272,20 +276,20 @@ function AdaptiveImpl(props) {
|
|
|
272
276
|
const goal = useMemo(() => normalize(props.goal), [props.goal]);
|
|
273
277
|
const goalLabel = typeof props.goal === "string" ? props.goal : goal.type;
|
|
274
278
|
useEffect3(() => {
|
|
275
|
-
var _a2,
|
|
276
|
-
if (!client || !variantId || !
|
|
279
|
+
var _a2, _b2;
|
|
280
|
+
if (!client || !variantId || !apiKey) return;
|
|
277
281
|
if (assignTrackedRef.current === variantId) return;
|
|
278
|
-
const rawHtml = (
|
|
282
|
+
const rawHtml = (_b2 = (_a2 = containerRef.current) == null ? void 0 : _a2.innerHTML) != null ? _b2 : "";
|
|
279
283
|
if (!rawHtml && !mounted) return;
|
|
280
284
|
assignTrackedRef.current = variantId;
|
|
281
285
|
client.track({
|
|
282
|
-
projectId,
|
|
286
|
+
projectId: apiKey,
|
|
283
287
|
componentId: props.id,
|
|
284
288
|
variantId,
|
|
285
289
|
eventType: "variant_assigned",
|
|
286
290
|
payload: rawHtml ? { previewHtml: rawHtml.slice(0, 3e4) } : {}
|
|
287
291
|
});
|
|
288
|
-
}, [client, variantId,
|
|
292
|
+
}, [client, variantId, apiKey, props.id, mounted]);
|
|
289
293
|
useEffect3(() => {
|
|
290
294
|
goalFiredRef.current = false;
|
|
291
295
|
}, [variantId]);
|
|
@@ -299,7 +303,7 @@ function AdaptiveImpl(props) {
|
|
|
299
303
|
hoverStart = Date.now();
|
|
300
304
|
timerId = setTimeout(() => {
|
|
301
305
|
client.track({
|
|
302
|
-
projectId,
|
|
306
|
+
projectId: apiKey,
|
|
303
307
|
componentId: props.id,
|
|
304
308
|
variantId,
|
|
305
309
|
eventType: "cursor_signal",
|
|
@@ -321,7 +325,7 @@ function AdaptiveImpl(props) {
|
|
|
321
325
|
node.removeEventListener("mouseleave", onLeave);
|
|
322
326
|
if (timerId !== null) clearTimeout(timerId);
|
|
323
327
|
};
|
|
324
|
-
}, [client, variantId,
|
|
328
|
+
}, [client, variantId, apiKey, props.id]);
|
|
325
329
|
useEffect3(() => {
|
|
326
330
|
if (!client || !variantId) return;
|
|
327
331
|
const node = containerRef.current;
|
|
@@ -330,7 +334,7 @@ function AdaptiveImpl(props) {
|
|
|
330
334
|
if (goalFiredRef.current) return;
|
|
331
335
|
goalFiredRef.current = true;
|
|
332
336
|
client.track({
|
|
333
|
-
projectId,
|
|
337
|
+
projectId: apiKey,
|
|
334
338
|
componentId: props.id,
|
|
335
339
|
variantId,
|
|
336
340
|
eventType: "goal_achieved",
|
|
@@ -392,11 +396,17 @@ function AdaptiveImpl(props) {
|
|
|
392
396
|
return () => {
|
|
393
397
|
for (const c of cleanups) c();
|
|
394
398
|
};
|
|
395
|
-
}, [client, variantId,
|
|
399
|
+
}, [client, variantId, apiKey, props.id, goal, goalLabel]);
|
|
396
400
|
if (props.clientOnly && (!mounted || !client)) return null;
|
|
397
401
|
if (!variantId) return null;
|
|
398
|
-
const
|
|
399
|
-
|
|
402
|
+
const jsxContent = (_a = props.variants[variantId]) != null ? _a : null;
|
|
403
|
+
const managedContent = jsxContent === null ? content : null;
|
|
404
|
+
if (((_b = process == null ? void 0 : process.env) == null ? void 0 : _b.NODE_ENV) !== "production" && jsxContent === null && managedContent === null) {
|
|
405
|
+
console.warn(
|
|
406
|
+
`[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.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return /* @__PURE__ */ jsx2("div", { ref: containerRef, "data-sentient-id": props.id, "data-sentient-variant": variantId, children: jsxContent != null ? jsxContent : managedContent });
|
|
400
410
|
}
|
|
401
411
|
var Adaptive = memo(AdaptiveImpl, (prev, next) => {
|
|
402
412
|
if (prev.id !== next.id) return false;
|
|
@@ -418,7 +428,7 @@ function AdaptiveText({
|
|
|
418
428
|
className
|
|
419
429
|
}) {
|
|
420
430
|
const client = useSentient();
|
|
421
|
-
const
|
|
431
|
+
const apiKey = useAdaptiveApiKey();
|
|
422
432
|
const onAssignment = useOnAssignment();
|
|
423
433
|
const [text, setText] = useState4(null);
|
|
424
434
|
const [variantId, setVariantId] = useState4(null);
|
|
@@ -436,18 +446,18 @@ function AdaptiveText({
|
|
|
436
446
|
};
|
|
437
447
|
}, [client, id]);
|
|
438
448
|
useEffect4(() => {
|
|
439
|
-
if (!client || !variantId || !
|
|
449
|
+
if (!client || !variantId || !apiKey) return;
|
|
440
450
|
if (trackedRef.current === variantId) return;
|
|
441
451
|
trackedRef.current = variantId;
|
|
442
452
|
client.track({
|
|
443
|
-
projectId,
|
|
453
|
+
projectId: apiKey,
|
|
444
454
|
componentId: id,
|
|
445
455
|
variantId,
|
|
446
456
|
eventType: "variant_assigned",
|
|
447
457
|
payload: {}
|
|
448
458
|
});
|
|
449
459
|
onAssignment == null ? void 0 : onAssignment(id, variantId);
|
|
450
|
-
}, [client, variantId,
|
|
460
|
+
}, [client, variantId, apiKey, id, onAssignment]);
|
|
451
461
|
return /* @__PURE__ */ jsx3(Tag, { className, children: text != null ? text : defaultText });
|
|
452
462
|
}
|
|
453
463
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx","../src/adaptive.tsx","../src/use-assignment.ts","../src/weights-store.ts","../src/adaptive-text.tsx","../src/segment.ts"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveProjectId, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, projectId, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, projectId, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, projectId, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n const content = props.variants[variantId] ?? null;\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {content}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via ε-greedy and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], isLoading: false };\n }\n return { variantId: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n return { variantId: cached.variantId, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n // If the API is unreachable or returns null, the fallback set above stays.\n if (!result || !variantIds.includes(result.variantId)) return;\n setState({ variantId: result.variantId, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { AssignResult } from '@sentientui/core';\nimport { useSentient, useAdaptiveProjectId, useOnAssignment } from './provider.js';\n\nexport type AdaptiveTextProps = {\n id: string;\n default: string;\n component?: keyof JSX.IntrinsicElements;\n className?: string;\n};\n\nexport function AdaptiveText({\n id,\n default: defaultText,\n component: Tag = 'span',\n className,\n}: AdaptiveTextProps) {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const onAssignment = useOnAssignment();\n const [text, setText] = useState<string | null>(null);\n const [variantId, setVariantId] = useState<string | null>(null);\n const trackedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n void client.assign(id).then((result: AssignResult | null) => {\n if (cancelled || !result) return;\n setVariantId(result.variantId);\n if (result.content) setText(result.content);\n });\n return () => {\n cancelled = true;\n };\n }, [client, id]);\n\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (trackedRef.current === variantId) return;\n trackedRef.current = variantId;\n client.track({\n projectId,\n componentId: id,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n onAssignment?.(id, variantId);\n }, [client, variantId, projectId, id, onAssignment]);\n\n return <Tag className={className}>{text ?? defaultText}</Tag>;\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":"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","names":["useEffect","useRef","useState","useEffect","useState","useState","useEffect","jsx","useRef","useState","useEffect","_a","useEffect","useRef","useState","jsx","useState","useRef","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx","../src/adaptive.tsx","../src/use-assignment.ts","../src/weights-store.ts","../src/adaptive-text.tsx","../src/segment.ts"],"sourcesContent":["'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n // The publishable API key (pk_…). Historically named `projectId` because the\n // API uses it as the project identifier on the wire, but it is the API key,\n // not the project UUID. Field renamed for clarity.\n apiKey: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n apiKey: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n apiKey: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: publishable API key carried alongside the client. */\nexport function useAdaptiveApiKey(): string {\n return useContext(AdaptiveContext).apiKey;\n}\n\n/**\n * @deprecated Renamed to `useAdaptiveApiKey`. Kept as an alias so external\n * imports from earlier versions of this package keep working. Will be removed\n * in 0.4.0.\n */\nlet warnedProjectIdAlias = false;\nexport function useAdaptiveProjectId(): string {\n if (\n !warnedProjectIdAlias &&\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV !== 'production'\n ) {\n warnedProjectIdAlias = true;\n // eslint-disable-next-line no-console\n console.warn(\n '[@sentientui/react] useAdaptiveProjectId is deprecated; use useAdaptiveApiKey. Will be removed in 0.4.0.',\n );\n }\n return useAdaptiveApiKey();\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveApiKey, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const apiKey = useAdaptiveApiKey();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId, content } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !apiKey) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId: apiKey,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, apiKey, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId: apiKey,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, apiKey, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId: apiKey,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, apiKey, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n\n const jsxContent = props.variants[variantId] ?? null;\n const managedContent = jsxContent === null ? content : null;\n\n if (process?.env?.NODE_ENV !== 'production' && jsxContent === null && managedContent === null) {\n console.warn(\n `[sentient] <Adaptive id=\"${props.id}\"> was assigned variant \"${variantId}\" but no matching key exists in props.variants.` +\n ` If this is a dashboard-managed text variant, use <AdaptiveText id=\"${props.id}\"> instead.`,\n );\n }\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {jsxContent ?? managedContent}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n /** Populated when the assigned variant is a dashboard-managed text variant. */\n content: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via Thompson Sampling and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, content: null, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, content: null, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], content: null, isLoading: false };\n }\n return { variantId: null, content: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n // Allow cached managed variants (content present) even if not in variantIds.\n if (cached && (variantIds.includes(cached.variantId) || cached.content)) {\n return { variantId: cached.variantId, content: cached.content ?? null, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, content: null, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, content: null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && (variantIds.includes(cached.variantId) || cached.content)) {\n setState({ variantId: cached.variantId, content: cached.content ?? null, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, content: null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n if (!result) return;\n // Allow the result if it's a known code variant OR a managed text variant (has content).\n if (!variantIds.includes(result.variantId) && !result.content) return;\n setState({ variantId: result.variantId, content: result.content ?? null, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && (variantIds.includes(cached.variantId) || cached.content)) {\n setState({ variantId: cached.variantId, content: cached.content ?? null, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, content: null, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, content: null, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { AssignResult } from '@sentientui/core';\nimport { useSentient, useAdaptiveApiKey, useOnAssignment } from './provider.js';\n\nexport type AdaptiveTextProps = {\n id: string;\n default: string;\n component?: keyof JSX.IntrinsicElements;\n className?: string;\n};\n\nexport function AdaptiveText({\n id,\n default: defaultText,\n component: Tag = 'span',\n className,\n}: AdaptiveTextProps) {\n const client = useSentient();\n const apiKey = useAdaptiveApiKey();\n const onAssignment = useOnAssignment();\n const [text, setText] = useState<string | null>(null);\n const [variantId, setVariantId] = useState<string | null>(null);\n const trackedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n void client.assign(id).then((result: AssignResult | null) => {\n if (cancelled || !result) return;\n setVariantId(result.variantId);\n if (result.content) setText(result.content);\n });\n return () => {\n cancelled = true;\n };\n }, [client, id]);\n\n useEffect(() => {\n if (!client || !variantId || !apiKey) return;\n if (trackedRef.current === variantId) return;\n trackedRef.current = variantId;\n client.track({\n projectId: apiKey,\n componentId: id,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n onAssignment?.(id, variantId);\n }, [client, variantId, apiKey, id, onAssignment]);\n\n return <Tag className={className}>{text ?? defaultText}</Tag>;\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":"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","names":["useEffect","useRef","useState","useEffect","useState","useState","useEffect","_a","jsx","useRef","useState","useEffect","_a","_b","useEffect","useRef","useState","jsx","useState","useRef","useEffect"]}
|
|
@@ -23,7 +23,7 @@ import { init } from "@sentientui/core";
|
|
|
23
23
|
import { jsx } from "react/jsx-runtime";
|
|
24
24
|
var AdaptiveContext = createContext({
|
|
25
25
|
client: null,
|
|
26
|
-
|
|
26
|
+
apiKey: "",
|
|
27
27
|
initialAssignments: {},
|
|
28
28
|
sessionSegment: "desktop:direct",
|
|
29
29
|
ssrFallback: "first",
|
|
@@ -60,7 +60,7 @@ function AdaptiveProvider(props) {
|
|
|
60
60
|
{
|
|
61
61
|
value: {
|
|
62
62
|
client,
|
|
63
|
-
|
|
63
|
+
apiKey: props.apiKey,
|
|
64
64
|
initialAssignments: (_b = props.initialAssignments) != null ? _b : {},
|
|
65
65
|
sessionSegment: (_c = props.sessionSegment) != null ? _c : "desktop:direct",
|
|
66
66
|
ssrFallback,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/provider.tsx","../../src/next/adaptive-root-client.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n
|
|
1
|
+
{"version":3,"sources":["../../src/provider.tsx","../../src/next/adaptive-root-client.tsx"],"sourcesContent":["'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n // The publishable API key (pk_…). Historically named `projectId` because the\n // API uses it as the project identifier on the wire, but it is the API key,\n // not the project UUID. Field renamed for clarity.\n apiKey: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n apiKey: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n apiKey: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: publishable API key carried alongside the client. */\nexport function useAdaptiveApiKey(): string {\n return useContext(AdaptiveContext).apiKey;\n}\n\n/**\n * @deprecated Renamed to `useAdaptiveApiKey`. Kept as an alias so external\n * imports from earlier versions of this package keep working. Will be removed\n * in 0.4.0.\n */\nlet warnedProjectIdAlias = false;\nexport function useAdaptiveProjectId(): string {\n if (\n !warnedProjectIdAlias &&\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV !== 'production'\n ) {\n warnedProjectIdAlias = true;\n // eslint-disable-next-line no-console\n console.warn(\n '[@sentientui/react] useAdaptiveProjectId is deprecated; use useAdaptiveApiKey. Will be removed in 0.4.0.',\n );\n }\n return useAdaptiveApiKey();\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { AdaptiveProvider, type AdaptiveProviderProps } from '../provider.js';\n\nexport type AdaptiveRootClientProps = AdaptiveProviderProps & {\n children: ReactNode;\n};\n\n/** Client boundary for {@link AdaptiveRoot} — keeps context/hooks out of the server bundle. */\nexport function AdaptiveRootClient(props: AdaptiveRootClientProps): JSX.Element {\n return <AdaptiveProvider {...props} />;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAIA,SAAS,eAAe,YAAY,WAAW,gBAAgC;AAC/E,SAAS,YAAsD;AA2G3D;AAzFJ,IAAM,kBAAkB,cAAoC;AAAA,EAC1D,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,oBAAoB,CAAC;AAAA,EACrB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AACtB,CAAC;AAiDM,SAAS,iBAAiB,OAA2C;AAhF5E;AAiFE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,IAAI;AAEhE,YAAU,MAAM;AAEd,QAAI,MAAM,YAAY,OAAO;AAC3B,gBAAU,CAAC,SAAgC;AACzC,qCAAM;AACN,eAAO;AAAA,MACT,CAAC;AACD;AAAA,IACF;AAEA,UAAM,IAAI,KAAK;AAAA,MACb,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,oBAAoB,MAAM;AAAA,MAC1B,gBAAgB,MAAM;AAAA,MACtB,SAAS,MAAM;AAAA,IACjB,CAAC;AACD,cAAU,CAAC;AACX,WAAO,MAAM;AACX,QAAE,QAAQ;AAAA,IACZ;AAAA,EAGF,GAAG,CAAC,MAAM,OAAO,CAAC;AAElB,QAAM,eAAc,WAAM,gBAAN,YAAqB;AAEzC,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,OAAO;AAAA,QACL;AAAA,QACA,QAAQ,MAAM;AAAA,QACd,qBAAoB,WAAM,uBAAN,YAA4B,CAAC;AAAA,QACjD,iBAAgB,WAAM,mBAAN,YAAwB;AAAA,QACxC;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,qBAAoB,WAAM,uBAAN,YAA4B;AAAA,MAClD;AAAA,MAEC,gBAAM;AAAA;AAAA,EACT;AAEJ;;;ACnHS,gBAAAA,YAAA;AADF,SAAS,mBAAmB,OAA6C;AAC9E,SAAO,gBAAAA,KAAC,qCAAqB,MAAO;AACtC;","names":["jsx"]}
|