@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 +242 -64
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/next/adaptive-root-client.d.ts +7 -0
- package/dist/next/adaptive-root-client.js +3 -2
- package/dist/next/adaptive-root-client.js.map +1 -1
- package/dist/next/adaptive-root.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
1
|
# @sentientui/react
|
|
2
2
|
|
|
3
|
-
React SDK for [SentientUI](https://
|
|
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
|
|
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
|
-
|
|
29
|
+
The SDK points at `https://sentient-api.fly.dev` (the hosted API) automatically — no ingest URL configuration required.
|
|
12
30
|
|
|
13
|
-
|
|
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
|
-
|
|
36
|
+
// app/layout.tsx
|
|
37
|
+
import { AdaptiveRoot } from '@sentientui/react/next';
|
|
17
38
|
|
|
18
|
-
export default function
|
|
39
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
19
40
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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="
|
|
36
|
-
goal="
|
|
70
|
+
id="hero_cta"
|
|
71
|
+
goal="signup_click"
|
|
37
72
|
variants={{
|
|
38
|
-
control:
|
|
39
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
## Next.js (App Router)
|
|
81
|
+
## Quick start — other React apps (Vite, CRA, Remix, Pages Router)
|
|
49
82
|
|
|
50
|
-
Use
|
|
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
|
-
|
|
54
|
-
import { AdaptiveRoot } from '@sentientui/react/next';
|
|
86
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
55
87
|
|
|
56
|
-
export default function
|
|
88
|
+
export default function App({ children }: { children: React.ReactNode }) {
|
|
57
89
|
return (
|
|
58
|
-
<
|
|
59
|
-
|
|
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
|
-
</
|
|
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
|
-
### `<
|
|
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
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
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.
|
|
97
|
-
| `clientOnly` | `boolean` |
|
|
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
|
-
//
|
|
103
|
-
goal="
|
|
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
|
|
145
|
+
// Click only on elements matching a CSS selector inside the variant.
|
|
106
146
|
goal={{ type: 'click', selector: 'button.cta' }}
|
|
107
147
|
|
|
108
|
-
//
|
|
109
|
-
goal={{ type: 'scroll_depth', threshold:
|
|
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
|
|
151
|
+
// A <form> inside the variant fires submit.
|
|
112
152
|
goal={{ type: 'form_submit' }}
|
|
113
153
|
|
|
114
|
-
//
|
|
115
|
-
goal={{
|
|
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
|
|
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
|
|
126
|
-
const
|
|
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 === '
|
|
190
|
+
return variantId === 'variant_a' ? <AccentHero /> : <DefaultHero />;
|
|
130
191
|
}
|
|
131
192
|
```
|
|
132
193
|
|
|
133
|
-
|
|
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=
|
|
238
|
+
apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
|
|
138
239
|
context="saas"
|
|
139
240
|
onAssignment={(componentId, variantId) => {
|
|
140
|
-
posthog.capture('$
|
|
141
|
-
|
|
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":"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","names":["import_react","import_react","_a","import_jsx_runtime","_a","_b","import_react","import_jsx_runtime","import_core"]}
|
package/dist/index.mjs
CHANGED
|
@@ -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(() => {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx","../src/adaptive.tsx","../src/use-assignment.ts","../src/weights-store.ts","../src/adaptive-text.tsx","../src/segment.ts"],"sourcesContent":["'use client';\n\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
|
|
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
|