@sentientui/react 0.2.1 → 0.3.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/dist/index.d.cts CHANGED
@@ -2,6 +2,7 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { SentientConfig, SentientClient } from '@sentientui/core';
4
4
  export { deriveSessionSegment as detectSegment } from '@sentientui/core';
5
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
6
 
6
7
  /** How to render adaptive slots during SSR when assignments are not preloaded. */
7
8
  type SsrFallback = 'first' | 'none';
@@ -39,6 +40,12 @@ type AdaptiveProviderProps = {
39
40
  * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.
40
41
  */
41
42
  onAssignment?: (componentId: string, variantId: string) => void;
43
+ /**
44
+ * SSR-preloaded section order from `loadAdaptiveDecision()`.
45
+ * Pass the `layoutOrder` field from `DecideResult`. When set,
46
+ * `useLayoutOrder()` returns this on first render so there is no layout shift.
47
+ */
48
+ initialLayoutOrder?: string[] | null;
42
49
  children: ReactNode;
43
50
  };
44
51
  /**
@@ -53,6 +60,11 @@ declare function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element;
53
60
  declare function useSentient(): SentientClient | null;
54
61
  /** Internal: SSR-preloaded assignments for hydration-safe first render. */
55
62
  declare function useInitialAssignments(): Record<string, string>;
63
+ /**
64
+ * Returns the persona-specific section order from SSR, or null when no
65
+ * sections were declared on AdaptiveRoot or reliability is below threshold.
66
+ */
67
+ declare function useLayoutOrder(): string[] | null;
56
68
 
57
69
  type ScrollDepthGoal = {
58
70
  type: 'scroll_depth';
@@ -85,6 +97,14 @@ declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
85
97
  /** Re-renders only when the chosen variant changes. */
86
98
  declare const Adaptive: react.MemoExoticComponent<typeof AdaptiveImpl>;
87
99
 
100
+ type AdaptiveTextProps = {
101
+ id: string;
102
+ default: string;
103
+ component?: keyof JSX.IntrinsicElements;
104
+ className?: string;
105
+ };
106
+ declare function AdaptiveText({ id, default: defaultText, component: Tag, className, }: AdaptiveTextProps): react_jsx_runtime.JSX.Element;
107
+
88
108
  declare global {
89
109
  interface Window {
90
110
  __sentient_overrides?: Record<string, string>;
@@ -132,4 +152,4 @@ declare function update(componentId: string, weights: ComponentWeights): void;
132
152
  */
133
153
  declare function getWeights(componentId: string): ComponentWeights | null;
134
154
 
135
- export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useSentient };
155
+ export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, AdaptiveText, type AdaptiveTextProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useLayoutOrder, useSentient };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { SentientConfig, SentientClient } from '@sentientui/core';
4
4
  export { deriveSessionSegment as detectSegment } from '@sentientui/core';
5
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
6
 
6
7
  /** How to render adaptive slots during SSR when assignments are not preloaded. */
7
8
  type SsrFallback = 'first' | 'none';
@@ -39,6 +40,12 @@ type AdaptiveProviderProps = {
39
40
  * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.
40
41
  */
41
42
  onAssignment?: (componentId: string, variantId: string) => void;
43
+ /**
44
+ * SSR-preloaded section order from `loadAdaptiveDecision()`.
45
+ * Pass the `layoutOrder` field from `DecideResult`. When set,
46
+ * `useLayoutOrder()` returns this on first render so there is no layout shift.
47
+ */
48
+ initialLayoutOrder?: string[] | null;
42
49
  children: ReactNode;
43
50
  };
44
51
  /**
@@ -53,6 +60,11 @@ declare function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element;
53
60
  declare function useSentient(): SentientClient | null;
54
61
  /** Internal: SSR-preloaded assignments for hydration-safe first render. */
55
62
  declare function useInitialAssignments(): Record<string, string>;
63
+ /**
64
+ * Returns the persona-specific section order from SSR, or null when no
65
+ * sections were declared on AdaptiveRoot or reliability is below threshold.
66
+ */
67
+ declare function useLayoutOrder(): string[] | null;
56
68
 
57
69
  type ScrollDepthGoal = {
58
70
  type: 'scroll_depth';
@@ -85,6 +97,14 @@ declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
85
97
  /** Re-renders only when the chosen variant changes. */
86
98
  declare const Adaptive: react.MemoExoticComponent<typeof AdaptiveImpl>;
87
99
 
100
+ type AdaptiveTextProps = {
101
+ id: string;
102
+ default: string;
103
+ component?: keyof JSX.IntrinsicElements;
104
+ className?: string;
105
+ };
106
+ declare function AdaptiveText({ id, default: defaultText, component: Tag, className, }: AdaptiveTextProps): react_jsx_runtime.JSX.Element;
107
+
88
108
  declare global {
89
109
  interface Window {
90
110
  __sentient_overrides?: Record<string, string>;
@@ -132,4 +152,4 @@ declare function update(componentId: string, weights: ComponentWeights): void;
132
152
  */
133
153
  declare function getWeights(componentId: string): ComponentWeights | null;
134
154
 
135
- export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useSentient };
155
+ export { Adaptive, type AdaptiveProps, AdaptiveProvider, type AdaptiveProviderProps, AdaptiveText, type AdaptiveTextProps, type AssignmentState, type ClickGoal, type ComponentWeights, type CompositeGoal, type FormSubmitGoal, type GoalConfig, type ScrollDepthGoal, type SsrFallback, type VariantWeight, getWeights, subscribe as subscribeWeights, update as updateWeights, useAssignment, useInitialAssignments, useLayoutOrder, useSentient };
package/dist/index.js CHANGED
@@ -23,12 +23,14 @@ var src_exports = {};
23
23
  __export(src_exports, {
24
24
  Adaptive: () => Adaptive,
25
25
  AdaptiveProvider: () => AdaptiveProvider,
26
+ AdaptiveText: () => AdaptiveText,
26
27
  detectSegment: () => import_core2.deriveSessionSegment,
27
28
  getWeights: () => getWeights,
28
29
  subscribeWeights: () => subscribe,
29
30
  updateWeights: () => update,
30
31
  useAssignment: () => useAssignment,
31
32
  useInitialAssignments: () => useInitialAssignments,
33
+ useLayoutOrder: () => useLayoutOrder,
32
34
  useSentient: () => useSentient
33
35
  });
34
36
  module.exports = __toCommonJS(src_exports);
@@ -43,10 +45,11 @@ var AdaptiveContext = (0, import_react.createContext)({
43
45
  initialAssignments: {},
44
46
  sessionSegment: "desktop:direct",
45
47
  ssrFallback: "first",
46
- onAssignment: void 0
48
+ onAssignment: void 0,
49
+ initialLayoutOrder: null
47
50
  });
48
51
  function AdaptiveProvider(props) {
49
- var _a, _b, _c;
52
+ var _a, _b, _c, _d;
50
53
  const [client, setClient] = (0, import_react.useState)(null);
51
54
  (0, import_react.useEffect)(() => {
52
55
  if (props.consent === false) {
@@ -79,7 +82,8 @@ function AdaptiveProvider(props) {
79
82
  initialAssignments: (_b = props.initialAssignments) != null ? _b : {},
80
83
  sessionSegment: (_c = props.sessionSegment) != null ? _c : "desktop:direct",
81
84
  ssrFallback,
82
- onAssignment: props.onAssignment
85
+ onAssignment: props.onAssignment,
86
+ initialLayoutOrder: (_d = props.initialLayoutOrder) != null ? _d : null
83
87
  },
84
88
  children: props.children
85
89
  }
@@ -103,6 +107,9 @@ function useSsrFallback() {
103
107
  function useOnAssignment() {
104
108
  return (0, import_react.useContext)(AdaptiveContext).onAssignment;
105
109
  }
110
+ function useLayoutOrder() {
111
+ return (0, import_react.useContext)(AdaptiveContext).initialLayoutOrder;
112
+ }
106
113
 
107
114
  // src/adaptive.tsx
108
115
  var import_react3 = require("react");
@@ -436,18 +443,63 @@ var Adaptive = (0, import_react3.memo)(AdaptiveImpl, (prev, next) => {
436
443
  return prevKeys.every((k) => k in next.variants);
437
444
  });
438
445
 
446
+ // src/adaptive-text.tsx
447
+ var import_react4 = require("react");
448
+ var import_jsx_runtime3 = require("react/jsx-runtime");
449
+ function AdaptiveText({
450
+ id,
451
+ default: defaultText,
452
+ component: Tag = "span",
453
+ className
454
+ }) {
455
+ const client = useSentient();
456
+ const projectId = useAdaptiveProjectId();
457
+ const onAssignment = useOnAssignment();
458
+ const [text, setText] = (0, import_react4.useState)(null);
459
+ const [variantId, setVariantId] = (0, import_react4.useState)(null);
460
+ const trackedRef = (0, import_react4.useRef)(null);
461
+ (0, import_react4.useEffect)(() => {
462
+ if (!client) return;
463
+ let cancelled = false;
464
+ void client.assign(id).then((result) => {
465
+ if (cancelled || !result) return;
466
+ setVariantId(result.variantId);
467
+ if (result.content) setText(result.content);
468
+ });
469
+ return () => {
470
+ cancelled = true;
471
+ };
472
+ }, [client, id]);
473
+ (0, import_react4.useEffect)(() => {
474
+ if (!client || !variantId || !projectId) return;
475
+ if (trackedRef.current === variantId) return;
476
+ trackedRef.current = variantId;
477
+ client.track({
478
+ projectId,
479
+ componentId: id,
480
+ variantId,
481
+ eventType: "variant_assigned",
482
+ payload: {}
483
+ });
484
+ onAssignment == null ? void 0 : onAssignment(id, variantId);
485
+ }, [client, variantId, projectId, id, onAssignment]);
486
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Tag, { className, children: text != null ? text : defaultText });
487
+ }
488
+
439
489
  // src/segment.ts
440
490
  var import_core2 = require("@sentientui/core");
441
491
  // Annotate the CommonJS export names for ESM import in node:
442
492
  0 && (module.exports = {
443
493
  Adaptive,
444
494
  AdaptiveProvider,
495
+ AdaptiveText,
445
496
  detectSegment,
446
497
  getWeights,
447
498
  subscribeWeights,
448
499
  updateWeights,
449
500
  useAssignment,
450
501
  useInitialAssignments,
502
+ useLayoutOrder,
451
503
  useSentient
452
504
  });
453
505
  //# sourceMappingURL=index.js.map
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/segment.ts"],"sourcesContent":["export { AdaptiveProvider, useSentient, useInitialAssignments } 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 { useAssignment } from './use-assignment.js';\nexport type { AssignmentState } from './use-assignment.js';\n\nexport {\n subscribe as subscribeWeights,\n update as updateWeights,\n getWeights,\n} from './weights-store.js';\nexport type { ComponentWeights, VariantWeight } from './weights-store.js';\n\nexport { detectSegment } from './segment.js';\n","'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\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 children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n","'use client';\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveProjectId, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, projectId, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, projectId, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, projectId, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n const content = props.variants[variantId] ?? null;\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {content}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via ε-greedy and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], isLoading: false };\n }\n return { variantId: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n return { variantId: cached.variantId, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n // If the API is unreachable or returns null, the fallback set above stays.\n if (!result || !variantIds.includes(result.variantId)) return;\n setState({ variantId: result.variantId, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","_a","import_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\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveProjectId, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, projectId, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, projectId, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, projectId, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n const content = props.variants[variantId] ?? null;\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {content}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via ε-greedy and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], isLoading: false };\n }\n return { variantId: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n return { variantId: cached.variantId, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n // If the API is unreachable or returns null, the fallback set above stays.\n if (!result || !variantIds.includes(result.variantId)) return;\n setState({ variantId: result.variantId, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { AssignResult } from '@sentientui/core';\nimport { useSentient, useAdaptiveProjectId, useOnAssignment } from './provider.js';\n\nexport type AdaptiveTextProps = {\n id: string;\n default: string;\n component?: keyof JSX.IntrinsicElements;\n className?: string;\n};\n\nexport function AdaptiveText({\n id,\n default: defaultText,\n component: Tag = 'span',\n className,\n}: AdaptiveTextProps) {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const onAssignment = useOnAssignment();\n const [text, setText] = useState<string | null>(null);\n const [variantId, setVariantId] = useState<string | null>(null);\n const trackedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n void client.assign(id).then((result: AssignResult | null) => {\n if (cancelled || !result) return;\n setVariantId(result.variantId);\n if (result.content) setText(result.content);\n });\n return () => {\n cancelled = true;\n };\n }, [client, id]);\n\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (trackedRef.current === variantId) return;\n trackedRef.current = variantId;\n client.track({\n projectId,\n componentId: id,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n onAssignment?.(id, variantId);\n }, [client, variantId, projectId, id, onAssignment]);\n\n return <Tag className={className}>{text ?? defaultText}</Tag>;\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","_a","import_react","import_jsx_runtime","import_core"]}
package/dist/index.mjs CHANGED
@@ -10,10 +10,11 @@ var AdaptiveContext = createContext({
10
10
  initialAssignments: {},
11
11
  sessionSegment: "desktop:direct",
12
12
  ssrFallback: "first",
13
- onAssignment: void 0
13
+ onAssignment: void 0,
14
+ initialLayoutOrder: null
14
15
  });
15
16
  function AdaptiveProvider(props) {
16
- var _a, _b, _c;
17
+ var _a, _b, _c, _d;
17
18
  const [client, setClient] = useState(null);
18
19
  useEffect(() => {
19
20
  if (props.consent === false) {
@@ -46,7 +47,8 @@ function AdaptiveProvider(props) {
46
47
  initialAssignments: (_b = props.initialAssignments) != null ? _b : {},
47
48
  sessionSegment: (_c = props.sessionSegment) != null ? _c : "desktop:direct",
48
49
  ssrFallback,
49
- onAssignment: props.onAssignment
50
+ onAssignment: props.onAssignment,
51
+ initialLayoutOrder: (_d = props.initialLayoutOrder) != null ? _d : null
50
52
  },
51
53
  children: props.children
52
54
  }
@@ -70,6 +72,9 @@ function useSsrFallback() {
70
72
  function useOnAssignment() {
71
73
  return useContext(AdaptiveContext).onAssignment;
72
74
  }
75
+ function useLayoutOrder() {
76
+ return useContext(AdaptiveContext).initialLayoutOrder;
77
+ }
73
78
 
74
79
  // src/adaptive.tsx
75
80
  import { memo, useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState3 } from "react";
@@ -403,17 +408,62 @@ var Adaptive = memo(AdaptiveImpl, (prev, next) => {
403
408
  return prevKeys.every((k) => k in next.variants);
404
409
  });
405
410
 
411
+ // src/adaptive-text.tsx
412
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
413
+ import { jsx as jsx3 } from "react/jsx-runtime";
414
+ function AdaptiveText({
415
+ id,
416
+ default: defaultText,
417
+ component: Tag = "span",
418
+ className
419
+ }) {
420
+ const client = useSentient();
421
+ const projectId = useAdaptiveProjectId();
422
+ const onAssignment = useOnAssignment();
423
+ const [text, setText] = useState4(null);
424
+ const [variantId, setVariantId] = useState4(null);
425
+ const trackedRef = useRef3(null);
426
+ useEffect4(() => {
427
+ if (!client) return;
428
+ let cancelled = false;
429
+ void client.assign(id).then((result) => {
430
+ if (cancelled || !result) return;
431
+ setVariantId(result.variantId);
432
+ if (result.content) setText(result.content);
433
+ });
434
+ return () => {
435
+ cancelled = true;
436
+ };
437
+ }, [client, id]);
438
+ useEffect4(() => {
439
+ if (!client || !variantId || !projectId) return;
440
+ if (trackedRef.current === variantId) return;
441
+ trackedRef.current = variantId;
442
+ client.track({
443
+ projectId,
444
+ componentId: id,
445
+ variantId,
446
+ eventType: "variant_assigned",
447
+ payload: {}
448
+ });
449
+ onAssignment == null ? void 0 : onAssignment(id, variantId);
450
+ }, [client, variantId, projectId, id, onAssignment]);
451
+ return /* @__PURE__ */ jsx3(Tag, { className, children: text != null ? text : defaultText });
452
+ }
453
+
406
454
  // src/segment.ts
407
455
  import { deriveSessionSegment } from "@sentientui/core";
408
456
  export {
409
457
  Adaptive,
410
458
  AdaptiveProvider,
459
+ AdaptiveText,
411
460
  deriveSessionSegment as detectSegment,
412
461
  getWeights,
413
462
  subscribe as subscribeWeights,
414
463
  update as updateWeights,
415
464
  useAssignment,
416
465
  useInitialAssignments,
466
+ useLayoutOrder,
417
467
  useSentient
418
468
  };
419
469
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx","../src/adaptive.tsx","../src/use-assignment.ts","../src/weights-store.ts","../src/segment.ts"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\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 children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n","'use client';\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveProjectId, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, projectId, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, projectId, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, projectId, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n const content = props.variants[variantId] ?? null;\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {content}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via ε-greedy and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], isLoading: false };\n }\n return { variantId: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n return { variantId: cached.variantId, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n // If the API is unreachable or returns null, the fallback set above stays.\n if (!result || !variantIds.includes(result.variantId)) return;\n setState({ variantId: result.variantId, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":";;;AAEA,SAAS,eAAe,YAAY,WAAW,gBAAgC;AAC/E,SAAS,YAAsD;AAgG3D;AAlFJ,IAAM,kBAAkB,cAAoC;AAAA,EAC1D,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,oBAAoB,CAAC;AAAA,EACrB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAChB,CAAC;AA2CM,SAAS,iBAAiB,OAA2C;AAnE5E;AAoEE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,IAAI;AAEhE,YAAU,MAAM;AAEd,QAAI,MAAM,YAAY,OAAO;AAC3B,gBAAU,CAAC,SAAgC;AACzC,qCAAM;AACN,eAAO;AAAA,MACT,CAAC;AACD;AAAA,IACF;AAEA,UAAM,IAAI,KAAK;AAAA,MACb,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,oBAAoB,MAAM;AAAA,MAC1B,gBAAgB,MAAM;AAAA,MACtB,SAAS,MAAM;AAAA,IACjB,CAAC;AACD,cAAU,CAAC;AACX,WAAO,MAAM;AACX,QAAE,QAAQ;AAAA,IACZ;AAAA,EAGF,GAAG,CAAC,MAAM,OAAO,CAAC;AAElB,QAAM,eAAc,WAAM,gBAAN,YAAqB;AAEzC,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,OAAO;AAAA,QACL;AAAA,QACA,WAAW,MAAM;AAAA,QACjB,qBAAoB,WAAM,uBAAN,YAA4B,CAAC;AAAA,QACjD,iBAAgB,WAAM,mBAAN,YAAwB;AAAA,QACxC;AAAA,QACA,cAAc,MAAM;AAAA,MACtB;AAAA,MAEC,gBAAM;AAAA;AAAA,EACT;AAEJ;AAMO,SAAS,cAAqC;AACnD,SAAO,WAAW,eAAe,EAAE;AACrC;AAGO,SAAS,uBAA+B;AAC7C,SAAO,WAAW,eAAe,EAAE;AACrC;AAGO,SAAS,wBAAgD;AAC9D,SAAO,WAAW,eAAe,EAAE;AACrC;AAGO,SAAS,oBAA4B;AAC1C,SAAO,WAAW,eAAe,EAAE;AACrC;AAGO,SAAS,iBAA8B;AAC5C,SAAO,WAAW,eAAe,EAAE;AACrC;AAGO,SAAS,kBAAkF;AAChG,SAAO,WAAW,eAAe,EAAE;AACrC;;;AC/IA,SAAS,MAAM,aAAAA,YAAW,SAAS,UAAAC,SAAQ,YAAAC,iBAAgC;;;ACF3E,SAAS,aAAAC,YAAW,QAAQ,YAAAC,iBAAgB;;;ACgB5C,IAAM,QAAQ,oBAAI,IAA8B;AAChD,IAAM,YAAY,oBAAI,IAA2B;AAM1C,SAAS,UAAU,aAAqB,IAA0B;AACvE,MAAI,MAAM,UAAU,IAAI,WAAW;AACnC,MAAI,CAAC,KAAK;AACR,UAAM,oBAAI,IAAI;AACd,cAAU,IAAI,aAAa,GAAG;AAAA,EAChC;AACA,MAAI,IAAI,EAAE;AACV,SAAO,MAAM;AACX,QAAK,OAAO,EAAE;AACd,QAAI,IAAK,SAAS,EAAG,WAAU,OAAO,WAAW;AAAA,EACnD;AACF;AAMO,SAAS,OAAO,aAAqB,SAAiC;AAC3E,QAAM,IAAI,aAAa,OAAO;AAC9B,QAAM,MAAM,UAAU,IAAI,WAAW;AACrC,MAAI,CAAC,IAAK;AACV,aAAW,MAAM,KAAK;AACpB,QAAI;AACF,SAAG,OAAO;AAAA,IACZ,SAAQ;AAAA,IAER;AAAA,EACF;AACF;AAKO,SAAS,WAAW,aAA8C;AAxDzE;AAyDE,UAAO,WAAM,IAAI,WAAW,MAArB,YAA0B;AACnC;;;ADjDA,SAAS,eAAe,aAAoC;AAT5D;AAUE,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,UAAS,YAAO,yBAAP,mBAA8B;AAC7C,MAAI,OAAQ,QAAO;AACnB,MAAI;AACF,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,eAAW,OAAO,OAAO,OAAO,kBAAkB,GAAG;AAEnD,YAAM,MAAM,IAAI,QAAQ,GAAG;AAC3B,UAAI,QAAQ,GAAI;AAChB,UAAI,IAAI,MAAM,GAAG,GAAG,MAAM,YAAa,QAAO,IAAI,MAAM,MAAM,CAAC;AAAA,IACjE;AAAA,EACF,SAAQ;AAAA,EAAwB;AAChC,SAAO;AACT;AAOA,SAAS,gBAAgB,SAA2B,YAAqC;AA9BzF;AA+BE,MAAI,OAAwD;AAC5D,aAAW,KAAK,QAAQ,UAAU;AAChC,QAAI,CAAC,WAAW,SAAS,EAAE,SAAS,EAAG;AACvC,QAAI,CAAC,QAAQ,EAAE,YAAY,KAAK,WAAW;AACzC,aAAO,EAAE,WAAW,EAAE,WAAW,WAAW,EAAE,UAAU;AAAA,IAC1D;AAAA,EACF;AACA,UAAO,kCAAM,cAAN,YAAmB;AAC5B;AAWO,SAAS,cAAc,aAAqB,YAAuC;AACxF,QAAM,qBAAqB,sBAAsB;AACjD,QAAM,cAAc,eAAe;AACnC,QAAM,SAAS,YAAY;AAC3B,QAAM,UAAU,kBAAkB;AAClC,QAAM,eAAe,gBAAgB;AACrC,QAAM,wBAAwB,OAAsB,IAAI;AAIxD,QAAM,cAAc,eAAe,WAAW;AAC9C,QAAM,kBAAkB,eAAe,WAAW,SAAS,WAAW,IAAI,cAAc;AACxF,MAAI,iBAAiB;AACnB,YAAQ,KAAK,+BAA+B,WAAW,OAAO,eAAe,EAAE;AAAA,EACjF;AAEA,QAAM,WAAW,MAAM;AAlEzB;AAmEI,QAAI,iBAAiB;AACnB,aAAO,EAAE,WAAW,iBAAiB,WAAW,MAAM;AAAA,IACxD;AAGA,QAAI,CAAC,QAAQ;AACX,YAAM,YAAY,mBAAmB,WAAW;AAChD,UAAI,aAAa,WAAW,SAAS,SAAS,GAAG;AAC/C,eAAO,EAAE,WAAW,WAAW,WAAW,MAAM;AAAA,MAClD;AACA,UAAI,gBAAgB,WAAW,WAAW,SAAS,GAAG;AACpD,eAAO,EAAE,WAAW,WAAW,CAAC,GAAG,WAAW,MAAM;AAAA,MACtD;AACA,aAAO,EAAE,WAAW,MAAM,WAAW,KAAK;AAAA,IAC5C;AACA,UAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AACxD,QAAI,UAAU,WAAW,SAAS,OAAO,SAAS,GAAG;AACnD,aAAO,EAAE,WAAW,OAAO,WAAW,WAAW,MAAM;AAAA,IACzD;AACA,UAAM,UAAU,WAAW,WAAW;AACtC,QAAI,SAAS;AACX,YAAM,SAAS,gBAAgB,SAAS,UAAU;AAClD,UAAI,OAAQ,QAAO,EAAE,WAAW,QAAQ,WAAW,MAAM;AAAA,IAC3D;AACA,WAAO,EAAE,YAAW,gBAAW,CAAC,MAAZ,YAAiB,MAAM,WAAW,MAAM;AAAA,EAC9D,GAAG;AAEH,QAAM,CAAC,OAAO,QAAQ,IAAIC,UAA0B,OAAO;AAG3D,QAAM,mBAAmB,CAAC,cAA4B;AACpD,QAAI,CAAC,aAAc;AACnB,QAAI,sBAAsB,YAAY,UAAW;AACjD,0BAAsB,UAAU;AAChC,iBAAa,aAAa,SAAS;AAAA,EACrC;AAKA,EAAAC,WAAU,MAAM;AACd,QAAI,gBAAiB;AACrB,QAAI,CAAC,OAAQ;AACb,UAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AACxD,QAAI,UAAU,WAAW,SAAS,OAAO,SAAS,GAAG;AACnD,eAAS,EAAE,WAAW,OAAO,WAAW,WAAW,MAAM,CAAC;AAC1D,uBAAiB,OAAO,SAAS;AACjC;AAAA,IACF;AACA,aAAS,CAAC,SAAM;AApHpB;AAoHuB,kBAAK,YAAY,OAAO,EAAE,YAAW,gBAAW,CAAC,MAAZ,YAAiB,MAAM,WAAW,MAAM;AAAA,KAAC;AAAA,EAEnG,GAAG,CAAC,iBAAiB,QAAQ,aAAa,OAAO,CAAC;AAGlD,EAAAA,WAAU,MAAM;AACd,QAAI,gBAAiB;AACrB,QAAI,CAAC,OAAQ;AACb,UAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AACxD,QAAI,UAAU,WAAW,SAAS,OAAO,SAAS,EAAG;AAErD,QAAI,YAAY;AAChB,SAAK,OAAO,OAAO,aAAa,UAAU,EAAE,KAAK,CAAC,WAAgC;AAChF,UAAI,UAAW;AAEf,UAAI,CAAC,UAAU,CAAC,WAAW,SAAS,OAAO,SAAS,EAAG;AACvD,eAAS,EAAE,WAAW,OAAO,WAAW,WAAW,MAAM,CAAC;AAC1D,uBAAiB,OAAO,SAAS;AAAA,IACnC,CAAC;AACD,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAM;AAAA,EAGnC,GAAG,CAAC,iBAAiB,QAAQ,aAAa,OAAO,CAAC;AAGlD,EAAAA,WAAU,MAAM;AACd,QAAI,gBAAiB;AACrB,QAAI,CAAC,OAAQ;AACb,WAAO,UAAU,aAAa,CAAC,YAAY;AACzC,YAAM,SAAS,OAAO,cAAc,aAAa,OAAO;AACxD,UAAI,UAAU,WAAW,SAAS,OAAO,SAAS,GAAG;AACnD,iBAAS,EAAE,WAAW,OAAO,WAAW,WAAW,MAAM,CAAC;AAC1D;AAAA,MACF;AACA,YAAM,SAAS,gBAAgB,SAAS,UAAU;AAClD,UAAI,OAAQ,UAAS,EAAE,WAAW,QAAQ,WAAW,MAAM,CAAC;AAAA,IAC9D,CAAC;AAAA,EAEH,GAAG,CAAC,iBAAiB,QAAQ,aAAa,OAAO,CAAC;AAElD,MAAI,iBAAiB;AACnB,WAAO,EAAE,WAAW,iBAAiB,WAAW,MAAM;AAAA,EACxD;AAEA,SAAO;AACT;;;ADkDI,gBAAAC,YAAA;AA3LJ,SAAS,UAAU,MAAuC;AACxD,MAAI,OAAO,SAAS,SAAU,QAAO,EAAE,MAAM,QAAQ;AACrD,SAAO;AACT;AAEA,SAAS,kBAAkB,IAAiC;AAC1D,MAAI,EAAE,cAAc,SAAU,QAAO;AACrC,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,MAAI,QAAQ,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAO,GAAG,aAAa,MAAM;AACnC,SAAO,SAAS;AAClB;AAEA,SAAS,cAAc,OAAgB,WAA6B;AAClE,MAAI,SAAyB;AAC7B,SAAO,UAAU,WAAW,WAAW;AACrC,QAAI,kBAAkB,MAAM,EAAG,QAAO;AACtC,aAAS,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,OAA0C;AA9ChE;AA+CE,QAAM,SAAS,YAAY;AAC3B,QAAM,YAAY,qBAAqB;AACvC,QAAM,aAAa,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ,GAAG,CAAC,MAAM,QAAQ,CAAC;AAC9E,QAAM,EAAE,UAAU,IAAI,cAAc,MAAM,IAAI,UAAU;AACxD,QAAM,eAAeC,QAAuB,IAAI;AAChD,QAAM,CAAC,SAAS,UAAU,IAAIC,UAAS,KAAK;AAE5C,EAAAC,WAAU,MAAM;AAAE,eAAW,IAAI;AAAA,EAAG,GAAG,CAAC,CAAC;AACzC,QAAM,eAAeF,QAAO,KAAK;AACjC,QAAM,mBAAmBA,QAAsB,IAAI;AACnD,QAAM,OAAO,QAAQ,MAAM,UAAU,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC;AAC9D,QAAM,YAAY,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,KAAK;AAOrE,EAAAE,WAAU,MAAM;AAjElB,QAAAC,KAAA;AAkEI,QAAI,CAAC,UAAU,CAAC,aAAa,CAAC,UAAW;AACzC,QAAI,iBAAiB,YAAY,UAAW;AAC5C,UAAM,WAAU,MAAAA,MAAA,aAAa,YAAb,gBAAAA,IAAsB,cAAtB,YAAmC;AAEnD,QAAI,CAAC,WAAW,CAAC,QAAS;AAC1B,qBAAiB,UAAU;AAC3B,WAAO,MAAM;AAAA,MACX;AAAA,MACA,aAAa,MAAM;AAAA,MACnB;AAAA,MACA,WAAW;AAAA,MACX,SAAS,UAAU,EAAE,aAAa,QAAQ,MAAM,GAAG,GAAM,EAAE,IAAI,CAAC;AAAA,IAClE,CAAC;AAAA,EACH,GAAG,CAAC,QAAQ,WAAW,WAAW,MAAM,IAAI,OAAO,CAAC;AAGpD,EAAAD,WAAU,MAAM;AACd,iBAAa,UAAU;AAAA,EACzB,GAAG,CAAC,SAAS,CAAC;AAGd,EAAAA,WAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,UAAW;AAC3B,UAAM,OAAO,aAAa;AAC1B,QAAI,CAAC,KAAM;AAEX,QAAI,UAAgD;AACpD,QAAI,aAAa;AAEjB,UAAM,UAAU,MAAY;AAC1B,mBAAa,KAAK,IAAI;AACtB,gBAAU,WAAW,MAAM;AACzB,eAAO,MAAM;AAAA,UACX;AAAA,UACA,aAAa,MAAM;AAAA,UACnB;AAAA,UACA,WAAW;AAAA,UACX,SAAS,EAAE,eAAe,KAAK,IAAI,IAAI,WAAW;AAAA,QACpD,CAAC;AACD,kBAAU;AAAA,MACZ,GAAG,GAAG;AAAA,IACR;AAEA,UAAM,UAAU,MAAY;AAC1B,UAAI,YAAY,MAAM;AACpB,qBAAa,OAAO;AACpB,kBAAU;AAAA,MACZ;AAAA,IACF;AAEA,SAAK,iBAAiB,cAAc,OAAO;AAC3C,SAAK,iBAAiB,cAAc,OAAO;AAC3C,WAAO,MAAM;AACX,WAAK,oBAAoB,cAAc,OAAO;AAC9C,WAAK,oBAAoB,cAAc,OAAO;AAC9C,UAAI,YAAY,KAAM,cAAa,OAAO;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,QAAQ,WAAW,WAAW,MAAM,EAAE,CAAC;AAG3C,EAAAA,WAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,UAAW;AAC3B,UAAM,OAAO,aAAa;AAC1B,QAAI,CAAC,KAAM;AAEX,UAAM,WAAW,MAAY;AAC3B,UAAI,aAAa,QAAS;AAC1B,mBAAa,UAAU;AACvB,aAAO,MAAM;AAAA,QACX;AAAA,QACA,aAAa,MAAM;AAAA,QACnB;AAAA,QACA,WAAW;AAAA,QACX,UAAU;AAAA,QACV,SAAS,EAAE,QAAQ,EAAI;AAAA,MACzB,CAAC;AAAA,IACH;AAEA,UAAM,WAAyB,KAAK,SAAS,cAAc,KAAK,MAAM,CAAC,IAAI;AAC3E,UAAM,YAAY,IAAI,IAAY,SAAS,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;AAC3D,UAAM,iBAAiB,CAAC,QAAsB;AAC5C,gBAAU,OAAO,GAAG;AACpB,UAAI,UAAU,SAAS,EAAG,UAAS;AAAA,IACrC;AAEA,UAAM,WAA8B,CAAC;AAErC,aAAS,QAAQ,CAAC,KAAK,QAAQ;AAC7B,UAAI,IAAI,SAAS,SAAS;AACxB,cAAM,UAAU,CAAC,MAAwB;AACvC,gBAAM,SAAS,EAAE;AACjB,cAAI,EAAE,kBAAkB,SAAU;AAClC,cAAI,CAAC,cAAc,QAAQ,IAAI,EAAG;AAClC,cAAI,KAAK,SAAS,YAAa,gBAAe,GAAG;AAAA,cAC5C,UAAS;AAAA,QAChB;AACA,aAAK,iBAAiB,SAAS,OAAO;AACtC,iBAAS,KAAK,MAAM,KAAK,oBAAoB,SAAS,OAAO,CAAC;AAC9D;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,eAAe;AAC9B,cAAM,WAAW,CAAC,MAAmB;AACnC,cAAI,EAAE,EAAE,kBAAkB,iBAAkB;AAC5C,cAAI,CAAC,KAAK,SAAS,EAAE,MAAM,EAAG;AAC9B,cAAI,KAAK,SAAS,YAAa,gBAAe,GAAG;AAAA,cAC5C,UAAS;AAAA,QAChB;AACA,aAAK,iBAAiB,UAAU,QAAQ;AACxC,iBAAS,KAAK,MAAM,KAAK,oBAAoB,UAAU,QAAQ,CAAC;AAChE;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,gBAAgB;AAC/B,cAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,SAAS,CAAC;AACxD,cAAM,KAAK,IAAI;AAAA,UACb,CAAC,YAAY;AACX,uBAAW,SAAS,SAAS;AAC3B,kBAAI,MAAM,qBAAqB,WAAW;AACxC,oBAAI,KAAK,SAAS,YAAa,gBAAe,GAAG;AAAA,oBAC5C,UAAS;AACd,mBAAG,WAAW;AACd;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,UACA,EAAE,WAAW,CAAC,SAAS,EAAE;AAAA,QAC3B;AACA,WAAG,QAAQ,IAAI;AACf,iBAAS,KAAK,MAAM,GAAG,WAAW,CAAC;AACnC;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,iBAAW,KAAK,SAAU,GAAE;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,QAAQ,WAAW,WAAW,MAAM,IAAI,MAAM,SAAS,CAAC;AAG5D,MAAI,MAAM,eAAe,CAAC,WAAW,CAAC,QAAS,QAAO;AACtD,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,WAAU,WAAM,SAAS,SAAS,MAAxB,YAA6B;AAE7C,SACE,gBAAAH,KAAC,SAAI,KAAK,cAAc,oBAAkB,MAAM,IAAI,yBAAuB,WACxE,mBACH;AAEJ;AAGO,IAAM,WAAW,KAAK,cAAc,CAAC,MAAM,SAAS;AACzD,MAAI,KAAK,OAAO,KAAK,GAAI,QAAO;AAChC,MAAI,KAAK,SAAS,KAAK,KAAM,QAAO;AACpC,MAAI,KAAK,aAAa,KAAK,SAAU,QAAO;AAC5C,QAAM,WAAW,OAAO,KAAK,KAAK,QAAQ;AAC1C,QAAM,WAAW,OAAO,KAAK,KAAK,QAAQ;AAC1C,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,SAAO,SAAS,MAAM,CAAC,MAAM,KAAK,KAAK,QAAQ;AACjD,CAAC;;;AGjOD,SAAiC,4BAAqB;","names":["useEffect","useRef","useState","useEffect","useState","useState","useEffect","jsx","useRef","useState","useEffect","_a"]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/adaptive.tsx","../src/use-assignment.ts","../src/weights-store.ts","../src/adaptive-text.tsx","../src/segment.ts"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\nimport { memo, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { useAdaptiveProjectId, useSentient } from './provider.js';\nimport { useAssignment } from './use-assignment.js';\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number };\nexport type ClickGoal = { type: 'click'; selector?: string };\nexport type FormSubmitGoal = { type: 'form_submit' };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal;\n\nexport type AdaptiveProps = {\n id: string;\n variants: Record<string, ReactNode>;\n goal: string | GoalConfig;\n /**\n * When true, renders nothing during SSR and before client hydration.\n * Use when you cannot pass `initialAssignments` and prefer a blank slot over\n * a hydration mismatch. Tradeoff: minor CLS on first paint.\n */\n clientOnly?: boolean;\n};\n\nfunction normalize(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element): boolean {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nfunction AdaptiveImpl(props: AdaptiveProps): JSX.Element | null {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const variantIds = useMemo(() => Object.keys(props.variants), [props.variants]);\n const { variantId } = useAssignment(props.id, variantIds);\n const containerRef = useRef<HTMLDivElement>(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n const goalFiredRef = useRef(false);\n const assignTrackedRef = useRef<string | null>(null);\n const goal = useMemo(() => normalize(props.goal), [props.goal]);\n const goalLabel = typeof props.goal === 'string' ? props.goal : goal.type;\n\n // Track variant_assigned exactly once per (component, variant) mount.\n // Captures innerHTML as previewHtml so the dashboard can render a live preview\n // without requiring manual registry calls. First-seen wins on the server.\n // `mounted` is in deps so the effect re-runs after clientOnly components hydrate\n // and their container div is actually in the DOM.\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (assignTrackedRef.current === variantId) return;\n const rawHtml = containerRef.current?.innerHTML ?? '';\n // For clientOnly, the container is null until mounted — defer until it exists.\n if (!rawHtml && !mounted) return;\n assignTrackedRef.current = variantId;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'variant_assigned',\n payload: rawHtml ? { previewHtml: rawHtml.slice(0, 30_000) } : {},\n });\n }, [client, variantId, projectId, props.id, mounted]);\n\n // Reset goal latch when variant changes.\n useEffect(() => {\n goalFiredRef.current = false;\n }, [variantId]);\n\n // Emit cursor_signal after 800 ms of continuous hover.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n let timerId: ReturnType<typeof setTimeout> | null = null;\n let hoverStart = 0;\n\n const onEnter = (): void => {\n hoverStart = Date.now();\n timerId = setTimeout(() => {\n client.track({\n projectId,\n componentId: props.id,\n variantId: variantId!,\n eventType: 'cursor_signal',\n payload: { hoverDuration: Date.now() - hoverStart },\n });\n timerId = null;\n }, 800);\n };\n\n const onLeave = (): void => {\n if (timerId !== null) {\n clearTimeout(timerId);\n timerId = null;\n }\n };\n\n node.addEventListener('mouseenter', onEnter);\n node.addEventListener('mouseleave', onLeave);\n return () => {\n node.removeEventListener('mouseenter', onEnter);\n node.removeEventListener('mouseleave', onLeave);\n if (timerId !== null) clearTimeout(timerId);\n };\n }, [client, variantId, projectId, props.id]);\n\n // Attach goal tracking.\n useEffect(() => {\n if (!client || !variantId) return;\n const node = containerRef.current;\n if (!node) return;\n\n const fireGoal = (): void => {\n if (goalFiredRef.current) return;\n goalFiredRef.current = true;\n client.track({\n projectId,\n componentId: props.id,\n variantId,\n eventType: 'goal_achieved',\n goalType: goalLabel,\n payload: { reward: 1.0 },\n });\n };\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n }, [client, variantId, projectId, props.id, goal, goalLabel]);\n\n // Decorative slots: empty in SSR HTML and until the client has mounted.\n if (props.clientOnly && (!mounted || !client)) return null;\n if (!variantId) return null;\n const content = props.variants[variantId] ?? null;\n\n return (\n <div ref={containerRef} data-sentient-id={props.id} data-sentient-variant={variantId}>\n {content}\n </div>\n );\n}\n\n/** Re-renders only when the chosen variant changes. */\nexport const Adaptive = memo(AdaptiveImpl, (prev, next) => {\n if (prev.id !== next.id) return false;\n if (prev.goal !== next.goal) return false;\n if (prev.variants === next.variants) return true;\n const prevKeys = Object.keys(prev.variants);\n const nextKeys = Object.keys(next.variants);\n if (prevKeys.length !== nextKeys.length) return false;\n return prevKeys.every((k) => k in next.variants);\n});\n","import { useEffect, useRef, useState } from 'react';\nimport { type AssignResult } from '@sentientui/core';\nimport { useSentient, useInitialAssignments, useSessionSegment, useSsrFallback, useOnAssignment } from './provider.js';\nimport { subscribe, getWeights, type ComponentWeights } from './weights-store.js';\n\ndeclare global {\n interface Window { __sentient_overrides?: Record<string, string>; }\n}\n\nfunction getDevOverride(componentId: string): string | null {\n if (typeof window === 'undefined') return null;\n const global = window.__sentient_overrides?.[componentId];\n if (global) return global;\n try {\n const params = new URLSearchParams(window.location.search);\n for (const raw of params.getAll('sentient_variant')) {\n // sentient_variant=componentId:variantId (repeatable for multiple components)\n const sep = raw.indexOf(':');\n if (sep === -1) continue;\n if (raw.slice(0, sep) === componentId) return raw.slice(sep + 1);\n }\n } catch { /* non-browser env */ }\n return null;\n}\n\nexport type AssignmentState = {\n variantId: string | null;\n isLoading: boolean;\n};\n\nfunction pickFromWeights(weights: ComponentWeights, variantIds: string[]): string | null {\n let best: { variantId: string; avgReward: number } | null = null;\n for (const v of weights.variants) {\n if (!variantIds.includes(v.variantId)) continue;\n if (!best || v.avgReward > best.avgReward) {\n best = { variantId: v.variantId, avgReward: v.avgReward };\n }\n }\n return best?.variantId ?? null;\n}\n\n/**\n * Returns a sticky variant assignment for a component.\n *\n * First render reads the local SDK cache; if empty, falls back to a\n * deterministic default and asynchronously calls `/v1/assign`. The server\n * picks the actual variant via ε-greedy and the result replaces the fallback\n * on the next render. Subsequent paints read synchronously from cache —\n * no flicker, no loading state after first paint.\n */\nexport function useAssignment(componentId: string, variantIds: string[]): AssignmentState {\n const initialAssignments = useInitialAssignments();\n const ssrFallback = useSsrFallback();\n const client = useSentient();\n const segment = useSessionSegment();\n const onAssignment = useOnAssignment();\n const assignmentReportedRef = useRef<string | null>(null);\n\n // Dev override: URL param ?sentient_variant=componentId:variantId\n // or window.__sentient_overrides[componentId]. Short-circuits the bandit entirely.\n const devOverride = getDevOverride(componentId);\n const overrideVariant = devOverride && variantIds.includes(devOverride) ? devOverride : null;\n if (overrideVariant) {\n console.info(`[sentient] override active: ${componentId} -> ${overrideVariant}`);\n }\n\n const initial = (() => {\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n // SSR / pre-hydration: no client yet. Use initialAssignments if provided so\n // the server and client first render agree on the same variant (no mismatch).\n if (!client) {\n const preloaded = initialAssignments[componentId];\n if (preloaded && variantIds.includes(preloaded)) {\n return { variantId: preloaded, isLoading: false };\n }\n if (ssrFallback === 'first' && variantIds.length > 0) {\n return { variantId: variantIds[0], isLoading: false };\n }\n return { variantId: null, isLoading: true };\n }\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n return { variantId: cached.variantId, isLoading: false };\n }\n const weights = getWeights(componentId);\n if (weights) {\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) return { variantId: chosen, isLoading: false };\n }\n return { variantId: variantIds[0] ?? null, isLoading: false };\n })();\n\n const [state, setState] = useState<AssignmentState>(initial);\n\n // Helper: call onAssignment at most once per resolved variant.\n const reportAssignment = (variantId: string): void => {\n if (!onAssignment) return;\n if (assignmentReportedRef.current === variantId) return;\n assignmentReportedRef.current = variantId;\n onAssignment(componentId, variantId);\n };\n\n // As soon as the client is ready, unblock the UI immediately with variantIds[0]\n // (or a cached value) so the component never stays invisible while assign is\n // in-flight. The async assign call below then swaps to the bandit-chosen variant.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n reportAssignment(cached.variantId);\n return;\n }\n setState((prev) => prev.variantId ? prev : { variantId: variantIds[0] ?? null, isLoading: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Ask the server for a real assignment when we have a client but no cached one.\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) return;\n\n let cancelled = false;\n void client.assign(componentId, variantIds).then((result: AssignResult | null) => {\n if (cancelled) return;\n // If the API is unreachable or returns null, the fallback set above stays.\n if (!result || !variantIds.includes(result.variantId)) return;\n setState({ variantId: result.variantId, isLoading: false });\n reportAssignment(result.variantId);\n });\n return () => { cancelled = true; };\n // variantIds intentionally excluded — changing variants mid-mount is unsupported\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n // Live weight updates from the dashboard SSE stream (when wired).\n useEffect(() => {\n if (overrideVariant) return;\n if (!client) return;\n return subscribe(componentId, (weights) => {\n const cached = client.getAssignment(componentId, segment);\n if (cached && variantIds.includes(cached.variantId)) {\n setState({ variantId: cached.variantId, isLoading: false });\n return;\n }\n const chosen = pickFromWeights(weights, variantIds);\n if (chosen) setState({ variantId: chosen, isLoading: false });\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [overrideVariant, client, componentId, segment]);\n\n if (overrideVariant) {\n return { variantId: overrideVariant, isLoading: false };\n }\n\n return state;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { AssignResult } from '@sentientui/core';\nimport { useSentient, useAdaptiveProjectId, useOnAssignment } from './provider.js';\n\nexport type AdaptiveTextProps = {\n id: string;\n default: string;\n component?: keyof JSX.IntrinsicElements;\n className?: string;\n};\n\nexport function AdaptiveText({\n id,\n default: defaultText,\n component: Tag = 'span',\n className,\n}: AdaptiveTextProps) {\n const client = useSentient();\n const projectId = useAdaptiveProjectId();\n const onAssignment = useOnAssignment();\n const [text, setText] = useState<string | null>(null);\n const [variantId, setVariantId] = useState<string | null>(null);\n const trackedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n void client.assign(id).then((result: AssignResult | null) => {\n if (cancelled || !result) return;\n setVariantId(result.variantId);\n if (result.content) setText(result.content);\n });\n return () => {\n cancelled = true;\n };\n }, [client, id]);\n\n useEffect(() => {\n if (!client || !variantId || !projectId) return;\n if (trackedRef.current === variantId) return;\n trackedRef.current = variantId;\n client.track({\n projectId,\n componentId: id,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n onAssignment?.(id, variantId);\n }, [client, variantId, projectId, id, onAssignment]);\n\n return <Tag className={className}>{text ?? defaultText}</Tag>;\n}\n","/** @deprecated Use `useSessionSegment()` from the provider, or `deriveSessionSegment` from `@sentientui/core`. */\nexport { deriveSessionSegment as detectSegment } from '@sentientui/core';\n"],"mappings":"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","names":["useEffect","useRef","useState","useEffect","useState","useState","useEffect","jsx","useRef","useState","useEffect","_a","useEffect","useRef","useState","jsx","useState","useRef","useEffect"]}
@@ -37,6 +37,12 @@ type AdaptiveProviderProps = {
37
37
  * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.
38
38
  */
39
39
  onAssignment?: (componentId: string, variantId: string) => void;
40
+ /**
41
+ * SSR-preloaded section order from `loadAdaptiveDecision()`.
42
+ * Pass the `layoutOrder` field from `DecideResult`. When set,
43
+ * `useLayoutOrder()` returns this on first render so there is no layout shift.
44
+ */
45
+ initialLayoutOrder?: string[] | null;
40
46
  children: ReactNode;
41
47
  };
42
48
 
@@ -27,10 +27,11 @@ var AdaptiveContext = createContext({
27
27
  initialAssignments: {},
28
28
  sessionSegment: "desktop:direct",
29
29
  ssrFallback: "first",
30
- onAssignment: void 0
30
+ onAssignment: void 0,
31
+ initialLayoutOrder: null
31
32
  });
32
33
  function AdaptiveProvider(props) {
33
- var _a, _b, _c;
34
+ var _a, _b, _c, _d;
34
35
  const [client, setClient] = useState(null);
35
36
  useEffect(() => {
36
37
  if (props.consent === false) {
@@ -63,7 +64,8 @@ function AdaptiveProvider(props) {
63
64
  initialAssignments: (_b = props.initialAssignments) != null ? _b : {},
64
65
  sessionSegment: (_c = props.sessionSegment) != null ? _c : "desktop:direct",
65
66
  ssrFallback,
66
- onAssignment: props.onAssignment
67
+ onAssignment: props.onAssignment,
68
+ initialLayoutOrder: (_d = props.initialLayoutOrder) != null ? _d : null
67
69
  },
68
70
  children: props.children
69
71
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/provider.tsx","../../src/next/adaptive-root-client.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\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 children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n","'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":";;;;;;;;;;;;;;;;;;;;AAEA,SAAS,eAAe,YAAY,WAAW,gBAAgC;AAC/E,SAAS,YAAsD;AAgG3D;AAlFJ,IAAM,kBAAkB,cAAoC;AAAA,EAC1D,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,oBAAoB,CAAC;AAAA,EACrB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAChB,CAAC;AA2CM,SAAS,iBAAiB,OAA2C;AAnE5E;AAoEE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,IAAI;AAEhE,YAAU,MAAM;AAEd,QAAI,MAAM,YAAY,OAAO;AAC3B,gBAAU,CAAC,SAAgC;AACzC,qCAAM;AACN,eAAO;AAAA,MACT,CAAC;AACD;AAAA,IACF;AAEA,UAAM,IAAI,KAAK;AAAA,MACb,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,oBAAoB,MAAM;AAAA,MAC1B,gBAAgB,MAAM;AAAA,MACtB,SAAS,MAAM;AAAA,IACjB,CAAC;AACD,cAAU,CAAC;AACX,WAAO,MAAM;AACX,QAAE,QAAQ;AAAA,IACZ;AAAA,EAGF,GAAG,CAAC,MAAM,OAAO,CAAC;AAElB,QAAM,eAAc,WAAM,gBAAN,YAAqB;AAEzC,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,OAAO;AAAA,QACL;AAAA,QACA,WAAW,MAAM;AAAA,QACjB,qBAAoB,WAAM,uBAAN,YAA4B,CAAC;AAAA,QACjD,iBAAgB,WAAM,mBAAN,YAAwB;AAAA,QACxC;AAAA,QACA,cAAc,MAAM;AAAA,MACtB;AAAA,MAEC,gBAAM;AAAA;AAAA,EACT;AAEJ;;;ACrGS,gBAAAA,YAAA;AADF,SAAS,mBAAmB,OAA6C;AAC9E,SAAO,gBAAAA,KAAC,qCAAqB,MAAO;AACtC;","names":["jsx"]}
1
+ {"version":3,"sources":["../../src/provider.tsx","../../src/next/adaptive-root-client.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { init, type SentientClient, type SentientConfig } from '@sentientui/core';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n projectId: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n projectId: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n\n useEffect(() => {\n // When consent is explicitly false, tear down any existing client.\n if (props.consent === false) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const c = init({\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment: props.sessionSegment,\n consent: props.consent,\n });\n setClient(c);\n return () => {\n c.destroy();\n };\n // Re-init when consent changes. Other props are intentionally stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n return (\n <AdaptiveContext.Provider\n value={{\n client,\n projectId: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment: props.sessionSegment ?? 'desktop:direct',\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }}\n >\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: project identifier carried alongside the client. */\nexport function useAdaptiveProjectId(): string {\n return useContext(AdaptiveContext).projectId;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n return useContext(AdaptiveContext).initialLayoutOrder;\n}\n","'use client';\n\nimport 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":";;;;;;;;;;;;;;;;;;;;AAEA,SAAS,eAAe,YAAY,WAAW,gBAAgC;AAC/E,SAAS,YAAsD;AAwG3D;AAzFJ,IAAM,kBAAkB,cAAoC;AAAA,EAC1D,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,oBAAoB,CAAC;AAAA,EACrB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AACtB,CAAC;AAiDM,SAAS,iBAAiB,OAA2C;AA3E5E;AA4EE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,IAAI;AAEhE,YAAU,MAAM;AAEd,QAAI,MAAM,YAAY,OAAO;AAC3B,gBAAU,CAAC,SAAgC;AACzC,qCAAM;AACN,eAAO;AAAA,MACT,CAAC;AACD;AAAA,IACF;AAEA,UAAM,IAAI,KAAK;AAAA,MACb,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,oBAAoB,MAAM;AAAA,MAC1B,gBAAgB,MAAM;AAAA,MACtB,SAAS,MAAM;AAAA,IACjB,CAAC;AACD,cAAU,CAAC;AACX,WAAO,MAAM;AACX,QAAE,QAAQ;AAAA,IACZ;AAAA,EAGF,GAAG,CAAC,MAAM,OAAO,CAAC;AAElB,QAAM,eAAc,WAAM,gBAAN,YAAqB;AAEzC,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,OAAO;AAAA,QACL;AAAA,QACA,WAAW,MAAM;AAAA,QACjB,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;;;AC9GS,gBAAAA,YAAA;AADF,SAAS,mBAAmB,OAA6C;AAC9E,SAAO,gBAAAA,KAAC,qCAAqB,MAAO;AACtC;","names":["jsx"]}
@@ -37,6 +37,12 @@ type AdaptiveProviderProps = {
37
37
  * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.
38
38
  */
39
39
  onAssignment?: (componentId: string, variantId: string) => void;
40
+ /**
41
+ * SSR-preloaded section order from `loadAdaptiveDecision()`.
42
+ * Pass the `layoutOrder` field from `DecideResult`. When set,
43
+ * `useLayoutOrder()` returns this on first render so there is no layout shift.
44
+ */
45
+ initialLayoutOrder?: string[] | null;
40
46
  children: ReactNode;
41
47
  };
42
48
 
@@ -45,44 +51,55 @@ type PreloadComponent = {
45
51
  variantIds: string[];
46
52
  };
47
53
  type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments'> & {
48
- /** Slots to resolve on the server before HTML is sent (SEO-safe). */
54
+ /** Components to assign server-side (SEO-safe). */
49
55
  components: PreloadComponent[];
50
- /** Server-only key for `/v1/sessions` and `/v1/assign` (usually the same `pk_` as `apiKey`). */
51
- serverApiKey: string;
52
56
  /**
53
- * App origin for SSR preload requests (must be in the project's `allowed_origins`).
54
- * @default `http://localhost:3001` in development
57
+ * Declare page section IDs in their default order. When provided,
58
+ * AdaptiveRoot calls `/v1/decide` (single round trip) instead of
59
+ * individual `/v1/assign` calls, and `useLayoutOrder()` returns the
60
+ * persona-specific order on first render.
61
+ *
62
+ * @example sections={['hero', 'pricing', 'features', 'social_proof']}
55
63
  */
64
+ sections?: string[];
65
+ /** Server-only API key for `/v1/sessions`, `/v1/assign`, and `/v1/decide`. */
66
+ serverApiKey: string;
67
+ /** App origin — must be in the project's `allowed_origins`. */
56
68
  appOrigin?: string;
57
- /**
58
- * Optional preloaded map; when set, `components` is not fetched (useful for tests
59
- * or custom loaders). Normally omit and let `loadAdaptiveAssignments` run.
60
- */
69
+ /** Override: when set no network fetch is made. Useful for tests. */
61
70
  initialAssignments?: ServerAssignments;
62
71
  children: ReactNode;
63
72
  };
64
73
  /**
65
- * Server Component wrapper: loads assignments, then renders `AdaptiveProvider`
66
- * with `initialAssignments` so crawlers and hydration see real variant content.
74
+ * Next.js Server Component that resolves variant assignments (and optionally
75
+ * section layout order) server-side for zero layout shift on first paint.
76
+ *
77
+ * When `sections` is provided, a single `POST /v1/decide` call returns both
78
+ * the persona-specific section order and all component assignments.
79
+ * Without `sections`, individual `/v1/assign` calls are made per component.
67
80
  *
68
81
  * @example
69
- * ```tsx
70
- * // app/layout.tsx
82
+ * // app/page.tsx — with section layout
71
83
  * import { AdaptiveRoot } from '@sentientui/react/next';
72
84
  *
73
- * export default function Layout({ children }) {
85
+ * export default async function Page() {
74
86
  * return (
75
87
  * <AdaptiveRoot
76
- * components={[{ id: 'hero_cta', variantIds: ['default', 'accent'] }]}
77
- * serverApiKey={process.env.SENTIENT_API_KEY!}
78
88
  * apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
79
- * context="saas"
89
+ * serverApiKey={process.env.SENTIENT_SECRET_KEY!}
90
+ * context={{ appId: 'my-app' }}
91
+ * sections={['hero', 'pricing', 'features', 'social_proof']}
92
+ * components={[
93
+ * { id: 'hero_cta', variantIds: ['default', 'accent'] },
94
+ * ]}
80
95
  * >
81
- * {children}
96
+ * <HeroSection />
97
+ * <PricingSection />
98
+ * <FeaturesSection />
99
+ * <SocialProofSection />
82
100
  * </AdaptiveRoot>
83
101
  * );
84
102
  * }
85
- * ```
86
103
  */
87
104
  declare function AdaptiveRoot(props: AdaptiveRootProps): Promise<JSX.Element>;
88
105
 
@@ -39,6 +39,7 @@ import {
39
39
  preloadAssignments,
40
40
  readSessionCookie
41
41
  } from "@sentientui/core";
42
+ import { preloadDecisions } from "@sentientui/core";
42
43
  function defaultSessionId() {
43
44
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
44
45
  return crypto.randomUUID();
@@ -56,6 +57,25 @@ async function loadAdaptiveAssignments(components, options) {
56
57
  referer: options.referer
57
58
  });
58
59
  }
60
+ async function loadAdaptiveDecision(options) {
61
+ var _a, _b, _c, _d;
62
+ const { preloadDecisions: preloadDecisions2, readSessionCookie: readSessionCookie2 } = await import("@sentientui/core");
63
+ const sessionId = (_c = (_b = readSessionCookie2(options.cookies)) != null ? _b : (_a = options.createSessionId) == null ? void 0 : _a.call(options)) != null ? _c : defaultSessionId();
64
+ return preloadDecisions2(
65
+ {
66
+ sections: options.sections,
67
+ components: (_d = options.components) != null ? _d : []
68
+ },
69
+ sessionId,
70
+ {
71
+ apiKey: options.apiKey,
72
+ baseUrl: options.baseUrl,
73
+ origin: options.origin,
74
+ userAgent: options.userAgent,
75
+ referer: options.referer
76
+ }
77
+ );
78
+ }
59
79
 
60
80
  // src/next/adaptive-root.tsx
61
81
  import { AdaptiveRootClient } from "./adaptive-root-client.js";
@@ -65,12 +85,14 @@ async function AdaptiveRoot(props) {
65
85
  var _b, _c;
66
86
  const _a = props, {
67
87
  components,
88
+ sections,
68
89
  serverApiKey,
69
90
  appOrigin,
70
91
  initialAssignments: initialAssignmentsOverride,
71
92
  children
72
93
  } = _a, providerProps = __objRest(_a, [
73
94
  "components",
95
+ "sections",
74
96
  "serverApiKey",
75
97
  "appOrigin",
76
98
  "initialAssignments",
@@ -81,18 +103,39 @@ async function AdaptiveRoot(props) {
81
103
  const userAgent = (_b = headerStore.get("user-agent")) != null ? _b : void 0;
82
104
  const referer = (_c = headerStore.get("referer")) != null ? _c : void 0;
83
105
  const sessionSegment = deriveSessionSegment({ userAgent, referer, appOrigin: resolvedOrigin });
84
- const initialAssignments = initialAssignmentsOverride != null ? initialAssignmentsOverride : await loadAdaptiveAssignments(components, {
85
- cookies: await cookies(),
86
- apiKey: serverApiKey,
87
- baseUrl: DEFAULT_API_BASE_URL,
88
- origin: resolvedOrigin,
89
- userAgent,
90
- referer
91
- });
106
+ const cookieStore = await cookies();
107
+ let initialAssignments;
108
+ let initialLayoutOrder = null;
109
+ if (initialAssignmentsOverride) {
110
+ initialAssignments = initialAssignmentsOverride;
111
+ } else if (sections && sections.length > 0) {
112
+ const decision = await loadAdaptiveDecision({
113
+ sections,
114
+ components,
115
+ cookies: cookieStore,
116
+ apiKey: serverApiKey,
117
+ baseUrl: DEFAULT_API_BASE_URL,
118
+ origin: resolvedOrigin,
119
+ userAgent,
120
+ referer
121
+ });
122
+ initialAssignments = decision.assignments;
123
+ initialLayoutOrder = decision.layoutOrder;
124
+ } else {
125
+ initialAssignments = await loadAdaptiveAssignments(components, {
126
+ cookies: cookieStore,
127
+ apiKey: serverApiKey,
128
+ baseUrl: DEFAULT_API_BASE_URL,
129
+ origin: resolvedOrigin,
130
+ userAgent,
131
+ referer
132
+ });
133
+ }
92
134
  return /* @__PURE__ */ jsx(
93
135
  AdaptiveRootClient,
94
136
  __spreadProps(__spreadValues({}, providerProps), {
95
137
  initialAssignments,
138
+ initialLayoutOrder,
96
139
  sessionSegment,
97
140
  children
98
141
  })
@@ -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 { loadAdaptiveAssignments, type ServerAssignments } from '../server.js';\nimport { AdaptiveRootClient } from './adaptive-root-client.js';\n\nconst DEFAULT_API_BASE_URL = 'https://sentient-api.fly.dev/v1';\n\nexport type PreloadComponent = { id: string; variantIds: string[] };\n\nexport type AdaptiveRootProps = Omit<AdaptiveProviderProps, 'initialAssignments'> & {\n /** Slots to resolve on the server before HTML is sent (SEO-safe). */\n components: PreloadComponent[];\n /** Server-only key for `/v1/sessions` and `/v1/assign` (usually the same `pk_` as `apiKey`). */\n serverApiKey: string;\n /**\n * App origin for SSR preload requests (must be in the project's `allowed_origins`).\n * @default `http://localhost:3001` in development\n */\n appOrigin?: string;\n /**\n * Optional preloaded map; when set, `components` is not fetched (useful for tests\n * or custom loaders). Normally omit and let `loadAdaptiveAssignments` run.\n */\n initialAssignments?: ServerAssignments;\n children: ReactNode;\n};\n\n/**\n * Server Component wrapper: loads assignments, then renders `AdaptiveProvider`\n * with `initialAssignments` so crawlers and hydration see real variant content.\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { AdaptiveRoot } from '@sentientui/react/next';\n *\n * export default function Layout({ children }) {\n * return (\n * <AdaptiveRoot\n * components={[{ id: 'hero_cta', variantIds: ['default', 'accent'] }]}\n * serverApiKey={process.env.SENTIENT_API_KEY!}\n * apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}\n * context=\"saas\"\n * >\n * {children}\n * </AdaptiveRoot>\n * );\n * }\n * ```\n */\nexport async function AdaptiveRoot(props: AdaptiveRootProps): Promise<JSX.Element> {\n const {\n components,\n serverApiKey,\n appOrigin,\n initialAssignments: initialAssignmentsOverride,\n children,\n ...providerProps\n } = props;\n\n const headerStore = await headers();\n const resolvedOrigin = appOrigin ?? '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 initialAssignments =\n initialAssignmentsOverride ??\n (await loadAdaptiveAssignments(components, {\n cookies: await cookies(),\n apiKey: serverApiKey,\n baseUrl: DEFAULT_API_BASE_URL,\n origin: resolvedOrigin,\n userAgent,\n referer,\n }));\n\n return (\n <AdaptiveRootClient\n {...providerProps}\n initialAssignments={initialAssignments}\n sessionSegment={sessionSegment}\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';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\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};\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 the result as `initialAssignments` on `<AdaptiveProvider>`.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<ServerAssignments> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,4BAA4B;AACrC,SAAS,SAAS,eAAe;;;ACEjC;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAoBP,SAAS,mBAA2B;AAClC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACrE;AAMA,eAAsB,wBACpB,YACA,SAC4B;AA1C9B;AA2CE,QAAM,aACJ,6BAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,SAAO,mBAAmB,YAAY,WAAW;AAAA,IAC/C,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ;AAAA,EACnB,CAAC;AACH;;;ADlDA,SAAS,0BAA0B;AA0E/B;AAxEJ,IAAM,uBAAuB;AA6C7B,eAAsB,aAAa,OAAgD;AApDnF;AAqDE,QAOI,YANF;AAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,EA1DJ,IA4DM,IADC,0BACD,IADC;AAAA,IALH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAIF,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,iBAAiB,gCAAa;AACpC,QAAM,aAAY,iBAAY,IAAI,YAAY,MAA5B,YAAiC;AACnD,QAAM,WAAU,iBAAY,IAAI,SAAS,MAAzB,YAA8B;AAC9C,QAAM,iBAAiB,qBAAqB,EAAE,WAAW,SAAS,WAAW,eAAe,CAAC;AAC7F,QAAM,qBACJ,kEACC,MAAM,wBAAwB,YAAY;AAAA,IACzC,SAAS,MAAM,QAAQ;AAAA,IACvB,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,EACF,CAAC;AAEH,SACE;AAAA,IAAC;AAAA,qCACK,gBADL;AAAA,MAEC;AAAA,MACA;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;","names":[]}
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://sentient-api.fly.dev/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 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 children,\n ...providerProps\n } = props;\n\n const headerStore = await headers();\n const resolvedOrigin = appOrigin ?? '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\n if (initialAssignmentsOverride) {\n initialAssignments = initialAssignmentsOverride;\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 });\n initialAssignments = decision.assignments;\n initialLayoutOrder = decision.layoutOrder;\n } else {\n initialAssignments = await loadAdaptiveAssignments(components, {\n cookies: cookieStore,\n apiKey: serverApiKey,\n baseUrl: DEFAULT_API_BASE_URL,\n origin: resolvedOrigin,\n userAgent,\n referer,\n });\n }\n\n return (\n <AdaptiveRootClient\n {...providerProps}\n initialAssignments={initialAssignments}\n initialLayoutOrder={initialLayoutOrder}\n sessionSegment={sessionSegment}\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';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\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};\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 the result as `initialAssignments` on `<AdaptiveProvider>`.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<ServerAssignments> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n });\n}\n\nexport { preloadDecisions, type DecideResult } from '@sentientui/core';\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 */\nexport async function loadAdaptiveDecision(\n options: LoadAdaptiveDecisionOptions,\n): Promise<import('@sentientui/core').DecideResult> {\n const { preloadDecisions, readSessionCookie } = await import('@sentientui/core');\n\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return 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 },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,4BAA4B;AACrC,SAAS,SAAS,eAAe;;;ACEjC;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAiDP,SAAS,wBAA2C;AA7BpD,SAAS,mBAA2B;AAClC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACrE;AAMA,eAAsB,wBACpB,YACA,SAC4B;AA1C9B;AA2CE,QAAM,aACJ,6BAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,SAAO,mBAAmB,YAAY,WAAW;AAAA,IAC/C,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ;AAAA,EACnB,CAAC;AACH;AAeA,eAAsB,qBACpB,SACkD;AAxEpD;AAyEE,QAAM,EAAE,kBAAAA,mBAAkB,mBAAAC,mBAAkB,IAAI,MAAM,OAAO,kBAAkB;AAE/E,QAAM,aACJ,WAAAA,mBAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,SAAOD;AAAA,IACL;AAAA,MACE,UAAU,QAAQ;AAAA,MAClB,aAAY,aAAQ,eAAR,YAAsB,CAAC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB,SAAS,QAAQ;AAAA,MACjB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;;;ADrFA,SAAS,0BAA0B;AA0G/B;AAxGJ,IAAM,uBAAuB;AAwD7B,eAAsB,aAAa,OAAgD;AAnEnF;AAoEE,QAQI,YAPF;AAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,EA1EJ,IA4EM,IADC,0BACD,IADC;AAAA,IANH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAIF,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,iBAAiB,gCAAa;AACpC,QAAM,aAAY,iBAAY,IAAI,YAAY,MAA5B,YAAiC;AACnD,QAAM,WAAU,iBAAY,IAAI,SAAS,MAAzB,YAA8B;AAC9C,QAAM,iBAAiB,qBAAqB,EAAE,WAAW,SAAS,WAAW,eAAe,CAAC;AAC7F,QAAM,cAAc,MAAM,QAAQ;AAElC,MAAI;AACJ,MAAI,qBAAsC;AAE1C,MAAI,4BAA4B;AAC9B,yBAAqB;AAAA,EACvB,WAAW,YAAY,SAAS,SAAS,GAAG;AAC1C,UAAM,WAAW,MAAM,qBAAqB;AAAA,MAC1C;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACF,CAAC;AACD,yBAAqB,SAAS;AAC9B,yBAAqB,SAAS;AAAA,EAChC,OAAO;AACL,yBAAqB,MAAM,wBAAwB,YAAY;AAAA,MAC7D,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SACE;AAAA,IAAC;AAAA,qCACK,gBADL;AAAA,MAEC;AAAA,MACA;AAAA,MACA;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;","names":["preloadDecisions","readSessionCookie"]}
package/dist/server.d.cts CHANGED
@@ -1,9 +1,6 @@
1
+ import * as _sentientui_core from '@sentientui/core';
1
2
  import { ServerAssignments } from '@sentientui/core';
2
- export { ServerAssignConfig, ServerAssignments, preloadAssignments, readSessionCookie } from '@sentientui/core';
3
-
4
- /**
5
- * Server-only helpers for Next.js / SSR. No React or DOM APIs.
6
- */
3
+ export { DecideResult, ServerAssignConfig, ServerAssignments, preloadAssignments, preloadDecisions, readSessionCookie } from '@sentientui/core';
7
4
 
8
5
  type LoadAdaptiveAssignmentsOptions = {
9
6
  /** Next.js `cookies()` return value, or any object with `get(name)`. */
@@ -32,4 +29,19 @@ declare function loadAdaptiveAssignments(components: Array<{
32
29
  variantIds: string[];
33
30
  }>, options: LoadAdaptiveAssignmentsOptions): Promise<ServerAssignments>;
34
31
 
35
- export { type LoadAdaptiveAssignmentsOptions, loadAdaptiveAssignments };
32
+ type LoadAdaptiveDecisionOptions = LoadAdaptiveAssignmentsOptions & {
33
+ /** Section IDs in default order. Passed to /v1/decide as the candidate layout. */
34
+ sections: string[];
35
+ /** Components to assign in the same decide call. */
36
+ components?: Array<{
37
+ id: string;
38
+ variantIds?: string[];
39
+ }>;
40
+ };
41
+ /**
42
+ * SSR helper for pages with a declared section layout. Calls `/v1/decide`
43
+ * instead of multiple `/v1/assign` round trips.
44
+ */
45
+ declare function loadAdaptiveDecision(options: LoadAdaptiveDecisionOptions): Promise<_sentientui_core.DecideResult>;
46
+
47
+ export { type LoadAdaptiveAssignmentsOptions, type LoadAdaptiveDecisionOptions, loadAdaptiveAssignments, loadAdaptiveDecision };
package/dist/server.d.ts CHANGED
@@ -1,9 +1,6 @@
1
+ import * as _sentientui_core from '@sentientui/core';
1
2
  import { ServerAssignments } from '@sentientui/core';
2
- export { ServerAssignConfig, ServerAssignments, preloadAssignments, readSessionCookie } from '@sentientui/core';
3
-
4
- /**
5
- * Server-only helpers for Next.js / SSR. No React or DOM APIs.
6
- */
3
+ export { DecideResult, ServerAssignConfig, ServerAssignments, preloadAssignments, preloadDecisions, readSessionCookie } from '@sentientui/core';
7
4
 
8
5
  type LoadAdaptiveAssignmentsOptions = {
9
6
  /** Next.js `cookies()` return value, or any object with `get(name)`. */
@@ -32,4 +29,19 @@ declare function loadAdaptiveAssignments(components: Array<{
32
29
  variantIds: string[];
33
30
  }>, options: LoadAdaptiveAssignmentsOptions): Promise<ServerAssignments>;
34
31
 
35
- export { type LoadAdaptiveAssignmentsOptions, loadAdaptiveAssignments };
32
+ type LoadAdaptiveDecisionOptions = LoadAdaptiveAssignmentsOptions & {
33
+ /** Section IDs in default order. Passed to /v1/decide as the candidate layout. */
34
+ sections: string[];
35
+ /** Components to assign in the same decide call. */
36
+ components?: Array<{
37
+ id: string;
38
+ variantIds?: string[];
39
+ }>;
40
+ };
41
+ /**
42
+ * SSR helper for pages with a declared section layout. Calls `/v1/decide`
43
+ * instead of multiple `/v1/assign` round trips.
44
+ */
45
+ declare function loadAdaptiveDecision(options: LoadAdaptiveDecisionOptions): Promise<_sentientui_core.DecideResult>;
46
+
47
+ export { type LoadAdaptiveAssignmentsOptions, type LoadAdaptiveDecisionOptions, loadAdaptiveAssignments, loadAdaptiveDecision };
package/dist/server.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,17 +17,28 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/server.ts
21
31
  var server_exports = {};
22
32
  __export(server_exports, {
23
33
  loadAdaptiveAssignments: () => loadAdaptiveAssignments,
34
+ loadAdaptiveDecision: () => loadAdaptiveDecision,
24
35
  preloadAssignments: () => import_core.preloadAssignments,
36
+ preloadDecisions: () => import_core2.preloadDecisions,
25
37
  readSessionCookie: () => import_core.readSessionCookie
26
38
  });
27
39
  module.exports = __toCommonJS(server_exports);
28
40
  var import_core = require("@sentientui/core");
41
+ var import_core2 = require("@sentientui/core");
29
42
  function defaultSessionId() {
30
43
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
31
44
  return crypto.randomUUID();
@@ -43,10 +56,31 @@ async function loadAdaptiveAssignments(components, options) {
43
56
  referer: options.referer
44
57
  });
45
58
  }
59
+ async function loadAdaptiveDecision(options) {
60
+ var _a, _b, _c, _d;
61
+ const { preloadDecisions: preloadDecisions2, readSessionCookie: readSessionCookie2 } = await import("@sentientui/core");
62
+ const sessionId = (_c = (_b = readSessionCookie2(options.cookies)) != null ? _b : (_a = options.createSessionId) == null ? void 0 : _a.call(options)) != null ? _c : defaultSessionId();
63
+ return preloadDecisions2(
64
+ {
65
+ sections: options.sections,
66
+ components: (_d = options.components) != null ? _d : []
67
+ },
68
+ sessionId,
69
+ {
70
+ apiKey: options.apiKey,
71
+ baseUrl: options.baseUrl,
72
+ origin: options.origin,
73
+ userAgent: options.userAgent,
74
+ referer: options.referer
75
+ }
76
+ );
77
+ }
46
78
  // Annotate the CommonJS export names for ESM import in node:
47
79
  0 && (module.exports = {
48
80
  loadAdaptiveAssignments,
81
+ loadAdaptiveDecision,
49
82
  preloadAssignments,
83
+ preloadDecisions,
50
84
  readSessionCookie
51
85
  });
52
86
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\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';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\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};\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 the result as `initialAssignments` on `<AdaptiveProvider>`.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<ServerAssignments> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,kBAKO;AAoBP,SAAS,mBAA2B;AAClC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACrE;AAMA,eAAsB,wBACpB,YACA,SAC4B;AA1C9B;AA2CE,QAAM,aACJ,8CAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,aAAO,gCAAmB,YAAY,WAAW;AAAA,IAC/C,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ;AAAA,EACnB,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\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';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\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};\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 the result as `initialAssignments` on `<AdaptiveProvider>`.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<ServerAssignments> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n });\n}\n\nexport { preloadDecisions, type DecideResult } from '@sentientui/core';\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 */\nexport async function loadAdaptiveDecision(\n options: LoadAdaptiveDecisionOptions,\n): Promise<import('@sentientui/core').DecideResult> {\n const { preloadDecisions, readSessionCookie } = await import('@sentientui/core');\n\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return 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 },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,kBAKO;AAiDP,IAAAA,eAAoD;AA7BpD,SAAS,mBAA2B;AAClC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACrE;AAMA,eAAsB,wBACpB,YACA,SAC4B;AA1C9B;AA2CE,QAAM,aACJ,8CAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,aAAO,gCAAmB,YAAY,WAAW;AAAA,IAC/C,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ;AAAA,EACnB,CAAC;AACH;AAeA,eAAsB,qBACpB,SACkD;AAxEpD;AAyEE,QAAM,EAAE,kBAAAC,mBAAkB,mBAAAC,mBAAkB,IAAI,MAAM,OAAO,kBAAkB;AAE/E,QAAM,aACJ,WAAAA,mBAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,SAAOD;AAAA,IACL;AAAA,MACE,UAAU,QAAQ;AAAA,MAClB,aAAY,aAAQ,eAAR,YAAsB,CAAC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB,SAAS,QAAQ;AAAA,MACjB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;","names":["import_core","preloadDecisions","readSessionCookie"]}
package/dist/server.mjs CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  preloadAssignments,
4
4
  readSessionCookie
5
5
  } from "@sentientui/core";
6
+ import { preloadDecisions } from "@sentientui/core";
6
7
  function defaultSessionId() {
7
8
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
8
9
  return crypto.randomUUID();
@@ -20,9 +21,30 @@ async function loadAdaptiveAssignments(components, options) {
20
21
  referer: options.referer
21
22
  });
22
23
  }
24
+ async function loadAdaptiveDecision(options) {
25
+ var _a, _b, _c, _d;
26
+ const { preloadDecisions: preloadDecisions2, readSessionCookie: readSessionCookie2 } = await import("@sentientui/core");
27
+ const sessionId = (_c = (_b = readSessionCookie2(options.cookies)) != null ? _b : (_a = options.createSessionId) == null ? void 0 : _a.call(options)) != null ? _c : defaultSessionId();
28
+ return preloadDecisions2(
29
+ {
30
+ sections: options.sections,
31
+ components: (_d = options.components) != null ? _d : []
32
+ },
33
+ sessionId,
34
+ {
35
+ apiKey: options.apiKey,
36
+ baseUrl: options.baseUrl,
37
+ origin: options.origin,
38
+ userAgent: options.userAgent,
39
+ referer: options.referer
40
+ }
41
+ );
42
+ }
23
43
  export {
24
44
  loadAdaptiveAssignments,
45
+ loadAdaptiveDecision,
25
46
  preloadAssignments,
47
+ preloadDecisions,
26
48
  readSessionCookie
27
49
  };
28
50
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\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';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\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};\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 the result as `initialAssignments` on `<AdaptiveProvider>`.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<ServerAssignments> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n });\n}\n"],"mappings":";AAGA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAoBP,SAAS,mBAA2B;AAClC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACrE;AAMA,eAAsB,wBACpB,YACA,SAC4B;AA1C9B;AA2CE,QAAM,aACJ,6BAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,SAAO,mBAAmB,YAAY,WAAW;AAAA,IAC/C,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ;AAAA,EACnB,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\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';\n\nexport { preloadAssignments, readSessionCookie };\nexport type { ServerAssignConfig, ServerAssignments };\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};\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 the result as `initialAssignments` on `<AdaptiveProvider>`.\n */\nexport async function loadAdaptiveAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n options: LoadAdaptiveAssignmentsOptions,\n): Promise<ServerAssignments> {\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return preloadAssignments(components, sessionId, {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n origin: options.origin,\n userAgent: options.userAgent,\n referer: options.referer,\n });\n}\n\nexport { preloadDecisions, type DecideResult } from '@sentientui/core';\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 */\nexport async function loadAdaptiveDecision(\n options: LoadAdaptiveDecisionOptions,\n): Promise<import('@sentientui/core').DecideResult> {\n const { preloadDecisions, readSessionCookie } = await import('@sentientui/core');\n\n const sessionId =\n readSessionCookie(options.cookies) ??\n options.createSessionId?.() ??\n defaultSessionId();\n\n return 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 },\n );\n}\n"],"mappings":";AAGA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAiDP,SAAS,wBAA2C;AA7BpD,SAAS,mBAA2B;AAClC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACrE;AAMA,eAAsB,wBACpB,YACA,SAC4B;AA1C9B;AA2CE,QAAM,aACJ,6BAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,SAAO,mBAAmB,YAAY,WAAW;AAAA,IAC/C,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ;AAAA,EACnB,CAAC;AACH;AAeA,eAAsB,qBACpB,SACkD;AAxEpD;AAyEE,QAAM,EAAE,kBAAAA,mBAAkB,mBAAAC,mBAAkB,IAAI,MAAM,OAAO,kBAAkB;AAE/E,QAAM,aACJ,WAAAA,mBAAkB,QAAQ,OAAO,MAAjC,aACA,aAAQ,oBAAR,qCADA,YAEA,iBAAiB;AAEnB,SAAOD;AAAA,IACL;AAAA,MACE,UAAU,QAAQ;AAAA,MAClB,aAAY,aAAQ,eAAR,YAAsB,CAAC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB,SAAS,QAAQ;AAAA,MACjB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;","names":["preloadDecisions","readSessionCookie"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/react",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",