@sentientui/react 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,30 +1,65 @@
1
1
  # @sentientui/react
2
2
 
3
- React SDK for [SentientUI](https://sentientui.dev) — drop-in components and hooks that let a contextual bandit automatically serve the best-performing UI variant for each visitor.
3
+ React SDK for [SentientUI](https://sentient-ui.com) — drop-in components and hooks that let a Thompson Sampling bandit, persona clusters, and visitor portraits automatically serve the best-performing UI variant (and section order) for each visitor.
4
+
5
+ Learning, assignment, portraits, clustering, and graph storage all run on the SentientUI hosted API. You only install the SDK and add API keys from the dashboard.
4
6
 
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- npm install @sentientui/react @sentientui/core
10
+ npm install @sentientui/react
11
+ # or
12
+ pnpm add @sentientui/react
13
+ ```
14
+
15
+ `@sentientui/core` is a transitive dependency — you do not need to install it directly. Only import from `@sentientui/react`.
16
+
17
+ ## API key
18
+
19
+ Create a project at [app.sentient-ui.com](https://app.sentient-ui.com) and copy the API key shown once at project creation. It looks like `pk_xxxxxxxx…`. The key is safe to ship in browser bundles — the API enforces an allowed-origins allowlist on every request. Add your production domain in Project → Settings → Allowed origins.
20
+
21
+ For Next.js, set the same key in two env vars (one for the server, one for the browser):
22
+
23
+ ```bash
24
+ # .env.local
25
+ SENTIENT_API_KEY=pk_your_key
26
+ NEXT_PUBLIC_SENTIENT_API_KEY=pk_your_key
9
27
  ```
10
28
 
11
- ## Quick start
29
+ The SDK points at `https://sentient-api.fly.dev` (the hosted API) automatically — no ingest URL configuration required.
12
30
 
13
- ### 1. Wrap your app with `AdaptiveProvider`
31
+ ## Quick start Next.js App Router (recommended)
32
+
33
+ Wrap your root layout with `<AdaptiveRoot>`. It is a Server Component: variant assignments and persona-specific layout order are resolved server-side before HTML is sent, so the first paint has the right content with no layout shift.
14
34
 
15
35
  ```tsx
16
- import { AdaptiveProvider } from '@sentientui/react';
36
+ // app/layout.tsx
37
+ import { AdaptiveRoot } from '@sentientui/react/next';
17
38
 
18
- export default function App({ children }) {
39
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
19
40
  return (
20
- <AdaptiveProvider apiKey="pk_your_key" context="saas">
21
- {children}
22
- </AdaptiveProvider>
41
+ <html lang="en">
42
+ <body>
43
+ <AdaptiveRoot
44
+ components={[
45
+ { id: 'hero_cta', variantIds: ['control', 'variant_a'] },
46
+ { id: 'pricing', variantIds: ['monthly', 'annual_first'] },
47
+ ]}
48
+ sections={['hero', 'pricing', 'features', 'social_proof']}
49
+ serverApiKey={process.env.SENTIENT_API_KEY!}
50
+ apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
51
+ appOrigin={process.env.NEXT_PUBLIC_APP_URL!}
52
+ context="saas"
53
+ >
54
+ {children}
55
+ </AdaptiveRoot>
56
+ </body>
57
+ </html>
23
58
  );
24
59
  }
25
60
  ```
26
61
 
27
- ### 2. Add an `<Adaptive>` component
62
+ Then drop `<Adaptive>` anywhere you want a tested component:
28
63
 
29
64
  ```tsx
30
65
  import { Adaptive } from '@sentientui/react';
@@ -32,118 +67,261 @@ import { Adaptive } from '@sentientui/react';
32
67
  export default function Hero() {
33
68
  return (
34
69
  <Adaptive
35
- id="hero-headline"
36
- goal="click"
70
+ id="hero_cta"
71
+ goal="signup_click"
37
72
  variants={{
38
- control: <h1>Ship faster with SentientUI</h1>,
39
- variant_b: <h1>Your UI, optimised automatically</h1>,
73
+ control: <button className="btn-dark">Start free trial</button>,
74
+ variant_a: <button className="btn-blue">Get instant access →</button>,
40
75
  }}
41
76
  />
42
77
  );
43
78
  }
44
79
  ```
45
80
 
46
- The bandit tracks which variant drives more clicks and gradually shifts traffic toward the winner.
47
-
48
- ## Next.js (App Router)
81
+ ## Quick start other React apps (Vite, CRA, Remix, Pages Router)
49
82
 
50
- Use `AdaptiveRoot` in your layout for SEO-safe server-rendered assignments with no variant flash:
83
+ Use `<AdaptiveProvider>` directly. Assignments are made on the client after mount — no SSR preloading, but the bandit still learns normally.
51
84
 
52
85
  ```tsx
53
- // app/layout.tsx
54
- import { AdaptiveRoot } from '@sentientui/react/next';
86
+ import { AdaptiveProvider } from '@sentientui/react';
55
87
 
56
- export default function Layout({ children }) {
88
+ export default function App({ children }: { children: React.ReactNode }) {
57
89
  return (
58
- <AdaptiveRoot
59
- components={[
60
- { id: 'hero-headline', variantIds: ['control', 'variant_b'] },
61
- { id: 'pricing-cta', variantIds: ['control', 'variant_b'] },
62
- ]}
63
- serverApiKey={process.env.SENTIENT_API_KEY!}
64
- apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
90
+ <AdaptiveProvider
91
+ apiKey={import.meta.env.VITE_SENTIENT_API_KEY}
65
92
  context="saas"
66
93
  >
67
94
  {children}
68
- </AdaptiveRoot>
95
+ </AdaptiveProvider>
69
96
  );
70
97
  }
71
98
  ```
72
99
 
73
- `AdaptiveRoot` is a Server Component that fetches assignments before HTML is sent, then passes them to `AdaptiveProvider` as `initialAssignments` so crawlers and hydration see real content.
74
-
75
100
  ## API
76
101
 
77
- ### `<AdaptiveProvider>`
102
+ ### `<AdaptiveRoot>` (Next.js App Router — server component)
103
+
104
+ Imported from `@sentientui/react/next`.
78
105
 
79
106
  | Prop | Type | Description |
80
107
  |------|------|-------------|
81
- | `apiKey` | `string` | Your public API key (`pk_…`) |
82
- | `context` | `string` | Site context `'landing'`, `'ecommerce'`, `'saas'`, or `'marketplace'` |
83
- | `initialAssignments` | `Record<string, string>` | SSR-preloaded assignments (prevents hydration mismatch) |
84
- | `sessionSegment` | `string` | Segment string from SSR (`device:source`) |
85
- | `ssrFallback` | `'first' \| 'none'` | How to render adaptive slots without preloaded assignments. `'first'` renders the first variant (good for SEO). Defaults to `'first'` |
86
- | `consent` | `boolean` | Set to `false` to disable tracking before cookie consent |
87
- | `onAssignment` | `(componentId, variantId) => void` | Called once per component per session forward to Mixpanel, PostHog, Segment, etc. |
88
- | `debug` | `boolean` | Logs events to the console |
108
+ | `components` | `Array<{ id: string; variantIds: string[] }>` | Components to preload server-side. `id` must match `<Adaptive id="…">`. |
109
+ | `sections` | `string[]` *(optional)* | Page section IDs in default order. When provided, a single `POST /v1/decide` returns both layout order and assignments; `useLayoutOrder()` becomes available. |
110
+ | `serverApiKey` | `string` | Same `pk_…` key as `apiKey`, but read from the non-public env var so it never reaches the client bundle. |
111
+ | `apiKey` | `string` | `pk_…` key used by the browser SDK for event tracking and cache hydration. |
112
+ | `appOrigin` | `string` *(default `http://localhost:3001`)* | Your app origin (e.g. `https://yourapp.com`). Must be on the project's allowed-origins list. Always set in production. |
113
+ | `context` | `'landing' \| 'ecommerce' \| 'saas' \| 'marketplace'` | Type of product. Used for segment weighting and analytics grouping. |
114
+ | `consent` | `boolean` *(default `true`)* | Set `false` to skip SDK init (no cookies, no events). Flip to `true` after the visitor accepts. |
115
+ | `ssrFallback` | `'first' \| 'none'` *(default `'first'`)* | What to render in SSR HTML for components not in `components`. `'first'` is safe for SEO. |
116
+ | `onAssignment` | `(componentId, variantId) => void` | Called once per component the first time a variant resolves. Forward to Mixpanel/PostHog/Segment/GA4. |
117
+ | `timeoutMs` | `number` *(default `1500`)* | Server-side fetch timeout before falling back to the first variant. |
118
+ | `debug` | `boolean` | Log assignment and event activity to the console. |
119
+
120
+ ### `<AdaptiveProvider>` (any React app)
121
+
122
+ Accepts the same `apiKey`, `context`, `consent`, `ssrFallback`, `onAssignment`, `debug` props as `<AdaptiveRoot>`, plus:
123
+
124
+ | Prop | Type | Description |
125
+ |------|------|-------------|
126
+ | `initialAssignments` | `Record<string, string>` | SSR-preloaded assignments (e.g. from `loadAdaptiveAssignments` in `getServerSideProps`). |
127
+ | `sessionSegment` | `string` | Segment from SSR (`device:source`). Must match the value used in `loadAdaptiveAssignments`. |
128
+ | `initialLayoutOrder` | `string[] \| null` | Preloaded section order from `loadAdaptiveDecision` (for Pages-Router-style SSR with sections). |
89
129
 
90
130
  ### `<Adaptive>`
91
131
 
92
132
  | Prop | Type | Description |
93
133
  |------|------|-------------|
94
- | `id` | `string` | Unique component identifier |
95
- | `variants` | `Record<string, ReactNode>` | Map of variant ID → content |
96
- | `goal` | `string \| GoalConfig` | Conversion goal. Pass a string label for click tracking, or a `GoalConfig` object for scroll depth, form submit, or composite goals |
97
- | `clientOnly` | `boolean` | Skip SSR rendering use for decorative slots where a blank placeholder is preferable to a hydration mismatch |
134
+ | `id` | `string` | Unique component identifier within your project. |
135
+ | `variants` | `Record<string, ReactNode>` | Map of variant ID → content. Any two or more keys; the bandit explores them all. |
136
+ | `goal` | `string \| GoalConfig` | Conversion goal. A string is a click-goal label; an object is an explicit `GoalConfig`. |
137
+ | `clientOnly` | `boolean` | Render nothing on the server; resolve on the client only. Use for cookie-dependent slots. |
98
138
 
99
139
  #### Goal types
100
140
 
101
141
  ```ts
102
- // Click on anything inside the variant
103
- goal="click"
142
+ // Any click inside the variant (button, a, role=button). The string is the analytics label.
143
+ goal="signup_click"
104
144
 
105
- // Click on a specific element
145
+ // Click only on elements matching a CSS selector inside the variant.
106
146
  goal={{ type: 'click', selector: 'button.cta' }}
107
147
 
108
- // User scrolls past 80% of the page
109
- goal={{ type: 'scroll_depth', threshold: 80 }}
148
+ // 80% of the component visible in the viewport (IntersectionObserver, 0–1 scale).
149
+ goal={{ type: 'scroll_depth', threshold: 0.8 }}
110
150
 
111
- // A form inside the variant is submitted
151
+ // A <form> inside the variant fires submit.
112
152
  goal={{ type: 'form_submit' }}
113
153
 
114
- // Multiple goals must all fire
115
- goal={{ type: 'composite', all: [{ type: 'scroll_depth', threshold: 50 }, { type: 'click' }] }}
154
+ // Composite — all sub-goals must fire (in any order) before the reward is recorded.
155
+ goal={{
156
+ type: 'composite',
157
+ all: [
158
+ { type: 'scroll_depth', threshold: 0.8 },
159
+ { type: 'click' },
160
+ ],
161
+ }}
162
+ ```
163
+
164
+ Each goal fires at most once per variant mount.
165
+
166
+ ### `<AdaptiveText>`
167
+
168
+ Lightweight text-only variant (no wrapper element, no automatic goal wiring). Useful when you publish text variants from the dashboard WYSIWYG.
169
+
170
+ ```tsx
171
+ import { AdaptiveText } from '@sentientui/react';
172
+
173
+ <h1>
174
+ <AdaptiveText id="hero_headline" default="Ship faster with SentientUI" />
175
+ </h1>
116
176
  ```
117
177
 
118
- ### `useAssignment(componentId, variantIds)`
178
+ ### `useAssignment(componentId, variantIds?)`
119
179
 
120
- Lower-level hook for manual variant resolution when you need full control over rendering.
180
+ Lower-level hook when you need the variant ID inside your own render logic (e.g. full-page layout tests where `<Adaptive>`'s wrapper `<div>` would break a flex/grid layout).
121
181
 
122
182
  ```tsx
123
- import { useAssignment } from '@sentientui/react';
183
+ import { useAssignment, useSentient } from '@sentientui/react';
124
184
 
125
- function MyComponent() {
126
- const { variantId, isLoading } = useAssignment('hero', ['control', 'variant_b']);
185
+ function Hero() {
186
+ const client = useSentient();
187
+ const { variantId, isLoading } = useAssignment('hero_cta', ['control', 'variant_a']);
127
188
 
128
189
  if (isLoading) return <Skeleton />;
129
- return variantId === 'variant_b' ? <NewHero /> : <DefaultHero />;
190
+ return variantId === 'variant_a' ? <AccentHero /> : <DefaultHero />;
130
191
  }
131
192
  ```
132
193
 
133
- ### `onAssignment` — forwarding to analytics
194
+ When using `useAssignment` you are responsible for firing the goal `<Adaptive>`'s automatic impression and goal tracking does not apply. Use `client?.track(…)` or `client?.goal(name)` from `useSentient()`.
195
+
196
+ ### `useLayoutOrder()`
197
+
198
+ Returns the resolved persona-specific section order, or `null` when not configured / below confidence threshold. Always fall back to your default order.
199
+
200
+ ```tsx
201
+ import { useLayoutOrder } from '@sentientui/react';
202
+
203
+ function Page() {
204
+ const order = useLayoutOrder();
205
+ const defaultOrder = ['hero', 'pricing', 'features', 'social_proof'];
206
+ const sections: Record<string, React.ReactNode> = {
207
+ hero: <Hero />, pricing: <Pricing />, features: <Features />, social_proof: <SocialProof />,
208
+ };
209
+ return <main>{(order ?? defaultOrder).map((id) => <Fragment key={id}>{sections[id]}</Fragment>)}</main>;
210
+ }
211
+ ```
212
+
213
+ ### SSR for Pages Router / custom SSR
214
+
215
+ ```ts
216
+ import { loadAdaptiveAssignments } from '@sentientui/react/server';
217
+
218
+ export async function getServerSideProps({ req }) {
219
+ const assignments = await loadAdaptiveAssignments(
220
+ [{ id: 'hero_cta', variantIds: ['control', 'variant_a'] }],
221
+ {
222
+ cookies: req.cookies,
223
+ apiKey: process.env.SENTIENT_API_KEY!,
224
+ baseUrl: 'https://sentient-api.fly.dev/v1',
225
+ origin: process.env.NEXT_PUBLIC_APP_URL,
226
+ },
227
+ );
228
+ return { props: { initialAssignments: assignments } };
229
+ }
230
+ ```
231
+
232
+ Pass the result as `initialAssignments` on `<AdaptiveProvider>` in `_app.tsx`. For pages with a `sections` layout, use `loadAdaptiveDecision` (same options shape, plus `sections: string[]`) — its return value carries both `assignments` and `layoutOrder`.
233
+
234
+ ### Forwarding to your own analytics
134
235
 
135
236
  ```tsx
136
237
  <AdaptiveProvider
137
- apiKey="pk_…"
238
+ apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
138
239
  context="saas"
139
240
  onAssignment={(componentId, variantId) => {
140
- posthog.capture('$experiment_started', {
141
- experiment_name: componentId,
142
- variant: variantId,
143
- });
241
+ posthog.capture('$feature_flag_called', { $feature_flag: componentId, $feature_flag_response: variantId });
242
+ mixpanel.register({ [`variant_${componentId}`]: variantId });
144
243
  }}
145
244
  >
245
+ {children}
246
+ </AdaptiveProvider>
247
+ ```
248
+
249
+ Fires at most once per component ID per page load. Full Segment / GA4 / Mixpanel recipes: see [sentient-ui.com/docs/integrations](https://sentient-ui.com/docs/integrations).
250
+
251
+ ### Local overrides (development)
252
+
253
+ Force a variant without touching the bandit:
254
+
146
255
  ```
256
+ # URL parameter (stackable)
257
+ https://yourapp.com?sentient_variant=hero_cta:variant_a&sentient_variant=pricing:annual_first
258
+
259
+ # Or, before SDK init:
260
+ window.__sentient_overrides = { hero_cta: 'variant_a' };
261
+ ```
262
+
263
+ Overrides bypass the bandit entirely — no events recorded, weights unchanged.
264
+
265
+ ## Consent management (GDPR)
266
+
267
+ SentientUI does not track visitors until consent is granted. To serve the best-performing variant while the cookie banner is pending (rather than freezing the UI), use `preConsentBehavior: 'statistical_winner'`:
268
+
269
+ ```tsx
270
+ <AdaptiveProvider
271
+ apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
272
+ context="saas"
273
+ consent={hasConsent}
274
+ preConsentBehavior="statistical_winner"
275
+ >
276
+ {children}
277
+ </AdaptiveProvider>
278
+ ```
279
+
280
+ Set `hasConsent` to `false` until the user accepts your cookie banner, then flip it to `true`. The provider re-initialises automatically and begins full tracking.
281
+
282
+ ### Without React
283
+
284
+ ```ts
285
+ import { init, grantConsent } from '@sentientui/core';
286
+
287
+ const client = init({
288
+ apiKey: 'pk_...',
289
+ context: 'saas',
290
+ consent: false,
291
+ preConsentBehavior: 'statistical_winner',
292
+ });
293
+
294
+ // After the user accepts the cookie banner:
295
+ grantConsent(); // upgrades client in place — no need to reassign
296
+ ```
297
+
298
+ ### OneTrust integration
299
+
300
+ ```ts
301
+ window.addEventListener('OneTrustGroupsUpdated', () => {
302
+ // C0002 = Analytics/Performance category in OneTrust
303
+ if (window.OnetrustActiveGroups?.includes('C0002')) {
304
+ grantConsent();
305
+ }
306
+ });
307
+ ```
308
+
309
+ ### Cookiebot integration
310
+
311
+ ```ts
312
+ window.addEventListener('CookiebotOnAccept', () => {
313
+ if (window.Cookiebot?.consent?.statistics) {
314
+ grantConsent();
315
+ }
316
+ });
317
+ ```
318
+
319
+ When `consent: false` without `preConsentBehavior`, the SDK is a complete no-op — no API calls, no cookies, nothing. The `preConsentBehavior: 'statistical_winner'` mode calls only `GET /v1/winner`, a read-only endpoint that returns the best variant without storing any visitor data.
320
+
321
+ ## Docs
322
+
323
+ Full SDK reference: [sentient-ui.com/docs](https://sentient-ui.com/docs).
324
+ Integrations: [sentient-ui.com/docs/integrations](https://sentient-ui.com/docs/integrations).
147
325
 
148
326
  ## License
149
327
 
package/dist/index.d.cts CHANGED
@@ -34,6 +34,13 @@ type AdaptiveProviderProps = {
34
34
  * initialise and begin tracking.
35
35
  */
36
36
  consent?: boolean;
37
+ /**
38
+ * Behavior before consent is granted. Pass `'statistical_winner'` to serve the
39
+ * best-performing variant via `GET /v1/winner` with zero tracking while the
40
+ * consent banner is showing. Requires `consent: false`.
41
+ * @see SentientConfig.preConsentBehavior
42
+ */
43
+ preConsentBehavior?: 'statistical_winner' | 'control';
37
44
  /**
38
45
  * Called once per component the first time a variant is resolved for that
39
46
  * component in this session. Use to forward assignments to your own analytics
@@ -92,6 +99,8 @@ type AdaptiveProps = {
92
99
  * a hydration mismatch. Tradeoff: minor CLS on first paint.
93
100
  */
94
101
  clientOnly?: boolean;
102
+ /** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */
103
+ agentData?: unknown;
95
104
  };
96
105
  declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
97
106
  /** Re-renders only when the chosen variant changes. */
@@ -125,7 +134,7 @@ type AssignmentState = {
125
134
  * on the next render. Subsequent paints read synchronously from cache —
126
135
  * no flicker, no loading state after first paint.
127
136
  */
128
- declare function useAssignment(componentId: string, variantIds: string[]): AssignmentState;
137
+ declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown): AssignmentState;
129
138
 
130
139
  /** Per-component weights store with isolated subscriptions. */
131
140
  type VariantWeight = {
package/dist/index.d.ts CHANGED
@@ -34,6 +34,13 @@ type AdaptiveProviderProps = {
34
34
  * initialise and begin tracking.
35
35
  */
36
36
  consent?: boolean;
37
+ /**
38
+ * Behavior before consent is granted. Pass `'statistical_winner'` to serve the
39
+ * best-performing variant via `GET /v1/winner` with zero tracking while the
40
+ * consent banner is showing. Requires `consent: false`.
41
+ * @see SentientConfig.preConsentBehavior
42
+ */
43
+ preConsentBehavior?: 'statistical_winner' | 'control';
37
44
  /**
38
45
  * Called once per component the first time a variant is resolved for that
39
46
  * component in this session. Use to forward assignments to your own analytics
@@ -92,6 +99,8 @@ type AdaptiveProps = {
92
99
  * a hydration mismatch. Tradeoff: minor CLS on first paint.
93
100
  */
94
101
  clientOnly?: boolean;
102
+ /** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */
103
+ agentData?: unknown;
95
104
  };
96
105
  declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
97
106
  /** Re-renders only when the chosen variant changes. */
@@ -125,7 +134,7 @@ type AssignmentState = {
125
134
  * on the next render. Subsequent paints read synchronously from cache —
126
135
  * no flicker, no loading state after first paint.
127
136
  */
128
- declare function useAssignment(componentId: string, variantIds: string[]): AssignmentState;
137
+ declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown): AssignmentState;
129
138
 
130
139
  /** Per-component weights store with isolated subscriptions. */
131
140
  type VariantWeight = {
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ function AdaptiveProvider(props) {
52
52
  var _a, _b, _c, _d;
53
53
  const [client, setClient] = (0, import_react.useState)(null);
54
54
  (0, import_react.useEffect)(() => {
55
- if (props.consent === false) {
55
+ if (props.consent === false && !props.preConsentBehavior) {
56
56
  setClient((prev) => {
57
57
  prev == null ? void 0 : prev.destroy();
58
58
  return null;
@@ -65,7 +65,8 @@ function AdaptiveProvider(props) {
65
65
  debug: props.debug,
66
66
  initialAssignments: props.initialAssignments,
67
67
  sessionSegment: props.sessionSegment,
68
- consent: props.consent
68
+ consent: props.consent,
69
+ preConsentBehavior: props.preConsentBehavior
69
70
  });
70
71
  setClient(c);
71
72
  return () => {
@@ -176,7 +177,7 @@ function pickFromWeights(weights, variantIds) {
176
177
  }
177
178
  return (_a = best == null ? void 0 : best.variantId) != null ? _a : null;
178
179
  }
179
- function useAssignment(componentId, variantIds) {
180
+ function useAssignment(componentId, variantIds, agentData) {
180
181
  const initialAssignments = useInitialAssignments();
181
182
  const ssrFallback = useSsrFallback();
182
183
  const client = useSentient();
@@ -242,7 +243,7 @@ function useAssignment(componentId, variantIds) {
242
243
  const cached = client.getAssignment(componentId, segment);
243
244
  if (cached && variantIds.includes(cached.variantId)) return;
244
245
  let cancelled = false;
245
- void client.assign(componentId, variantIds).then((result) => {
246
+ void client.assign(componentId, variantIds, agentData).then((result) => {
246
247
  var _a;
247
248
  if (cancelled) return;
248
249
  if (!result) return;
@@ -300,7 +301,7 @@ function AdaptiveImpl(props) {
300
301
  const client = useSentient();
301
302
  const apiKey = useAdaptiveApiKey();
302
303
  const variantIds = (0, import_react3.useMemo)(() => Object.keys(props.variants), [props.variants]);
303
- const { variantId, content } = useAssignment(props.id, variantIds);
304
+ const { variantId, content } = useAssignment(props.id, variantIds, props.agentData);
304
305
  const containerRef = (0, import_react3.useRef)(null);
305
306
  const [mounted, setMounted] = (0, import_react3.useState)(false);
306
307
  (0, import_react3.useEffect)(() => {
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\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"]}
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 * Behavior before consent is granted. Pass `'statistical_winner'` to serve the\n * best-performing variant via `GET /v1/winner` with zero tracking while the\n * consent banner is showing. Requires `consent: false`.\n * @see SentientConfig.preConsentBehavior\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\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 with no preConsentBehavior, tear down any existing client.\n if (props.consent === false && !props.preConsentBehavior) {\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 preConsentBehavior: props.preConsentBehavior,\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 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 /** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */\n agentData?: unknown;\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, props.agentData);\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[], agentData?: unknown): 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, agentData).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":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,mBAA+E;AAC/E,kBAA+D;AAmH3D;AAjGJ,IAAM,sBAAkB,4BAAoC;AAAA,EAC1D,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,oBAAoB,CAAC;AAAA,EACrB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AACtB,CAAC;AAwDM,SAAS,iBAAiB,OAA2C;AAvF5E;AAwFE,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAgC,IAAI;AAEhE,8BAAU,MAAM;AAEd,QAAI,MAAM,YAAY,SAAS,CAAC,MAAM,oBAAoB;AACxD,gBAAU,CAAC,SAAgC;AACzC,qCAAM;AACN,eAAO;AAAA,MACT,CAAC;AACD;AAAA,IACF;AAEA,UAAM,QAAI,kBAAK;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,MACf,oBAAoB,MAAM;AAAA,IAC5B,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;AAMO,SAAS,cAAqC;AACnD,aAAO,yBAAW,eAAe,EAAE;AACrC;AAGO,SAAS,oBAA4B;AAC1C,aAAO,yBAAW,eAAe,EAAE;AACrC;AAuBO,SAAS,wBAAgD;AAC9D,aAAO,yBAAW,eAAe,EAAE;AACrC;AAGO,SAAS,oBAA4B;AAC1C,aAAO,yBAAW,eAAe,EAAE;AACrC;AAGO,SAAS,iBAA8B;AAC5C,aAAO,yBAAW,eAAe,EAAE;AACrC;AAGO,SAAS,kBAAkF;AAChG,aAAO,yBAAW,eAAe,EAAE;AACrC;AAMO,SAAS,iBAAkC;AAChD,aAAO,yBAAW,eAAe,EAAE;AACrC;;;AC/LA,IAAAA,gBAA2E;;;ACJ3E,IAAAC,gBAA4C;;;ACgB5C,IAAM,QAAQ,oBAAI,IAA8B;AAChD,IAAM,YAAY,oBAAI,IAA2B;AAM1C,SAAS,UAAU,aAAqB,IAA0B;AACvE,MAAI,MAAM,UAAU,IAAI,WAAW;AACnC,MAAI,CAAC,KAAK;AACR,UAAM,oBAAI,IAAI;AACd,cAAU,IAAI,aAAa,GAAG;AAAA,EAChC;AACA,MAAI,IAAI,EAAE;AACV,SAAO,MAAM;AACX,QAAK,OAAO,EAAE;AACd,QAAI,IAAK,SAAS,EAAG,WAAU,OAAO,WAAW;AAAA,EACnD;AACF;AAMO,SAAS,OAAO,aAAqB,SAAiC;AAC3E,QAAM,IAAI,aAAa,OAAO;AAC9B,QAAM,MAAM,UAAU,IAAI,WAAW;AACrC,MAAI,CAAC,IAAK;AACV,aAAW,MAAM,KAAK;AACpB,QAAI;AACF,SAAG,OAAO;AAAA,IACZ,SAAQ;AAAA,IAER;AAAA,EACF;AACF;AAKO,SAAS,WAAW,aAA8C;AAxDzE;AAyDE,UAAO,WAAM,IAAI,WAAW,MAArB,YAA0B;AACnC;;;ADjDA,SAAS,eAAe,aAAoC;AAT5D;AAUE,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,UAAS,YAAO,yBAAP,mBAA8B;AAC7C,MAAI,OAAQ,QAAO;AACnB,MAAI;AACF,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,eAAW,OAAO,OAAO,OAAO,kBAAkB,GAAG;AAEnD,YAAM,MAAM,IAAI,QAAQ,GAAG;AAC3B,UAAI,QAAQ,GAAI;AAChB,UAAI,IAAI,MAAM,GAAG,GAAG,MAAM,YAAa,QAAO,IAAI,MAAM,MAAM,CAAC;AAAA,IACjE;AAAA,EACF,SAAQ;AAAA,EAAwB;AAChC,SAAO;AACT;AASA,SAAS,gBAAgB,SAA2B,YAAqC;AAhCzF;AAiCE,MAAI,OAAwD;AAC5D,aAAW,KAAK,QAAQ,UAAU;AAChC,QAAI,CAAC,WAAW,SAAS,EAAE,SAAS,EAAG;AACvC,QAAI,CAAC,QAAQ,EAAE,YAAY,KAAK,WAAW;AACzC,aAAO,EAAE,WAAW,EAAE,WAAW,WAAW,EAAE,UAAU;AAAA,IAC1D;AAAA,EACF;AACA,UAAO,kCAAM,cAAN,YAAmB;AAC5B;AAWO,SAAS,cAAc,aAAqB,YAAsB,WAAsC;AAC7G,QAAM,qBAAqB,sBAAsB;AACjD,QAAM,cAAc,eAAe;AACnC,QAAM,SAAS,YAAY;AAC3B,QAAM,UAAU,kBAAkB;AAClC,QAAM,eAAe,gBAAgB;AACrC,QAAM,4BAAwB,sBAAsB,IAAI;AAIxD,QAAM,cAAc,eAAe,WAAW;AAC9C,QAAM,kBAAkB,eAAe,WAAW,SAAS,WAAW,IAAI,cAAc;AACxF,MAAI,iBAAiB;AACnB,YAAQ,KAAK,+BAA+B,WAAW,OAAO,eAAe,EAAE;AAAA,EACjF;AAEA,QAAM,WAAW,MAAM;AApEzB;AAqEI,QAAI,iBAAiB;AACnB,aAAO,EAAE,WAAW,iBAAiB,SAAS,MAAM,WAAW,MAAM;AAAA,IACvE;AAGA,QAAI,CAAC,QAAQ;AACX,YAAM,YAAY,mBAAmB,WAAW;AAChD,UAAI,aAAa,WAAW,SAAS,SAAS,GAAG;AAC/C,eAAO,EAAE,WAAW,WAAW,SAAS,MAAM,WAAW,MAAM;AAAA,MACjE;AACA,UAAI,gBAAgB,WAAW,WAAW,SAAS,GAAG;AACpD,eAAO,EAAE,WAAW,WAAW,CAAC,GAAG,SAAS,MAAM,WAAW,MAAM;AAAA,MACrE;AACA,aAAO,EAAE,WAAW,MAAM,SAAS,MAAM,WAAW,KAAK;AAAA,IAC3D;AACA,UAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AAExD,QAAI,WAAW,WAAW,SAAS,OAAO,SAAS,KAAK,OAAO,UAAU;AACvE,aAAO,EAAE,WAAW,OAAO,WAAW,UAAS,YAAO,YAAP,YAAkB,MAAM,WAAW,MAAM;AAAA,IAC1F;AACA,UAAM,UAAU,WAAW,WAAW;AACtC,QAAI,SAAS;AACX,YAAM,SAAS,gBAAgB,SAAS,UAAU;AAClD,UAAI,OAAQ,QAAO,EAAE,WAAW,QAAQ,SAAS,MAAM,WAAW,MAAM;AAAA,IAC1E;AACA,WAAO,EAAE,YAAW,gBAAW,CAAC,MAAZ,YAAiB,MAAM,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7E,GAAG;AAEH,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAA0B,OAAO;AAG3D,QAAM,mBAAmB,CAAC,cAA4B;AACpD,QAAI,CAAC,aAAc;AACnB,QAAI,sBAAsB,YAAY,UAAW;AACjD,0BAAsB,UAAU;AAChC,iBAAa,aAAa,SAAS;AAAA,EACrC;AAKA,+BAAU,MAAM;AA9GlB;AA+GI,QAAI,gBAAiB;AACrB,QAAI,CAAC,OAAQ;AACb,UAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AACxD,QAAI,WAAW,WAAW,SAAS,OAAO,SAAS,KAAK,OAAO,UAAU;AACvE,eAAS,EAAE,WAAW,OAAO,WAAW,UAAS,YAAO,YAAP,YAAkB,MAAM,WAAW,MAAM,CAAC;AAC3F,uBAAiB,OAAO,SAAS;AACjC;AAAA,IACF;AACA,aAAS,CAAC,SAAM;AAvHpB,UAAAC;AAuHuB,kBAAK,YAAY,OAAO,EAAE,YAAWA,MAAA,WAAW,CAAC,MAAZ,OAAAA,MAAiB,MAAM,SAAS,MAAM,WAAW,MAAM;AAAA,KAAC;AAAA,EAElH,GAAG,CAAC,iBAAiB,QAAQ,aAAa,OAAO,CAAC;AAGlD,+BAAU,MAAM;AACd,QAAI,gBAAiB;AACrB,QAAI,CAAC,OAAQ;AACb,UAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AACxD,QAAI,UAAU,WAAW,SAAS,OAAO,SAAS,EAAG;AAErD,QAAI,YAAY;AAChB,SAAK,OAAO,OAAO,aAAa,YAAY,SAAS,EAAE,KAAK,CAAC,WAAgC;AAnIjG;AAoIM,UAAI,UAAW;AACf,UAAI,CAAC,OAAQ;AAEb,UAAI,CAAC,WAAW,SAAS,OAAO,SAAS,KAAK,CAAC,OAAO,QAAS;AAC/D,eAAS,EAAE,WAAW,OAAO,WAAW,UAAS,YAAO,YAAP,YAAkB,MAAM,WAAW,MAAM,CAAC;AAC3F,uBAAiB,OAAO,SAAS;AAAA,IACnC,CAAC;AACD,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAM;AAAA,EAGnC,GAAG,CAAC,iBAAiB,QAAQ,aAAa,OAAO,CAAC;AAGlD,+BAAU,MAAM;AACd,QAAI,gBAAiB;AACrB,QAAI,CAAC,OAAQ;AACb,WAAO,UAAU,aAAa,CAAC,YAAY;AApJ/C;AAqJM,YAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AACxD,UAAI,WAAW,WAAW,SAAS,OAAO,SAAS,KAAK,OAAO,UAAU;AACvE,iBAAS,EAAE,WAAW,OAAO,WAAW,UAAS,YAAO,YAAP,YAAkB,MAAM,WAAW,MAAM,CAAC;AAC3F;AAAA,MACF;AACA,YAAM,SAAS,gBAAgB,SAAS,UAAU;AAClD,UAAI,OAAQ,UAAS,EAAE,WAAW,QAAQ,SAAS,MAAM,WAAW,MAAM,CAAC;AAAA,IAC7E,CAAC;AAAA,EAEH,GAAG,CAAC,iBAAiB,QAAQ,aAAa,OAAO,CAAC;AAElD,MAAI,iBAAiB;AACnB,WAAO,EAAE,WAAW,iBAAiB,SAAS,MAAM,WAAW,MAAM;AAAA,EACvE;AAEA,SAAO;AACT;;;AD2DI,IAAAC,sBAAA;AApMJ,SAAS,UAAU,MAAuC;AACxD,MAAI,OAAO,SAAS,SAAU,QAAO,EAAE,MAAM,QAAQ;AACrD,SAAO;AACT;AAEA,SAAS,kBAAkB,IAAiC;AAC1D,MAAI,EAAE,cAAc,SAAU,QAAO;AACrC,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,MAAI,QAAQ,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAO,GAAG,aAAa,MAAM;AACnC,SAAO,SAAS;AAClB;AAEA,SAAS,cAAc,OAAgB,WAA6B;AAClE,MAAI,SAAyB;AAC7B,SAAO,UAAU,WAAW,WAAW;AACrC,QAAI,kBAAkB,MAAM,EAAG,QAAO;AACtC,aAAS,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,OAA0C;AAlDhE;AAmDE,QAAM,SAAS,YAAY;AAC3B,QAAM,SAAS,kBAAkB;AACjC,QAAM,iBAAa,uBAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ,GAAG,CAAC,MAAM,QAAQ,CAAC;AAC9E,QAAM,EAAE,WAAW,QAAQ,IAAI,cAAc,MAAM,IAAI,YAAY,MAAM,SAAS;AAClF,QAAM,mBAAe,sBAAuB,IAAI;AAChD,QAAM,CAAC,SAAS,UAAU,QAAI,wBAAS,KAAK;AAE5C,+BAAU,MAAM;AAAE,eAAW,IAAI;AAAA,EAAG,GAAG,CAAC,CAAC;AACzC,QAAM,mBAAe,sBAAO,KAAK;AACjC,QAAM,uBAAmB,sBAAsB,IAAI;AACnD,QAAM,WAAO,uBAAQ,MAAM,UAAU,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC;AAC9D,QAAM,YAAY,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,KAAK;AAOrE,+BAAU,MAAM;AArElB,QAAAC,KAAAC;AAsEI,QAAI,CAAC,UAAU,CAAC,aAAa,CAAC,OAAQ;AACtC,QAAI,iBAAiB,YAAY,UAAW;AAC5C,UAAM,WAAUA,OAAAD,MAAA,aAAa,YAAb,gBAAAA,IAAsB,cAAtB,OAAAC,MAAmC;AAEnD,QAAI,CAAC,WAAW,CAAC,QAAS;AAC1B,qBAAiB,UAAU;AAC3B,WAAO,MAAM;AAAA,MACX,WAAW;AAAA,MACX,aAAa,MAAM;AAAA,MACnB;AAAA,MACA,WAAW;AAAA,MACX,SAAS,UAAU,EAAE,aAAa,QAAQ,MAAM,GAAG,GAAM,EAAE,IAAI,CAAC;AAAA,IAClE,CAAC;AAAA,EACH,GAAG,CAAC,QAAQ,WAAW,QAAQ,MAAM,IAAI,OAAO,CAAC;AAGjD,+BAAU,MAAM;AACd,iBAAa,UAAU;AAAA,EACzB,GAAG,CAAC,SAAS,CAAC;AAGd,+BAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,UAAW;AAC3B,UAAM,OAAO,aAAa;AAC1B,QAAI,CAAC,KAAM;AAEX,QAAI,UAAgD;AACpD,QAAI,aAAa;AAEjB,UAAM,UAAU,MAAY;AAC1B,mBAAa,KAAK,IAAI;AACtB,gBAAU,WAAW,MAAM;AACzB,eAAO,MAAM;AAAA,UACX,WAAW;AAAA,UACX,aAAa,MAAM;AAAA,UACnB;AAAA,UACA,WAAW;AAAA,UACX,SAAS,EAAE,eAAe,KAAK,IAAI,IAAI,WAAW;AAAA,QACpD,CAAC;AACD,kBAAU;AAAA,MACZ,GAAG,GAAG;AAAA,IACR;AAEA,UAAM,UAAU,MAAY;AAC1B,UAAI,YAAY,MAAM;AACpB,qBAAa,OAAO;AACpB,kBAAU;AAAA,MACZ;AAAA,IACF;AAEA,SAAK,iBAAiB,cAAc,OAAO;AAC3C,SAAK,iBAAiB,cAAc,OAAO;AAC3C,WAAO,MAAM;AACX,WAAK,oBAAoB,cAAc,OAAO;AAC9C,WAAK,oBAAoB,cAAc,OAAO;AAC9C,UAAI,YAAY,KAAM,cAAa,OAAO;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,QAAQ,WAAW,QAAQ,MAAM,EAAE,CAAC;AAGxC,+BAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,UAAW;AAC3B,UAAM,OAAO,aAAa;AAC1B,QAAI,CAAC,KAAM;AAEX,UAAM,WAAW,MAAY;AAC3B,UAAI,aAAa,QAAS;AAC1B,mBAAa,UAAU;AACvB,aAAO,MAAM;AAAA,QACX,WAAW;AAAA,QACX,aAAa,MAAM;AAAA,QACnB;AAAA,QACA,WAAW;AAAA,QACX,UAAU;AAAA,QACV,SAAS,EAAE,QAAQ,EAAI;AAAA,MACzB,CAAC;AAAA,IACH;AAEA,UAAM,WAAyB,KAAK,SAAS,cAAc,KAAK,MAAM,CAAC,IAAI;AAC3E,UAAM,YAAY,IAAI,IAAY,SAAS,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;AAC3D,UAAM,iBAAiB,CAAC,QAAsB;AAC5C,gBAAU,OAAO,GAAG;AACpB,UAAI,UAAU,SAAS,EAAG,UAAS;AAAA,IACrC;AAEA,UAAM,WAA8B,CAAC;AAErC,aAAS,QAAQ,CAAC,KAAK,QAAQ;AAC7B,UAAI,IAAI,SAAS,SAAS;AACxB,cAAM,UAAU,CAAC,MAAwB;AACvC,gBAAM,SAAS,EAAE;AACjB,cAAI,EAAE,kBAAkB,SAAU;AAClC,cAAI,CAAC,cAAc,QAAQ,IAAI,EAAG;AAClC,cAAI,KAAK,SAAS,YAAa,gBAAe,GAAG;AAAA,cAC5C,UAAS;AAAA,QAChB;AACA,aAAK,iBAAiB,SAAS,OAAO;AACtC,iBAAS,KAAK,MAAM,KAAK,oBAAoB,SAAS,OAAO,CAAC;AAC9D;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,eAAe;AAC9B,cAAM,WAAW,CAAC,MAAmB;AACnC,cAAI,EAAE,EAAE,kBAAkB,iBAAkB;AAC5C,cAAI,CAAC,KAAK,SAAS,EAAE,MAAM,EAAG;AAC9B,cAAI,KAAK,SAAS,YAAa,gBAAe,GAAG;AAAA,cAC5C,UAAS;AAAA,QAChB;AACA,aAAK,iBAAiB,UAAU,QAAQ;AACxC,iBAAS,KAAK,MAAM,KAAK,oBAAoB,UAAU,QAAQ,CAAC;AAChE;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,gBAAgB;AAC/B,cAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,SAAS,CAAC;AACxD,cAAM,KAAK,IAAI;AAAA,UACb,CAAC,YAAY;AACX,uBAAW,SAAS,SAAS;AAC3B,kBAAI,MAAM,qBAAqB,WAAW;AACxC,oBAAI,KAAK,SAAS,YAAa,gBAAe,GAAG;AAAA,oBAC5C,UAAS;AACd,mBAAG,WAAW;AACd;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,UACA,EAAE,WAAW,CAAC,SAAS,EAAE;AAAA,QAC3B;AACA,WAAG,QAAQ,IAAI;AACf,iBAAS,KAAK,MAAM,GAAG,WAAW,CAAC;AACnC;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,iBAAW,KAAK,SAAU,GAAE;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,QAAQ,WAAW,QAAQ,MAAM,IAAI,MAAM,SAAS,CAAC;AAGzD,MAAI,MAAM,eAAe,CAAC,WAAW,CAAC,QAAS,QAAO;AACtD,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,cAAa,WAAM,SAAS,SAAS,MAAxB,YAA6B;AAChD,QAAM,iBAAiB,eAAe,OAAO,UAAU;AAEvD,QAAI,wCAAS,QAAT,mBAAc,cAAa,gBAAgB,eAAe,QAAQ,mBAAmB,MAAM;AAC7F,YAAQ;AAAA,MACN,4BAA4B,MAAM,EAAE,4BAA4B,SAAS,sHACF,MAAM,EAAE;AAAA,IACjF;AAAA,EACF;AAEA,SACE,6CAAC,SAAI,KAAK,cAAc,oBAAkB,MAAM,IAAI,yBAAuB,WACxE,4CAAc,gBACjB;AAEJ;AAGO,IAAM,eAAW,oBAAK,cAAc,CAAC,MAAM,SAAS;AACzD,MAAI,KAAK,OAAO,KAAK,GAAI,QAAO;AAChC,MAAI,KAAK,SAAS,KAAK,KAAM,QAAO;AACpC,MAAI,KAAK,aAAa,KAAK,SAAU,QAAO;AAC5C,QAAM,WAAW,OAAO,KAAK,KAAK,QAAQ;AAC1C,QAAM,WAAW,OAAO,KAAK,KAAK,QAAQ;AAC1C,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,SAAO,SAAS,MAAM,CAAC,MAAM,KAAK,KAAK,QAAQ;AACjD,CAAC;;;AG7OD,IAAAC,gBAA4C;AAmDnC,IAAAC,sBAAA;AAxCF,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EACjB;AACF,GAAsB;AACpB,QAAM,SAAS,YAAY;AAC3B,QAAM,SAAS,kBAAkB;AACjC,QAAM,eAAe,gBAAgB;AACrC,QAAM,CAAC,MAAM,OAAO,QAAI,wBAAwB,IAAI;AACpD,QAAM,CAAC,WAAW,YAAY,QAAI,wBAAwB,IAAI;AAC9D,QAAM,iBAAa,sBAAsB,IAAI;AAE7C,+BAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,QAAI,YAAY;AAChB,SAAK,OAAO,OAAO,EAAE,EAAE,KAAK,CAAC,WAAgC;AAC3D,UAAI,aAAa,CAAC,OAAQ;AAC1B,mBAAa,OAAO,SAAS;AAC7B,UAAI,OAAO,QAAS,SAAQ,OAAO,OAAO;AAAA,IAC5C,CAAC;AACD,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,EAAE,CAAC;AAEf,+BAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,aAAa,CAAC,OAAQ;AACtC,QAAI,WAAW,YAAY,UAAW;AACtC,eAAW,UAAU;AACrB,WAAO,MAAM;AAAA,MACX,WAAW;AAAA,MACX,aAAa;AAAA,MACb;AAAA,MACA,WAAW;AAAA,MACX,SAAS,CAAC;AAAA,IACZ,CAAC;AACD,iDAAe,IAAI;AAAA,EACrB,GAAG,CAAC,QAAQ,WAAW,QAAQ,IAAI,YAAY,CAAC;AAEhD,SAAO,6CAAC,OAAI,WAAuB,gCAAQ,aAAY;AACzD;;;ACrDA,IAAAC,eAAsD;","names":["import_react","import_react","_a","import_jsx_runtime","_a","_b","import_react","import_jsx_runtime","import_core"]}
package/dist/index.mjs CHANGED
@@ -17,7 +17,7 @@ function AdaptiveProvider(props) {
17
17
  var _a, _b, _c, _d;
18
18
  const [client, setClient] = useState(null);
19
19
  useEffect(() => {
20
- if (props.consent === false) {
20
+ if (props.consent === false && !props.preConsentBehavior) {
21
21
  setClient((prev) => {
22
22
  prev == null ? void 0 : prev.destroy();
23
23
  return null;
@@ -30,7 +30,8 @@ function AdaptiveProvider(props) {
30
30
  debug: props.debug,
31
31
  initialAssignments: props.initialAssignments,
32
32
  sessionSegment: props.sessionSegment,
33
- consent: props.consent
33
+ consent: props.consent,
34
+ preConsentBehavior: props.preConsentBehavior
34
35
  });
35
36
  setClient(c);
36
37
  return () => {
@@ -141,7 +142,7 @@ function pickFromWeights(weights, variantIds) {
141
142
  }
142
143
  return (_a = best == null ? void 0 : best.variantId) != null ? _a : null;
143
144
  }
144
- function useAssignment(componentId, variantIds) {
145
+ function useAssignment(componentId, variantIds, agentData) {
145
146
  const initialAssignments = useInitialAssignments();
146
147
  const ssrFallback = useSsrFallback();
147
148
  const client = useSentient();
@@ -207,7 +208,7 @@ function useAssignment(componentId, variantIds) {
207
208
  const cached = client.getAssignment(componentId, segment);
208
209
  if (cached && variantIds.includes(cached.variantId)) return;
209
210
  let cancelled = false;
210
- void client.assign(componentId, variantIds).then((result) => {
211
+ void client.assign(componentId, variantIds, agentData).then((result) => {
211
212
  var _a;
212
213
  if (cancelled) return;
213
214
  if (!result) return;
@@ -265,7 +266,7 @@ function AdaptiveImpl(props) {
265
266
  const client = useSentient();
266
267
  const apiKey = useAdaptiveApiKey();
267
268
  const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);
268
- const { variantId, content } = useAssignment(props.id, variantIds);
269
+ const { variantId, content } = useAssignment(props.id, variantIds, props.agentData);
269
270
  const containerRef = useRef2(null);
270
271
  const [mounted, setMounted] = useState3(false);
271
272
  useEffect3(() => {
@@ -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\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"]}
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 * Behavior before consent is granted. Pass `'statistical_winner'` to serve the\n * best-performing variant via `GET /v1/winner` with zero tracking while the\n * consent banner is showing. Requires `consent: false`.\n * @see SentientConfig.preConsentBehavior\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\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 with no preConsentBehavior, tear down any existing client.\n if (props.consent === false && !props.preConsentBehavior) {\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 preConsentBehavior: props.preConsentBehavior,\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 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 /** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */\n agentData?: unknown;\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, props.agentData);\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[], agentData?: unknown): 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, agentData).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"]}
@@ -31,6 +31,13 @@ type AdaptiveProviderProps = {
31
31
  * initialise and begin tracking.
32
32
  */
33
33
  consent?: boolean;
34
+ /**
35
+ * Behavior before consent is granted. Pass `'statistical_winner'` to serve the
36
+ * best-performing variant via `GET /v1/winner` with zero tracking while the
37
+ * consent banner is showing. Requires `consent: false`.
38
+ * @see SentientConfig.preConsentBehavior
39
+ */
40
+ preConsentBehavior?: 'statistical_winner' | 'control';
34
41
  /**
35
42
  * Called once per component the first time a variant is resolved for that
36
43
  * component in this session. Use to forward assignments to your own analytics
@@ -34,7 +34,7 @@ function AdaptiveProvider(props) {
34
34
  var _a, _b, _c, _d;
35
35
  const [client, setClient] = useState(null);
36
36
  useEffect(() => {
37
- if (props.consent === false) {
37
+ if (props.consent === false && !props.preConsentBehavior) {
38
38
  setClient((prev) => {
39
39
  prev == null ? void 0 : prev.destroy();
40
40
  return null;
@@ -47,7 +47,8 @@ function AdaptiveProvider(props) {
47
47
  debug: props.debug,
48
48
  initialAssignments: props.initialAssignments,
49
49
  sessionSegment: props.sessionSegment,
50
- consent: props.consent
50
+ consent: props.consent,
51
+ preConsentBehavior: props.preConsentBehavior
51
52
  });
52
53
  setClient(c);
53
54
  return () => {
@@ -1 +1 @@
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"]}
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 * Behavior before consent is granted. Pass `'statistical_winner'` to serve the\n * best-performing variant via `GET /v1/winner` with zero tracking while the\n * consent banner is showing. Requires `consent: false`.\n * @see SentientConfig.preConsentBehavior\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\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 with no preConsentBehavior, tear down any existing client.\n if (props.consent === false && !props.preConsentBehavior) {\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 preConsentBehavior: props.preConsentBehavior,\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 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;AAmH3D;AAjGJ,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;AAwDM,SAAS,iBAAiB,OAA2C;AAvF5E;AAwFE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,IAAI;AAEhE,YAAU,MAAM;AAEd,QAAI,MAAM,YAAY,SAAS,CAAC,MAAM,oBAAoB;AACxD,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,MACf,oBAAoB,MAAM;AAAA,IAC5B,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;;;AC3HS,gBAAAA,YAAA;AADF,SAAS,mBAAmB,OAA6C;AAC9E,SAAO,gBAAAA,KAAC,qCAAqB,MAAO;AACtC;","names":["jsx"]}
@@ -31,6 +31,13 @@ type AdaptiveProviderProps = {
31
31
  * initialise and begin tracking.
32
32
  */
33
33
  consent?: boolean;
34
+ /**
35
+ * Behavior before consent is granted. Pass `'statistical_winner'` to serve the
36
+ * best-performing variant via `GET /v1/winner` with zero tracking while the
37
+ * consent banner is showing. Requires `consent: false`.
38
+ * @see SentientConfig.preConsentBehavior
39
+ */
40
+ preConsentBehavior?: 'statistical_winner' | 'control';
34
41
  /**
35
42
  * Called once per component the first time a variant is resolved for that
36
43
  * component in this session. Use to forward assignments to your own analytics
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/react",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",