@sentientui/react 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,15 +18,12 @@ pnpm add @sentientui/react
18
18
 
19
19
  Create a project at [sentient-ui.com](https://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
20
 
21
- For Next.js, set the same key in two env vars (one for the server, one for the browser):
22
-
23
21
  ```bash
24
22
  # .env.local
25
- SENTIENT_API_KEY=pk_your_key
26
23
  NEXT_PUBLIC_SENTIENT_API_KEY=pk_your_key
27
24
  ```
28
25
 
29
- The SDK points at `https://api.sentient-ui.com` (the hosted API) automatically — no ingest URL configuration required.
26
+ One env var is all you need. The SDK points at `https://api.sentient-ui.com` automatically — no ingest URL required.
30
27
 
31
28
  ## Quick start — Next.js App Router (recommended)
32
29
 
@@ -46,7 +43,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
46
43
  { id: 'pricing', variantIds: ['monthly', 'annual_first'] },
47
44
  ]}
48
45
  sections={['hero', 'pricing', 'features', 'social_proof']}
49
- serverApiKey={process.env.SENTIENT_API_KEY!}
50
46
  apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
51
47
  appOrigin={process.env.NEXT_PUBLIC_APP_URL!}
52
48
  context="saas"
@@ -107,19 +103,17 @@ Imported from `@sentientui/react/next`.
107
103
  |------|------|-------------|
108
104
  | `components` | `Array<{ id: string; variantIds: string[] }>` | Components to preload server-side. `id` must match `<Adaptive id="…">`. |
109
105
  | `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. |
106
+ | `apiKey` | `string` | `pk_…` key used by both the browser SDK and server-side SSR requests. |
112
107
  | `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
108
  | `context` | `'landing' \| 'ecommerce' \| 'saas' \| 'marketplace'` | Type of product. Used for segment weighting and analytics grouping. |
114
109
  | `consent` | `boolean` *(default `true`)* | Set `false` to skip SDK init (no cookies, no events). Flip to `true` after the visitor accepts. |
115
110
  | `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
111
  | `timeoutMs` | `number` *(default `1500`)* | Server-side fetch timeout before falling back to the first variant. |
118
112
  | `debug` | `boolean` | Log assignment and event activity to the console. |
119
113
 
120
114
  ### `<AdaptiveProvider>` (any React app)
121
115
 
122
- Accepts the same `apiKey`, `context`, `consent`, `ssrFallback`, `onAssignment`, `debug` props as `<AdaptiveRoot>`, plus:
116
+ Accepts the same `apiKey`, `context`, `consent`, `ssrFallback`, `debug` props as `<AdaptiveRoot>`, plus `onAssignment` (not available on `<AdaptiveRoot>` — function props can't cross the RSC boundary) and:
123
117
 
124
118
  | Prop | Type | Description |
125
119
  |------|------|-------------|
@@ -234,7 +228,7 @@ export async function getServerSideProps({ req }) {
234
228
  [{ id: 'hero_cta', variantIds: ['control', 'variant_a'] }],
235
229
  {
236
230
  cookies: req.cookies,
237
- apiKey: process.env.SENTIENT_API_KEY!,
231
+ apiKey: process.env.NEXT_PUBLIC_SENTIENT_API_KEY!,
238
232
  baseUrl: 'https://api.sentient-ui.com/v1',
239
233
  origin: process.env.NEXT_PUBLIC_APP_URL,
240
234
  },
@@ -66,7 +66,7 @@ type PreloadComponent = {
66
66
  id: string;
67
67
  variantIds: string[];
68
68
  };
69
- type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments'> & {
69
+ type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments' | 'onAssignment'> & {
70
70
  /** Components to assign server-side (SEO-safe). */
71
71
  components: PreloadComponent[];
72
72
  /**
@@ -78,8 +78,6 @@ type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments'> & {
78
78
  * @example sections={['hero', 'pricing', 'features', 'social_proof']}
79
79
  */
80
80
  sections?: string[];
81
- /** Server-only API key for `/v1/sessions`, `/v1/assign`, and `/v1/decide`. */
82
- serverApiKey: string;
83
81
  /** App origin — must be in the project's `allowed_origins`. */
84
82
  appOrigin?: string;
85
83
  /** Override: when set no network fetch is made. Useful for tests. */
@@ -107,8 +105,7 @@ type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments'> & {
107
105
  * return (
108
106
  * <AdaptiveRoot
109
107
  * apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
110
- * serverApiKey={process.env.SENTIENT_SECRET_KEY!}
111
- * context={{ appId: 'my-app' }}
108
+ * context="landing"
112
109
  * sections={['hero', 'pricing', 'features', 'social_proof']}
113
110
  * components={[
114
111
  * { id: 'hero_cta', variantIds: ['default', 'accent'] },
@@ -1,2 +1,2 @@
1
- var E=Object.defineProperty,N=Object.defineProperties;var _=Object.getOwnPropertyDescriptors;var m=Object.getOwnPropertySymbols;var U=Object.prototype.hasOwnProperty,x=Object.prototype.propertyIsEnumerable;var R=(e,s,i)=>s in e?E(e,s,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[s]=i,A=(e,s)=>{for(var i in s||(s={}))U.call(s,i)&&R(e,i,s[i]);if(m)for(var i of m(s))x.call(s,i)&&R(e,i,s[i]);return e},u=(e,s)=>N(e,_(s));var L=(e,s)=>{var i={};for(var t in e)U.call(e,t)&&s.indexOf(t)<0&&(i[t]=e[t]);if(e!=null&&m)for(var t of m(e))s.indexOf(t)<0&&x.call(e,t)&&(i[t]=e[t]);return i};import{deriveSessionSegment as B}from"@sentientui/core";import{cookies as F,headers as H}from"next/headers";import{preloadAssignments as $,readSessionCookie as V}from"@sentientui/core/server";import{preloadDecisions as G}from"@sentientui/core/server";function b(){return typeof crypto!="undefined"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`snt-${Date.now()}-${Math.random().toString(36).slice(2,11)}`}async function k(e,s){var n,r,o;let i=(o=(r=V(s.cookies))!=null?r:(n=s.createSessionId)==null?void 0:n.call(s))!=null?o:b();return{assignments:await $(e,i,{apiKey:s.apiKey,baseUrl:s.baseUrl,origin:s.origin,userAgent:s.userAgent,referer:s.referer,timeoutMs:s.timeoutMs}),sessionId:i}}async function w(e){var r,o,d,p;let{preloadDecisions:s,readSessionCookie:i}=await import("@sentientui/core/server"),t=(d=(o=i(e.cookies))!=null?o:(r=e.createSessionId)==null?void 0:r.call(e))!=null?d:b(),n=await s({sections:e.sections,components:(p=e.components)!=null?p:[]},t,{apiKey:e.apiKey,baseUrl:e.baseUrl,origin:e.origin,userAgent:e.userAgent,referer:e.referer,timeoutMs:e.timeoutMs});return u(A({},n),{sessionId:t})}import{AdaptiveRootClient as J}from"./adaptive-root-client.js";import{jsx as T}from"react/jsx-runtime";var K="https://api.sentient-ui.com/v1";async function se(e){var h,P;let O=e,{components:s,sections:i,serverApiKey:t,appOrigin:n,initialAssignments:r,ssrSessionId:o,timeoutMs:d,children:p}=O,C=L(O,["components","sections","serverApiKey","appOrigin","initialAssignments","ssrSessionId","timeoutMs","children"]),v=await H(),g=v.get("host");!n&&process.env.NODE_ENV==="production"&&!g&&console.error("[SentientUI] AdaptiveRoot: Host header is absent and no appOrigin was provided. Pass appOrigin explicitly \u2014 the https://localhost fallback will likely fail allowed_origins validation.");let f=n!=null?n:process.env.NODE_ENV==="production"?`https://${g!=null?g:"localhost"}`:"http://localhost:3001",y=(h=v.get("user-agent"))!=null?h:void 0,S=(P=v.get("referer"))!=null?P:void 0,M=B({userAgent:y,referer:S,appOrigin:f}),I=await F(),c,D=null,l;if(r)c=r,l=o;else if(i&&i.length>0){let a=await w({sections:i,components:s,cookies:I,apiKey:t,baseUrl:K,origin:f,userAgent:y,referer:S,timeoutMs:d});c=a.assignments,D=a.layoutOrder,l=a.sessionId}else{let a=await k(s,{cookies:I,apiKey:t,baseUrl:K,origin:f,userAgent:y,referer:S,timeoutMs:d});c=a.assignments,l=a.sessionId}return T(J,u(A({},C),{initialAssignments:c,initialLayoutOrder:D,sessionSegment:M,ssrSessionId:l,children:p}))}export{se as AdaptiveRoot};
1
+ var M=Object.defineProperty,E=Object.defineProperties;var N=Object.getOwnPropertyDescriptors;var l=Object.getOwnPropertySymbols;var U=Object.prototype.hasOwnProperty,x=Object.prototype.propertyIsEnumerable;var R=(e,s,i)=>s in e?M(e,s,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[s]=i,A=(e,s)=>{for(var i in s||(s={}))U.call(s,i)&&R(e,i,s[i]);if(l)for(var i of l(s))x.call(s,i)&&R(e,i,s[i]);return e},u=(e,s)=>E(e,N(s));var L=(e,s)=>{var i={};for(var t in e)U.call(e,t)&&s.indexOf(t)<0&&(i[t]=e[t]);if(e!=null&&l)for(var t of l(e))s.indexOf(t)<0&&x.call(e,t)&&(i[t]=e[t]);return i};import{deriveSessionSegment as V}from"@sentientui/core";import{cookies as B,headers as F}from"next/headers";import{preloadAssignments as _,readSessionCookie as $}from"@sentientui/core/server";import{preloadDecisions as z}from"@sentientui/core/server";function b(){return typeof crypto!="undefined"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`snt-${Date.now()}-${Math.random().toString(36).slice(2,11)}`}async function k(e,s){var r,o,n;let i=(n=(o=$(s.cookies))!=null?o:(r=s.createSessionId)==null?void 0:r.call(s))!=null?n:b();return{assignments:await _(e,i,{apiKey:s.apiKey,baseUrl:s.baseUrl,origin:s.origin,userAgent:s.userAgent,referer:s.referer,timeoutMs:s.timeoutMs}),sessionId:i}}async function w(e){var o,n,p,a;let{preloadDecisions:s,readSessionCookie:i}=await import("@sentientui/core/server"),t=(p=(n=i(e.cookies))!=null?n:(o=e.createSessionId)==null?void 0:o.call(e))!=null?p:b(),r=await s({sections:e.sections,components:(a=e.components)!=null?a:[]},t,{apiKey:e.apiKey,baseUrl:e.baseUrl,origin:e.origin,userAgent:e.userAgent,referer:e.referer,timeoutMs:e.timeoutMs});return u(A({},r),{sessionId:t})}import{AdaptiveRootClient as H}from"./adaptive-root-client.js";import{jsx as J}from"react/jsx-runtime";var K="https://api.sentient-ui.com/v1";async function ee(e){var h,P;let O=e,{components:s,sections:i,appOrigin:t,initialAssignments:r,ssrSessionId:o,timeoutMs:n,children:p}=O,a=L(O,["components","sections","appOrigin","initialAssignments","ssrSessionId","timeoutMs","children"]),v=await F(),g=v.get("host");!t&&process.env.NODE_ENV==="production"&&!g&&console.error("[SentientUI] AdaptiveRoot: Host header is absent and no appOrigin was provided. Pass appOrigin explicitly \u2014 the https://localhost fallback will likely fail allowed_origins validation.");let f=t!=null?t:process.env.NODE_ENV==="production"?`https://${g!=null?g:"localhost"}`:"http://localhost:3001",y=(h=v.get("user-agent"))!=null?h:void 0,S=(P=v.get("referer"))!=null?P:void 0,C=V({userAgent:y,referer:S,appOrigin:f}),I=await B(),c,D=null,m;if(r)c=r,m=o;else if(i&&i.length>0){let d=await w({sections:i,components:s,cookies:I,apiKey:a.apiKey,baseUrl:K,origin:f,userAgent:y,referer:S,timeoutMs:n});c=d.assignments,D=d.layoutOrder,m=d.sessionId}else{let d=await k(s,{cookies:I,apiKey:a.apiKey,baseUrl:K,origin:f,userAgent:y,referer:S,timeoutMs:n});c=d.assignments,m=d.sessionId}return J(H,u(A({},a),{initialAssignments:c,initialLayoutOrder:D,sessionSegment:C,ssrSessionId:m,children:p}))}export{ee as AdaptiveRoot};
2
2
  //# sourceMappingURL=adaptive-root.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/adaptive-root.tsx","../../src/server.ts"],"sourcesContent":["import { deriveSessionSegment } from '@sentientui/core';\nimport { cookies, headers } from 'next/headers';\nimport type { ReactNode } from 'react';\nimport type { AdaptiveProviderProps } from '../provider.js';\nimport {\n loadAdaptiveAssignments,\n loadAdaptiveDecision,\n type ServerAssignments,\n} from '../server.js';\nimport { AdaptiveRootClient } from './adaptive-root-client.js';\n\nconst DEFAULT_API_BASE_URL = 'https://api.sentient-ui.com/v1';\n\nexport type PreloadComponent = { id: string; variantIds: string[] };\n\nexport type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments'> & {\n /** Components to assign server-side (SEO-safe). */\n components: PreloadComponent[];\n /**\n * Declare page section IDs in their default order. When provided,\n * AdaptiveRoot calls `/v1/decide` (single round trip) instead of\n * individual `/v1/assign` calls, and `useLayoutOrder()` returns the\n * persona-specific order on first render.\n *\n * @example sections={['hero', 'pricing', 'features', 'social_proof']}\n */\n sections?: string[];\n /** Server-only API key for `/v1/sessions`, `/v1/assign`, and `/v1/decide`. */\n serverApiKey: string;\n /** App origin — must be in the project's `allowed_origins`. */\n appOrigin?: string;\n /** Override: when set no network fetch is made. Useful for tests. */\n initialAssignments?: ServerAssignments;\n /**\n * Milliseconds to wait for the API before rendering default variants.\n * Defaults to 1500. Increase if you see unexpected fallbacks in production.\n */\n timeoutMs?: number;\n children: ReactNode;\n};\n\n/**\n * Next.js Server Component that resolves variant assignments (and optionally\n * section layout order) server-side for zero layout shift on first paint.\n *\n * When `sections` is provided, a single `POST /v1/decide` call returns both\n * the persona-specific section order and all component assignments.\n * Without `sections`, individual `/v1/assign` calls are made per component.\n *\n * @example\n * // app/page.tsx — with section layout\n * import { AdaptiveRoot } from '@sentientui/react/next';\n *\n * export default async function Page() {\n * return (\n * <AdaptiveRoot\n * apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}\n * serverApiKey={process.env.SENTIENT_SECRET_KEY!}\n * context={{ appId: 'my-app' }}\n * sections={['hero', 'pricing', 'features', 'social_proof']}\n * components={[\n * { id: 'hero_cta', variantIds: ['default', 'accent'] },\n * ]}\n * >\n * <HeroSection />\n * <PricingSection />\n * <FeaturesSection />\n * <SocialProofSection />\n * </AdaptiveRoot>\n * );\n * }\n */\nexport async function AdaptiveRoot(props: AdaptiveRootProps): Promise<JSX.Element> {\n const {\n components,\n sections,\n serverApiKey,\n appOrigin,\n initialAssignments: initialAssignmentsOverride,\n ssrSessionId: ssrSessionIdProp,\n timeoutMs,\n children,\n ...providerProps\n } = props;\n\n const headerStore = await headers();\n const host = headerStore.get('host');\n if (!appOrigin && process.env.NODE_ENV === 'production' && !host) {\n console.error(\n '[SentientUI] AdaptiveRoot: Host header is absent and no appOrigin was provided. ' +\n 'Pass appOrigin explicitly — the https://localhost fallback will likely fail allowed_origins validation.',\n );\n }\n const resolvedOrigin = appOrigin ??\n (process.env.NODE_ENV === 'production'\n ? `https://${host ?? 'localhost'}`\n : 'http://localhost:3001');\n const userAgent = headerStore.get('user-agent') ?? undefined;\n const referer = headerStore.get('referer') ?? undefined;\n const sessionSegment = deriveSessionSegment({ userAgent, referer, appOrigin: resolvedOrigin });\n const cookieStore = await cookies();\n\n let initialAssignments: ServerAssignments;\n let initialLayoutOrder: string[] | null = null;\n let ssrSessionId: string | undefined;\n\n if (initialAssignmentsOverride) {\n initialAssignments = initialAssignmentsOverride;\n ssrSessionId = ssrSessionIdProp;\n } else if (sections && sections.length > 0) {\n const decision = await loadAdaptiveDecision({\n sections,\n components,\n cookies: cookieStore,\n apiKey: serverApiKey,\n baseUrl: DEFAULT_API_BASE_URL,\n origin: resolvedOrigin,\n userAgent,\n referer,\n timeoutMs,\n });\n initialAssignments = decision.assignments;\n initialLayoutOrder = decision.layoutOrder;\n ssrSessionId = decision.sessionId;\n } else {\n const result = await loadAdaptiveAssignments(components, {\n cookies: cookieStore,\n apiKey: serverApiKey,\n baseUrl: DEFAULT_API_BASE_URL,\n origin: resolvedOrigin,\n userAgent,\n referer,\n timeoutMs,\n });\n initialAssignments = result.assignments;\n ssrSessionId = result.sessionId;\n }\n\n return (\n <AdaptiveRootClient\n {...providerProps}\n initialAssignments={initialAssignments}\n initialLayoutOrder={initialLayoutOrder}\n sessionSegment={sessionSegment}\n ssrSessionId={ssrSessionId}\n >\n {children}\n </AdaptiveRootClient>\n );\n}\n","/**\n * Server-only helpers for Next.js / SSR. No React or DOM APIs.\n */\nimport {\n preloadAssignments,\n readSessionCookie,\n type ServerAssignConfig,\n type ServerAssignments,\n} from '@sentientui/core/server';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\n\n/** Return value of `loadAdaptiveAssignments` — includes the session ID used for SSR. */\nexport type LoadAdaptiveAssignmentsResult = {\n assignments: ServerAssignments;\n /** The session ID used for SSR assignment. Pass as `ssrSessionId` to `<AdaptiveProvider>`. */\n sessionId: string;\n};\n\nexport type LoadAdaptiveAssignmentsOptions = {\n /** Next.js `cookies()` return value, or any object with `get(name)`. */\n cookies: { get(name: string): { value: string } | undefined };\n apiKey: string;\n baseUrl: string;\n /** Used when `_snt_uid` is absent (e.g. first visit, many crawlers). */\n createSessionId?: () => string;\n /** Must match a value in the project's `allowed_origins` (e.g. `http://localhost:3001`). */\n origin?: string;\n /** From Next.js `headers().get('user-agent')` — aligns SSR segment with the client. */\n userAgent?: string;\n /** From Next.js `headers().get('referer')`. */\n referer?: string;\n /** Milliseconds to wait for the API before returning default variants. Defaults to 1500. */\n timeoutMs?: number;\n};\n\nfunction defaultSessionId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return `snt-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n\n/**\n * Fetches bandit assignments on the server for SEO-safe HTML.\n * Pass `assignments` as `initialAssignments` and `sessionId` as `ssrSessionId`\n * on `<AdaptiveProvider>` so the client adopts the same session on first visit.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<LoadAdaptiveAssignmentsResult> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n const assignments = await preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n timeoutMs: options.timeoutMs,\n });\n\n return { assignments, sessionId };\n}\n\nexport { preloadDecisions } from '@sentientui/core/server';\nexport type { DecideResult } from '@sentientui/core/server';\n\n/** Return value of `loadAdaptiveDecision` — includes the session ID used for SSR. */\nexport type LoadAdaptiveDecisionResult = import('@sentientui/core/server').DecideResult & {\n sessionId: string;\n};\n\nexport type LoadAdaptiveDecisionOptions = LoadAdaptiveAssignmentsOptions & {\n /** Section IDs in default order. Passed to /v1/decide as the candidate layout. */\n sections: string[];\n /** Components to assign in the same decide call. */\n components?: Array<{ id: string; variantIds?: string[] }>;\n};\n\n/**\n * SSR helper for pages with a declared section layout. Calls `/v1/decide`\n * instead of multiple `/v1/assign` round trips.\n * Pass `sessionId` as `ssrSessionId` on `<AdaptiveProvider>` so the client\n * adopts the same session on first visit.\n */\nexport async function loadAdaptiveDecision(\n options: LoadAdaptiveDecisionOptions,\n): Promise<LoadAdaptiveDecisionResult> {\n const { preloadDecisions, readSessionCookie } = await import('@sentientui/core/server');\n\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n const result = await preloadDecisions(\n {\n sections: options.sections,\n components: options.components ?? [],\n },\n sessionId,\n {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n timeoutMs: options.timeoutMs,\n },\n );\n\n return { ...result, sessionId };\n}\n"],"mappings":"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","names":["deriveSessionSegment","cookies","headers","preloadAssignments","readSessionCookie","preloadDecisions","defaultSessionId","loadAdaptiveAssignments","components","options","_a","_b","_c","sessionId","readSessionCookie","preloadAssignments","loadAdaptiveDecision","_d","preloadDecisions","result","__spreadProps","__spreadValues","AdaptiveRootClient","jsx","DEFAULT_API_BASE_URL","AdaptiveRoot","props","_b","_c","_a","components","sections","serverApiKey","appOrigin","initialAssignmentsOverride","ssrSessionIdProp","timeoutMs","children","providerProps","__objRest","headerStore","headers","host","resolvedOrigin","userAgent","referer","sessionSegment","deriveSessionSegment","cookieStore","cookies","initialAssignments","initialLayoutOrder","ssrSessionId","decision","loadAdaptiveDecision","result","loadAdaptiveAssignments","AdaptiveRootClient","__spreadProps","__spreadValues"]}
1
+ {"version":3,"sources":["../../src/next/adaptive-root.tsx","../../src/server.ts"],"sourcesContent":["import { deriveSessionSegment } from '@sentientui/core';\nimport { cookies, headers } from 'next/headers';\nimport type { ReactNode } from 'react';\nimport type { AdaptiveProviderProps } from '../provider.js';\nimport {\n loadAdaptiveAssignments,\n loadAdaptiveDecision,\n type ServerAssignments,\n} from '../server.js';\nimport { AdaptiveRootClient } from './adaptive-root-client.js';\n\nconst DEFAULT_API_BASE_URL = 'https://api.sentient-ui.com/v1';\n\nexport type PreloadComponent = { id: string; variantIds: string[] };\n\nexport type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments' | 'onAssignment'> & {\n /** Components to assign server-side (SEO-safe). */\n components: PreloadComponent[];\n /**\n * Declare page section IDs in their default order. When provided,\n * AdaptiveRoot calls `/v1/decide` (single round trip) instead of\n * individual `/v1/assign` calls, and `useLayoutOrder()` returns the\n * persona-specific order on first render.\n *\n * @example sections={['hero', 'pricing', 'features', 'social_proof']}\n */\n sections?: string[];\n /** App origin — must be in the project's `allowed_origins`. */\n appOrigin?: string;\n /** Override: when set no network fetch is made. Useful for tests. */\n initialAssignments?: ServerAssignments;\n /**\n * Milliseconds to wait for the API before rendering default variants.\n * Defaults to 1500. Increase if you see unexpected fallbacks in production.\n */\n timeoutMs?: number;\n children: ReactNode;\n};\n\n/**\n * Next.js Server Component that resolves variant assignments (and optionally\n * section layout order) server-side for zero layout shift on first paint.\n *\n * When `sections` is provided, a single `POST /v1/decide` call returns both\n * the persona-specific section order and all component assignments.\n * Without `sections`, individual `/v1/assign` calls are made per component.\n *\n * @example\n * // app/page.tsx — with section layout\n * import { AdaptiveRoot } from '@sentientui/react/next';\n *\n * export default async function Page() {\n * return (\n * <AdaptiveRoot\n * apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}\n * context=\"landing\"\n * sections={['hero', 'pricing', 'features', 'social_proof']}\n * components={[\n * { id: 'hero_cta', variantIds: ['default', 'accent'] },\n * ]}\n * >\n * <HeroSection />\n * <PricingSection />\n * <FeaturesSection />\n * <SocialProofSection />\n * </AdaptiveRoot>\n * );\n * }\n */\nexport async function AdaptiveRoot(props: AdaptiveRootProps): Promise<JSX.Element> {\n const {\n components,\n sections,\n appOrigin,\n initialAssignments: initialAssignmentsOverride,\n ssrSessionId: ssrSessionIdProp,\n timeoutMs,\n children,\n ...providerProps\n } = props;\n\n const headerStore = await headers();\n const host = headerStore.get('host');\n if (!appOrigin && process.env.NODE_ENV === 'production' && !host) {\n console.error(\n '[SentientUI] AdaptiveRoot: Host header is absent and no appOrigin was provided. ' +\n 'Pass appOrigin explicitly — the https://localhost fallback will likely fail allowed_origins validation.',\n );\n }\n const resolvedOrigin = appOrigin ??\n (process.env.NODE_ENV === 'production'\n ? `https://${host ?? 'localhost'}`\n : 'http://localhost:3001');\n const userAgent = headerStore.get('user-agent') ?? undefined;\n const referer = headerStore.get('referer') ?? undefined;\n const sessionSegment = deriveSessionSegment({ userAgent, referer, appOrigin: resolvedOrigin });\n const cookieStore = await cookies();\n\n let initialAssignments: ServerAssignments;\n let initialLayoutOrder: string[] | null = null;\n let ssrSessionId: string | undefined;\n\n if (initialAssignmentsOverride) {\n initialAssignments = initialAssignmentsOverride;\n ssrSessionId = ssrSessionIdProp;\n } else if (sections && sections.length > 0) {\n const decision = await loadAdaptiveDecision({\n sections,\n components,\n cookies: cookieStore,\n apiKey: providerProps.apiKey,\n baseUrl: DEFAULT_API_BASE_URL,\n origin: resolvedOrigin,\n userAgent,\n referer,\n timeoutMs,\n });\n initialAssignments = decision.assignments;\n initialLayoutOrder = decision.layoutOrder;\n ssrSessionId = decision.sessionId;\n } else {\n const result = await loadAdaptiveAssignments(components, {\n cookies: cookieStore,\n apiKey: providerProps.apiKey,\n baseUrl: DEFAULT_API_BASE_URL,\n origin: resolvedOrigin,\n userAgent,\n referer,\n timeoutMs,\n });\n initialAssignments = result.assignments;\n ssrSessionId = result.sessionId;\n }\n\n return (\n <AdaptiveRootClient\n {...providerProps}\n initialAssignments={initialAssignments}\n initialLayoutOrder={initialLayoutOrder}\n sessionSegment={sessionSegment}\n ssrSessionId={ssrSessionId}\n >\n {children}\n </AdaptiveRootClient>\n );\n}\n","/**\n * Server-only helpers for Next.js / SSR. No React or DOM APIs.\n */\nimport {\n preloadAssignments,\n readSessionCookie,\n type ServerAssignConfig,\n type ServerAssignments,\n} from '@sentientui/core/server';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\n\n/** Return value of `loadAdaptiveAssignments` — includes the session ID used for SSR. */\nexport type LoadAdaptiveAssignmentsResult = {\n assignments: ServerAssignments;\n /** The session ID used for SSR assignment. Pass as `ssrSessionId` to `<AdaptiveProvider>`. */\n sessionId: string;\n};\n\nexport type LoadAdaptiveAssignmentsOptions = {\n /** Next.js `cookies()` return value, or any object with `get(name)`. */\n cookies: { get(name: string): { value: string } | undefined };\n apiKey: string;\n baseUrl: string;\n /** Used when `_snt_uid` is absent (e.g. first visit, many crawlers). */\n createSessionId?: () => string;\n /** Must match a value in the project's `allowed_origins` (e.g. `http://localhost:3001`). */\n origin?: string;\n /** From Next.js `headers().get('user-agent')` — aligns SSR segment with the client. */\n userAgent?: string;\n /** From Next.js `headers().get('referer')`. */\n referer?: string;\n /** Milliseconds to wait for the API before returning default variants. Defaults to 1500. */\n timeoutMs?: number;\n};\n\nfunction defaultSessionId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return `snt-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n\n/**\n * Fetches bandit assignments on the server for SEO-safe HTML.\n * Pass `assignments` as `initialAssignments` and `sessionId` as `ssrSessionId`\n * on `<AdaptiveProvider>` so the client adopts the same session on first visit.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<LoadAdaptiveAssignmentsResult> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n const assignments = await preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n timeoutMs: options.timeoutMs,\n });\n\n return { assignments, sessionId };\n}\n\nexport { preloadDecisions } from '@sentientui/core/server';\nexport type { DecideResult } from '@sentientui/core/server';\n\n/** Return value of `loadAdaptiveDecision` — includes the session ID used for SSR. */\nexport type LoadAdaptiveDecisionResult = import('@sentientui/core/server').DecideResult & {\n sessionId: string;\n};\n\nexport type LoadAdaptiveDecisionOptions = LoadAdaptiveAssignmentsOptions & {\n /** Section IDs in default order. Passed to /v1/decide as the candidate layout. */\n sections: string[];\n /** Components to assign in the same decide call. */\n components?: Array<{ id: string; variantIds?: string[] }>;\n};\n\n/**\n * SSR helper for pages with a declared section layout. Calls `/v1/decide`\n * instead of multiple `/v1/assign` round trips.\n * Pass `sessionId` as `ssrSessionId` on `<AdaptiveProvider>` so the client\n * adopts the same session on first visit.\n */\nexport async function loadAdaptiveDecision(\n options: LoadAdaptiveDecisionOptions,\n): Promise<LoadAdaptiveDecisionResult> {\n const { preloadDecisions, readSessionCookie } = await import('@sentientui/core/server');\n\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n const result = await preloadDecisions(\n {\n sections: options.sections,\n components: options.components ?? [],\n },\n sessionId,\n {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n timeoutMs: options.timeoutMs,\n },\n );\n\n return { ...result, sessionId };\n}\n"],"mappings":"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","names":["deriveSessionSegment","cookies","headers","preloadAssignments","readSessionCookie","preloadDecisions","defaultSessionId","loadAdaptiveAssignments","components","options","_a","_b","_c","sessionId","readSessionCookie","preloadAssignments","loadAdaptiveDecision","_d","preloadDecisions","result","__spreadProps","__spreadValues","AdaptiveRootClient","jsx","DEFAULT_API_BASE_URL","AdaptiveRoot","props","_b","_c","_a","components","sections","appOrigin","initialAssignmentsOverride","ssrSessionIdProp","timeoutMs","children","providerProps","__objRest","headerStore","headers","host","resolvedOrigin","userAgent","referer","sessionSegment","deriveSessionSegment","cookieStore","cookies","initialAssignments","initialLayoutOrder","ssrSessionId","decision","loadAdaptiveDecision","result","loadAdaptiveAssignments","AdaptiveRootClient","__spreadProps","__spreadValues"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/react",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "dist"
28
28
  ],
29
29
  "dependencies": {
30
- "@sentientui/core": "0.8.0"
30
+ "@sentientui/core": "0.8.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18.0.0",