@sentientui/core 0.16.10 → 0.17.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index-engagement.ts","../src/engagement/classify.ts","../src/index.ts","../src/micro-signals.ts","../src/engagement/capture.ts"],"sourcesContent":["// Lazy engagement entry — loaded on demand (mirrors ./graph) so the lean\n// bundle never carries the section classifier or IntersectionObserver logic.\nexport { startEngagementCapture, type EngagementCaptureOptions } from './engagement/capture';\nexport {\n classifyFeatures,\n classifySection,\n featuresFromElement,\n SEMANTIC_TYPES,\n type SectionFeatures,\n type SemanticType,\n} from './engagement/classify';\n","// Semantic section classification for no-code section capture (Phase 3 §2.4).\n// Pure heuristic: element → one of the graph_nodes semantic_type enum. Mirrors\n// the SDK graph scanner's vocabulary so the persona × section matrix consumes\n// snippet-captured sections unchanged.\n//\n// The classifier is split into a pure feature-based core (classifyFeatures —\n// also used server-side on crawled HTML by the site-audit classification job)\n// and a DOM wrapper (classifySection). Content-based patterns detect\n// pricing/social-proof/trust/comparison from BODY TEXT, so div-soup pages with\n// uninformative ids/classes still classify (persona-coverage spec 2026-07-23).\n\nexport type SemanticType =\n | 'pricing' | 'hero' | 'social_proof' | 'cta' | 'features'\n | 'faq' | 'comparison' | 'trust' | 'navigation' | 'generic';\n\n/** Canonical vocabulary — mirrors the graph_nodes semantic_type CHECK enum. */\nexport const SEMANTIC_TYPES: readonly SemanticType[] = [\n 'pricing', 'hero', 'social_proof', 'cta', 'features',\n 'faq', 'comparison', 'trust', 'navigation', 'generic',\n] as const;\n\n/** Environment-agnostic section features — buildable from a browser Element or\n * a server-parsed node (node-html-parser). */\nexport type SectionFeatures = {\n tag: string; // lowercase tag name\n idClass: string; // `${id} ${className}`\n headingText: string;\n bodyText: string; // normalized text content, first 2000 chars\n actionCount: number;\n textLength: number;\n};\n\n// Ordered most-specific first — the first keyword hit wins.\nconst KEYWORDS: Array<[SemanticType, RegExp]> = [\n ['pricing', /\\b(pricing|price|plans?|subscriptions?|per month|\\/mo|tier)\\b/i],\n ['faq', /\\b(faq|frequently asked|common questions?)\\b/i],\n ['comparison', /\\b(compare|comparison|versus|vs\\.)\\b/i],\n ['social_proof', /\\b(testimonial|reviews?|trusted by|loved by|customers?|logos|rated|case stud)\\b/i],\n ['trust', /\\b(security|guarantee|privacy|compliance|secure|gdpr|soc ?2|encrypt)\\b/i],\n ['features', /\\b(features?|how it works|benefits?|capabilit|what you get)\\b/i],\n];\n\n// Content-evidence patterns — run against bodyText. Ordered most-specific first.\n// Deliberately conservative: pricing needs per-period/plan context next to money\n// so an article mentioning \"$5 million\" stays generic.\nconst CONTENT_PATTERNS: Array<[SemanticType, RegExp]> = [\n ['pricing', /(?:[$€£]\\s?\\d[\\d,.]*\\s*(?:\\/|per\\s)\\s*(?:mo|month|yr|year|seat|user))|(?:\\b(?:starter|basic|pro|growth|premium|enterprise)\\b[^.]{0,60}[$€£]\\s?\\d)/i],\n ['social_proof', /(?:★{2,})|(?:\\b\\d(?:\\.\\d)?\\s*(?:out of|\\/)\\s*5\\b)|(?:\\brated\\b)|(?:[\"“][^\"”]{20,160}[\"”]\\s*[—–-]\\s*[A-Z][a-z]+)/],\n ['trust', /\\b(?:money[- ]back guarantee|refund(?:s)? within|encrypted|soc ?2|iso ?27001|gdpr[- ]compliant|cancel anytime)\\b/i],\n ['comparison', /\\b(?:vs|versus)\\b\\.?[^.!?]{0,80}\\b(?:compare|comparison|plans?|features?|alternative)\\b|\\bhow (?:we|it) compares?\\b/i],\n];\n\nfunction headingText(el: Element): string {\n const h = el.querySelector('h1, h2, h3');\n return (h?.textContent ?? '').slice(0, 160);\n}\n\n/**\n * Pure classification over extracted features. `strong` = keyword or content\n * evidence (trustable enough to auto-apply); `weak` = structural fallback\n * (cta/hero/navigation/generic — capture-worthy but not persona evidence).\n */\nexport function classifyFeatures(f: SectionFeatures): { type: SemanticType; strength: 'strong' | 'weak' } {\n if (f.tag === 'nav' || f.tag === 'footer') return { type: 'navigation', strength: 'weak' };\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n for (const [type, re] of KEYWORDS) {\n if (re.test(hay)) return { type, strength: 'strong' };\n }\n for (const [type, re] of CONTENT_PATTERNS) {\n if (re.test(f.bodyText)) return { type, strength: 'strong' };\n }\n if (f.actionCount >= 1 && f.textLength > 0 && f.textLength < 200) return { type: 'cta', strength: 'weak' };\n if (f.tag === 'header') return { type: 'hero', strength: 'weak' };\n if (/\\b(hero|headline|banner)\\b/i.test(hay)) return { type: 'hero', strength: 'weak' };\n return { type: 'generic', strength: 'weak' };\n}\n\n/** Feature extraction from a live DOM element (browser paths). */\nexport function featuresFromElement(el: Element): SectionFeatures {\n const text = (el.textContent ?? '').replace(/\\s+/g, ' ').trim();\n return {\n tag: el.tagName.toLowerCase(),\n idClass: `${el.id} ${String(el.className ?? '')}`,\n headingText: headingText(el),\n bodyText: text.slice(0, 2000),\n actionCount: el.querySelectorAll('a, button, [role=\"button\"]').length,\n textLength: text.length,\n };\n}\n\n/** Classify a page section into a semantic type (never null — falls back to\n * 'generic' so the caller can still capture attention on it). */\nexport function classifySection(el: Element): SemanticType {\n return classifyFeatures(featuresFromElement(el)).type;\n}\n","import { initSession, type SessionConfig, type SessionManager } from './session';\nimport {\n createEventQueue,\n retryStorageKey,\n type EventQueue,\n type EventType,\n type QueueConfig,\n type SentientEvent,\n} from './queue';\nimport {\n createAssignmentCache,\n type Assignment,\n} from './cache';\nimport type {\n GraphConfig,\n GraphSnapshot,\n} from './graph';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n referrerDomainFromReferer,\n uaTokenMatch,\n} from './session-meta.js';\nimport {\n toWireSlot,\n baselineResultFor,\n armOfResult,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\nimport { readSnapshot, writeSnapshot, SNAPSHOT_STORAGE_KEY_PREFIX, type SlotConfigEntry, type CompoundLocator } from './snapshot.js';\nimport { confidenceBand } from '@sentientui/policy';\nimport { createLocalModeClient } from './local-mode.js';\nimport { randomUuidV4 } from './uuid.js';\n\nexport { PROD_KEYLESS_ERROR, LOCAL_MODE_BANNER } from './local-mode.js';\n\nexport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n deriveSessionSegment,\n referrerDomainFromReferer,\n uaTokenMatch,\n matchedAgentToken,\n agentUaList,\n agentIntent,\n classifiedAgents,\n AGENT_INTENTS,\n} from './session-meta.js';\nexport type { AgentIntent } from './session-meta.js';\n\nconst DEFAULT_INGEST_URL = 'https://api.sentient-ui.com/v1/events';\n\n// Keyed by apiKey so multiple init() calls (HMR, multi-project) don't collide.\nconst _clients = new Map<string, {\n config: SentientConfig;\n upgrade: ((c: SentientClient) => void) | null;\n // Teardown for the live client bound to this key (stops its queue interval +\n // unload listeners). Present only for the full tracking client; the no-op /\n // pre-consent / local entries have nothing to tear down. Called before a\n // re-init for the same key replaces it, so timers/listeners can't leak.\n dispose?: () => void;\n}>();\nlet _lastApiKey: string | null = null;\n\nexport type SentientConfig = {\n apiKey: string;\n context: 'landing' | 'ecommerce' | 'saas' | 'marketplace';\n /** @internal — not exposed to users; defaults to the hosted SentientUI API. */\n ingestUrl?: string;\n debug?: boolean;\n /**\n * Pre-seeded assignments from `preloadAssignments()` (SSR).\n * Seeds the local cache so `assign()` returns without a network call for\n * listed code variants, guaranteeing server and client render the same\n * variant on first paint. Managed-text components (assign with no\n * variantIds) still fetch once when the seed carries no content.\n */\n initialAssignments?: Record<string, string>;\n /**\n * Segment used for SSR preload (`device:source`). When set with `initialAssignments`,\n * seeds the assignment cache under this key so hydration matches the server bandit row.\n */\n sessionSegment?: string;\n /**\n * Consent gate. When `false`, returns a no-op client and performs no tracking.\n * Defaults to `true`. Re-call `init()` (via `AdaptiveProvider` consent prop) when\n * the user grants or revokes consent mid-session.\n */\n consent?: boolean;\n /**\n * Behavior before consent is granted. `'statistical_winner'` fetches the\n * best-performing variant via `GET /v1/winner` — no session or tracking data\n * is stored. `'control'` (default) shows `variantIds[0]` with no API call.\n * Applies when tracking is gated off — either `consent: false` or an active\n * Do Not Track signal.\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Whether to honor the browser's Do Not Track (DNT) signal. Defaults to `true`.\n * When `true` and the visitor has DNT enabled, the SDK sets no cookies and\n * sends no tracking data — behaving exactly as `consent: false` (still serving\n * the read-only `preConsentBehavior` winner if configured), and `grantConsent()`\n * will not upgrade it. Set `false` to make your own consent gate authoritative.\n */\n respectDoNotTrack?: boolean;\n userId?: string;\n /**\n * Session ID generated server-side (from `loadAdaptiveAssignments` / `loadAdaptiveDecision`).\n * When provided, the client adopts this ID on first visit instead of generating a new one,\n * ensuring events and goals are attributed to the same session the server used for assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code for the visitor. When provided (e.g. from\n * the `CF-IPCountry` header in a Next.js server component), it is included in\n * the session upsert so country-based segmentation works without client-side\n * geo lookup.\n */\n country?: string;\n /**\n * Pre-seeded slot results from `preloadDecisions()` / `loadAdaptiveDecision()`\n * (SSR). Seeds the local slot state so `getSlotResult()` agrees with the\n * server-rendered markup on first paint.\n */\n initialSlots?: Record<string, SlotResult>;\n /**\n * Persona decided during SSR. Takes priority over the html-attribute\n * adoption and the local snapshot.\n */\n initialPersona?: { persona: string; confidence: number };\n /**\n * Keyless local mode. 'auto' (default) simulates decisions on-device when no\n * valid API key is configured — but only in development builds (the\n * `development` export condition); production bundles physically exclude the\n * engine. `true` forces the local engine regardless of key (escape hatch);\n * `false` restores the silent keyless no-op.\n */\n localMode?: 'auto' | boolean;\n};\n\nexport type AssignResult = { variantId: string; assignmentTtlMs: number; content?: string };\n\nexport type { SlotDeclInput, SlotResult };\nexport { armOfResult, baselineResultFor, baselineSlots, toWireSlot } from './slots.js';\n\nexport {\n SNAPSHOT_STORAGE_KEY_PREFIX,\n readSnapshot,\n writeSnapshot,\n renderPrePaintScript,\n} from './snapshot.js';\nexport type { DecisionSnapshot, SlotConfigEntry, SlotOps, CompoundLocator } from './snapshot.js';\n\n/** An editor-defined goal delivered with a registry-mode decision, for the\n * snippet to install delegated listeners from. */\nexport type GoalDefinition = {\n goalId: string;\n event: 'click' | 'form_submit' | 'url_reached';\n locator?: CompoundLocator;\n urlPattern?: string;\n slotId?: string;\n};\n\n/** One served section-classification row (registry mode): where it is on the\n * page (url match + compound locator) and its semantic type. The snippet\n * resolves the locator to build capture's `typeOf` hook. */\nexport type SectionMapEntry = {\n urlMatch: string;\n locator: CompoundLocator;\n type: string;\n};\n\nexport type DecideOutcome = {\n layoutOrder: string[] | null;\n assignments: Record<string, string>;\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n // Registry mode only: where/what to apply for server-defined slots.\n slotConfig?: Record<string, SlotConfigEntry>;\n // Registry mode only: editor-defined goals to wire up.\n goals?: GoalDefinition[];\n // Registry mode only: served section-classification map the snippet turns\n // into capture's `typeOf` hook.\n sectionMap?: SectionMapEntry[];\n};\n\nexport type DecideInput = {\n sections?: string[];\n components?: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n // 'registry' → serve the project's published slot_definitions in addition to\n // any declared slots (registry wins on id collision). Default 'request'.\n slotsFrom?: 'request' | 'registry';\n /**\n * Caller's build version (e.g. the snippet's `__SNIPPET_VERSION__`), sent\n * as `v` on the wire. Additive/best-effort: the server persists it for\n * version-skew reporting (see apps/api decide route) and ignores it\n * entirely on older deployments. Omit if the caller has no version to report.\n */\n v?: string;\n};\n\nexport type WeightEntry = { variantId: string; pulls: number; avgReward: number | null };\nexport type ComponentWeightEntry = { componentId: string; updatedAt: number; variants: WeightEntry[] };\n\nexport type ComponentGoalOptions = {\n /** Reward credited to the served variant (0–1). Defaults to 1. */\n reward?: number;\n /** Extra fields merged into the event payload. */\n metadata?: Record<string, unknown>;\n};\n\nexport type SentientClient = {\n track(\n event: Omit<SentientEvent, 'id' | 'sessionId' | 'timestamp' | 'timeInSession'>,\n ): void;\n goal(name: string, metadata?: Record<string, unknown>, weight?: number, stepIndex?: number): void;\n /**\n * Records a conversion attributed to the variant currently served for\n * `componentId`, so it feeds the per-variant CVR funnel. Resolves the served\n * variant from the local assignment cache — no need to pass variantId or\n * projectId. No-ops if the component has not been assigned yet (render its\n * `<Adaptive>`/call `assign()` first). Prefer this over bare `goal()` for\n * variant experiments; `goal()` is session-level only (no component attribution).\n */\n componentGoal(componentId: string, goalType: string, opts?: ComponentGoalOptions): void;\n identify(userId: string): void;\n getAssignment(componentId: string, segment: string): Assignment | null;\n /** Server-side variant assignment. Caches the result locally per (component, segment). */\n assign(componentId: string, variantIds?: string[], agentData?: unknown, agentDataByVariant?: Record<string, unknown>): Promise<AssignResult | null>;\n /**\n * Single-roundtrip decision for layout sections, component variants, and\n * adaptive slots. Awaits the session upsert (like `assign`) so the server\n * never decides for a session row that doesn't exist yet. A response\n * without a `slots` field means the server predates slots — every declared\n * slot resolves to its baseline and no retry is made.\n */\n decide(input: DecideInput): Promise<DecideOutcome | null>;\n /** Slot result served this session (decide result, SSR seed, snapshot, or failure baseline). Null when unknown. */\n getSlotResult(slotId: string): SlotResult | null;\n /** Current persona estimate. Band is always `confidenceBand(confidence)`. Null when nothing is known yet. */\n getPersona(): { persona: string; confidence: number; band: 'low' | 'medium' | 'high' } | null;\n /** Fetches current bandit weights for all components in this project. Used by the provider to keep live-weight polling fresh. */\n fetchWeights(): Promise<ComponentWeightEntry[]>;\n getGraph(): GraphSnapshot;\n /**\n * Routine teardown: stops timers/listeners and flushes pending events, but\n * KEEPS the visitor identity, decision snapshot, and retry bucket. Use for\n * component unmount / re-init (framework providers call this on cleanup).\n */\n dispose(): void;\n /**\n * Consent-revocation / forget-me teardown: everything `dispose()` does,\n * plus deletion of the visitor identity (`_snt_uid`), the decision\n * snapshot, and the persisted retry bucket. The next visit starts as a\n * brand-new visitor.\n */\n destroy(): void;\n /** True when this client is the keyless local-mode client (dev only). */\n readonly isLocal?: boolean;\n};\n\n// SSR preload helpers moved to the `@sentientui/core/server` entry in 0.6.0 so\n// ~200 lines of Node-only fetch logic stop shipping in the browser bundle.\n\nexport { attachMicroSignalDetectors } from './micro-signals.js';\nexport type { MicroSignalEmitter, MicroSignalType } from './micro-signals.js';\n\nexport type {\n SessionConfig,\n SessionManager,\n EventType,\n SentientEvent,\n QueueConfig,\n Assignment,\n GraphSnapshot,\n GraphConfig,\n};\n\nfunction generateEventId(): string {\n return randomUuidV4();\n}\n\nconst SSR_CLIENT: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n assign: () => Promise.resolve(null),\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n fetchWeights: () => Promise.resolve([]),\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n};\n\nfunction readUtmParams(): Record<string, string> {\n try {\n const out: Record<string, string> = {};\n const sp = new URLSearchParams(window.location.search);\n for (const [k, v] of sp) {\n if (k.startsWith('utm_')) out[k] = v;\n }\n return out;\n } catch {\n return {};\n }\n}\n\nfunction deriveBaseUrl(ingestUrl: string): string {\n return ingestUrl.replace(/\\/events\\/?$/, '');\n}\n\n/**\n * Detects whether the visitor has signalled a tracking opt-out. Honors Global\n * Privacy Control (`navigator.globalPrivacyControl`) — the legally-enforceable\n * CCPA/CPRA signal — as well as Do Not Track (`navigator.doNotTrack`, the legacy\n * `window.doNotTrack` on older Firefox, and `navigator.msDoNotTrack` on old\n * IE/Edge). GPC is a boolean; DNT is opt-out only when explicitly `'1'`/`'yes'`.\n */\nexport function isDoNotTrackEnabled(): boolean {\n // GPC is a boolean flag, checked separately from the DNT string signals.\n if (\n typeof navigator !== 'undefined' &&\n (navigator as unknown as { globalPrivacyControl?: boolean }).globalPrivacyControl === true\n ) {\n return true;\n }\n const signals = [\n typeof navigator !== 'undefined' ? navigator.doNotTrack : undefined,\n typeof window !== 'undefined'\n ? (window as unknown as { doNotTrack?: string | null }).doNotTrack\n : undefined,\n typeof navigator !== 'undefined'\n ? (navigator as unknown as { msDoNotTrack?: string | null }).msDoNotTrack\n : undefined,\n ];\n return signals.some((v) => v === '1' || v === 'yes');\n}\n\n/**\n * Upgrades a pre-consent client (any client created with `consent: false`, in\n * either `preConsentBehavior` mode) to a fully-tracking client, in place and\n * with no page reload. Call this from your consent management platform callback.\n * For React apps, prefer updating the `consent` prop on `<AdaptiveProvider>`.\n * Pass `apiKey` to target a specific project; omit to upgrade the most-recently-initialized client.\n */\nexport function grantConsent(apiKey?: string): void {\n if (typeof window === 'undefined') return;\n\n const key = apiKey ?? _lastApiKey;\n if (!key) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const entry = _clients.get(key);\n if (!entry) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const { config, upgrade } = entry;\n if (!upgrade) return;\n\n // Honor an active Do Not Track signal — consent cannot override a global opt-out.\n if (config.respectDoNotTrack !== false && isDoNotTrackEnabled()) return;\n\n const fullClient = init({ ...config, consent: true });\n upgrade(fullClient);\n // init() just registered the full client (with its dispose) under `key`.\n // Preserve that dispose so a later re-init/teardown can still tear it down —\n // we only need to clear the upgrade hook now that consent is granted.\n const disposed = _clients.get(key)?.dispose;\n _clients.set(key, { config: { ...config, consent: true }, upgrade: null, dispose: disposed });\n}\n\nfunction createPreConsentProxy(config: SentientConfig): { proxy: SentientClient; setInner: (c: SentientClient) => void } {\n // 'control' (the default) must reach the network zero times before consent.\n // The proxy still exists so grantConsent() has something to upgrade in place\n // — without it, a site wanting no pre-consent traffic could only start\n // tracking by reloading the page.\n const servesWinner = config.preConsentBehavior === 'statistical_winner';\n const baseUrl = deriveBaseUrl(config.ingestUrl ?? DEFAULT_INGEST_URL);\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n let inner: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n fetchWeights: () => Promise.resolve([]),\n async assign(componentId, variantIds, _agentData?) {\n // Control mode: no request. Callers fall back to variantIds[0] through\n // ssrFallback, exactly as they did against the old no-op client.\n if (!servesWinner) return null;\n try {\n const params = new URLSearchParams({ componentId });\n for (const v of variantIds ?? []) params.append('variantIds[]', v);\n const res = await fetch(`${baseUrl}/winner?${params.toString()}`, {\n headers: authHeaders,\n });\n if (!res.ok) return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n const body = (await res.json()) as { variantId: string };\n return { variantId: body.variantId, assignmentTtlMs: 0 };\n } catch {\n return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n }\n },\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n };\n\n const proxy: SentientClient = {\n track: (e) => inner.track(e),\n goal: (n, m, w, s) => inner.goal(n, m, w, s),\n componentGoal: (c, g, o) => inner.componentGoal(c, g, o),\n identify: (u) => inner.identify(u),\n getAssignment: (c, s) => inner.getAssignment(c, s),\n assign: (c, v, a, av) => inner.assign(c, v, a, av),\n decide: (i) => inner.decide(i),\n getSlotResult: (s) => inner.getSlotResult(s),\n getPersona: () => inner.getPersona(),\n fetchWeights: () => inner.fetchWeights(),\n getGraph: () => inner.getGraph(),\n dispose: () => inner.dispose(),\n destroy: () => inner.destroy(),\n };\n\n function setInner(fullClient: SentientClient) {\n inner = fullClient;\n }\n\n return { proxy, setInner };\n}\n\n/**\n * Initializes the Sentient client. Returns a no-op client during SSR.\n */\nexport function init(config: SentientConfig): SentientClient {\n if (typeof window === 'undefined') {\n return SSR_CLIENT;\n }\n\n _lastApiKey = config.apiKey;\n\n // A re-init for the same key (HMR, consent toggle, provider remount) supersedes\n // the prior client. Dispose it first so its queue's setInterval and\n // visibilitychange/pagehide listeners don't leak — the map only ever held its\n // config, so without this the old client kept flushing forever.\n const prevEntry = _clients.get(config.apiKey || 'local');\n if (prevEntry?.dispose) {\n try {\n prevEntry.dispose();\n } catch {\n /* teardown must never throw on re-init */\n }\n }\n\n // DNT/GPC (a global opt-out) or an explicit `consent: false` must be evaluated\n // BEFORE the local-mode branch: createLocalModeClient() calls initSession()\n // unconditionally, so a gated visitor would otherwise be issued the 365-day\n // `_snt_uid` identity cookie in keyless/local mode (audit P2). DNT/GPC also\n // gates tracking off even when the site passes `consent: true`, and blocks\n // `grantConsent()` from upgrading.\n const dntBlocked = config.respectDoNotTrack !== false && isDoNotTrackEnabled();\n const gated = config.consent === false || dntBlocked;\n\n // Keyless local mode. `localMode: true` forces the local engine (documented\n // escape hatch); 'auto' (default) engages it only when no valid key is\n // present. In production builds `@sentientui/core/local` resolves to a stub\n // and this degrades to a no-op client + one console.error per page load\n // (createLocalModeClient handles that), so no NODE_ENV check is needed here.\n const keyValid = typeof config.apiKey === 'string' && config.apiKey.startsWith('pk_');\n if (config.localMode === true || (!keyValid && config.localMode !== false)) {\n // A gated visitor must never get the identity cookie. Local mode has no\n // server to serve a statistical winner from, so return a plain no-op.\n if (gated) {\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return SSR_CLIENT;\n }\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return createLocalModeClient(config);\n }\n\n if (gated) {\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n if (config.preConsentBehavior === 'statistical_winner') {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n }\n _clients.set(config.apiKey, { config, upgrade: null });\n return SSR_CLIENT;\n }\n // Every gated client gets an upgradeable proxy, not just the winner-serving\n // one — otherwise grantConsent() is silently dead for the 'control' default\n // and the site has to reload to start tracking. Control mode still makes no\n // request; the proxy only exists so consent can swap the inner client.\n const { proxy, setInner } = createPreConsentProxy(config);\n // Under DNT the read-only winner still serves, but consent can never\n // upgrade it to tracking — so drop the upgrade hook.\n _clients.set(config.apiKey, { config, upgrade: dntBlocked ? null : setInner });\n return proxy;\n }\n\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n return SSR_CLIENT;\n }\n\n if (config.ingestUrl === '') {\n console.warn('[sentient] init() called with an empty ingestUrl. SDK disabled.');\n return SSR_CLIENT;\n }\n\n const resolvedIngestUrl = config.ingestUrl ?? DEFAULT_INGEST_URL;\n\n const sessionStart = Date.now();\n const session = initSession({ ssrSessionId: config.ssrSessionId, apiKey: config.apiKey });\n const assignmentCache = createAssignmentCache(undefined, config.apiKey);\n const eventQueue = createEventQueue({ ingestUrl: resolvedIngestUrl, apiKey: config.apiKey });\n const baseUrl = deriveBaseUrl(resolvedIngestUrl);\n\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n const deviceClass = detectDeviceClass(navigator.userAgent ?? '');\n const appOrigin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const trafficSource = detectTrafficSource(document.referrer ?? '', appOrigin);\n const sessionSegment =\n config.sessionSegment ?? `${deviceClass}:${trafficSource}`;\n const inflightAssigns = new Map<string, Promise<AssignResult | null>>();\n\n // --- Adaptive-slot state (decide) ---\n // Results served for this session, keyed by slot id. Written by decide();\n // read by getSlotResult() (Task 3.3) and componentGoal's slot fallback.\n const slotStore = new Map<string, SlotResult>();\n let personaState: { persona: string; confidence: number } | null = null;\n\n // On decide failure every declared slot must still resolve — to its baseline.\n // Never overwrite a previously served result.\n const seedSlotBaselines = (decls: SlotDeclInput[]): void => {\n for (const d of decls) {\n if (!slotStore.has(d.id)) slotStore.set(d.id, baselineResultFor(d));\n }\n };\n\n // Seed slot/persona state. Priority: explicit SSR seeds → snapshot.\n if (config.initialSlots) {\n for (const [slotId, result] of Object.entries(config.initialSlots)) {\n slotStore.set(slotId, result);\n }\n }\n const seedSnapshot = readSnapshot(config.apiKey);\n if (seedSnapshot) {\n for (const [slotId, result] of Object.entries(seedSnapshot.slots)) {\n if (!slotStore.has(slotId)) slotStore.set(slotId, result);\n }\n }\n\n // Band-only persona sources (html attrs, snapshot) become a band-consistent\n // numeric confidence so confidenceBand(confidence) always equals the band.\n const BAND_CONFIDENCE: Record<string, number> = { low: 0.15, medium: 0.5, high: 0.85 };\n if (config.initialPersona) {\n personaState = { ...config.initialPersona };\n } else {\n // Single-writer rule: the inline pre-paint script owns the <html>\n // attributes. The client ADOPTS them as truth and never rewrites them\n // mid-session (next visit's script picks up the new snapshot instead).\n const ds = document.documentElement.dataset;\n if (ds.sentientPersona) {\n personaState = {\n persona: ds.sentientPersona,\n confidence: BAND_CONFIDENCE[ds.sentientConfidence ?? 'low'] ?? 0.15,\n };\n } else if (seedSnapshot) {\n personaState = {\n persona: seedSnapshot.persona,\n confidence: BAND_CONFIDENCE[seedSnapshot.band] ?? 0.15,\n };\n }\n }\n\n // Seed SSR-preloaded assignments into the local cache so assign() finds a\n // cache hit immediately — no network call, no variant flash on hydration.\n if (config.initialAssignments) {\n for (const [componentId, variantId] of Object.entries(config.initialAssignments)) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n }\n\n // Upsert session metadata once on init. assign() awaits this promise so\n // the server isn't asked to assign for a session row that doesn't exist yet.\n let sessionReady: Promise<void> = Promise.resolve();\n\n const sessionId = session.getSessionId();\n if (sessionId) {\n const referrerDomain = referrerDomainFromReferer(document.referrer ?? '');\n const sessionBody = {\n sessionId,\n deviceClass,\n trafficSource,\n referrerDomain,\n utmParams: readUtmParams(),\n timeOfDay: detectTimeOfDay(new Date()),\n dayOfWeek: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][new Date().getDay()],\n ephemeral: session.isEphemeral(),\n // Likely-automation hint: navigator.webdriver (set under automation\n // control) or a known agent token in the UA. Probabilistic — used for\n // metrics + bandit exclusion server-side, never to change what's served.\n automation:\n (typeof navigator !== 'undefined' && navigator.webdriver === true) ||\n uaTokenMatch(navigator.userAgent ?? ''),\n ...(config.userId ? { userId: config.userId } : {}),\n ...(config.country ? { country: config.country } : {}),\n };\n try {\n sessionReady = fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify(sessionBody),\n headers: authHeaders,\n })\n .then((res) => {\n if (res.status === 402) {\n console.warn(\n '[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing',\n );\n }\n return undefined;\n })\n .catch(() => undefined);\n } catch {\n /* never throw on init */\n }\n }\n\n if (config.debug) {\n console.log('[sentient] initialized', { context: config.context });\n (\n window as unknown as {\n __sentient?: {\n client: SentientClient;\n queue: EventQueue;\n };\n }\n ).__sentient = {\n client: null as unknown as SentientClient,\n queue: eventQueue,\n };\n }\n\n const client: SentientClient = {\n goal(name, metadata = {}, weight = 1.0, stepIndex = 0) {\n const sid = session.getSessionId();\n if (!sid) return;\n const goalId = generateEventId();\n sessionReady.then(() => {\n fetch(`${baseUrl}/goals`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify({ sessionId: sid, name, metadata, weight, stepIndex, goalId }),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n componentGoal(componentId, goalType, opts) {\n const sid = session.getSessionId();\n if (!sid) return;\n // Variant experiments resolve from the assignment cache; adaptive slots\n // (useAdaptiveTokens / AdaptiveGroup) resolve from the slot state, using\n // the canonical arm string as the attributed variantId.\n const assignment = assignmentCache.get(componentId, sessionSegment);\n const slotResult = assignment ? null : slotStore.get(componentId) ?? null;\n if (!assignment && slotResult === null) {\n if (config.debug) {\n console.warn(\n `[sentient] componentGoal(\"${componentId}\"): no assignment or slot decision yet — render its <Adaptive>/adaptive hook or call assign()/decide() before recording a goal.`,\n );\n }\n return;\n }\n const attributedVariantId = assignment ? assignment.variantId : armOfResult(slotResult!);\n const fullEvent: SentientEvent = {\n id: generateEventId(),\n sessionId: sid,\n projectId: config.apiKey,\n componentId,\n variantId: attributedVariantId,\n eventType: 'goal_achieved',\n goalType,\n payload: { reward: opts?.reward ?? 1, ...(opts?.metadata ?? {}) },\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n if (config.debug) {\n console.log('[sentient] componentGoal', fullEvent);\n }\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n identify(userId) {\n const sid = session.getSessionId();\n if (!sid) return;\n sessionReady.then(() => {\n fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify({ sessionId: sid, userId, ephemeral: session.isEphemeral() }),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n track(event) {\n const sessionId = session.getSessionId();\n if (!sessionId) return;\n\n const fullEvent: SentientEvent = {\n ...event,\n id: generateEventId(),\n sessionId,\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n\n if (config.debug) {\n console.log('[sentient] track', fullEvent);\n }\n\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n getAssignment(componentId, segment) {\n return assignmentCache.get(componentId, segment);\n },\n\n async assign(componentId, variantIds, agentData?, agentDataByVariant?) {\n const sid = session.getSessionId();\n if (!sid) return null;\n\n const cached = assignmentCache.get(componentId, sessionSegment);\n // When variantIds are provided (A/B code variant), a cache hit is always final.\n // When variantIds are absent (managed text component), only hit the cache if content\n // is present — a seed from initialAssignments has no content and must still fetch.\n if (cached && (variantIds?.length || cached.content !== undefined)) {\n // Surface the entry's remaining TTL (server-provided when set) instead of\n // a hardcoded 0, so callers can reason about when a re-assign is due.\n const remainingTtlMs =\n cached.ttlMs && cached.ttlMs > 0\n ? Math.max(0, cached.assignedAt + cached.ttlMs - Date.now())\n : 0;\n return { variantId: cached.variantId, assignmentTtlMs: remainingTtlMs, content: cached.content };\n }\n\n // Coalesce concurrent assigns for the same component (e.g. several\n // mounted slots sharing one id) into a single network request.\n const inflight = inflightAssigns.get(componentId);\n if (inflight) return inflight;\n\n const request = (async (): Promise<AssignResult | null> => {\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid, componentId, variantIds };\n if (agentDataByVariant !== undefined) body.agentDataByVariant = agentDataByVariant;\n else if (agentData !== undefined) body.agentData = agentData;\n const res = await fetch(`${baseUrl}/assign`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) return null;\n const result = (await res.json()) as AssignResult;\n assignmentCache.set(componentId, sessionSegment, {\n variantId: result.variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n content: result.content,\n // Honor the server's TTL as this entry's expiry; omit when absent/0\n // so the cache falls back to its default (DEFAULT_TTL_MS).\n ...(result.assignmentTtlMs && result.assignmentTtlMs > 0\n ? { ttlMs: result.assignmentTtlMs }\n : {}),\n });\n return result;\n } catch {\n return null;\n } finally {\n inflightAssigns.delete(componentId);\n }\n })();\n inflightAssigns.set(componentId, request);\n return request;\n },\n\n async decide(input) {\n const sid = session.getSessionId();\n if (!sid) return null;\n const declared = input.slots ?? [];\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid };\n if (input.sections && input.sections.length > 0) {\n body.sections = input.sections.map((id) => ({ id }));\n }\n body.components = input.components ?? [];\n if (declared.length > 0) body.slots = declared.map(toWireSlot);\n if (input.slotsFrom === 'registry') body.slotsFrom = 'registry';\n if (input.v) body.v = input.v;\n\n const res = await fetch(`${baseUrl}/decide`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) {\n seedSlotBaselines(declared);\n return null;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[] | null;\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n slotConfig?: Record<string, SlotConfigEntry>;\n goals?: GoalDefinition[];\n sectionMap?: SectionMapEntry[];\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declared) {\n // `data.slots === undefined` means the server predates the slots\n // contract: serve the declared baseline everywhere, do NOT retry.\n // (Distinct from `slots: {}`, which also falls back per-slot.)\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n // Registry mode: the server returns slots the request never declared.\n // Take them verbatim (classic mode returns only declared slots, so this\n // union is a no-op there — back-compatible).\n if (data.slots) {\n for (const [slotId, result] of Object.entries(data.slots)) {\n if (!(slotId in slots)) slots[slotId] = result;\n }\n }\n for (const [slotId, result] of Object.entries(slots)) slotStore.set(slotId, result);\n // Only overwrite persona when the response actually carries one, and\n // never downgrade a known persona to 'unknown' — a decide that omits\n // persona (or returns 'unknown') must not clobber a good SSR/snapshot/\n // initialPersona value, and must not persist that regression below.\n const known = personaState != null && personaState.persona !== 'unknown';\n if (data.persona && !(data.persona === 'unknown' && known)) {\n personaState = { persona: data.persona, confidence: data.confidence ?? 0 };\n } else if (!personaState) {\n personaState = { persona: 'unknown', confidence: 0 };\n }\n\n // Seed component assignments so <Adaptive>/assign() agree with this\n // decide (same shape as the initialAssignments seed above).\n for (const [componentId, variantId] of Object.entries(data.assignments ?? {})) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n\n // Persist for the next visit's pre-paint (SPA cache-first pattern).\n writeSnapshot(config.apiKey, {\n v: 1,\n persona: personaState.persona,\n band: confidenceBand(personaState.confidence),\n slots: Object.fromEntries(slotStore),\n layoutOrder: data.layoutOrder ?? null,\n savedAt: Date.now(),\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n });\n\n return {\n layoutOrder: data.layoutOrder ?? null,\n assignments: data.assignments ?? {},\n slots,\n persona: personaState.persona,\n confidence: personaState.confidence,\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n ...(data.goals ? { goals: data.goals } : {}),\n ...(data.sectionMap ? { sectionMap: data.sectionMap } : {}),\n };\n } catch {\n seedSlotBaselines(declared);\n return null;\n }\n },\n\n getSlotResult(slotId) {\n return slotStore.get(slotId) ?? null;\n },\n\n getPersona() {\n if (!personaState) return null;\n return {\n persona: personaState.persona,\n confidence: personaState.confidence,\n band: confidenceBand(personaState.confidence),\n };\n },\n\n async fetchWeights() {\n try {\n const res = await fetch(`${baseUrl}/weights`, { headers: authHeaders });\n if (!res.ok) return [];\n const data = (await res.json()) as { components: ComponentWeightEntry[] };\n return data.components ?? [];\n } catch {\n return [];\n }\n },\n\n getGraph() {\n return { pageNodes: [], capturedAt: 0 };\n },\n\n dispose() {\n // Stops the flush timer and unload listeners (with a final flush) but\n // leaves identity, snapshot, and retry bucket for the next client.\n eventQueue.destroy();\n // Drop the registry entry so a later re-init doesn't try to dispose an\n // already-torn-down client (and so the map doesn't pin this closure).\n if (_clients.get(config.apiKey)?.dispose === client.dispose) {\n _clients.delete(config.apiKey);\n }\n if (config.debug) {\n console.log('[sentient] disposed');\n }\n },\n\n destroy() {\n eventQueue.destroy();\n session.destroy();\n if (_clients.get(config.apiKey)?.dispose === client.dispose) {\n _clients.delete(config.apiKey);\n }\n // Forget-me must be total: a surviving decision snapshot would\n // re-personalize the next visit via the pre-paint script, and a\n // persisted retry bucket would re-send events for the deleted identity.\n try {\n localStorage.removeItem(SNAPSHOT_STORAGE_KEY_PREFIX + config.apiKey);\n localStorage.removeItem(retryStorageKey(config.apiKey));\n } catch {\n /* storage unavailable — nothing persisted to remove */\n }\n if (config.debug) {\n console.log('[sentient] destroyed');\n }\n },\n };\n\n _clients.set(config.apiKey, { config, upgrade: null, dispose: client.dispose });\n\n if (config.debug) {\n const win = window as unknown as { __sentient?: { client: SentientClient } };\n if (win.__sentient) {\n win.__sentient.client = client;\n }\n }\n\n return client;\n}\n","export type MicroSignalEmitter = (\n signalType: 'rage_click' | 'text_copy' | 'scroll_hesitation' | 'tab_loss',\n extra?: Record<string, unknown>,\n) => void;\n\nexport type MicroSignalType = Parameters<MicroSignalEmitter>[0];\n\n/**\n * Attaches passive behavioral detectors to `node`. Calls `emit` when a signal\n * fires. Each signal type fires at most once per call to this function.\n * Returns a cleanup function that removes all listeners.\n */\nexport function attachMicroSignalDetectors(\n emit: MicroSignalEmitter,\n node: Element,\n variantAssignedAt?: number,\n options?: { tabLoss?: boolean },\n): () => void {\n const cleanups: Array<() => void> = [];\n\n // --- Rage click: 3+ clicks within 500ms ---\n {\n const WINDOW_MS = 500;\n const THRESHOLD = 3;\n let firedOnce = false;\n const timestamps: number[] = [];\n\n const onClick = (): void => {\n if (firedOnce) return;\n const now = Date.now();\n timestamps.push(now);\n while (timestamps.length > 0 && now - timestamps[0]! > WINDOW_MS) {\n timestamps.shift();\n }\n if (timestamps.length >= THRESHOLD) {\n firedOnce = true;\n emit('rage_click');\n }\n };\n\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n }\n\n // --- Text copy: copy event within node ---\n {\n let firedOnce = false;\n\n const onCopy = (e: Event): void => {\n if (firedOnce) return;\n if (!(e.target instanceof Node)) return;\n if (!node.contains(e.target) && node !== e.target) return;\n firedOnce = true;\n const sel = typeof window !== 'undefined' ? window.getSelection() : null;\n const selectionLength = sel ? sel.toString().length : 0;\n emit('text_copy', { selectionLength });\n };\n\n document.addEventListener('copy', onCopy);\n cleanups.push(() => document.removeEventListener('copy', onCopy));\n }\n\n // --- Scroll hesitation: scroll stops 3s while component visible ---\n {\n let firedOnce = false;\n let isVisible = false;\n let hesitationTimer: ReturnType<typeof setTimeout> | null = null;\n\n const clearHesitation = (): void => {\n if (hesitationTimer !== null) {\n clearTimeout(hesitationTimer);\n hesitationTimer = null;\n }\n };\n\n const startHesitation = (): void => {\n if (firedOnce || !isVisible) return;\n clearHesitation();\n hesitationTimer = setTimeout(() => {\n if (!firedOnce && isVisible) {\n firedOnce = true;\n emit('scroll_hesitation');\n }\n }, 3000);\n };\n\n const onScroll = (): void => {\n clearHesitation();\n startHesitation();\n };\n\n const ioCallback = (entries: IntersectionObserverEntry[]): void => {\n for (const entry of entries) {\n isVisible = entry.intersectionRatio > 0.3;\n if (!isVisible) clearHesitation();\n else startHesitation();\n }\n };\n const io = new IntersectionObserver(ioCallback, { threshold: [0.3] });\n io.observe(node);\n window.addEventListener('scroll', onScroll, { passive: true });\n\n cleanups.push(() => {\n io.disconnect();\n window.removeEventListener('scroll', onScroll);\n clearHesitation();\n });\n }\n\n // --- Tab loss: tab hidden within 15s of variant_assigned ---\n // Document-level (not node-scoped), so callers that attach detectors to many\n // nodes at once (e.g. the snippet's per-option slot signals) can opt out on all\n // but one node to avoid emitting a duplicate tab_loss per node (audit M5).\n if (options?.tabLoss !== false) {\n let firedOnce = false;\n const assignedAt = variantAssignedAt ?? Date.now();\n\n const onVisibility = (): void => {\n if (firedOnce) return;\n if (document.visibilityState !== 'hidden') return;\n const elapsed = Date.now() - assignedAt;\n if (elapsed < 15_000) {\n firedOnce = true;\n emit('tab_loss', { timeOnPage: elapsed });\n }\n };\n\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() => document.removeEventListener('visibilitychange', onVisibility));\n }\n\n return () => {\n for (const c of cleanups) c();\n };\n}\n","import { classifySection, SEMANTIC_TYPES, type SemanticType } from './classify';\nimport { isDoNotTrackEnabled } from '../index.js';\nimport { attachMicroSignalDetectors } from '../micro-signals.js';\n\n// Shared engagement capture (spec 2026-07-22-persona-signal-capture). Detects\n// semantic sections, registers them via /v1/section-map, and records per-section\n// dwell/scroll via IntersectionObserver — emitting the same 'dwell' events the\n// persona pipeline consumes. Used by the React provider (default on) and the\n// no-code snippet. Defense-in-depth: checks DNT internally even though callers\n// gate on consent/DNT too; a missing IntersectionObserver → no-op.\n\ntype CaptureClient = {\n track(event: { projectId: string; componentId: string; eventType: string; payload: Record<string, unknown> }): void;\n};\n\nexport type EngagementCaptureOptions = {\n apiKey: string;\n /** API base, no trailing slash. Defaults to the hosted API. */\n apiBase?: string;\n doc?: Document;\n /**\n * Also attach per-section micro-signal detectors (rage click, text copy,\n * scroll hesitation, tab loss), attributed to the section's `nc-<type>`\n * component. For the no-code snippet, whose pages have no `<Adaptive>`\n * components carrying their own detectors. Default false — the React SDK\n * keeps its per-component detectors and must not double-attach.\n */\n microSignals?: boolean;\n /**\n * Server-served section-map lookup (persona-coverage auto-classification):\n * consulted after explicit `data-sentient-type` markup, before the local\n * heuristic. Return null when the element has no served label.\n */\n typeOf?: (el: Element) => SemanticType | null;\n};\n\nconst SECTION_SELECTOR = 'section, header, footer, nav, main > div, [data-sentient-section]';\n\n/** Skip a section nested inside another candidate section (avoid double count). */\nfunction isNested(el: Element): boolean {\n return el.parentElement?.closest(SECTION_SELECTOR) != null;\n}\n\nfunction registerSections(\n apiKey: string,\n apiBase: string,\n pageUrl: string,\n sections: Array<{ componentId: string; semanticType: SemanticType; source: 'markup' | 'auto' }>,\n): void {\n try {\n void fetch(`${apiBase}/v1/section-map`, {\n method: 'POST',\n keepalive: true,\n headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ pageUrl, sections }),\n }).catch(() => undefined);\n } catch {\n /* fail-safe */\n }\n}\n\nconst NOOP = (): void => undefined;\n\nexport function startEngagementCapture(\n client: CaptureClient,\n opts: EngagementCaptureOptions,\n): () => void {\n const doc = opts.doc ?? (typeof document !== 'undefined' ? document : undefined);\n if (!doc || typeof IntersectionObserver === 'undefined') return NOOP;\n if (isDoNotTrackEnabled()) return NOOP;\n // Keyless zero-network contract: capture exists to feed the hosted persona\n // pipeline — with no api key there is nothing to feed, and the section-map\n // registration fetch must never fire.\n if (!opts.apiKey) return NOOP;\n // Normalize so both a ROOT base (`https://api.sentient-ui.com`) and a\n // `/v1`-suffixed base resolve to exactly one `/v1/section-map` — some callers\n // pass the versioned base, which would otherwise produce `/v1/v1/section-map`\n // (a silent 404). Strip trailing slashes, then a single trailing `/v1`.\n const apiBase = (opts.apiBase ?? 'https://api.sentient-ui.com')\n .replace(/\\/+$/, '')\n .replace(/\\/v1$/, '');\n\n const els = Array.from(doc.querySelectorAll(SECTION_SELECTOR)).filter((el) => !isNested(el));\n if (els.length === 0) return NOOP;\n\n // Collapse to one component per semantic type per page (the matrix aggregates\n // by semantic type anyway). Per-element precedence: explicit data-sentient-type\n // markup → served section map (opts.typeOf) → local heuristic.\n const componentOf = new Map<Element, string>();\n const types = new Map<string, SemanticType>();\n const sources = new Map<string, 'markup' | 'auto'>();\n for (const el of els) {\n const explicit = el.getAttribute('data-sentient-type');\n const markup = explicit && (SEMANTIC_TYPES as readonly string[]).includes(explicit)\n ? (explicit as SemanticType)\n : null;\n const type = markup ?? opts.typeOf?.(el) ?? classifySection(el);\n const componentId = `nc-${type}`;\n componentOf.set(el, componentId);\n types.set(componentId, type);\n // Markup wins if the same collapsed component gets both provenances.\n if (markup) sources.set(componentId, 'markup');\n else if (!sources.has(componentId)) sources.set(componentId, 'auto');\n }\n\n const pageUrl = (doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined))?.location?.pathname ?? '/';\n registerSections(opts.apiKey, apiBase, pageUrl, [...types.entries()].map(([componentId, semanticType]) => ({\n componentId, semanticType, source: sources.get(componentId) ?? 'auto',\n })));\n\n // Accumulate visible dwell (ms) + max scroll ratio per component. `intersecting`\n // tracks in-viewport state independently of `enterAt` (the running clock) so a\n // tab-hide can pause the clock and a tab-show can resume it for still-visible\n // sections — IntersectionObserver does not re-fire on visibilitychange.\n const state = new Map<string, { ms: number; scroll: number; enterAt: number | null; intersecting: boolean }>();\n const get = (id: string) => {\n let s = state.get(id);\n if (!s) { s = { ms: 0, scroll: 0, enterAt: null, intersecting: false }; state.set(id, s); }\n return s;\n };\n\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n const id = componentOf.get(entry.target);\n if (!id) continue;\n const s = get(id);\n if (entry.isIntersecting) {\n s.intersecting = true;\n s.enterAt = Date.now();\n if (entry.intersectionRatio > s.scroll) s.scroll = entry.intersectionRatio;\n } else {\n s.intersecting = false;\n if (s.enterAt != null) { s.ms += Date.now() - s.enterAt; s.enterAt = null; }\n }\n }\n }, { threshold: [0, 0.25, 0.5, 0.75, 1] });\n for (const el of componentOf.keys()) observer.observe(el);\n\n // Bank accumulated dwell and RESET the accumulators (so a later emit can't\n // double-count) WITHOUT disconnecting — a visitor who hides/re-shows the tab\n // or tab-switches keeps being measured. Pauses the running clock; the tab-show\n // handler restarts it for still-visible sections so hidden time isn't counted.\n const emit = (): void => {\n const now = Date.now();\n for (const [id, s] of state) {\n if (s.enterAt != null) { s.ms += now - s.enterAt; s.enterAt = null; }\n if (s.ms <= 0) continue;\n try {\n client.track({\n projectId: opts.apiKey, // SDK convention: server derives the real project from the key\n componentId: id,\n eventType: 'dwell',\n payload: { dwell_time: Math.round(s.ms), scroll_depth: Number(s.scroll.toFixed(2)) },\n });\n } catch {\n /* fail-safe */\n }\n s.ms = 0;\n }\n };\n\n const onVisibility = (): void => {\n if (doc.hidden) {\n emit(); // bank + pause\n } else {\n const now = Date.now(); // resume the clock for sections still on screen\n for (const s of state.values()) if (s.intersecting) s.enterAt = now;\n }\n };\n const onPageHide = (): void => {\n emit();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n doc.addEventListener('visibilitychange', onVisibility);\n const win = doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined);\n win?.addEventListener('pagehide', onPageHide);\n\n // Per-section micro-signal detectors (opt-in; see EngagementCaptureOptions).\n // Attributed to the section's nc-<type> id with no variant — they feed the\n // persona attention fallback and auto-discovery, never rewards.\n const detectorCleanups: Array<() => void> = [];\n if (opts.microSignals) {\n // tab_loss is a single document-level `visibilitychange` signal, so enabling\n // it on every section detector would emit one tab_loss per nc-<type> section\n // on a single tab-hide — attributing one page-level exit to every section\n // (audit M5). Enable it on only the first section so the exit is recorded\n // once, mirroring the per-option path in slot-signals.ts ({ tabLoss: index === 0 }).\n [...componentOf.entries()].forEach(([el, componentId], i) => {\n detectorCleanups.push(\n attachMicroSignalDetectors((signalType, extra = {}) => {\n try {\n client.track({\n projectId: opts.apiKey,\n componentId,\n eventType: 'micro_signal',\n payload: { signalType, ...extra },\n });\n } catch {\n /* fail-safe */\n }\n }, el, undefined, { tabLoss: i === 0 }),\n );\n });\n }\n\n // Cleanup: bank any remaining dwell, then detach everything (provider unmount\n // / consent re-init must not leak observers or listeners).\n return () => {\n emit();\n doc.removeEventListener('visibilitychange', onVisibility);\n win?.removeEventListener('pagehide', onPageHide);\n for (const c of detectorCleanups) c();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n}\n"],"mappings":"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","names":["index_engagement_exports","__export","SEMANTIC_TYPES","classifyFeatures","classifySection","featuresFromElement","startEngagementCapture","__toCommonJS","SEMANTIC_TYPES","KEYWORDS","CONTENT_PATTERNS","headingText","el","_a","h","classifyFeatures","f","hay","type","re","featuresFromElement","_b","text","classifySection","import_policy","attachMicroSignalDetectors","emit","node","variantAssignedAt","options","cleanups","firedOnce","timestamps","onClick","now","onCopy","e","sel","selectionLength","isVisible","hesitationTimer","clearHesitation","startHesitation","onScroll","ioCallback","entries","entry","io","assignedAt","onVisibility","elapsed","isDoNotTrackEnabled","v","SECTION_SELECTOR","isNested","el","_a","registerSections","apiKey","apiBase","pageUrl","sections","e","NOOP","startEngagementCapture","client","opts","_b","_c","_d","_e","_f","_g","_h","_i","doc","isDoNotTrackEnabled","els","componentOf","types","sources","explicit","markup","SEMANTIC_TYPES","type","classifySection","componentId","semanticType","state","get","id","s","observer","entries","entry","emit","now","onVisibility","onPageHide","win","detectorCleanups","attachMicroSignalDetectors","signalType","extra","__spreadValues","c"]}
1
+ {"version":3,"sources":["../src/index-engagement.ts","../src/engagement/classify.ts","../src/index.ts","../src/micro-signals.ts","../src/engagement/capture.ts"],"sourcesContent":["// Lazy engagement entry — loaded on demand (mirrors ./graph) so the lean\n// bundle never carries the section classifier or IntersectionObserver logic.\nexport { startEngagementCapture, type EngagementCaptureOptions } from './engagement/capture';\nexport {\n classifyFeatures,\n classifySection,\n featuresFromElement,\n SEMANTIC_TYPES,\n type SectionFeatures,\n type SemanticType,\n} from './engagement/classify';\n","// Semantic section classification for no-code section capture (Phase 3 §2.4).\n// Pure heuristic: element → one of the graph_nodes semantic_type enum. Mirrors\n// the SDK graph scanner's vocabulary so the persona × section matrix consumes\n// snippet-captured sections unchanged.\n//\n// The classifier is split into a pure feature-based core (classifyFeatures —\n// also used server-side on crawled HTML by the site-audit classification job)\n// and a DOM wrapper (classifySection). Content-based patterns detect\n// pricing/social-proof/trust/comparison from BODY TEXT, so div-soup pages with\n// uninformative ids/classes still classify (persona-coverage spec 2026-07-23).\n\nexport type SemanticType =\n | 'pricing' | 'hero' | 'social_proof' | 'cta' | 'features'\n | 'faq' | 'comparison' | 'trust' | 'navigation' | 'generic';\n\n/** Canonical vocabulary — mirrors the graph_nodes semantic_type CHECK enum. */\nexport const SEMANTIC_TYPES: readonly SemanticType[] = [\n 'pricing', 'hero', 'social_proof', 'cta', 'features',\n 'faq', 'comparison', 'trust', 'navigation', 'generic',\n] as const;\n\n/** Environment-agnostic section features — buildable from a browser Element or\n * a server-parsed node (node-html-parser). */\nexport type SectionFeatures = {\n tag: string; // lowercase tag name\n idClass: string; // `${id} ${className}`\n headingText: string;\n bodyText: string; // normalized text content, first 2000 chars\n actionCount: number;\n textLength: number;\n};\n\n// Ordered most-specific first — the first keyword hit wins.\nconst KEYWORDS: Array<[SemanticType, RegExp]> = [\n ['pricing', /\\b(pricing|price|plans?|subscriptions?|per month|\\/mo|tier)\\b/i],\n ['faq', /\\b(faq|frequently asked|common questions?)\\b/i],\n ['comparison', /\\b(compare|comparison|versus|vs\\.)\\b/i],\n ['social_proof', /\\b(testimonial|reviews?|trusted by|loved by|customers?|logos|rated|case stud)\\b/i],\n ['trust', /\\b(security|guarantee|privacy|compliance|secure|gdpr|soc ?2|encrypt)\\b/i],\n ['features', /\\b(features?|how it works|benefits?|capabilit|what you get)\\b/i],\n];\n\n// Content-evidence patterns — run against bodyText. Ordered most-specific first.\n// Deliberately conservative: pricing needs per-period/plan context next to money\n// so an article mentioning \"$5 million\" stays generic.\nconst CONTENT_PATTERNS: Array<[SemanticType, RegExp]> = [\n ['pricing', /(?:[$€£]\\s?\\d[\\d,.]*\\s*(?:\\/|per\\s)\\s*(?:mo|month|yr|year|seat|user))|(?:\\b(?:starter|basic|pro|growth|premium|enterprise)\\b[^.]{0,60}[$€£]\\s?\\d)/i],\n ['social_proof', /(?:★{2,})|(?:\\b\\d(?:\\.\\d)?\\s*(?:out of|\\/)\\s*5\\b)|(?:\\brated\\b)|(?:[\"“][^\"”]{20,160}[\"”]\\s*[—–-]\\s*[A-Z][a-z]+)/],\n ['trust', /\\b(?:money[- ]back guarantee|refund(?:s)? within|encrypted|soc ?2|iso ?27001|gdpr[- ]compliant|cancel anytime)\\b/i],\n ['comparison', /\\b(?:vs|versus)\\b\\.?[^.!?]{0,80}\\b(?:compare|comparison|plans?|features?|alternative)\\b|\\bhow (?:we|it) compares?\\b/i],\n];\n\nfunction headingText(el: Element): string {\n const h = el.querySelector('h1, h2, h3');\n return (h?.textContent ?? '').slice(0, 160);\n}\n\n/**\n * Pure classification over extracted features. `strong` = keyword or content\n * evidence (trustable enough to auto-apply); `weak` = structural fallback\n * (cta/hero/navigation/generic — capture-worthy but not persona evidence).\n */\nexport function classifyFeatures(f: SectionFeatures): { type: SemanticType; strength: 'strong' | 'weak' } {\n if (f.tag === 'nav' || f.tag === 'footer') return { type: 'navigation', strength: 'weak' };\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n for (const [type, re] of KEYWORDS) {\n if (re.test(hay)) return { type, strength: 'strong' };\n }\n for (const [type, re] of CONTENT_PATTERNS) {\n if (re.test(f.bodyText)) return { type, strength: 'strong' };\n }\n if (f.actionCount >= 1 && f.textLength > 0 && f.textLength < 200) return { type: 'cta', strength: 'weak' };\n if (f.tag === 'header') return { type: 'hero', strength: 'weak' };\n if (/\\b(hero|headline|banner)\\b/i.test(hay)) return { type: 'hero', strength: 'weak' };\n return { type: 'generic', strength: 'weak' };\n}\n\n/** Feature extraction from a live DOM element (browser paths). */\nexport function featuresFromElement(el: Element): SectionFeatures {\n const text = (el.textContent ?? '').replace(/\\s+/g, ' ').trim();\n return {\n tag: el.tagName.toLowerCase(),\n idClass: `${el.id} ${String(el.className ?? '')}`,\n headingText: headingText(el),\n bodyText: text.slice(0, 2000),\n actionCount: el.querySelectorAll('a, button, [role=\"button\"]').length,\n textLength: text.length,\n };\n}\n\n/** Classify a page section into a semantic type (never null — falls back to\n * 'generic' so the caller can still capture attention on it). */\nexport function classifySection(el: Element): SemanticType {\n return classifyFeatures(featuresFromElement(el)).type;\n}\n","import { initSession, type SessionConfig, type SessionManager } from './session';\nimport {\n createEventQueue,\n retryStorageKey,\n type EventQueue,\n type EventType,\n type QueueConfig,\n type SentientEvent,\n} from './queue';\nimport {\n createAssignmentCache,\n type Assignment,\n} from './cache';\nimport type {\n GraphConfig,\n GraphSnapshot,\n} from './graph';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n referrerDomainFromReferer,\n uaTokenMatch,\n} from './session-meta.js';\nimport {\n toWireSlot,\n baselineResultFor,\n armOfResult,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\nimport { readSnapshot, writeSnapshot, SNAPSHOT_STORAGE_KEY_PREFIX, type SlotConfigEntry, type CompoundLocator } from './snapshot.js';\nimport { confidenceBand } from '@sentientui/policy';\nimport { createLocalModeClient } from './local-mode.js';\nimport { randomUuidV4 } from './uuid.js';\n\nexport { PROD_KEYLESS_ERROR, LOCAL_MODE_BANNER } from './local-mode.js';\n\nexport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n deriveSessionSegment,\n referrerDomainFromReferer,\n uaTokenMatch,\n matchedAgentToken,\n agentUaList,\n agentIntent,\n classifiedAgents,\n AGENT_INTENTS,\n} from './session-meta.js';\nexport type { AgentIntent } from './session-meta.js';\n\nconst DEFAULT_INGEST_URL = 'https://api.sentient-ui.com/v1/events';\n\n// Keyed by apiKey so multiple init() calls (HMR, multi-project) don't collide.\nconst _clients = new Map<string, {\n config: SentientConfig;\n upgrade: ((c: SentientClient) => void) | null;\n // Teardown for the live client bound to this key (stops its queue interval +\n // unload listeners). Present only for the full tracking client; the no-op /\n // pre-consent / local entries have nothing to tear down. Called before a\n // re-init for the same key replaces it, so timers/listeners can't leak.\n dispose?: () => void;\n}>();\nlet _lastApiKey: string | null = null;\n\nexport type SentientConfig = {\n apiKey: string;\n context: 'landing' | 'ecommerce' | 'saas' | 'marketplace';\n /** @internal — not exposed to users; defaults to the hosted SentientUI API. */\n ingestUrl?: string;\n debug?: boolean;\n /**\n * Pre-seeded assignments from `preloadAssignments()` (SSR).\n * Seeds the local cache so `assign()` returns without a network call for\n * listed code variants, guaranteeing server and client render the same\n * variant on first paint. Managed-text components (assign with no\n * variantIds) still fetch once when the seed carries no content.\n */\n initialAssignments?: Record<string, string>;\n /**\n * Segment used for SSR preload (`device:source`). When set with `initialAssignments`,\n * seeds the assignment cache under this key so hydration matches the server bandit row.\n */\n sessionSegment?: string;\n /**\n * Consent gate. When `false`, returns a no-op client and performs no tracking.\n * Defaults to `true`. Re-call `init()` (via `AdaptiveProvider` consent prop) when\n * the user grants or revokes consent mid-session.\n */\n consent?: boolean;\n /**\n * Behavior before consent is granted. `'statistical_winner'` fetches the\n * best-performing variant via `GET /v1/winner` — no session or tracking data\n * is stored. `'control'` (default) shows `variantIds[0]` with no API call.\n * Applies when tracking is gated off — either `consent: false` or an active\n * Do Not Track signal.\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Whether to honor the browser's Do Not Track (DNT) signal. Defaults to `true`.\n * When `true` and the visitor has DNT enabled, the SDK sets no cookies and\n * sends no tracking data — behaving exactly as `consent: false` (still serving\n * the read-only `preConsentBehavior` winner if configured), and `grantConsent()`\n * will not upgrade it. Set `false` to make your own consent gate authoritative.\n */\n respectDoNotTrack?: boolean;\n userId?: string;\n /**\n * Session ID generated server-side (from `loadAdaptiveAssignments` / `loadAdaptiveDecision`).\n * When provided, the client adopts this ID on first visit instead of generating a new one,\n * ensuring events and goals are attributed to the same session the server used for assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code for the visitor. When provided (e.g. from\n * the `CF-IPCountry` header in a Next.js server component), it is included in\n * the session upsert so country-based segmentation works without client-side\n * geo lookup.\n */\n country?: string;\n /**\n * Pre-seeded slot results from `preloadDecisions()` / `loadAdaptiveDecision()`\n * (SSR). Seeds the local slot state so `getSlotResult()` agrees with the\n * server-rendered markup on first paint.\n */\n initialSlots?: Record<string, SlotResult>;\n /**\n * Persona decided during SSR. Takes priority over the html-attribute\n * adoption and the local snapshot.\n */\n initialPersona?: { persona: string; confidence: number };\n /**\n * Keyless local mode. 'auto' (default) simulates decisions on-device when no\n * valid API key is configured — but only in development builds (the\n * `development` export condition); production bundles physically exclude the\n * engine. `true` forces the local engine regardless of key (escape hatch);\n * `false` restores the silent keyless no-op.\n */\n localMode?: 'auto' | boolean;\n};\n\nexport type AssignResult = { variantId: string; assignmentTtlMs: number; content?: string };\n\nexport type { SlotDeclInput, SlotResult };\nexport { armOfResult, baselineResultFor, baselineSlots, toWireSlot } from './slots.js';\n\nexport {\n SNAPSHOT_STORAGE_KEY_PREFIX,\n readSnapshot,\n writeSnapshot,\n renderPrePaintScript,\n} from './snapshot.js';\nexport type { DecisionSnapshot, SlotConfigEntry, SlotOps, CompoundLocator } from './snapshot.js';\n\n/** An editor-defined goal delivered with a registry-mode decision, for the\n * snippet to install delegated listeners from. */\nexport type GoalDefinition = {\n goalId: string;\n event: 'click' | 'form_submit' | 'url_reached';\n locator?: CompoundLocator;\n urlPattern?: string;\n slotId?: string;\n};\n\n/** One served section-classification row (registry mode): where it is on the\n * page (url match + compound locator) and its semantic type. The snippet\n * resolves the locator to build capture's `typeOf` hook. */\nexport type SectionMapEntry = {\n urlMatch: string;\n locator: CompoundLocator;\n type: string;\n};\n\nexport type DecideOutcome = {\n layoutOrder: string[] | null;\n assignments: Record<string, string>;\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n // Registry mode only: where/what to apply for server-defined slots.\n slotConfig?: Record<string, SlotConfigEntry>;\n // Registry mode only: editor-defined goals to wire up.\n goals?: GoalDefinition[];\n // Registry mode only: served section-classification map the snippet turns\n // into capture's `typeOf` hook.\n sectionMap?: SectionMapEntry[];\n};\n\nexport type DecideInput = {\n sections?: string[];\n components?: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n // 'registry' → serve the project's published slot_definitions in addition to\n // any declared slots (registry wins on id collision). Default 'request'.\n slotsFrom?: 'request' | 'registry';\n /**\n * Caller's build version (e.g. the snippet's `__SNIPPET_VERSION__`), sent\n * as `v` on the wire. Additive/best-effort: the server persists it for\n * version-skew reporting (see apps/api decide route) and ignores it\n * entirely on older deployments. Omit if the caller has no version to report.\n */\n v?: string;\n};\n\nexport type WeightEntry = { variantId: string; pulls: number; avgReward: number | null };\nexport type ComponentWeightEntry = { componentId: string; updatedAt: number; variants: WeightEntry[] };\n\n/** Options accepted by goal() (and inherited by componentGoal / the React\n * hooks / the snippet) — one shape everywhere (spec §5). */\nexport type GoalOptions = {\n /** Revenue of this conversion, in the project currency. */\n value?: number;\n /** ISO-4217 code, only when it differs from the project currency. */\n currency?: string;\n /** Merchant order/transaction id — dedupes retries, enables refunds later. */\n externalId?: string;\n /** Extra fields merged into the event payload / goal metadata. */\n metadata?: Record<string, unknown>;\n /** Advanced: partial-credit weight in [0,1] (composite steps). */\n weight?: number;\n /** Advanced: funnel step index (0-based). */\n stepIndex?: number;\n};\n\n/** goal()'s second arg is the options object iff it carries a reserved key;\n * anything else keeps the legacy bare-metadata interpretation. Reserved keys\n * inside legacy metadata were inert on the server, so reinterpretation is the\n * upgrade the sender wanted (spec §5). */\nfunction isGoalOptions(v: Record<string, unknown>): boolean {\n return 'value' in v || 'currency' in v || 'externalId' in v || 'metadata' in v || 'weight' in v || 'stepIndex' in v;\n}\n\nexport type ComponentGoalOptions = GoalOptions & {\n /** Reward credited to the served variant (0–1). Defaults to 1. */\n reward?: number;\n};\n\nexport type SentientClient = {\n track(\n event: Omit<SentientEvent, 'id' | 'sessionId' | 'timestamp' | 'timeInSession'>,\n ): void;\n goal(name: string, options?: GoalOptions): void;\n /** @deprecated positional form — prefer goal(name, options). */\n goal(name: string, metadata?: Record<string, unknown>, weight?: number, stepIndex?: number): void;\n /**\n * Records a conversion attributed to the variant currently served for\n * `componentId`, so it feeds the per-variant CVR funnel. Resolves the served\n * variant from the local assignment cache — no need to pass variantId or\n * projectId. No-ops if the component has not been assigned yet (render its\n * `<Adaptive>`/call `assign()` first). Prefer this over bare `goal()` for\n * variant experiments; `goal()` is session-level only (no component attribution).\n */\n componentGoal(componentId: string, goalType: string, opts?: ComponentGoalOptions): void;\n identify(userId: string): void;\n getAssignment(componentId: string, segment: string): Assignment | null;\n /** Server-side variant assignment. Caches the result locally per (component, segment). */\n assign(componentId: string, variantIds?: string[], agentData?: unknown, agentDataByVariant?: Record<string, unknown>): Promise<AssignResult | null>;\n /**\n * Single-roundtrip decision for layout sections, component variants, and\n * adaptive slots. Awaits the session upsert (like `assign`) so the server\n * never decides for a session row that doesn't exist yet. A response\n * without a `slots` field means the server predates slots — every declared\n * slot resolves to its baseline and no retry is made.\n */\n decide(input: DecideInput): Promise<DecideOutcome | null>;\n /** Slot result served this session (decide result, SSR seed, snapshot, or failure baseline). Null when unknown. */\n getSlotResult(slotId: string): SlotResult | null;\n /** Current persona estimate. Band is always `confidenceBand(confidence)`. Null when nothing is known yet. */\n getPersona(): { persona: string; confidence: number; band: 'low' | 'medium' | 'high' } | null;\n /** Fetches current bandit weights for all components in this project. Used by the provider to keep live-weight polling fresh. */\n fetchWeights(): Promise<ComponentWeightEntry[]>;\n getGraph(): GraphSnapshot;\n /**\n * Routine teardown: stops timers/listeners and flushes pending events, but\n * KEEPS the visitor identity, decision snapshot, and retry bucket. Use for\n * component unmount / re-init (framework providers call this on cleanup).\n */\n dispose(): void;\n /**\n * Consent-revocation / forget-me teardown: everything `dispose()` does,\n * plus deletion of the visitor identity (`_snt_uid`), the decision\n * snapshot, and the persisted retry bucket. The next visit starts as a\n * brand-new visitor.\n */\n destroy(): void;\n /** True when this client is the keyless local-mode client (dev only). */\n readonly isLocal?: boolean;\n};\n\n// SSR preload helpers moved to the `@sentientui/core/server` entry in 0.6.0 so\n// ~200 lines of Node-only fetch logic stop shipping in the browser bundle.\n\nexport { attachMicroSignalDetectors } from './micro-signals.js';\nexport type { MicroSignalEmitter, MicroSignalType } from './micro-signals.js';\n\nexport type {\n SessionConfig,\n SessionManager,\n EventType,\n SentientEvent,\n QueueConfig,\n Assignment,\n GraphSnapshot,\n GraphConfig,\n};\n\nfunction generateEventId(): string {\n return randomUuidV4();\n}\n\nconst SSR_CLIENT: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n assign: () => Promise.resolve(null),\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n fetchWeights: () => Promise.resolve([]),\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n};\n\nfunction readUtmParams(): Record<string, string> {\n try {\n const out: Record<string, string> = {};\n const sp = new URLSearchParams(window.location.search);\n for (const [k, v] of sp) {\n if (k.startsWith('utm_')) out[k] = v;\n }\n return out;\n } catch {\n return {};\n }\n}\n\nfunction deriveBaseUrl(ingestUrl: string): string {\n return ingestUrl.replace(/\\/events\\/?$/, '');\n}\n\n/**\n * Detects whether the visitor has signalled a tracking opt-out. Honors Global\n * Privacy Control (`navigator.globalPrivacyControl`) — the legally-enforceable\n * CCPA/CPRA signal — as well as Do Not Track (`navigator.doNotTrack`, the legacy\n * `window.doNotTrack` on older Firefox, and `navigator.msDoNotTrack` on old\n * IE/Edge). GPC is a boolean; DNT is opt-out only when explicitly `'1'`/`'yes'`.\n */\nexport function isDoNotTrackEnabled(): boolean {\n // GPC is a boolean flag, checked separately from the DNT string signals.\n if (\n typeof navigator !== 'undefined' &&\n (navigator as unknown as { globalPrivacyControl?: boolean }).globalPrivacyControl === true\n ) {\n return true;\n }\n const signals = [\n typeof navigator !== 'undefined' ? navigator.doNotTrack : undefined,\n typeof window !== 'undefined'\n ? (window as unknown as { doNotTrack?: string | null }).doNotTrack\n : undefined,\n typeof navigator !== 'undefined'\n ? (navigator as unknown as { msDoNotTrack?: string | null }).msDoNotTrack\n : undefined,\n ];\n return signals.some((v) => v === '1' || v === 'yes');\n}\n\n/**\n * Upgrades a pre-consent client (any client created with `consent: false`, in\n * either `preConsentBehavior` mode) to a fully-tracking client, in place and\n * with no page reload. Call this from your consent management platform callback.\n * For React apps, prefer updating the `consent` prop on `<AdaptiveProvider>`.\n * Pass `apiKey` to target a specific project; omit to upgrade the most-recently-initialized client.\n */\nexport function grantConsent(apiKey?: string): void {\n if (typeof window === 'undefined') return;\n\n const key = apiKey ?? _lastApiKey;\n if (!key) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const entry = _clients.get(key);\n if (!entry) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const { config, upgrade } = entry;\n if (!upgrade) return;\n\n // Honor an active Do Not Track signal — consent cannot override a global opt-out.\n if (config.respectDoNotTrack !== false && isDoNotTrackEnabled()) return;\n\n const fullClient = init({ ...config, consent: true });\n upgrade(fullClient);\n // init() just registered the full client (with its dispose) under `key`.\n // Preserve that dispose so a later re-init/teardown can still tear it down —\n // we only need to clear the upgrade hook now that consent is granted.\n const disposed = _clients.get(key)?.dispose;\n _clients.set(key, { config: { ...config, consent: true }, upgrade: null, dispose: disposed });\n}\n\nfunction createPreConsentProxy(config: SentientConfig): { proxy: SentientClient; setInner: (c: SentientClient) => void } {\n // 'control' (the default) must reach the network zero times before consent.\n // The proxy still exists so grantConsent() has something to upgrade in place\n // — without it, a site wanting no pre-consent traffic could only start\n // tracking by reloading the page.\n const servesWinner = config.preConsentBehavior === 'statistical_winner';\n const baseUrl = deriveBaseUrl(config.ingestUrl ?? DEFAULT_INGEST_URL);\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n let inner: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n fetchWeights: () => Promise.resolve([]),\n async assign(componentId, variantIds, _agentData?) {\n // Control mode: no request. Callers fall back to variantIds[0] through\n // ssrFallback, exactly as they did against the old no-op client.\n if (!servesWinner) return null;\n try {\n const params = new URLSearchParams({ componentId });\n for (const v of variantIds ?? []) params.append('variantIds[]', v);\n const res = await fetch(`${baseUrl}/winner?${params.toString()}`, {\n headers: authHeaders,\n });\n if (!res.ok) return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n const body = (await res.json()) as { variantId: string };\n return { variantId: body.variantId, assignmentTtlMs: 0 };\n } catch {\n return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n }\n },\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n };\n\n const proxy: SentientClient = {\n track: (e) => inner.track(e),\n // Cast: a single arrow can't structurally satisfy the overloaded member;\n // the passthrough forwards both call shapes untouched.\n goal: ((n: string, m?: Record<string, unknown>, w?: number, s?: number) => inner.goal(n, m, w, s)) as SentientClient['goal'],\n componentGoal: (c, g, o) => inner.componentGoal(c, g, o),\n identify: (u) => inner.identify(u),\n getAssignment: (c, s) => inner.getAssignment(c, s),\n assign: (c, v, a, av) => inner.assign(c, v, a, av),\n decide: (i) => inner.decide(i),\n getSlotResult: (s) => inner.getSlotResult(s),\n getPersona: () => inner.getPersona(),\n fetchWeights: () => inner.fetchWeights(),\n getGraph: () => inner.getGraph(),\n dispose: () => inner.dispose(),\n destroy: () => inner.destroy(),\n };\n\n function setInner(fullClient: SentientClient) {\n inner = fullClient;\n }\n\n return { proxy, setInner };\n}\n\n/**\n * Initializes the Sentient client. Returns a no-op client during SSR.\n */\nexport function init(config: SentientConfig): SentientClient {\n if (typeof window === 'undefined') {\n return SSR_CLIENT;\n }\n\n _lastApiKey = config.apiKey;\n\n // A re-init for the same key (HMR, consent toggle, provider remount) supersedes\n // the prior client. Dispose it first so its queue's setInterval and\n // visibilitychange/pagehide listeners don't leak — the map only ever held its\n // config, so without this the old client kept flushing forever.\n const prevEntry = _clients.get(config.apiKey || 'local');\n if (prevEntry?.dispose) {\n try {\n prevEntry.dispose();\n } catch {\n /* teardown must never throw on re-init */\n }\n }\n\n // DNT/GPC (a global opt-out) or an explicit `consent: false` must be evaluated\n // BEFORE the local-mode branch: createLocalModeClient() calls initSession()\n // unconditionally, so a gated visitor would otherwise be issued the 365-day\n // `_snt_uid` identity cookie in keyless/local mode (audit P2). DNT/GPC also\n // gates tracking off even when the site passes `consent: true`, and blocks\n // `grantConsent()` from upgrading.\n const dntBlocked = config.respectDoNotTrack !== false && isDoNotTrackEnabled();\n const gated = config.consent === false || dntBlocked;\n\n // Keyless local mode. `localMode: true` forces the local engine (documented\n // escape hatch); 'auto' (default) engages it only when no valid key is\n // present. In production builds `@sentientui/core/local` resolves to a stub\n // and this degrades to a no-op client + one console.error per page load\n // (createLocalModeClient handles that), so no NODE_ENV check is needed here.\n const keyValid = typeof config.apiKey === 'string' && config.apiKey.startsWith('pk_');\n if (config.localMode === true || (!keyValid && config.localMode !== false)) {\n // A gated visitor must never get the identity cookie. Local mode has no\n // server to serve a statistical winner from, so return a plain no-op.\n if (gated) {\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return SSR_CLIENT;\n }\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return createLocalModeClient(config);\n }\n\n if (gated) {\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n if (config.preConsentBehavior === 'statistical_winner') {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n }\n _clients.set(config.apiKey, { config, upgrade: null });\n return SSR_CLIENT;\n }\n // Every gated client gets an upgradeable proxy, not just the winner-serving\n // one — otherwise grantConsent() is silently dead for the 'control' default\n // and the site has to reload to start tracking. Control mode still makes no\n // request; the proxy only exists so consent can swap the inner client.\n const { proxy, setInner } = createPreConsentProxy(config);\n // Under DNT the read-only winner still serves, but consent can never\n // upgrade it to tracking — so drop the upgrade hook.\n _clients.set(config.apiKey, { config, upgrade: dntBlocked ? null : setInner });\n return proxy;\n }\n\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n return SSR_CLIENT;\n }\n\n if (config.ingestUrl === '') {\n console.warn('[sentient] init() called with an empty ingestUrl. SDK disabled.');\n return SSR_CLIENT;\n }\n\n const resolvedIngestUrl = config.ingestUrl ?? DEFAULT_INGEST_URL;\n\n const sessionStart = Date.now();\n const session = initSession({ ssrSessionId: config.ssrSessionId, apiKey: config.apiKey });\n const assignmentCache = createAssignmentCache(undefined, config.apiKey);\n const eventQueue = createEventQueue({ ingestUrl: resolvedIngestUrl, apiKey: config.apiKey });\n const baseUrl = deriveBaseUrl(resolvedIngestUrl);\n\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n const deviceClass = detectDeviceClass(navigator.userAgent ?? '');\n const appOrigin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const trafficSource = detectTrafficSource(document.referrer ?? '', appOrigin);\n const sessionSegment =\n config.sessionSegment ?? `${deviceClass}:${trafficSource}`;\n const inflightAssigns = new Map<string, Promise<AssignResult | null>>();\n\n // --- Adaptive-slot state (decide) ---\n // Results served for this session, keyed by slot id. Written by decide();\n // read by getSlotResult() (Task 3.3) and componentGoal's slot fallback.\n const slotStore = new Map<string, SlotResult>();\n let personaState: { persona: string; confidence: number } | null = null;\n\n // On decide failure every declared slot must still resolve — to its baseline.\n // Never overwrite a previously served result.\n const seedSlotBaselines = (decls: SlotDeclInput[]): void => {\n for (const d of decls) {\n if (!slotStore.has(d.id)) slotStore.set(d.id, baselineResultFor(d));\n }\n };\n\n // Seed slot/persona state. Priority: explicit SSR seeds → snapshot.\n if (config.initialSlots) {\n for (const [slotId, result] of Object.entries(config.initialSlots)) {\n slotStore.set(slotId, result);\n }\n }\n const seedSnapshot = readSnapshot(config.apiKey);\n if (seedSnapshot) {\n for (const [slotId, result] of Object.entries(seedSnapshot.slots)) {\n if (!slotStore.has(slotId)) slotStore.set(slotId, result);\n }\n }\n\n // Band-only persona sources (html attrs, snapshot) become a band-consistent\n // numeric confidence so confidenceBand(confidence) always equals the band.\n const BAND_CONFIDENCE: Record<string, number> = { low: 0.15, medium: 0.5, high: 0.85 };\n if (config.initialPersona) {\n personaState = { ...config.initialPersona };\n } else {\n // Single-writer rule: the inline pre-paint script owns the <html>\n // attributes. The client ADOPTS them as truth and never rewrites them\n // mid-session (next visit's script picks up the new snapshot instead).\n const ds = document.documentElement.dataset;\n if (ds.sentientPersona) {\n personaState = {\n persona: ds.sentientPersona,\n confidence: BAND_CONFIDENCE[ds.sentientConfidence ?? 'low'] ?? 0.15,\n };\n } else if (seedSnapshot) {\n personaState = {\n persona: seedSnapshot.persona,\n confidence: BAND_CONFIDENCE[seedSnapshot.band] ?? 0.15,\n };\n }\n }\n\n // Seed SSR-preloaded assignments into the local cache so assign() finds a\n // cache hit immediately — no network call, no variant flash on hydration.\n if (config.initialAssignments) {\n for (const [componentId, variantId] of Object.entries(config.initialAssignments)) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n }\n\n // Upsert session metadata once on init. assign() awaits this promise so\n // the server isn't asked to assign for a session row that doesn't exist yet.\n let sessionReady: Promise<void> = Promise.resolve();\n\n const sessionId = session.getSessionId();\n if (sessionId) {\n const referrerDomain = referrerDomainFromReferer(document.referrer ?? '');\n const sessionBody = {\n sessionId,\n deviceClass,\n trafficSource,\n referrerDomain,\n utmParams: readUtmParams(),\n timeOfDay: detectTimeOfDay(new Date()),\n dayOfWeek: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][new Date().getDay()],\n ephemeral: session.isEphemeral(),\n // Likely-automation hint: navigator.webdriver (set under automation\n // control) or a known agent token in the UA. Probabilistic — used for\n // metrics + bandit exclusion server-side, never to change what's served.\n automation:\n (typeof navigator !== 'undefined' && navigator.webdriver === true) ||\n uaTokenMatch(navigator.userAgent ?? ''),\n ...(config.userId ? { userId: config.userId } : {}),\n ...(config.country ? { country: config.country } : {}),\n };\n try {\n sessionReady = fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify(sessionBody),\n headers: authHeaders,\n })\n .then((res) => {\n if (res.status === 402) {\n console.warn(\n '[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing',\n );\n }\n return undefined;\n })\n .catch(() => undefined);\n } catch {\n /* never throw on init */\n }\n }\n\n if (config.debug) {\n console.log('[sentient] initialized', { context: config.context });\n (\n window as unknown as {\n __sentient?: {\n client: SentientClient;\n queue: EventQueue;\n };\n }\n ).__sentient = {\n client: null as unknown as SentientClient,\n queue: eventQueue,\n };\n }\n\n const client: SentientClient = {\n goal(name: string, metadataOrOpts: Record<string, unknown> = {}, weight = 1.0, stepIndex = 0) {\n const sid = session.getSessionId();\n if (!sid) return;\n const opts: GoalOptions = isGoalOptions(metadataOrOpts)\n ? (metadataOrOpts as GoalOptions)\n : { metadata: metadataOrOpts };\n const goalId = generateEventId();\n // undefined values vanish at JSON.stringify time, so optional fields\n // need no conditional assembly.\n const body = {\n sessionId: sid,\n name,\n metadata: opts.metadata ?? {},\n weight: opts.weight ?? weight,\n stepIndex: opts.stepIndex ?? stepIndex,\n goalId,\n value: opts.value,\n currency: opts.currency,\n externalId: opts.externalId,\n };\n sessionReady.then(() => {\n fetch(`${baseUrl}/goals`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify(body),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n componentGoal(componentId, goalType, opts) {\n const sid = session.getSessionId();\n if (!sid) return;\n // Variant experiments resolve from the assignment cache; adaptive slots\n // (useAdaptiveTokens / AdaptiveGroup) resolve from the slot state, using\n // the canonical arm string as the attributed variantId.\n const assignment = assignmentCache.get(componentId, sessionSegment);\n const slotResult = assignment ? null : slotStore.get(componentId) ?? null;\n if (!assignment && slotResult === null) {\n if (config.debug) {\n console.warn(\n `[sentient] componentGoal(\"${componentId}\"): no assignment or slot decision yet — render its <Adaptive>/adaptive hook or call assign()/decide() before recording a goal.`,\n );\n }\n return;\n }\n const attributedVariantId = assignment ? assignment.variantId : armOfResult(slotResult!);\n const fullEvent: SentientEvent = {\n id: generateEventId(),\n sessionId: sid,\n projectId: config.apiKey,\n componentId,\n variantId: attributedVariantId,\n eventType: 'goal_achieved',\n goalType,\n // undefined goalValue/currency vanish at JSON.stringify time.\n payload: {\n reward: opts?.reward ?? 1,\n goalValue: opts?.value,\n currency: opts?.currency,\n ...(opts?.metadata ?? {}),\n },\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n if (config.debug) {\n console.log('[sentient] componentGoal', fullEvent);\n }\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n identify(userId) {\n const sid = session.getSessionId();\n if (!sid) return;\n sessionReady.then(() => {\n fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify({ sessionId: sid, userId, ephemeral: session.isEphemeral() }),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n track(event) {\n const sessionId = session.getSessionId();\n if (!sessionId) return;\n\n const fullEvent: SentientEvent = {\n ...event,\n id: generateEventId(),\n sessionId,\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n\n if (config.debug) {\n console.log('[sentient] track', fullEvent);\n }\n\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n getAssignment(componentId, segment) {\n return assignmentCache.get(componentId, segment);\n },\n\n async assign(componentId, variantIds, agentData?, agentDataByVariant?) {\n const sid = session.getSessionId();\n if (!sid) return null;\n\n const cached = assignmentCache.get(componentId, sessionSegment);\n // When variantIds are provided (A/B code variant), a cache hit is always final.\n // When variantIds are absent (managed text component), only hit the cache if content\n // is present — a seed from initialAssignments has no content and must still fetch.\n if (cached && (variantIds?.length || cached.content !== undefined)) {\n // Surface the entry's remaining TTL (server-provided when set) instead of\n // a hardcoded 0, so callers can reason about when a re-assign is due.\n const remainingTtlMs =\n cached.ttlMs && cached.ttlMs > 0\n ? Math.max(0, cached.assignedAt + cached.ttlMs - Date.now())\n : 0;\n return { variantId: cached.variantId, assignmentTtlMs: remainingTtlMs, content: cached.content };\n }\n\n // Coalesce concurrent assigns for the same component (e.g. several\n // mounted slots sharing one id) into a single network request.\n const inflight = inflightAssigns.get(componentId);\n if (inflight) return inflight;\n\n const request = (async (): Promise<AssignResult | null> => {\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid, componentId, variantIds };\n if (agentDataByVariant !== undefined) body.agentDataByVariant = agentDataByVariant;\n else if (agentData !== undefined) body.agentData = agentData;\n const res = await fetch(`${baseUrl}/assign`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) return null;\n const result = (await res.json()) as AssignResult;\n assignmentCache.set(componentId, sessionSegment, {\n variantId: result.variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n content: result.content,\n // Honor the server's TTL as this entry's expiry; omit when absent/0\n // so the cache falls back to its default (DEFAULT_TTL_MS).\n ...(result.assignmentTtlMs && result.assignmentTtlMs > 0\n ? { ttlMs: result.assignmentTtlMs }\n : {}),\n });\n return result;\n } catch {\n return null;\n } finally {\n inflightAssigns.delete(componentId);\n }\n })();\n inflightAssigns.set(componentId, request);\n return request;\n },\n\n async decide(input) {\n const sid = session.getSessionId();\n if (!sid) return null;\n const declared = input.slots ?? [];\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid };\n if (input.sections && input.sections.length > 0) {\n body.sections = input.sections.map((id) => ({ id }));\n }\n body.components = input.components ?? [];\n if (declared.length > 0) body.slots = declared.map(toWireSlot);\n if (input.slotsFrom === 'registry') body.slotsFrom = 'registry';\n if (input.v) body.v = input.v;\n\n const res = await fetch(`${baseUrl}/decide`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) {\n seedSlotBaselines(declared);\n return null;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[] | null;\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n slotConfig?: Record<string, SlotConfigEntry>;\n goals?: GoalDefinition[];\n sectionMap?: SectionMapEntry[];\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declared) {\n // `data.slots === undefined` means the server predates the slots\n // contract: serve the declared baseline everywhere, do NOT retry.\n // (Distinct from `slots: {}`, which also falls back per-slot.)\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n // Registry mode: the server returns slots the request never declared.\n // Take them verbatim (classic mode returns only declared slots, so this\n // union is a no-op there — back-compatible).\n if (data.slots) {\n for (const [slotId, result] of Object.entries(data.slots)) {\n if (!(slotId in slots)) slots[slotId] = result;\n }\n }\n for (const [slotId, result] of Object.entries(slots)) slotStore.set(slotId, result);\n // Only overwrite persona when the response actually carries one, and\n // never downgrade a known persona to 'unknown' — a decide that omits\n // persona (or returns 'unknown') must not clobber a good SSR/snapshot/\n // initialPersona value, and must not persist that regression below.\n const known = personaState != null && personaState.persona !== 'unknown';\n if (data.persona && !(data.persona === 'unknown' && known)) {\n personaState = { persona: data.persona, confidence: data.confidence ?? 0 };\n } else if (!personaState) {\n personaState = { persona: 'unknown', confidence: 0 };\n }\n\n // Seed component assignments so <Adaptive>/assign() agree with this\n // decide (same shape as the initialAssignments seed above).\n for (const [componentId, variantId] of Object.entries(data.assignments ?? {})) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n\n // Persist for the next visit's pre-paint (SPA cache-first pattern).\n writeSnapshot(config.apiKey, {\n v: 1,\n persona: personaState.persona,\n band: confidenceBand(personaState.confidence),\n slots: Object.fromEntries(slotStore),\n layoutOrder: data.layoutOrder ?? null,\n savedAt: Date.now(),\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n });\n\n return {\n layoutOrder: data.layoutOrder ?? null,\n assignments: data.assignments ?? {},\n slots,\n persona: personaState.persona,\n confidence: personaState.confidence,\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n ...(data.goals ? { goals: data.goals } : {}),\n ...(data.sectionMap ? { sectionMap: data.sectionMap } : {}),\n };\n } catch {\n seedSlotBaselines(declared);\n return null;\n }\n },\n\n getSlotResult(slotId) {\n return slotStore.get(slotId) ?? null;\n },\n\n getPersona() {\n if (!personaState) return null;\n return {\n persona: personaState.persona,\n confidence: personaState.confidence,\n band: confidenceBand(personaState.confidence),\n };\n },\n\n async fetchWeights() {\n try {\n const res = await fetch(`${baseUrl}/weights`, { headers: authHeaders });\n if (!res.ok) return [];\n const data = (await res.json()) as { components: ComponentWeightEntry[] };\n return data.components ?? [];\n } catch {\n return [];\n }\n },\n\n getGraph() {\n return { pageNodes: [], capturedAt: 0 };\n },\n\n dispose() {\n // Stops the flush timer and unload listeners (with a final flush) but\n // leaves identity, snapshot, and retry bucket for the next client.\n eventQueue.destroy();\n // Drop the registry entry so a later re-init doesn't try to dispose an\n // already-torn-down client (and so the map doesn't pin this closure).\n if (_clients.get(config.apiKey)?.dispose === client.dispose) {\n _clients.delete(config.apiKey);\n }\n if (config.debug) {\n console.log('[sentient] disposed');\n }\n },\n\n destroy() {\n eventQueue.destroy();\n session.destroy();\n if (_clients.get(config.apiKey)?.dispose === client.dispose) {\n _clients.delete(config.apiKey);\n }\n // Forget-me must be total: a surviving decision snapshot would\n // re-personalize the next visit via the pre-paint script, and a\n // persisted retry bucket would re-send events for the deleted identity.\n try {\n localStorage.removeItem(SNAPSHOT_STORAGE_KEY_PREFIX + config.apiKey);\n localStorage.removeItem(retryStorageKey(config.apiKey));\n } catch {\n /* storage unavailable — nothing persisted to remove */\n }\n if (config.debug) {\n console.log('[sentient] destroyed');\n }\n },\n };\n\n _clients.set(config.apiKey, { config, upgrade: null, dispose: client.dispose });\n\n if (config.debug) {\n const win = window as unknown as { __sentient?: { client: SentientClient } };\n if (win.__sentient) {\n win.__sentient.client = client;\n }\n }\n\n return client;\n}\n","export type MicroSignalEmitter = (\n signalType: 'rage_click' | 'text_copy' | 'scroll_hesitation' | 'tab_loss',\n extra?: Record<string, unknown>,\n) => void;\n\nexport type MicroSignalType = Parameters<MicroSignalEmitter>[0];\n\n/**\n * Attaches passive behavioral detectors to `node`. Calls `emit` when a signal\n * fires. Each signal type fires at most once per call to this function.\n * Returns a cleanup function that removes all listeners.\n */\nexport function attachMicroSignalDetectors(\n emit: MicroSignalEmitter,\n node: Element,\n variantAssignedAt?: number,\n options?: { tabLoss?: boolean },\n): () => void {\n const cleanups: Array<() => void> = [];\n\n // --- Rage click: 3+ clicks within 500ms ---\n {\n const WINDOW_MS = 500;\n const THRESHOLD = 3;\n let firedOnce = false;\n const timestamps: number[] = [];\n\n const onClick = (): void => {\n if (firedOnce) return;\n const now = Date.now();\n timestamps.push(now);\n while (timestamps.length > 0 && now - timestamps[0]! > WINDOW_MS) {\n timestamps.shift();\n }\n if (timestamps.length >= THRESHOLD) {\n firedOnce = true;\n emit('rage_click');\n }\n };\n\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n }\n\n // --- Text copy: copy event within node ---\n {\n let firedOnce = false;\n\n const onCopy = (e: Event): void => {\n if (firedOnce) return;\n if (!(e.target instanceof Node)) return;\n if (!node.contains(e.target) && node !== e.target) return;\n firedOnce = true;\n const sel = typeof window !== 'undefined' ? window.getSelection() : null;\n const selectionLength = sel ? sel.toString().length : 0;\n emit('text_copy', { selectionLength });\n };\n\n document.addEventListener('copy', onCopy);\n cleanups.push(() => document.removeEventListener('copy', onCopy));\n }\n\n // --- Scroll hesitation: scroll stops 3s while component visible ---\n {\n let firedOnce = false;\n let isVisible = false;\n let hesitationTimer: ReturnType<typeof setTimeout> | null = null;\n\n const clearHesitation = (): void => {\n if (hesitationTimer !== null) {\n clearTimeout(hesitationTimer);\n hesitationTimer = null;\n }\n };\n\n const startHesitation = (): void => {\n if (firedOnce || !isVisible) return;\n clearHesitation();\n hesitationTimer = setTimeout(() => {\n if (!firedOnce && isVisible) {\n firedOnce = true;\n emit('scroll_hesitation');\n }\n }, 3000);\n };\n\n const onScroll = (): void => {\n clearHesitation();\n startHesitation();\n };\n\n const ioCallback = (entries: IntersectionObserverEntry[]): void => {\n for (const entry of entries) {\n isVisible = entry.intersectionRatio > 0.3;\n if (!isVisible) clearHesitation();\n else startHesitation();\n }\n };\n const io = new IntersectionObserver(ioCallback, { threshold: [0.3] });\n io.observe(node);\n window.addEventListener('scroll', onScroll, { passive: true });\n\n cleanups.push(() => {\n io.disconnect();\n window.removeEventListener('scroll', onScroll);\n clearHesitation();\n });\n }\n\n // --- Tab loss: tab hidden within 15s of variant_assigned ---\n // Document-level (not node-scoped), so callers that attach detectors to many\n // nodes at once (e.g. the snippet's per-option slot signals) can opt out on all\n // but one node to avoid emitting a duplicate tab_loss per node (audit M5).\n if (options?.tabLoss !== false) {\n let firedOnce = false;\n const assignedAt = variantAssignedAt ?? Date.now();\n\n const onVisibility = (): void => {\n if (firedOnce) return;\n if (document.visibilityState !== 'hidden') return;\n const elapsed = Date.now() - assignedAt;\n if (elapsed < 15_000) {\n firedOnce = true;\n emit('tab_loss', { timeOnPage: elapsed });\n }\n };\n\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() => document.removeEventListener('visibilitychange', onVisibility));\n }\n\n return () => {\n for (const c of cleanups) c();\n };\n}\n","import { classifySection, SEMANTIC_TYPES, type SemanticType } from './classify';\nimport { isDoNotTrackEnabled } from '../index.js';\nimport { attachMicroSignalDetectors } from '../micro-signals.js';\n\n// Shared engagement capture (spec 2026-07-22-persona-signal-capture). Detects\n// semantic sections, registers them via /v1/section-map, and records per-section\n// dwell/scroll via IntersectionObserver — emitting the same 'dwell' events the\n// persona pipeline consumes. Used by the React provider (default on) and the\n// no-code snippet. Defense-in-depth: checks DNT internally even though callers\n// gate on consent/DNT too; a missing IntersectionObserver → no-op.\n\ntype CaptureClient = {\n track(event: { projectId: string; componentId: string; eventType: string; payload: Record<string, unknown> }): void;\n};\n\nexport type EngagementCaptureOptions = {\n apiKey: string;\n /** API base, no trailing slash. Defaults to the hosted API. */\n apiBase?: string;\n doc?: Document;\n /**\n * Also attach per-section micro-signal detectors (rage click, text copy,\n * scroll hesitation, tab loss), attributed to the section's `nc-<type>`\n * component. For the no-code snippet, whose pages have no `<Adaptive>`\n * components carrying their own detectors. Default false — the React SDK\n * keeps its per-component detectors and must not double-attach.\n */\n microSignals?: boolean;\n /**\n * Server-served section-map lookup (persona-coverage auto-classification):\n * consulted after explicit `data-sentient-type` markup, before the local\n * heuristic. Return null when the element has no served label.\n */\n typeOf?: (el: Element) => SemanticType | null;\n};\n\nconst SECTION_SELECTOR = 'section, header, footer, nav, main > div, [data-sentient-section]';\n\n/** Skip a section nested inside another candidate section (avoid double count). */\nfunction isNested(el: Element): boolean {\n return el.parentElement?.closest(SECTION_SELECTOR) != null;\n}\n\nfunction registerSections(\n apiKey: string,\n apiBase: string,\n pageUrl: string,\n sections: Array<{ componentId: string; semanticType: SemanticType; source: 'markup' | 'auto' }>,\n): void {\n try {\n void fetch(`${apiBase}/v1/section-map`, {\n method: 'POST',\n keepalive: true,\n headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ pageUrl, sections }),\n }).catch(() => undefined);\n } catch {\n /* fail-safe */\n }\n}\n\nconst NOOP = (): void => undefined;\n\nexport function startEngagementCapture(\n client: CaptureClient,\n opts: EngagementCaptureOptions,\n): () => void {\n const doc = opts.doc ?? (typeof document !== 'undefined' ? document : undefined);\n if (!doc || typeof IntersectionObserver === 'undefined') return NOOP;\n if (isDoNotTrackEnabled()) return NOOP;\n // Keyless zero-network contract: capture exists to feed the hosted persona\n // pipeline — with no api key there is nothing to feed, and the section-map\n // registration fetch must never fire.\n if (!opts.apiKey) return NOOP;\n // Normalize so both a ROOT base (`https://api.sentient-ui.com`) and a\n // `/v1`-suffixed base resolve to exactly one `/v1/section-map` — some callers\n // pass the versioned base, which would otherwise produce `/v1/v1/section-map`\n // (a silent 404). Strip trailing slashes, then a single trailing `/v1`.\n const apiBase = (opts.apiBase ?? 'https://api.sentient-ui.com')\n .replace(/\\/+$/, '')\n .replace(/\\/v1$/, '');\n\n const els = Array.from(doc.querySelectorAll(SECTION_SELECTOR)).filter((el) => !isNested(el));\n if (els.length === 0) return NOOP;\n\n // Collapse to one component per semantic type per page (the matrix aggregates\n // by semantic type anyway). Per-element precedence: explicit data-sentient-type\n // markup → served section map (opts.typeOf) → local heuristic.\n const componentOf = new Map<Element, string>();\n const types = new Map<string, SemanticType>();\n const sources = new Map<string, 'markup' | 'auto'>();\n for (const el of els) {\n const explicit = el.getAttribute('data-sentient-type');\n const markup = explicit && (SEMANTIC_TYPES as readonly string[]).includes(explicit)\n ? (explicit as SemanticType)\n : null;\n const type = markup ?? opts.typeOf?.(el) ?? classifySection(el);\n const componentId = `nc-${type}`;\n componentOf.set(el, componentId);\n types.set(componentId, type);\n // Markup wins if the same collapsed component gets both provenances.\n if (markup) sources.set(componentId, 'markup');\n else if (!sources.has(componentId)) sources.set(componentId, 'auto');\n }\n\n const pageUrl = (doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined))?.location?.pathname ?? '/';\n registerSections(opts.apiKey, apiBase, pageUrl, [...types.entries()].map(([componentId, semanticType]) => ({\n componentId, semanticType, source: sources.get(componentId) ?? 'auto',\n })));\n\n // Accumulate visible dwell (ms) + max scroll ratio per component. `intersecting`\n // tracks in-viewport state independently of `enterAt` (the running clock) so a\n // tab-hide can pause the clock and a tab-show can resume it for still-visible\n // sections — IntersectionObserver does not re-fire on visibilitychange.\n const state = new Map<string, { ms: number; scroll: number; enterAt: number | null; intersecting: boolean }>();\n const get = (id: string) => {\n let s = state.get(id);\n if (!s) { s = { ms: 0, scroll: 0, enterAt: null, intersecting: false }; state.set(id, s); }\n return s;\n };\n\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n const id = componentOf.get(entry.target);\n if (!id) continue;\n const s = get(id);\n if (entry.isIntersecting) {\n s.intersecting = true;\n s.enterAt = Date.now();\n if (entry.intersectionRatio > s.scroll) s.scroll = entry.intersectionRatio;\n } else {\n s.intersecting = false;\n if (s.enterAt != null) { s.ms += Date.now() - s.enterAt; s.enterAt = null; }\n }\n }\n }, { threshold: [0, 0.25, 0.5, 0.75, 1] });\n for (const el of componentOf.keys()) observer.observe(el);\n\n // Bank accumulated dwell and RESET the accumulators (so a later emit can't\n // double-count) WITHOUT disconnecting — a visitor who hides/re-shows the tab\n // or tab-switches keeps being measured. Pauses the running clock; the tab-show\n // handler restarts it for still-visible sections so hidden time isn't counted.\n const emit = (): void => {\n const now = Date.now();\n for (const [id, s] of state) {\n if (s.enterAt != null) { s.ms += now - s.enterAt; s.enterAt = null; }\n if (s.ms <= 0) continue;\n try {\n client.track({\n projectId: opts.apiKey, // SDK convention: server derives the real project from the key\n componentId: id,\n eventType: 'dwell',\n payload: { dwell_time: Math.round(s.ms), scroll_depth: Number(s.scroll.toFixed(2)) },\n });\n } catch {\n /* fail-safe */\n }\n s.ms = 0;\n }\n };\n\n const onVisibility = (): void => {\n if (doc.hidden) {\n emit(); // bank + pause\n } else {\n const now = Date.now(); // resume the clock for sections still on screen\n for (const s of state.values()) if (s.intersecting) s.enterAt = now;\n }\n };\n const onPageHide = (): void => {\n emit();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n doc.addEventListener('visibilitychange', onVisibility);\n const win = doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined);\n win?.addEventListener('pagehide', onPageHide);\n\n // Per-section micro-signal detectors (opt-in; see EngagementCaptureOptions).\n // Attributed to the section's nc-<type> id with no variant — they feed the\n // persona attention fallback and auto-discovery, never rewards.\n const detectorCleanups: Array<() => void> = [];\n if (opts.microSignals) {\n // tab_loss is a single document-level `visibilitychange` signal, so enabling\n // it on every section detector would emit one tab_loss per nc-<type> section\n // on a single tab-hide — attributing one page-level exit to every section\n // (audit M5). Enable it on only the first section so the exit is recorded\n // once, mirroring the per-option path in slot-signals.ts ({ tabLoss: index === 0 }).\n [...componentOf.entries()].forEach(([el, componentId], i) => {\n detectorCleanups.push(\n attachMicroSignalDetectors((signalType, extra = {}) => {\n try {\n client.track({\n projectId: opts.apiKey,\n componentId,\n eventType: 'micro_signal',\n payload: { signalType, ...extra },\n });\n } catch {\n /* fail-safe */\n }\n }, el, undefined, { tabLoss: i === 0 }),\n );\n });\n }\n\n // Cleanup: bank any remaining dwell, then detach everything (provider unmount\n // / consent re-init must not leak observers or listeners).\n return () => {\n emit();\n doc.removeEventListener('visibilitychange', onVisibility);\n win?.removeEventListener('pagehide', onPageHide);\n for (const c of detectorCleanups) c();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n}\n"],"mappings":"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","names":["index_engagement_exports","__export","SEMANTIC_TYPES","classifyFeatures","classifySection","featuresFromElement","startEngagementCapture","__toCommonJS","SEMANTIC_TYPES","KEYWORDS","CONTENT_PATTERNS","headingText","el","_a","h","classifyFeatures","f","hay","type","re","featuresFromElement","_b","text","classifySection","import_policy","attachMicroSignalDetectors","emit","node","variantAssignedAt","options","cleanups","firedOnce","timestamps","onClick","now","onCopy","e","sel","selectionLength","isVisible","hesitationTimer","clearHesitation","startHesitation","onScroll","ioCallback","entries","entry","io","assignedAt","onVisibility","elapsed","isDoNotTrackEnabled","v","SECTION_SELECTOR","isNested","el","_a","registerSections","apiKey","apiBase","pageUrl","sections","e","NOOP","startEngagementCapture","client","opts","_b","_c","_d","_e","_f","_g","_h","_i","doc","isDoNotTrackEnabled","els","componentOf","types","sources","explicit","markup","SEMANTIC_TYPES","type","classifySection","componentId","semanticType","state","get","id","s","observer","entries","entry","emit","now","onVisibility","onPageHide","win","detectorCleanups","attachMicroSignalDetectors","signalType","extra","__spreadValues","c"]}
@@ -1,2 +1,2 @@
1
- import{a as v,b as j,c as B,d as h}from"./chunk-CD2A55US.mjs";import{h as L,i as _}from"./chunk-Q7L7OPE4.mjs";import"./chunk-MYT7OJBV.mjs";import{a as x}from"./chunk-TMCGHANO.mjs";var K="section, header, footer, nav, main > div, [data-sentient-section]";function F(a){var i;return((i=a.parentElement)==null?void 0:i.closest(K))!=null}function R(a,i,r,f){try{fetch(`${i}/v1/section-map`,{method:"POST",keepalive:!0,headers:{"content-type":"application/json",authorization:`Bearer ${a}`},body:JSON.stringify({pageUrl:r,sections:f})}).catch(()=>{})}catch(m){}}var u=()=>{};function V(a,i){var T,A,C,O,I,k,M,N,D;let r=(T=i.doc)!=null?T:typeof document!="undefined"?document:void 0;if(!r||typeof IntersectionObserver=="undefined"||_()||!i.apiKey)return u;let f=((A=i.apiBase)!=null?A:"https://api.sentient-ui.com").replace(/\/+$/,"").replace(/\/v1$/,""),m=Array.from(r.querySelectorAll(K)).filter(e=>!F(e));if(m.length===0)return u;let l=new Map,E=new Map,d=new Map;for(let e of m){let t=e.getAttribute("data-sentient-type"),n=t&&v.includes(t)?t:null,o=(O=n!=null?n:(C=i.typeOf)==null?void 0:C.call(i,e))!=null?O:h(e),s=`nc-${o}`;l.set(e,s),E.set(s,o),n?d.set(s,"markup"):d.has(s)||d.set(s,"auto")}let P=(N=(M=(k=(I=r.defaultView)!=null?I:typeof window!="undefined"?window:void 0)==null?void 0:k.location)==null?void 0:M.pathname)!=null?N:"/";R(i.apiKey,f,P,[...E.entries()].map(([e,t])=>{var n;return{componentId:e,semanticType:t,source:(n=d.get(e))!=null?n:"auto"}}));let p=new Map,$=e=>{let t=p.get(e);return t||(t={ms:0,scroll:0,enterAt:null,intersecting:!1},p.set(e,t)),t},y=new IntersectionObserver(e=>{for(let t of e){let n=l.get(t.target);if(!n)continue;let o=$(n);t.isIntersecting?(o.intersecting=!0,o.enterAt=Date.now(),t.intersectionRatio>o.scroll&&(o.scroll=t.intersectionRatio)):(o.intersecting=!1,o.enterAt!=null&&(o.ms+=Date.now()-o.enterAt,o.enterAt=null))}},{threshold:[0,.25,.5,.75,1]});for(let e of l.keys())y.observe(e);let g=()=>{let e=Date.now();for(let[t,n]of p)if(n.enterAt!=null&&(n.ms+=e-n.enterAt,n.enterAt=null),!(n.ms<=0)){try{a.track({projectId:i.apiKey,componentId:t,eventType:"dwell",payload:{dwell_time:Math.round(n.ms),scroll_depth:Number(n.scroll.toFixed(2))}})}catch(o){}n.ms=0}},S=()=>{if(r.hidden)g();else{let e=Date.now();for(let t of p.values())t.intersecting&&(t.enterAt=e)}},w=()=>{g();try{y.disconnect()}catch(e){}};r.addEventListener("visibilitychange",S);let c=(D=r.defaultView)!=null?D:typeof window!="undefined"?window:void 0;c==null||c.addEventListener("pagehide",w);let b=[];return i.microSignals&&[...l.entries()].forEach(([e,t],n)=>{b.push(L((o,s={})=>{try{a.track({projectId:i.apiKey,componentId:t,eventType:"micro_signal",payload:x({signalType:o},s)})}catch(Y){}},e,void 0,{tabLoss:n===0}))}),()=>{g(),r.removeEventListener("visibilitychange",S),c==null||c.removeEventListener("pagehide",w);for(let e of b)e();try{y.disconnect()}catch(e){}}}export{v as SEMANTIC_TYPES,j as classifyFeatures,h as classifySection,B as featuresFromElement,V as startEngagementCapture};
1
+ import{a as v,b as j,c as B,d as h}from"./chunk-CD2A55US.mjs";import{h as L,i as _}from"./chunk-JJK4LUR2.mjs";import"./chunk-MYT7OJBV.mjs";import{a as x}from"./chunk-TMCGHANO.mjs";var K="section, header, footer, nav, main > div, [data-sentient-section]";function F(a){var i;return((i=a.parentElement)==null?void 0:i.closest(K))!=null}function R(a,i,r,f){try{fetch(`${i}/v1/section-map`,{method:"POST",keepalive:!0,headers:{"content-type":"application/json",authorization:`Bearer ${a}`},body:JSON.stringify({pageUrl:r,sections:f})}).catch(()=>{})}catch(m){}}var u=()=>{};function V(a,i){var T,A,C,O,I,k,M,N,D;let r=(T=i.doc)!=null?T:typeof document!="undefined"?document:void 0;if(!r||typeof IntersectionObserver=="undefined"||_()||!i.apiKey)return u;let f=((A=i.apiBase)!=null?A:"https://api.sentient-ui.com").replace(/\/+$/,"").replace(/\/v1$/,""),m=Array.from(r.querySelectorAll(K)).filter(e=>!F(e));if(m.length===0)return u;let l=new Map,E=new Map,d=new Map;for(let e of m){let t=e.getAttribute("data-sentient-type"),n=t&&v.includes(t)?t:null,o=(O=n!=null?n:(C=i.typeOf)==null?void 0:C.call(i,e))!=null?O:h(e),s=`nc-${o}`;l.set(e,s),E.set(s,o),n?d.set(s,"markup"):d.has(s)||d.set(s,"auto")}let P=(N=(M=(k=(I=r.defaultView)!=null?I:typeof window!="undefined"?window:void 0)==null?void 0:k.location)==null?void 0:M.pathname)!=null?N:"/";R(i.apiKey,f,P,[...E.entries()].map(([e,t])=>{var n;return{componentId:e,semanticType:t,source:(n=d.get(e))!=null?n:"auto"}}));let p=new Map,$=e=>{let t=p.get(e);return t||(t={ms:0,scroll:0,enterAt:null,intersecting:!1},p.set(e,t)),t},y=new IntersectionObserver(e=>{for(let t of e){let n=l.get(t.target);if(!n)continue;let o=$(n);t.isIntersecting?(o.intersecting=!0,o.enterAt=Date.now(),t.intersectionRatio>o.scroll&&(o.scroll=t.intersectionRatio)):(o.intersecting=!1,o.enterAt!=null&&(o.ms+=Date.now()-o.enterAt,o.enterAt=null))}},{threshold:[0,.25,.5,.75,1]});for(let e of l.keys())y.observe(e);let g=()=>{let e=Date.now();for(let[t,n]of p)if(n.enterAt!=null&&(n.ms+=e-n.enterAt,n.enterAt=null),!(n.ms<=0)){try{a.track({projectId:i.apiKey,componentId:t,eventType:"dwell",payload:{dwell_time:Math.round(n.ms),scroll_depth:Number(n.scroll.toFixed(2))}})}catch(o){}n.ms=0}},S=()=>{if(r.hidden)g();else{let e=Date.now();for(let t of p.values())t.intersecting&&(t.enterAt=e)}},w=()=>{g();try{y.disconnect()}catch(e){}};r.addEventListener("visibilitychange",S);let c=(D=r.defaultView)!=null?D:typeof window!="undefined"?window:void 0;c==null||c.addEventListener("pagehide",w);let b=[];return i.microSignals&&[...l.entries()].forEach(([e,t],n)=>{b.push(L((o,s={})=>{try{a.track({projectId:i.apiKey,componentId:t,eventType:"micro_signal",payload:x({signalType:o},s)})}catch(Y){}},e,void 0,{tabLoss:n===0}))}),()=>{g(),r.removeEventListener("visibilitychange",S),c==null||c.removeEventListener("pagehide",w);for(let e of b)e();try{y.disconnect()}catch(e){}}}export{v as SEMANTIC_TYPES,j as classifyFeatures,h as classifySection,B as featuresFromElement,V as startEngagementCapture};
2
2
  //# sourceMappingURL=index-engagement.mjs.map
@@ -1,5 +1,5 @@
1
- import { o as SentientConfig, n as SentientClient } from './index-a8WMLbpe.cjs';
2
- export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, h as GraphClient, i as GraphConfig, j as GraphSnapshot, l as PageNode, Q as QueueConfig, p as SentientEvent, q as SessionConfig, r as SessionManager, B as sanitizePageUrl } from './index-a8WMLbpe.cjs';
1
+ import { p as SentientConfig, o as SentientClient } from './index-BDZqTgpp.cjs';
2
+ export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, i as GraphClient, j as GraphConfig, k as GraphSnapshot, m as PageNode, Q as QueueConfig, q as SentientEvent, r as SessionConfig, s as SessionManager, F as sanitizePageUrl } from './index-BDZqTgpp.cjs';
3
3
  export { j as deriveSessionSegment, k as detectDeviceClass, l as detectTimeOfDay, m as detectTrafficSource, r as referrerDomainFromReferer } from './session-meta-BVvq5RBB.cjs';
4
4
  import '@sentientui/policy';
5
5
 
@@ -1,5 +1,5 @@
1
- import { o as SentientConfig, n as SentientClient } from './index-sGcvT3pc.js';
2
- export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, h as GraphClient, i as GraphConfig, j as GraphSnapshot, l as PageNode, Q as QueueConfig, p as SentientEvent, q as SessionConfig, r as SessionManager, B as sanitizePageUrl } from './index-sGcvT3pc.js';
1
+ import { p as SentientConfig, o as SentientClient } from './index-BlaRyUyv.js';
2
+ export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, i as GraphClient, j as GraphConfig, k as GraphSnapshot, m as PageNode, Q as QueueConfig, q as SentientEvent, r as SessionConfig, s as SessionManager, F as sanitizePageUrl } from './index-BlaRyUyv.js';
3
3
  export { j as deriveSessionSegment, k as detectDeviceClass, l as detectTimeOfDay, m as detectTrafficSource, r as referrerDomainFromReferer } from './session-meta-BVvq5RBB.js';
4
4
  import '@sentientui/policy';
5
5
 
@@ -1,2 +1,2 @@
1
- "use strict";var et=Object.create;var re=Object.defineProperty,tt=Object.defineProperties,nt=Object.getOwnPropertyDescriptor,ot=Object.getOwnPropertyDescriptors,rt=Object.getOwnPropertyNames,Re=Object.getOwnPropertySymbols,st=Object.getPrototypeOf,ke=Object.prototype.hasOwnProperty,it=Object.prototype.propertyIsEnumerable;var _e=(e,t,n)=>t in e?re(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,w=(e,t)=>{for(var n in t||(t={}))ke.call(t,n)&&_e(e,n,t[n]);if(Re)for(var n of Re(t))it.call(t,n)&&_e(e,n,t[n]);return e},B=(e,t)=>tt(e,ot(t));var at=(e,t)=>{for(var n in t)re(e,n,{get:t[n],enumerable:!0})},De=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of rt(t))!ke.call(e,l)&&l!==n&&re(e,l,{get:()=>t[l],enumerable:!(r=nt(t,l))||r.enumerable});return e};var ct=(e,t,n)=>(n=e!=null?et(st(e)):{},De(t||!e||!e.__esModule?re(n,"default",{value:e,enumerable:!0}):n,e)),dt=e=>De(re({},"__esModule",{value:!0}),e);var ln={};at(ln,{deriveSessionSegment:()=>Se,detectDeviceClass:()=>J,detectTimeOfDay:()=>Y,detectTrafficSource:()=>Q,init:()=>dn,referrerDomainFromReferer:()=>V,sanitizePageUrl:()=>Ce});module.exports=dt(ln);function ae(){try{if(typeof crypto!="undefined"&&typeof crypto.randomUUID=="function")return crypto.randomUUID()}catch(e){}try{if(typeof crypto!="undefined"&&typeof crypto.getRandomValues=="function"){let e=new Uint8Array(16);crypto.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t="";for(let n=0;n<16;n++)t+=e[n].toString(16).padStart(2,"0"),(n===3||n===5||n===7||n===9)&&(t+="-");return t}}catch(e){}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function z(e){return e?`_${e.slice(0,12)}`:""}var lt="_snt_uid",ut=365,pt="_snt_uid";function gt(){return ae()}function mt(e){try{let t=document.cookie.match(new RegExp(`(?:^|; )${e}=([^;]*)`));return t?decodeURIComponent(t[1]):null}catch(t){return null}}function ft(e,t,n){try{document.cookie=`${e}=${encodeURIComponent(t)}; max-age=${n}; SameSite=strict; path=/`}catch(r){}}function yt(e){try{return localStorage.getItem(e)}catch(t){return null}}function ht(e,t){try{return localStorage.setItem(e,t),!0}catch(n){return!1}}function St(e){try{return sessionStorage.getItem(e)}catch(t){return null}}function vt(e,t){try{return sessionStorage.setItem(e,t),!0}catch(n){return!1}}function bt(e){try{sessionStorage.removeItem(e)}catch(t){}}function wt(e){try{return document.cookie=`${e}_probe=1; max-age=1; SameSite=strict; path=/`,document.cookie.indexOf(`${e}_probe=1`)!==-1}catch(t){return!1}}function It(e){try{localStorage.removeItem(e)}catch(t){}}function Et(e){try{document.cookie=`${e}=; max-age=0; SameSite=strict; path=/`}catch(t){}}var Ct={getSessionId:()=>null,isEphemeral:()=>!1,destroy:()=>{}};function ce(e){var m,g,f,S,C,E;if(typeof window=="undefined")return Ct;let t=z(e==null?void 0:e.apiKey),n=(m=e==null?void 0:e.cookieName)!=null?m:`${lt}${t}`,r=`${pt}${t}`,p=((g=e==null?void 0:e.cookieTTLDays)!=null?g:ut)*24*60*60,i=v=>v&&v.length>0?v:null,d=(E=(C=(S=(f=i(mt(n)))!=null?f:i(yt(r)))!=null?S:i(St(r)))!=null?C:i(e==null?void 0:e.ssrSessionId))!=null?E:gt();ft(n,d,p);let s=ht(r,d),o=wt(n),a=s?!1:vt(r,d),c=!s&&!o&&!a;return{getSessionId:()=>d,isEphemeral:()=>c,destroy:()=>{d=null,Et(n),It(r),bt(r)}}}function he(e){return`_snt_retry_${e.slice(0,12)}`}var xt={push:()=>{},flush:()=>{},destroy:()=>{}};function At(e,t){try{let n=localStorage.getItem(t);if(!n)return[];let r=JSON.parse(n);return Array.isArray(r)?(localStorage.removeItem(t),r.slice(-e)):[]}catch(n){return[]}}function ye(e,t,n){try{let r=(()=>{try{let i=localStorage.getItem(n);if(!i)return[];let d=JSON.parse(i);return Array.isArray(d)?d:[]}catch(i){return[]}})(),l=new Map;for(let i of r)l.set(i.id,i);for(let i of e)l.set(i.id,i);let p=[...l.values()].slice(-t);localStorage.setItem(n,JSON.stringify(p))}catch(r){}}function Tt(e,t){try{let n=localStorage.getItem(t);if(!n)return;let r=JSON.parse(n);if(!Array.isArray(r))return;let l=new Set(e),p=r.filter(i=>!l.has(i.id));if(p.length===r.length)return;p.length===0?localStorage.removeItem(t):localStorage.setItem(t,JSON.stringify(p))}catch(n){}}function Oe(e){var Z,ee,te;if(typeof window=="undefined")return xt;let t=(Z=e.flushIntervalMs)!=null?Z:5e3,n=(ee=e.maxBatchSize)!=null?ee:20,r=(te=e.maxRetrySize)!=null?te:100,l=e.ingestUrl,p=e.apiKey,i=he(p),d=[],s=new Set,o=[],a=h=>{for(let b of h)c.delete(b),!s.has(b)&&(s.add(b),o.push(b));for(;o.length>500;){let b=o.shift();b&&s.delete(b)}Tt(h,i)},c=new Set,m=h=>{s.has(h.id)||c.has(h.id)||(c.add(h.id),d.push(h))},g=h=>{for(let b of h)s.has(b.id)||(c.add(b.id),d.push(b))},f=At(r,i);for(let h of f)m(h);let S=0,C=0,E=h=>{if(h.length===0)return;let b=JSON.stringify(h),L=h.map(u=>u.id),O;try{O=fetch(l,{method:"POST",keepalive:!0,body:b,headers:{"Content-Type":"application/json",Authorization:`Bearer ${p}`}})}catch(u){ye(h,r,i),g(h),C++,S=Date.now()+Math.min(6e4,1e3*2**Math.min(C,6));return}let K=u=>{if(u.ok||u.status>=400&&u.status<500&&u.status!==429){a(L),C=0,S=0;return}ye(h,r,i),g(h),C++,S=Date.now()+Math.min(6e4,1e3*2**Math.min(C,6))};O instanceof Promise?O.then(K).catch(()=>{ye(h,r,i),g(h),C++,S=Date.now()+Math.min(6e4,1e3*2**Math.min(C,6))}):K(O)},v=typeof TextEncoder!="undefined"?new TextEncoder:null,ie=h=>v?v.encode(h).length:h.length,j=h=>{let b=[],L=2;for(let O of h){let K=ie(JSON.stringify(O))+1;if(b.length>0&&L+K>57344||b.length>=n)break;b.push(O),L+=K}return b},M=()=>{try{if(Date.now()<S)return;for(;d.length>0&&!(Date.now()<S);){let h=d.filter(L=>!s.has(L.id));if(d.length=0,h.length===0)break;let b=j(h);if(b.length===0)break;b.length<h.length&&d.push(...h.slice(b.length)),E(b)}}catch(h){}},D=!0,F=null;F=setInterval(()=>{D&&M()},t);let U=()=>{document.visibilityState==="hidden"&&M()},X=()=>{M()};return document.addEventListener("visibilitychange",U),window.addEventListener("pagehide",X),{push(h){m(h),d.length>=n&&M()},flush:M,destroy(){D=!1,F!==null&&(clearInterval(F),F=null),document.removeEventListener("visibilitychange",U),window.removeEventListener("pagehide",X),M()}}}var Rt=1800*1e3;function de(e,t){return`${encodeURIComponent(e)}:${encodeURIComponent(t)}`}function Ne(e=Rt,t){let n=new Map,r=`_snt_asgn${z(t)}_`,l=(o,a)=>`${r}${encodeURIComponent(o)}:${encodeURIComponent(a)}`,p=o=>{let a=o.slice(r.length),c=a.indexOf(":");if(c<0)return null;try{return{componentId:decodeURIComponent(a.slice(0,c)),segment:decodeURIComponent(a.slice(c+1))}}catch(m){return null}},i=()=>{try{let o=[];for(let a=0;a<localStorage.length;a++){let c=localStorage.key(a);c!=null&&c.startsWith(r)&&o.push(c)}return o}catch(o){return[]}},d=o=>o.assignedAt+(o.ttlMs&&o.ttlMs>0?o.ttlMs:e)<Date.now();return typeof window!="undefined"&&(()=>{for(let o of i())try{let a=localStorage.getItem(o);if(!a)continue;let c=JSON.parse(a);if(d(c)){localStorage.removeItem(o);continue}let m=p(o);if(!m)continue;n.set(de(m.componentId,m.segment),c)}catch(a){}})(),{get(o,a){let c=n.get(de(o,a));return c?d(c)?(n.delete(de(o,a)),null):c:null},set(o,a,c){let m=de(o,a);n.set(m,c);try{localStorage.setItem(l(o,a),JSON.stringify(c))}catch(g){}},invalidate(o){let a=`${encodeURIComponent(o)}:`;for(let c of[...n.keys()])c.startsWith(a)&&n.delete(c);for(let c of i()){let m=p(c);if((m==null?void 0:m.componentId)===o)try{localStorage.removeItem(c)}catch(g){}}},clear(){n.clear();for(let o of i())try{localStorage.removeItem(o)}catch(a){}}}}var Pe=["GPTBot","ChatGPT-User","OAI-SearchBot","ClaudeBot","Claude-User","Claude-SearchBot","PerplexityBot","Perplexity-User","Google-Extended","Applebot-Extended","Meta-ExternalAgent","Bytespider","CCBot","Amazonbot","cohere-ai","Diffbot"];function le(e){return Me(e)!==null}function Me(e){var n;if(!e)return null;let t=e.toLowerCase();return(n=Pe.find(r=>t.includes(r.toLowerCase())))!=null?n:null}function J(e){let t=e.toLowerCase();return/ipad|tablet|playbook|kindle|silk/.test(t)?"tablet":/mobi|iphone|ipod|android.*mobile|phone/.test(t)?"mobile":"desktop"}function Q(e,t){if(!e)return"direct";try{let n=new URL(e);if(t)try{if(new URL(t).host===n.host)return"direct"}catch(l){}let r=n.hostname.toLowerCase();return/(^|\.)(google|bing|duckduckgo|yahoo)\./.test(r)?"search":/(^|\.)(twitter\.com|x\.com|facebook\.com|linkedin\.com|reddit\.com|t\.co)$/.test(r)?"social":"referral"}catch(n){return"direct"}}function V(e){if(!e)return null;try{return new URL(e).hostname}catch(t){return null}}function Y(e){let t=e.getHours();return t<6?"night":t<12?"morning":t<18?"afternoon":"evening"}function Se(e){let t=_t("__segment__",e);return`${t.deviceClass}:${t.trafficSource}`}function _t(e,t){var p,i,d,s,o,a,c;let n=(i=(p=t==null?void 0:t.userAgent)==null?void 0:p.trim())!=null?i:"",r=(s=(d=t==null?void 0:t.referer)==null?void 0:d.trim())!=null?s:"",l=(o=t==null?void 0:t.now)!=null?o:new Date;return{sessionId:e,ephemeral:!1,utmParams:(a=t==null?void 0:t.utmParams)!=null?a:{},deviceClass:n?J(n):"desktop",trafficSource:r?Q(r,t==null?void 0:t.appOrigin):"direct",referrerDomain:V(r),timeOfDay:Y(l),dayOfWeek:(c=["sun","mon","tue","wed","thu","fri","sat"][l.getDay()])!=null?c:"sun",automation:(t==null?void 0:t.webdriver)===!0||le(n)}}var H=require("@sentientui/policy");function ve(e){return w(w(w({id:e.id},e.arms?{arms:[...e.arms]}:{}),e.dims?{dims:Object.fromEntries(Object.entries(e.dims).map(([t,n])=>[t,[...n]]))}:{}),e.baseline!==void 0?{baseline:e.baseline}:{})}function be(e){let t=ve(e);return(0,H.slotResultFor)(t,(0,H.slotBaselineArm)(t))}function Le(e){return typeof e=="string"?e:(0,H.canonicalArm)(e)}var ue="_snt_snap:",kt=["low","medium","high"];function Ue(e){try{let t=localStorage.getItem(ue+e);if(!t)return null;let n=JSON.parse(t);return!n||typeof n!="object"||n.v!==1||typeof n.persona!="string"||typeof n.band!="string"||!kt.includes(n.band)||typeof n.slots!="object"||n.slots===null||Array.isArray(n.slots)||!(n.layoutOrder===null||Array.isArray(n.layoutOrder))||typeof n.savedAt!="number"||!(n.slotConfig===void 0||typeof n.slotConfig=="object"&&n.slotConfig!==null&&!Array.isArray(n.slotConfig))?null:n}catch(t){return null}}function pe(e,t){try{localStorage.setItem(ue+e,JSON.stringify(t))}catch(n){}}var Ie=require("@sentientui/policy");var me=require("@sentientui/policy");var Ke="[sentient] No API key configured \u2014 nothing is being learned. Set NEXT_PUBLIC_SENTIENT_API_KEY or pass localMode: true for local development.",Dt="[sentient] Local mode \u2014 decisions are simulated on-device and nothing is sent over the network. Add an API key to learn from real traffic.",Ge=!1,ge=!1;function Ot(){var e;try{return(e=new URLSearchParams(window.location.search).get("sentient_persona"))!=null?e:void 0}catch(t){return}}function $e(e){var d;let t=ce({ssrSessionId:e.ssrSessionId,apiKey:e.apiKey}),n=(d=t.getSessionId())!=null?d:"local",r=Ot(),l=import("@sentientui/core/local").then(s=>{let o=s;return o.LOCAL_ENGINE_AVAILABLE?(Ge||(Ge=!0,console.info(Dt)),o):(ge||(ge=!0,console.error(Ke)),null)}).catch(()=>(ge||(ge=!0,console.error(Ke)),null)),p=null;function i(s){let o=document.documentElement;o.dataset.sentientPersona===void 0&&(o.dataset.sentientPersona=s.persona,o.dataset.sentientConfidence=(0,me.confidenceBand)(s.confidence))}return{isLocal:!0,async decide(s){var c,m,g;let o=await l;if(!o)return null;let a=o.createLocalEngine({sessionId:n,forcedPersona:r}).decide(s);return p=B(w({},a),{layoutOrder:(m=(c=a.layoutOrder)!=null?c:p==null?void 0:p.layoutOrder)!=null?m:null,slots:w(w({},(g=p==null?void 0:p.slots)!=null?g:{}),a.slots)}),pe(e.apiKey||"local",{v:1,persona:p.persona,band:(0,me.confidenceBand)(p.confidence),slots:p.slots,layoutOrder:p.layoutOrder,savedAt:Date.now()}),i(a),a},getSlotResult(s){var o,a,c;return(c=(a=p==null?void 0:p.slots[s])!=null?a:(o=e.initialSlots)==null?void 0:o[s])!=null?c:null},getPersona(){return p?{persona:p.persona,confidence:p.confidence,band:(0,me.confidenceBand)(p.confidence)}:null},async assign(s,o){var m;let a=await l;return!a||!o||o.length===0?o!=null&&o[0]?{variantId:o[0],assignmentTtlMs:0}:null:{variantId:(m=a.createLocalEngine({sessionId:n,forcedPersona:r}).decide({components:[{id:s,variantIds:o}]}).assignments[s])!=null?m:o[0],assignmentTtlMs:0}},track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},getAssignment:()=>null,fetchWeights:()=>Promise.resolve([]),getGraph:()=>({pageNodes:[],capturedAt:0}),dispose:()=>{},destroy:()=>t.destroy()}}var Be="https://api.sentient-ui.com/v1/events",P=new Map,Nt=null;function we(){return ae()}var se={track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},getAssignment:()=>null,assign:()=>Promise.resolve(null),decide:()=>Promise.resolve(null),getSlotResult:()=>null,getPersona:()=>null,fetchWeights:()=>Promise.resolve([]),getGraph:()=>({pageNodes:[],capturedAt:0}),dispose:()=>{},destroy:()=>{}};function Pt(){try{let e={},t=new URLSearchParams(window.location.search);for(let[n,r]of t)n.startsWith("utm_")&&(e[n]=r);return e}catch(e){return{}}}function je(e){return e.replace(/\/events\/?$/,"")}function Ee(){return typeof navigator!="undefined"&&navigator.globalPrivacyControl===!0?!0:[typeof navigator!="undefined"?navigator.doNotTrack:void 0,typeof window!="undefined"?window.doNotTrack:void 0,typeof navigator!="undefined"?navigator.msDoNotTrack:void 0].some(t=>t==="1"||t==="yes")}function Mt(e){var d;let t=e.preConsentBehavior==="statistical_winner",n=je((d=e.ingestUrl)!=null?d:Be),r={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},l={track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},getAssignment:()=>null,fetchWeights:()=>Promise.resolve([]),async assign(s,o,a){if(!t)return null;try{let c=new URLSearchParams({componentId:s});for(let f of o!=null?o:[])c.append("variantIds[]",f);let m=await fetch(`${n}/winner?${c.toString()}`,{headers:r});return m.ok?{variantId:(await m.json()).variantId,assignmentTtlMs:0}:o!=null&&o[0]?{variantId:o[0],assignmentTtlMs:0}:null}catch(c){return o!=null&&o[0]?{variantId:o[0],assignmentTtlMs:0}:null}},decide:()=>Promise.resolve(null),getSlotResult:()=>null,getPersona:()=>null,getGraph:()=>({pageNodes:[],capturedAt:0}),dispose:()=>{},destroy:()=>{}},p={track:s=>l.track(s),goal:(s,o,a,c)=>l.goal(s,o,a,c),componentGoal:(s,o,a)=>l.componentGoal(s,o,a),identify:s=>l.identify(s),getAssignment:(s,o)=>l.getAssignment(s,o),assign:(s,o,a,c)=>l.assign(s,o,a,c),decide:s=>l.decide(s),getSlotResult:s=>l.getSlotResult(s),getPersona:()=>l.getPersona(),fetchWeights:()=>l.fetchWeights(),getGraph:()=>l.getGraph(),dispose:()=>l.dispose(),destroy:()=>l.destroy()};function i(s){l=s}return{proxy:p,setInner:i}}function Fe(e){var X,Z,ee,te,h,b,L,O,K;if(typeof window=="undefined")return se;Nt=e.apiKey;let t=P.get(e.apiKey||"local");if(t!=null&&t.dispose)try{t.dispose()}catch(u){}let n=e.respectDoNotTrack!==!1&&Ee(),r=e.consent===!1||n,l=typeof e.apiKey=="string"&&e.apiKey.startsWith("pk_");if(e.localMode===!0||!l&&e.localMode!==!1)return r?(P.set(e.apiKey||"local",{config:e,upgrade:null}),se):(P.set(e.apiKey||"local",{config:e,upgrade:null}),$e(e));if(r){if(!e.apiKey||!e.apiKey.startsWith("pk_"))return e.preConsentBehavior==="statistical_winner"&&console.warn("[sentient] init() called with an invalid apiKey \u2014 expected a pk_ public key. SDK disabled."),P.set(e.apiKey,{config:e,upgrade:null}),se;let{proxy:u,setInner:y}=Mt(e);return P.set(e.apiKey,{config:e,upgrade:n?null:y}),u}if(!e.apiKey||!e.apiKey.startsWith("pk_"))return console.warn("[sentient] init() called with an invalid apiKey \u2014 expected a pk_ public key. SDK disabled."),se;if(e.ingestUrl==="")return console.warn("[sentient] init() called with an empty ingestUrl. SDK disabled."),se;let p=(X=e.ingestUrl)!=null?X:Be,i=Date.now(),d=ce({ssrSessionId:e.ssrSessionId,apiKey:e.apiKey}),s=Ne(void 0,e.apiKey),o=Oe({ingestUrl:p,apiKey:e.apiKey}),a=je(p),c={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},m=J((Z=navigator.userAgent)!=null?Z:""),g=typeof window!="undefined"?window.location.origin:void 0,f=Q((ee=document.referrer)!=null?ee:"",g),S=(te=e.sessionSegment)!=null?te:`${m}:${f}`,C=new Map,E=new Map,v=null,ie=u=>{for(let y of u)E.has(y.id)||E.set(y.id,be(y))};if(e.initialSlots)for(let[u,y]of Object.entries(e.initialSlots))E.set(u,y);let j=Ue(e.apiKey);if(j)for(let[u,y]of Object.entries(j.slots))E.has(u)||E.set(u,y);let M={low:.15,medium:.5,high:.85};if(e.initialPersona)v=w({},e.initialPersona);else{let u=document.documentElement.dataset;u.sentientPersona?v={persona:u.sentientPersona,confidence:(b=M[(h=u.sentientConfidence)!=null?h:"low"])!=null?b:.15}:j&&(v={persona:j.persona,confidence:(L=M[j.band])!=null?L:.15})}if(e.initialAssignments)for(let[u,y]of Object.entries(e.initialAssignments))s.set(u,S,{variantId:y,assignedAt:Date.now(),segment:S,confidence:1});let D=Promise.resolve(),F=d.getSessionId();if(F){let u=V((O=document.referrer)!=null?O:""),y=w(w({sessionId:F,deviceClass:m,trafficSource:f,referrerDomain:u,utmParams:Pt(),timeOfDay:Y(new Date),dayOfWeek:["sun","mon","tue","wed","thu","fri","sat"][new Date().getDay()],ephemeral:d.isEphemeral(),automation:typeof navigator!="undefined"&&navigator.webdriver===!0||le((K=navigator.userAgent)!=null?K:"")},e.userId?{userId:e.userId}:{}),e.country?{country:e.country}:{});try{D=fetch(`${a}/sessions`,{method:"POST",keepalive:!0,body:JSON.stringify(y),headers:c}).then(I=>{I.status===402&&console.warn("[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing")}).catch(()=>{})}catch(I){}}e.debug&&(console.log("[sentient] initialized",{context:e.context}),window.__sentient={client:null,queue:o});let U={goal(u,y={},I=1,N=0){let T=d.getSessionId();if(!T)return;let A=we();D.then(()=>{fetch(`${a}/goals`,{method:"POST",keepalive:!0,body:JSON.stringify({sessionId:T,name:u,metadata:y,weight:I,stepIndex:N,goalId:A}),headers:c}).catch(()=>{})})},componentGoal(u,y,I){var R,$,_;let N=d.getSessionId();if(!N)return;let T=s.get(u,S),A=T?null:(R=E.get(u))!=null?R:null;if(!T&&A===null){e.debug&&console.warn(`[sentient] componentGoal("${u}"): no assignment or slot decision yet \u2014 render its <Adaptive>/adaptive hook or call assign()/decide() before recording a goal.`);return}let W=T?T.variantId:Le(A),G={id:we(),sessionId:N,projectId:e.apiKey,componentId:u,variantId:W,eventType:"goal_achieved",goalType:y,payload:w({reward:($=I==null?void 0:I.reward)!=null?$:1},(_=I==null?void 0:I.metadata)!=null?_:{}),timestamp:Date.now(),timeInSession:Date.now()-i};e.debug&&console.log("[sentient] componentGoal",G),D.then(()=>o.push(G))},identify(u){let y=d.getSessionId();y&&D.then(()=>{fetch(`${a}/sessions`,{method:"POST",keepalive:!0,body:JSON.stringify({sessionId:y,userId:u,ephemeral:d.isEphemeral()}),headers:c}).catch(()=>{})})},track(u){let y=d.getSessionId();if(!y)return;let I=B(w({},u),{id:we(),sessionId:y,timestamp:Date.now(),timeInSession:Date.now()-i});e.debug&&console.log("[sentient] track",I),D.then(()=>o.push(I))},getAssignment(u,y){return s.get(u,y)},async assign(u,y,I,N){let T=d.getSessionId();if(!T)return null;let A=s.get(u,S);if(A&&(y!=null&&y.length||A.content!==void 0)){let R=A.ttlMs&&A.ttlMs>0?Math.max(0,A.assignedAt+A.ttlMs-Date.now()):0;return{variantId:A.variantId,assignmentTtlMs:R,content:A.content}}let W=C.get(u);if(W)return W;let G=(async()=>{await D;try{let R={sessionId:T,componentId:u,variantIds:y};N!==void 0?R.agentDataByVariant=N:I!==void 0&&(R.agentData=I);let $=await fetch(`${a}/assign`,{method:"POST",body:JSON.stringify(R),headers:c});if(!$.ok)return null;let _=await $.json();return s.set(u,S,w({variantId:_.variantId,assignedAt:Date.now(),segment:S,confidence:1,content:_.content},_.assignmentTtlMs&&_.assignmentTtlMs>0?{ttlMs:_.assignmentTtlMs}:{})),_}catch(R){return null}finally{C.delete(u)}})();return C.set(u,G),G},async decide(u){var N,T,A,W,G,R,$,_,Ae;let y=d.getSessionId();if(!y)return null;let I=(N=u.slots)!=null?N:[];await D;try{let q={sessionId:y};u.sections&&u.sections.length>0&&(q.sections=u.sections.map(k=>({id:k}))),q.components=(T=u.components)!=null?T:[],I.length>0&&(q.slots=I.map(ve)),u.slotsFrom==="registry"&&(q.slotsFrom="registry"),u.v&&(q.v=u.v);let Te=await fetch(`${a}/decide`,{method:"POST",body:JSON.stringify(q),headers:c});if(!Te.ok)return ie(I),null;let x=await Te.json(),ne={};for(let k of I)ne[k.id]=(W=(A=x.slots)==null?void 0:A[k.id])!=null?W:be(k);if(x.slots)for(let[k,oe]of Object.entries(x.slots))k in ne||(ne[k]=oe);for(let[k,oe]of Object.entries(ne))E.set(k,oe);let Ze=v!=null&&v.persona!=="unknown";x.persona&&!(x.persona==="unknown"&&Ze)?v={persona:x.persona,confidence:(G=x.confidence)!=null?G:0}:v||(v={persona:"unknown",confidence:0});for(let[k,oe]of Object.entries((R=x.assignments)!=null?R:{}))s.set(k,S,{variantId:oe,assignedAt:Date.now(),segment:S,confidence:1});return pe(e.apiKey,w({v:1,persona:v.persona,band:(0,Ie.confidenceBand)(v.confidence),slots:Object.fromEntries(E),layoutOrder:($=x.layoutOrder)!=null?$:null,savedAt:Date.now()},x.slotConfig?{slotConfig:x.slotConfig}:{})),w(w(w({layoutOrder:(_=x.layoutOrder)!=null?_:null,assignments:(Ae=x.assignments)!=null?Ae:{},slots:ne,persona:v.persona,confidence:v.confidence},x.slotConfig?{slotConfig:x.slotConfig}:{}),x.goals?{goals:x.goals}:{}),x.sectionMap?{sectionMap:x.sectionMap}:{})}catch(q){return ie(I),null}},getSlotResult(u){var y;return(y=E.get(u))!=null?y:null},getPersona(){return v?{persona:v.persona,confidence:v.confidence,band:(0,Ie.confidenceBand)(v.confidence)}:null},async fetchWeights(){var u;try{let y=await fetch(`${a}/weights`,{headers:c});return y.ok?(u=(await y.json()).components)!=null?u:[]:[]}catch(y){return[]}},getGraph(){return{pageNodes:[],capturedAt:0}},dispose(){var u;o.destroy(),((u=P.get(e.apiKey))==null?void 0:u.dispose)===U.dispose&&P.delete(e.apiKey),e.debug&&console.log("[sentient] disposed")},destroy(){var u;o.destroy(),d.destroy(),((u=P.get(e.apiKey))==null?void 0:u.dispose)===U.dispose&&P.delete(e.apiKey);try{localStorage.removeItem(ue+e.apiKey),localStorage.removeItem(he(e.apiKey))}catch(y){}e.debug&&console.log("[sentient] destroyed")}};if(P.set(e.apiKey,{config:e,upgrade:null,dispose:U.dispose}),e.debug){let u=window;u.__sentient&&(u.__sentient.client=U)}return U}var We="_snt_graph_edges",Lt={pricing:["features","faq"],features:["pricing"],faq:["pricing"],social_proof:["cta"],cta:["social_proof","hero","trust"],hero:["cta"],comparison:["pricing"],trust:["cta"]};function Ut(e){var t;return(t=Lt[e])!=null?t:[]}function Kt(e,t){try{let n=localStorage.getItem(e);return n?JSON.parse(n):t}catch(n){return t}}function Gt(e,t){try{localStorage.setItem(e,JSON.stringify(t))}catch(n){}}var $t=new Set(["pricing","hero","social_proof","cta","features","faq","comparison","trust","navigation","generic"]);function Bt(e){return $t.has(e)?e:"generic"}function Ce(e){try{let t=new URL(e);return`${t.origin}${t.pathname}`}catch(t){return"/"}}function jt(e,t,n){let r=`${e}:${t}:${n.join(",")}`,l=5381;for(let p=0;p<r.length;p++)l=(l<<5)+l+r.charCodeAt(p)&4294967295;return(l>>>0).toString(16).padStart(8,"0")}function qe(e){let t=new Map,n=new Map,r=`_snt_graph_nodes${z(e==null?void 0:e.apiKey)}`,l=()=>{typeof window!="undefined"&&Gt(r,[...t.values()])},p=i=>{var d;try{let s=JSON.parse(i);t.clear();for(let o of(d=s.pageNodes)!=null?d:[])t.set(o.componentId,o)}catch(s){}};if(typeof window!="undefined"){let i=Kt(r,[]);for(let d of i)t.set(d.componentId,d);try{localStorage.removeItem(We)}catch(d){}}return{addPageNode(i){t.set(i.componentId,i),l()},addStructuralEdge(i){let d=`${i.fromComponentId}->${i.toComponentId}`;n.set(d,i)},syncOnce(){var d,s;if(!(e!=null&&e.syncUrl)||typeof window=="undefined")return;let i=[...t.values()];if(i.length!==0)try{let o=new Map;for(let f of i){let S=(d=o.get(f.semanticType))!=null?d:[];S.push(f),o.set(f.semanticType,S)}let a=[],c=new Set;for(let f of i)for(let S of Ut(f.semanticType)){let C=(s=o.get(S))!=null?s:[];for(let E of C){if(E.componentId===f.componentId)continue;let v=`semantic:${f.componentId}->${E.componentId}`;c.has(v)||(c.add(v),a.push({fromComponentId:f.componentId,toComponentId:E.componentId,type:"semantic",weight:.4,confidence:.9}))}}let m=new Set(i.map(f=>f.componentId));for(let f of n.values()){if(!m.has(f.fromComponentId)||!m.has(f.toComponentId))continue;let S=`structural:${f.fromComponentId}->${f.toComponentId}`;c.has(S)||(c.add(S),a.push({fromComponentId:f.fromComponentId,toComponentId:f.toComponentId,type:"structural",weight:f.weight,confidence:1}))}let g=B(w(w({pageUrl:Ce(window.location.href)},e.sessionId?{sessionId:e.sessionId}:{}),e.projectId?{projectId:e.projectId}:{}),{nodes:i.map(f=>{let S=Bt(f.semanticType);return{componentId:f.componentId,semanticType:S,answers:f.answers,contentHash:jt(f.componentId,S,f.answers),prominenceScore:f.prominenceScore,depthInPage:f.depth}}),edges:a});fetch(e.syncUrl,{method:"POST",keepalive:!0,headers:w({"Content-Type":"application/json"},e.apiKey?{Authorization:`Bearer ${e.apiKey}`}:{}),body:JSON.stringify(g)}).catch(()=>{})}catch(o){}},snapshot(){return{pageNodes:[...t.values()],capturedAt:Date.now()}},serialize(){return JSON.stringify({pageNodes:[...t.values()]})},restore:p,destroy(){if(typeof window!="undefined")try{localStorage.removeItem(r),localStorage.removeItem(We)}catch(i){}}}}var ze=["pricing","hero","social_proof","cta","features","faq","comparison","trust","navigation","generic"],Ft=[["pricing",/\b(pricing|price|plans?|subscriptions?|per month|\/mo|tier)\b/i],["faq",/\b(faq|frequently asked|common questions?)\b/i],["comparison",/\b(compare|comparison|versus|vs\.)\b/i],["social_proof",/\b(testimonial|reviews?|trusted by|loved by|customers?|logos|rated|case stud)\b/i],["trust",/\b(security|guarantee|privacy|compliance|secure|gdpr|soc ?2|encrypt)\b/i],["features",/\b(features?|how it works|benefits?|capabilit|what you get)\b/i]],Wt=[["pricing",/(?:[$€£]\s?\d[\d,.]*\s*(?:\/|per\s)\s*(?:mo|month|yr|year|seat|user))|(?:\b(?:starter|basic|pro|growth|premium|enterprise)\b[^.]{0,60}[$€£]\s?\d)/i],["social_proof",/(?:★{2,})|(?:\b\d(?:\.\d)?\s*(?:out of|\/)\s*5\b)|(?:\brated\b)|(?:["“][^"”]{20,160}["”]\s*[—–-]\s*[A-Z][a-z]+)/],["trust",/\b(?:money[- ]back guarantee|refund(?:s)? within|encrypted|soc ?2|iso ?27001|gdpr[- ]compliant|cancel anytime)\b/i],["comparison",/\b(?:vs|versus)\b\.?[^.!?]{0,80}\b(?:compare|comparison|plans?|features?|alternative)\b|\bhow (?:we|it) compares?\b/i]];function qt(e){var n;let t=e.querySelector("h1, h2, h3");return((n=t==null?void 0:t.textContent)!=null?n:"").slice(0,160)}function zt(e){if(e.tag==="nav"||e.tag==="footer")return{type:"navigation",strength:"weak"};let t=`${e.idClass} ${e.headingText}`.toLowerCase();for(let[n,r]of Ft)if(r.test(t))return{type:n,strength:"strong"};for(let[n,r]of Wt)if(r.test(e.bodyText))return{type:n,strength:"strong"};return e.actionCount>=1&&e.textLength>0&&e.textLength<200?{type:"cta",strength:"weak"}:e.tag==="header"?{type:"hero",strength:"weak"}:/\b(hero|headline|banner)\b/i.test(t)?{type:"hero",strength:"weak"}:{type:"generic",strength:"weak"}}function Jt(e){var n,r;let t=((n=e.textContent)!=null?n:"").replace(/\s+/g," ").trim();return{tag:e.tagName.toLowerCase(),idClass:`${e.id} ${String((r=e.className)!=null?r:"")}`,headingText:qt(e),bodyText:t.slice(0,2e3),actionCount:e.querySelectorAll('a, button, [role="button"]').length,textLength:t.length}}function Je(e){return zt(Jt(e)).type}var Qt=new Set(["SECTION","ARTICLE","MAIN","DIV"]),Vt="h1, h2, h3",Qe=30,Yt=1500,Ht={scan:async()=>({nodes:[],edges:[],scannedAt:0}),observe:()=>{},getProminenceScore:()=>0,destroy:()=>{}};function Ve(e,t,n){return n<=t?0:Math.max(0,Math.min(1,(e-t)/(n-t)))}function Xt(e){var t,n,r;try{let l=e;for(let p of Object.keys(l)){if(!p.startsWith("__reactFiber")&&!p.startsWith("__reactInternalInstance"))continue;let i=l[p],d=(r=(t=i==null?void 0:i.type)==null?void 0:t.displayName)!=null?r:(n=i==null?void 0:i.type)==null?void 0:n.name;if(d&&d.length>1)return d}}catch(l){}}function Zt(e){let t={};for(let n of Array.from(e.attributes))n.name.startsWith("data-")&&(t[n.name]=n.value);return t}var Ye=new Set(ze);function en(e){let t=e.getAttribute("data-sentient-type");if(t&&Ye.has(t))return t;let n=e.getAttribute("role");return n&&Ye.has(n)?n:Je(e)}function tn(e){let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e.charCodeAt(n)&4294967295;return(t>>>0).toString(16).padStart(8,"0")}function nn(e){let t=[],n=e;for(;n;){let r=n.parentElement;if(!r){t.push(n.tagName.toLowerCase());break}let l=Array.prototype.indexOf.call(r.children,n);t.push(`${n.tagName.toLowerCase()}[${l}]`),n=r}return t.reverse().join("/")}function on(e){var n;let t=(n=e.getAttribute("data-sentient-id"))!=null?n:e.getAttribute("id");return t||`${e.tagName.toLowerCase()}-${tn(nn(e))}`}function rn(e){let t=0,n=e.parentElement;for(;n;)t++,n=n.parentElement;return t}function xe(e,t){var r,l,p;let n=e.querySelector(Vt);return{componentId:on(e),semanticType:en(e),ariaLabel:(r=e.getAttribute("aria-label"))!=null?r:void 0,headingText:(p=(l=n==null?void 0:n.textContent)==null?void 0:l.trim())!=null?p:void 0,isAboveFold:e.getBoundingClientRect().top<window.innerHeight,prominenceScore:t(e),depth:rn(e),reactComponentName:Xt(e),dataAttributes:Zt(e)}}function He(e){var p;let t=[],n=new Set,r="__root__",l=new Map;for(let[i,d]of e){let s=i.parentElement,o=r;for(;s;){if(e.has(s)){o=e.get(s);let c=e.get(i),m=`${o}->${c}`;!n.has(m)&&o!==c&&(n.add(m),t.push({fromComponentId:o,toComponentId:c,weight:.6}));break}s=s.parentElement}let a=(p=l.get(o))!=null?p:[];a.push(i),l.set(o,a)}for(let i of l.values()){if(i.length<2)continue;let d=i.length>Qe?i.slice(0,Qe):i;for(let s=0;s<d.length;s++)for(let o=s+1;o<d.length;o++){if(t.length>=Yt)return t;let a=e.get(d[s]),c=e.get(d[o]);if(a===c)continue;let m=`${a}->${c}::sib`,g=`${c}->${a}::sib`;n.has(m)||(n.add(m),t.push({fromComponentId:a,toComponentId:c,weight:.3})),n.has(g)||(n.add(g),t.push({fromComponentId:c,toComponentId:a,weight:.3}))}}return t}function sn(e){let t=[],n=new Set,r=new Map;return document.querySelectorAll("[data-sentient-id]").forEach(i=>{if(i instanceof Element&&!n.has(i)){n.add(i);let d=xe(i,e);t.push(d),r.set(i,d.componentId)}}),document.querySelectorAll("section, article, main, aside").forEach(i=>{if(!(i instanceof Element)||n.has(i))return;let d=i.hasAttribute("aria-label"),s=i.hasAttribute("data-sentient-id");if(!d&&!s)return;n.add(i);let o=xe(i,e);t.push(o),r.set(i,o.componentId)}),{nodes:t,edges:He(r),elementToId:r}}function Xe(){if(typeof window=="undefined")return Ht;let e=null,t=0,n=null,r=new Map,l=s=>{try{let o=window.getComputedStyle(s),a=parseFloat(o.fontSize)||12,c=parseFloat(o.zIndex)||0,m=s.getBoundingClientRect(),g=Math.max(m.top,0),f=window.innerHeight||1,S=1/(g/f+1),C=Ve(a,12,48)*.4+S*.4+Ve(c,0,100)*.2;return Math.max(0,Math.min(1,C))}catch(o){return .5}};return{scan:()=>new Promise(s=>{let o=()=>{let{nodes:a,edges:c,elementToId:m}=sn(l);r.clear();for(let[g,f]of m)r.set(g,f);s({nodes:a,edges:c,scannedAt:Date.now()})};try{typeof requestIdleCallback=="function"?t=requestIdleCallback(o,{timeout:100}):o()}catch(a){o()}}),observe:s=>{n=s;try{e=new MutationObserver(o=>{let a=[],c=new Set;for(let g of o)g.type==="childList"&&g.addedNodes.forEach(f=>{if(!(f instanceof Element)||!Qt.has(f.tagName))return;let S=f.hasAttribute("data-sentient-id"),C=f.hasAttribute("aria-label");if(!S&&!C)return;let E=xe(f,l);a.push(E),c.add(E.componentId),r.set(f,E.componentId)});if(a.length===0||!n)return;for(let g of[...r.keys()])g.isConnected||r.delete(g);let m=He(r).filter(g=>c.has(g.fromComponentId)||c.has(g.toComponentId));n({nodes:a,edges:m,addedAt:Date.now()})}),e.observe(document.body,{childList:!0,subtree:!0})}catch(o){}},getProminenceScore:l,destroy:()=>{if(e&&(e.disconnect(),e=null),t&&typeof cancelIdleCallback=="function")try{cancelIdleCallback(t)}catch(s){}t=0,n=null,r.clear()}}}var an="https://api.sentient-ui.com/v1/events";function cn(){try{let e=document.cookie.match(/(?:^|; )_snt_uid=([^;]*)/);return e?decodeURIComponent(e[1]):void 0}catch(e){return}}var fe=new Map;function dn(e){var c;let t=Fe(e),n=e.respectDoNotTrack!==!1&&Ee(),r=e.consent===!1||n;if(!e.graph||!e.apiKey||r||typeof window=="undefined")return t;let l=fe.get(e.apiKey);if(l)try{l()}catch(m){}let p=Xe(),i=(c=e.ingestUrl)!=null?c:an,d=qe({syncUrl:i.replace(/\/events\/?$/,"/graph/sync"),apiKey:e.apiKey,projectId:e.apiKey,sessionId:cn()});p.scan().then(m=>{for(let g of m.nodes)d.addPageNode({id:g.componentId,componentId:g.componentId,semanticType:g.semanticType,answers:e.captureDomText&&g.headingText?[g.headingText]:[],prominenceScore:g.prominenceScore,depth:g.depth});for(let g of m.edges)d.addStructuralEdge(g);d.syncOnce()});let s=null,o=()=>{s!==null&&clearTimeout(s),s=setTimeout(()=>{s=null,d.syncOnce()},500)};p.observe(m=>{for(let g of m.nodes)d.addPageNode({id:g.componentId,componentId:g.componentId,semanticType:g.semanticType,answers:e.captureDomText&&g.headingText?[g.headingText]:[],prominenceScore:g.prominenceScore,depth:g.depth});for(let g of m.edges)d.addStructuralEdge(g);o()});let a=()=>{s!==null&&(clearTimeout(s),s=null),p.destroy(),d.destroy(),fe.get(e.apiKey)===a&&fe.delete(e.apiKey)};return fe.set(e.apiKey,a),B(w({},t),{getGraph:()=>d.snapshot(),dispose:()=>{a(),t.dispose()},destroy:()=>{a(),t.destroy()}})}0&&(module.exports={deriveSessionSegment,detectDeviceClass,detectTimeOfDay,detectTrafficSource,init,referrerDomainFromReferer,sanitizePageUrl});
1
+ "use strict";var et=Object.create;var re=Object.defineProperty,tt=Object.defineProperties,nt=Object.getOwnPropertyDescriptor,ot=Object.getOwnPropertyDescriptors,rt=Object.getOwnPropertyNames,Re=Object.getOwnPropertySymbols,st=Object.getPrototypeOf,_e=Object.prototype.hasOwnProperty,it=Object.prototype.propertyIsEnumerable;var ke=(e,t,n)=>t in e?re(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,I=(e,t)=>{for(var n in t||(t={}))_e.call(t,n)&&ke(e,n,t[n]);if(Re)for(var n of Re(t))it.call(t,n)&&ke(e,n,t[n]);return e},j=(e,t)=>tt(e,ot(t));var at=(e,t)=>{for(var n in t)re(e,n,{get:t[n],enumerable:!0})},Oe=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of rt(t))!_e.call(e,l)&&l!==n&&re(e,l,{get:()=>t[l],enumerable:!(r=nt(t,l))||r.enumerable});return e};var ct=(e,t,n)=>(n=e!=null?et(st(e)):{},Oe(t||!e||!e.__esModule?re(n,"default",{value:e,enumerable:!0}):n,e)),dt=e=>Oe(re({},"__esModule",{value:!0}),e);var un={};at(un,{deriveSessionSegment:()=>Se,detectDeviceClass:()=>J,detectTimeOfDay:()=>Y,detectTrafficSource:()=>Q,init:()=>ln,referrerDomainFromReferer:()=>V,sanitizePageUrl:()=>xe});module.exports=dt(un);function ae(){try{if(typeof crypto!="undefined"&&typeof crypto.randomUUID=="function")return crypto.randomUUID()}catch(e){}try{if(typeof crypto!="undefined"&&typeof crypto.getRandomValues=="function"){let e=new Uint8Array(16);crypto.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t="";for(let n=0;n<16;n++)t+=e[n].toString(16).padStart(2,"0"),(n===3||n===5||n===7||n===9)&&(t+="-");return t}}catch(e){}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function z(e){return e?`_${e.slice(0,12)}`:""}var lt="_snt_uid",ut=365,gt="_snt_uid";function pt(){return ae()}function mt(e){try{let t=document.cookie.match(new RegExp(`(?:^|; )${e}=([^;]*)`));return t?decodeURIComponent(t[1]):null}catch(t){return null}}function ft(e,t,n){try{document.cookie=`${e}=${encodeURIComponent(t)}; max-age=${n}; SameSite=strict; path=/`}catch(r){}}function yt(e){try{return localStorage.getItem(e)}catch(t){return null}}function ht(e,t){try{return localStorage.setItem(e,t),!0}catch(n){return!1}}function St(e){try{return sessionStorage.getItem(e)}catch(t){return null}}function bt(e,t){try{return sessionStorage.setItem(e,t),!0}catch(n){return!1}}function vt(e){try{sessionStorage.removeItem(e)}catch(t){}}function wt(e){try{return document.cookie=`${e}_probe=1; max-age=1; SameSite=strict; path=/`,document.cookie.indexOf(`${e}_probe=1`)!==-1}catch(t){return!1}}function It(e){try{localStorage.removeItem(e)}catch(t){}}function Et(e){try{document.cookie=`${e}=; max-age=0; SameSite=strict; path=/`}catch(t){}}var xt={getSessionId:()=>null,isEphemeral:()=>!1,destroy:()=>{}};function ce(e){var m,p,f,S,C,E;if(typeof window=="undefined")return xt;let t=z(e==null?void 0:e.apiKey),n=(m=e==null?void 0:e.cookieName)!=null?m:`${lt}${t}`,r=`${gt}${t}`,g=((p=e==null?void 0:e.cookieTTLDays)!=null?p:ut)*24*60*60,i=v=>v&&v.length>0?v:null,d=(E=(C=(S=(f=i(mt(n)))!=null?f:i(yt(r)))!=null?S:i(St(r)))!=null?C:i(e==null?void 0:e.ssrSessionId))!=null?E:pt();ft(n,d,g);let s=ht(r,d),o=wt(n),a=s?!1:bt(r,d),c=!s&&!o&&!a;return{getSessionId:()=>d,isEphemeral:()=>c,destroy:()=>{d=null,Et(n),It(r),vt(r)}}}function he(e){return`_snt_retry_${e.slice(0,12)}`}var Ct={push:()=>{},flush:()=>{},destroy:()=>{}};function At(e,t){try{let n=localStorage.getItem(t);if(!n)return[];let r=JSON.parse(n);return Array.isArray(r)?(localStorage.removeItem(t),r.slice(-e)):[]}catch(n){return[]}}function ye(e,t,n){try{let r=(()=>{try{let i=localStorage.getItem(n);if(!i)return[];let d=JSON.parse(i);return Array.isArray(d)?d:[]}catch(i){return[]}})(),l=new Map;for(let i of r)l.set(i.id,i);for(let i of e)l.set(i.id,i);let g=[...l.values()].slice(-t);localStorage.setItem(n,JSON.stringify(g))}catch(r){}}function Tt(e,t){try{let n=localStorage.getItem(t);if(!n)return;let r=JSON.parse(n);if(!Array.isArray(r))return;let l=new Set(e),g=r.filter(i=>!l.has(i.id));if(g.length===r.length)return;g.length===0?localStorage.removeItem(t):localStorage.setItem(t,JSON.stringify(g))}catch(n){}}function De(e){var Z,ee,te;if(typeof window=="undefined")return Ct;let t=(Z=e.flushIntervalMs)!=null?Z:5e3,n=(ee=e.maxBatchSize)!=null?ee:20,r=(te=e.maxRetrySize)!=null?te:100,l=e.ingestUrl,g=e.apiKey,i=he(g),d=[],s=new Set,o=[],a=h=>{for(let w of h)c.delete(w),!s.has(w)&&(s.add(w),o.push(w));for(;o.length>500;){let w=o.shift();w&&s.delete(w)}Tt(h,i)},c=new Set,m=h=>{s.has(h.id)||c.has(h.id)||(c.add(h.id),d.push(h))},p=h=>{for(let w of h)s.has(w.id)||(c.add(w.id),d.push(w))},f=At(r,i);for(let h of f)m(h);let S=0,C=0,E=h=>{if(h.length===0)return;let w=JSON.stringify(h),G=h.map(u=>u.id),N;try{N=fetch(l,{method:"POST",keepalive:!0,body:w,headers:{"Content-Type":"application/json",Authorization:`Bearer ${g}`}})}catch(u){ye(h,r,i),p(h),C++,S=Date.now()+Math.min(6e4,1e3*2**Math.min(C,6));return}let B=u=>{if(u.ok||u.status>=400&&u.status<500&&u.status!==429){a(G),C=0,S=0;return}ye(h,r,i),p(h),C++,S=Date.now()+Math.min(6e4,1e3*2**Math.min(C,6))};N instanceof Promise?N.then(B).catch(()=>{ye(h,r,i),p(h),C++,S=Date.now()+Math.min(6e4,1e3*2**Math.min(C,6))}):B(N)},v=typeof TextEncoder!="undefined"?new TextEncoder:null,ie=h=>v?v.encode(h).length:h.length,F=h=>{let w=[],G=2;for(let N of h){let B=ie(JSON.stringify(N))+1;if(w.length>0&&G+B>57344||w.length>=n)break;w.push(N),G+=B}return w},U=()=>{try{if(Date.now()<S)return;for(;d.length>0&&!(Date.now()<S);){let h=d.filter(G=>!s.has(G.id));if(d.length=0,h.length===0)break;let w=F(h);if(w.length===0)break;w.length<h.length&&d.push(...h.slice(w.length)),E(w)}}catch(h){}},D=!0,W=null;W=setInterval(()=>{D&&U()},t);let $=()=>{document.visibilityState==="hidden"&&U()},X=()=>{U()};return document.addEventListener("visibilitychange",$),window.addEventListener("pagehide",X),{push(h){m(h),d.length>=n&&U()},flush:U,destroy(){D=!1,W!==null&&(clearInterval(W),W=null),document.removeEventListener("visibilitychange",$),window.removeEventListener("pagehide",X),U()}}}var Rt=1800*1e3;function de(e,t){return`${encodeURIComponent(e)}:${encodeURIComponent(t)}`}function Ne(e=Rt,t){let n=new Map,r=`_snt_asgn${z(t)}_`,l=(o,a)=>`${r}${encodeURIComponent(o)}:${encodeURIComponent(a)}`,g=o=>{let a=o.slice(r.length),c=a.indexOf(":");if(c<0)return null;try{return{componentId:decodeURIComponent(a.slice(0,c)),segment:decodeURIComponent(a.slice(c+1))}}catch(m){return null}},i=()=>{try{let o=[];for(let a=0;a<localStorage.length;a++){let c=localStorage.key(a);c!=null&&c.startsWith(r)&&o.push(c)}return o}catch(o){return[]}},d=o=>o.assignedAt+(o.ttlMs&&o.ttlMs>0?o.ttlMs:e)<Date.now();return typeof window!="undefined"&&(()=>{for(let o of i())try{let a=localStorage.getItem(o);if(!a)continue;let c=JSON.parse(a);if(d(c)){localStorage.removeItem(o);continue}let m=g(o);if(!m)continue;n.set(de(m.componentId,m.segment),c)}catch(a){}})(),{get(o,a){let c=n.get(de(o,a));return c?d(c)?(n.delete(de(o,a)),null):c:null},set(o,a,c){let m=de(o,a);n.set(m,c);try{localStorage.setItem(l(o,a),JSON.stringify(c))}catch(p){}},invalidate(o){let a=`${encodeURIComponent(o)}:`;for(let c of[...n.keys()])c.startsWith(a)&&n.delete(c);for(let c of i()){let m=g(c);if((m==null?void 0:m.componentId)===o)try{localStorage.removeItem(c)}catch(p){}}},clear(){n.clear();for(let o of i())try{localStorage.removeItem(o)}catch(a){}}}}var Pe=["GPTBot","ChatGPT-User","OAI-SearchBot","ClaudeBot","Claude-User","Claude-SearchBot","PerplexityBot","Perplexity-User","Google-Extended","Applebot-Extended","Meta-ExternalAgent","Bytespider","CCBot","Amazonbot","cohere-ai","Diffbot"];function le(e){return Me(e)!==null}function Me(e){var n;if(!e)return null;let t=e.toLowerCase();return(n=Pe.find(r=>t.includes(r.toLowerCase())))!=null?n:null}function J(e){let t=e.toLowerCase();return/ipad|tablet|playbook|kindle|silk/.test(t)?"tablet":/mobi|iphone|ipod|android.*mobile|phone/.test(t)?"mobile":"desktop"}function Q(e,t){if(!e)return"direct";try{let n=new URL(e);if(t)try{if(new URL(t).host===n.host)return"direct"}catch(l){}let r=n.hostname.toLowerCase();return/(^|\.)(google|bing|duckduckgo|yahoo)\./.test(r)?"search":/(^|\.)(twitter\.com|x\.com|facebook\.com|linkedin\.com|reddit\.com|t\.co)$/.test(r)?"social":"referral"}catch(n){return"direct"}}function V(e){if(!e)return null;try{return new URL(e).hostname}catch(t){return null}}function Y(e){let t=e.getHours();return t<6?"night":t<12?"morning":t<18?"afternoon":"evening"}function Se(e){let t=kt("__segment__",e);return`${t.deviceClass}:${t.trafficSource}`}function kt(e,t){var g,i,d,s,o,a,c;let n=(i=(g=t==null?void 0:t.userAgent)==null?void 0:g.trim())!=null?i:"",r=(s=(d=t==null?void 0:t.referer)==null?void 0:d.trim())!=null?s:"",l=(o=t==null?void 0:t.now)!=null?o:new Date;return{sessionId:e,ephemeral:!1,utmParams:(a=t==null?void 0:t.utmParams)!=null?a:{},deviceClass:n?J(n):"desktop",trafficSource:r?Q(r,t==null?void 0:t.appOrigin):"direct",referrerDomain:V(r),timeOfDay:Y(l),dayOfWeek:(c=["sun","mon","tue","wed","thu","fri","sat"][l.getDay()])!=null?c:"sun",automation:(t==null?void 0:t.webdriver)===!0||le(n)}}var H=require("@sentientui/policy");function be(e){return I(I(I({id:e.id},e.arms?{arms:[...e.arms]}:{}),e.dims?{dims:Object.fromEntries(Object.entries(e.dims).map(([t,n])=>[t,[...n]]))}:{}),e.baseline!==void 0?{baseline:e.baseline}:{})}function ve(e){let t=be(e);return(0,H.slotResultFor)(t,(0,H.slotBaselineArm)(t))}function Le(e){return typeof e=="string"?e:(0,H.canonicalArm)(e)}var ue="_snt_snap:",_t=["low","medium","high"];function Ue(e){try{let t=localStorage.getItem(ue+e);if(!t)return null;let n=JSON.parse(t);return!n||typeof n!="object"||n.v!==1||typeof n.persona!="string"||typeof n.band!="string"||!_t.includes(n.band)||typeof n.slots!="object"||n.slots===null||Array.isArray(n.slots)||!(n.layoutOrder===null||Array.isArray(n.layoutOrder))||typeof n.savedAt!="number"||!(n.slotConfig===void 0||typeof n.slotConfig=="object"&&n.slotConfig!==null&&!Array.isArray(n.slotConfig))?null:n}catch(t){return null}}function ge(e,t){try{localStorage.setItem(ue+e,JSON.stringify(t))}catch(n){}}var Ie=require("@sentientui/policy");var me=require("@sentientui/policy");var Ge="[sentient] No API key configured \u2014 nothing is being learned. Set NEXT_PUBLIC_SENTIENT_API_KEY or pass localMode: true for local development.",Ot="[sentient] Local mode \u2014 decisions are simulated on-device and nothing is sent over the network. Add an API key to learn from real traffic.",Ke=!1,pe=!1;function Dt(){var e;try{return(e=new URLSearchParams(window.location.search).get("sentient_persona"))!=null?e:void 0}catch(t){return}}function $e(e){var d;let t=ce({ssrSessionId:e.ssrSessionId,apiKey:e.apiKey}),n=(d=t.getSessionId())!=null?d:"local",r=Dt(),l=import("@sentientui/core/local").then(s=>{let o=s;return o.LOCAL_ENGINE_AVAILABLE?(Ke||(Ke=!0,console.info(Ot)),o):(pe||(pe=!0,console.error(Ge)),null)}).catch(()=>(pe||(pe=!0,console.error(Ge)),null)),g=null;function i(s){let o=document.documentElement;o.dataset.sentientPersona===void 0&&(o.dataset.sentientPersona=s.persona,o.dataset.sentientConfidence=(0,me.confidenceBand)(s.confidence))}return{isLocal:!0,async decide(s){var c,m,p;let o=await l;if(!o)return null;let a=o.createLocalEngine({sessionId:n,forcedPersona:r}).decide(s);return g=j(I({},a),{layoutOrder:(m=(c=a.layoutOrder)!=null?c:g==null?void 0:g.layoutOrder)!=null?m:null,slots:I(I({},(p=g==null?void 0:g.slots)!=null?p:{}),a.slots)}),ge(e.apiKey||"local",{v:1,persona:g.persona,band:(0,me.confidenceBand)(g.confidence),slots:g.slots,layoutOrder:g.layoutOrder,savedAt:Date.now()}),i(a),a},getSlotResult(s){var o,a,c;return(c=(a=g==null?void 0:g.slots[s])!=null?a:(o=e.initialSlots)==null?void 0:o[s])!=null?c:null},getPersona(){return g?{persona:g.persona,confidence:g.confidence,band:(0,me.confidenceBand)(g.confidence)}:null},async assign(s,o){var m;let a=await l;return!a||!o||o.length===0?o!=null&&o[0]?{variantId:o[0],assignmentTtlMs:0}:null:{variantId:(m=a.createLocalEngine({sessionId:n,forcedPersona:r}).decide({components:[{id:s,variantIds:o}]}).assignments[s])!=null?m:o[0],assignmentTtlMs:0}},track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},getAssignment:()=>null,fetchWeights:()=>Promise.resolve([]),getGraph:()=>({pageNodes:[],capturedAt:0}),dispose:()=>{},destroy:()=>t.destroy()}}var Be="https://api.sentient-ui.com/v1/events",L=new Map,Nt=null;function Pt(e){return"value"in e||"currency"in e||"externalId"in e||"metadata"in e||"weight"in e||"stepIndex"in e}function we(){return ae()}var se={track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},getAssignment:()=>null,assign:()=>Promise.resolve(null),decide:()=>Promise.resolve(null),getSlotResult:()=>null,getPersona:()=>null,fetchWeights:()=>Promise.resolve([]),getGraph:()=>({pageNodes:[],capturedAt:0}),dispose:()=>{},destroy:()=>{}};function Mt(){try{let e={},t=new URLSearchParams(window.location.search);for(let[n,r]of t)n.startsWith("utm_")&&(e[n]=r);return e}catch(e){return{}}}function je(e){return e.replace(/\/events\/?$/,"")}function Ee(){return typeof navigator!="undefined"&&navigator.globalPrivacyControl===!0?!0:[typeof navigator!="undefined"?navigator.doNotTrack:void 0,typeof window!="undefined"?window.doNotTrack:void 0,typeof navigator!="undefined"?navigator.msDoNotTrack:void 0].some(t=>t==="1"||t==="yes")}function Lt(e){var d;let t=e.preConsentBehavior==="statistical_winner",n=je((d=e.ingestUrl)!=null?d:Be),r={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},l={track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},getAssignment:()=>null,fetchWeights:()=>Promise.resolve([]),async assign(s,o,a){if(!t)return null;try{let c=new URLSearchParams({componentId:s});for(let f of o!=null?o:[])c.append("variantIds[]",f);let m=await fetch(`${n}/winner?${c.toString()}`,{headers:r});return m.ok?{variantId:(await m.json()).variantId,assignmentTtlMs:0}:o!=null&&o[0]?{variantId:o[0],assignmentTtlMs:0}:null}catch(c){return o!=null&&o[0]?{variantId:o[0],assignmentTtlMs:0}:null}},decide:()=>Promise.resolve(null),getSlotResult:()=>null,getPersona:()=>null,getGraph:()=>({pageNodes:[],capturedAt:0}),dispose:()=>{},destroy:()=>{}},g={track:s=>l.track(s),goal:((s,o,a,c)=>l.goal(s,o,a,c)),componentGoal:(s,o,a)=>l.componentGoal(s,o,a),identify:s=>l.identify(s),getAssignment:(s,o)=>l.getAssignment(s,o),assign:(s,o,a,c)=>l.assign(s,o,a,c),decide:s=>l.decide(s),getSlotResult:s=>l.getSlotResult(s),getPersona:()=>l.getPersona(),fetchWeights:()=>l.fetchWeights(),getGraph:()=>l.getGraph(),dispose:()=>l.dispose(),destroy:()=>l.destroy()};function i(s){l=s}return{proxy:g,setInner:i}}function Fe(e){var X,Z,ee,te,h,w,G,N,B;if(typeof window=="undefined")return se;Nt=e.apiKey;let t=L.get(e.apiKey||"local");if(t!=null&&t.dispose)try{t.dispose()}catch(u){}let n=e.respectDoNotTrack!==!1&&Ee(),r=e.consent===!1||n,l=typeof e.apiKey=="string"&&e.apiKey.startsWith("pk_");if(e.localMode===!0||!l&&e.localMode!==!1)return r?(L.set(e.apiKey||"local",{config:e,upgrade:null}),se):(L.set(e.apiKey||"local",{config:e,upgrade:null}),$e(e));if(r){if(!e.apiKey||!e.apiKey.startsWith("pk_"))return e.preConsentBehavior==="statistical_winner"&&console.warn("[sentient] init() called with an invalid apiKey \u2014 expected a pk_ public key. SDK disabled."),L.set(e.apiKey,{config:e,upgrade:null}),se;let{proxy:u,setInner:y}=Lt(e);return L.set(e.apiKey,{config:e,upgrade:n?null:y}),u}if(!e.apiKey||!e.apiKey.startsWith("pk_"))return console.warn("[sentient] init() called with an invalid apiKey \u2014 expected a pk_ public key. SDK disabled."),se;if(e.ingestUrl==="")return console.warn("[sentient] init() called with an empty ingestUrl. SDK disabled."),se;let g=(X=e.ingestUrl)!=null?X:Be,i=Date.now(),d=ce({ssrSessionId:e.ssrSessionId,apiKey:e.apiKey}),s=Ne(void 0,e.apiKey),o=De({ingestUrl:g,apiKey:e.apiKey}),a=je(g),c={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},m=J((Z=navigator.userAgent)!=null?Z:""),p=typeof window!="undefined"?window.location.origin:void 0,f=Q((ee=document.referrer)!=null?ee:"",p),S=(te=e.sessionSegment)!=null?te:`${m}:${f}`,C=new Map,E=new Map,v=null,ie=u=>{for(let y of u)E.has(y.id)||E.set(y.id,ve(y))};if(e.initialSlots)for(let[u,y]of Object.entries(e.initialSlots))E.set(u,y);let F=Ue(e.apiKey);if(F)for(let[u,y]of Object.entries(F.slots))E.has(u)||E.set(u,y);let U={low:.15,medium:.5,high:.85};if(e.initialPersona)v=I({},e.initialPersona);else{let u=document.documentElement.dataset;u.sentientPersona?v={persona:u.sentientPersona,confidence:(w=U[(h=u.sentientConfidence)!=null?h:"low"])!=null?w:.15}:F&&(v={persona:F.persona,confidence:(G=U[F.band])!=null?G:.15})}if(e.initialAssignments)for(let[u,y]of Object.entries(e.initialAssignments))s.set(u,S,{variantId:y,assignedAt:Date.now(),segment:S,confidence:1});let D=Promise.resolve(),W=d.getSessionId();if(W){let u=V((N=document.referrer)!=null?N:""),y=I(I({sessionId:W,deviceClass:m,trafficSource:f,referrerDomain:u,utmParams:Mt(),timeOfDay:Y(new Date),dayOfWeek:["sun","mon","tue","wed","thu","fri","sat"][new Date().getDay()],ephemeral:d.isEphemeral(),automation:typeof navigator!="undefined"&&navigator.webdriver===!0||le((B=navigator.userAgent)!=null?B:"")},e.userId?{userId:e.userId}:{}),e.country?{country:e.country}:{});try{D=fetch(`${a}/sessions`,{method:"POST",keepalive:!0,body:JSON.stringify(y),headers:c}).then(b=>{b.status===402&&console.warn("[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing")}).catch(()=>{})}catch(b){}}e.debug&&(console.log("[sentient] initialized",{context:e.context}),window.__sentient={client:null,queue:o});let $={goal(u,y={},b=1,P=0){var T,O,R;let k=d.getSessionId();if(!k)return;let x=Pt(y)?y:{metadata:y},K=we(),M={sessionId:k,name:u,metadata:(T=x.metadata)!=null?T:{},weight:(O=x.weight)!=null?O:b,stepIndex:(R=x.stepIndex)!=null?R:P,goalId:K,value:x.value,currency:x.currency,externalId:x.externalId};D.then(()=>{fetch(`${a}/goals`,{method:"POST",keepalive:!0,body:JSON.stringify(M),headers:c}).catch(()=>{})})},componentGoal(u,y,b){var T,O,R;let P=d.getSessionId();if(!P)return;let k=s.get(u,S),x=k?null:(T=E.get(u))!=null?T:null;if(!k&&x===null){e.debug&&console.warn(`[sentient] componentGoal("${u}"): no assignment or slot decision yet \u2014 render its <Adaptive>/adaptive hook or call assign()/decide() before recording a goal.`);return}let K=k?k.variantId:Le(x),M={id:we(),sessionId:P,projectId:e.apiKey,componentId:u,variantId:K,eventType:"goal_achieved",goalType:y,payload:I({reward:(O=b==null?void 0:b.reward)!=null?O:1,goalValue:b==null?void 0:b.value,currency:b==null?void 0:b.currency},(R=b==null?void 0:b.metadata)!=null?R:{}),timestamp:Date.now(),timeInSession:Date.now()-i};e.debug&&console.log("[sentient] componentGoal",M),D.then(()=>o.push(M))},identify(u){let y=d.getSessionId();y&&D.then(()=>{fetch(`${a}/sessions`,{method:"POST",keepalive:!0,body:JSON.stringify({sessionId:y,userId:u,ephemeral:d.isEphemeral()}),headers:c}).catch(()=>{})})},track(u){let y=d.getSessionId();if(!y)return;let b=j(I({},u),{id:we(),sessionId:y,timestamp:Date.now(),timeInSession:Date.now()-i});e.debug&&console.log("[sentient] track",b),D.then(()=>o.push(b))},getAssignment(u,y){return s.get(u,y)},async assign(u,y,b,P){let k=d.getSessionId();if(!k)return null;let x=s.get(u,S);if(x&&(y!=null&&y.length||x.content!==void 0)){let T=x.ttlMs&&x.ttlMs>0?Math.max(0,x.assignedAt+x.ttlMs-Date.now()):0;return{variantId:x.variantId,assignmentTtlMs:T,content:x.content}}let K=C.get(u);if(K)return K;let M=(async()=>{await D;try{let T={sessionId:k,componentId:u,variantIds:y};P!==void 0?T.agentDataByVariant=P:b!==void 0&&(T.agentData=b);let O=await fetch(`${a}/assign`,{method:"POST",body:JSON.stringify(T),headers:c});if(!O.ok)return null;let R=await O.json();return s.set(u,S,I({variantId:R.variantId,assignedAt:Date.now(),segment:S,confidence:1,content:R.content},R.assignmentTtlMs&&R.assignmentTtlMs>0?{ttlMs:R.assignmentTtlMs}:{})),R}catch(T){return null}finally{C.delete(u)}})();return C.set(u,M),M},async decide(u){var P,k,x,K,M,T,O,R,Ae;let y=d.getSessionId();if(!y)return null;let b=(P=u.slots)!=null?P:[];await D;try{let q={sessionId:y};u.sections&&u.sections.length>0&&(q.sections=u.sections.map(_=>({id:_}))),q.components=(k=u.components)!=null?k:[],b.length>0&&(q.slots=b.map(be)),u.slotsFrom==="registry"&&(q.slotsFrom="registry"),u.v&&(q.v=u.v);let Te=await fetch(`${a}/decide`,{method:"POST",body:JSON.stringify(q),headers:c});if(!Te.ok)return ie(b),null;let A=await Te.json(),ne={};for(let _ of b)ne[_.id]=(K=(x=A.slots)==null?void 0:x[_.id])!=null?K:ve(_);if(A.slots)for(let[_,oe]of Object.entries(A.slots))_ in ne||(ne[_]=oe);for(let[_,oe]of Object.entries(ne))E.set(_,oe);let Ze=v!=null&&v.persona!=="unknown";A.persona&&!(A.persona==="unknown"&&Ze)?v={persona:A.persona,confidence:(M=A.confidence)!=null?M:0}:v||(v={persona:"unknown",confidence:0});for(let[_,oe]of Object.entries((T=A.assignments)!=null?T:{}))s.set(_,S,{variantId:oe,assignedAt:Date.now(),segment:S,confidence:1});return ge(e.apiKey,I({v:1,persona:v.persona,band:(0,Ie.confidenceBand)(v.confidence),slots:Object.fromEntries(E),layoutOrder:(O=A.layoutOrder)!=null?O:null,savedAt:Date.now()},A.slotConfig?{slotConfig:A.slotConfig}:{})),I(I(I({layoutOrder:(R=A.layoutOrder)!=null?R:null,assignments:(Ae=A.assignments)!=null?Ae:{},slots:ne,persona:v.persona,confidence:v.confidence},A.slotConfig?{slotConfig:A.slotConfig}:{}),A.goals?{goals:A.goals}:{}),A.sectionMap?{sectionMap:A.sectionMap}:{})}catch(q){return ie(b),null}},getSlotResult(u){var y;return(y=E.get(u))!=null?y:null},getPersona(){return v?{persona:v.persona,confidence:v.confidence,band:(0,Ie.confidenceBand)(v.confidence)}:null},async fetchWeights(){var u;try{let y=await fetch(`${a}/weights`,{headers:c});return y.ok?(u=(await y.json()).components)!=null?u:[]:[]}catch(y){return[]}},getGraph(){return{pageNodes:[],capturedAt:0}},dispose(){var u;o.destroy(),((u=L.get(e.apiKey))==null?void 0:u.dispose)===$.dispose&&L.delete(e.apiKey),e.debug&&console.log("[sentient] disposed")},destroy(){var u;o.destroy(),d.destroy(),((u=L.get(e.apiKey))==null?void 0:u.dispose)===$.dispose&&L.delete(e.apiKey);try{localStorage.removeItem(ue+e.apiKey),localStorage.removeItem(he(e.apiKey))}catch(y){}e.debug&&console.log("[sentient] destroyed")}};if(L.set(e.apiKey,{config:e,upgrade:null,dispose:$.dispose}),e.debug){let u=window;u.__sentient&&(u.__sentient.client=$)}return $}var We="_snt_graph_edges",Ut={pricing:["features","faq"],features:["pricing"],faq:["pricing"],social_proof:["cta"],cta:["social_proof","hero","trust"],hero:["cta"],comparison:["pricing"],trust:["cta"]};function Gt(e){var t;return(t=Ut[e])!=null?t:[]}function Kt(e,t){try{let n=localStorage.getItem(e);return n?JSON.parse(n):t}catch(n){return t}}function $t(e,t){try{localStorage.setItem(e,JSON.stringify(t))}catch(n){}}var Bt=new Set(["pricing","hero","social_proof","cta","features","faq","comparison","trust","navigation","generic"]);function jt(e){return Bt.has(e)?e:"generic"}function xe(e){try{let t=new URL(e);return`${t.origin}${t.pathname}`}catch(t){return"/"}}function Ft(e,t,n){let r=`${e}:${t}:${n.join(",")}`,l=5381;for(let g=0;g<r.length;g++)l=(l<<5)+l+r.charCodeAt(g)&4294967295;return(l>>>0).toString(16).padStart(8,"0")}function qe(e){let t=new Map,n=new Map,r=`_snt_graph_nodes${z(e==null?void 0:e.apiKey)}`,l=()=>{typeof window!="undefined"&&$t(r,[...t.values()])},g=i=>{var d;try{let s=JSON.parse(i);t.clear();for(let o of(d=s.pageNodes)!=null?d:[])t.set(o.componentId,o)}catch(s){}};if(typeof window!="undefined"){let i=Kt(r,[]);for(let d of i)t.set(d.componentId,d);try{localStorage.removeItem(We)}catch(d){}}return{addPageNode(i){t.set(i.componentId,i),l()},addStructuralEdge(i){let d=`${i.fromComponentId}->${i.toComponentId}`;n.set(d,i)},syncOnce(){var d,s;if(!(e!=null&&e.syncUrl)||typeof window=="undefined")return;let i=[...t.values()];if(i.length!==0)try{let o=new Map;for(let f of i){let S=(d=o.get(f.semanticType))!=null?d:[];S.push(f),o.set(f.semanticType,S)}let a=[],c=new Set;for(let f of i)for(let S of Gt(f.semanticType)){let C=(s=o.get(S))!=null?s:[];for(let E of C){if(E.componentId===f.componentId)continue;let v=`semantic:${f.componentId}->${E.componentId}`;c.has(v)||(c.add(v),a.push({fromComponentId:f.componentId,toComponentId:E.componentId,type:"semantic",weight:.4,confidence:.9}))}}let m=new Set(i.map(f=>f.componentId));for(let f of n.values()){if(!m.has(f.fromComponentId)||!m.has(f.toComponentId))continue;let S=`structural:${f.fromComponentId}->${f.toComponentId}`;c.has(S)||(c.add(S),a.push({fromComponentId:f.fromComponentId,toComponentId:f.toComponentId,type:"structural",weight:f.weight,confidence:1}))}let p=j(I(I({pageUrl:xe(window.location.href)},e.sessionId?{sessionId:e.sessionId}:{}),e.projectId?{projectId:e.projectId}:{}),{nodes:i.map(f=>{let S=jt(f.semanticType);return{componentId:f.componentId,semanticType:S,answers:f.answers,contentHash:Ft(f.componentId,S,f.answers),prominenceScore:f.prominenceScore,depthInPage:f.depth}}),edges:a});fetch(e.syncUrl,{method:"POST",keepalive:!0,headers:I({"Content-Type":"application/json"},e.apiKey?{Authorization:`Bearer ${e.apiKey}`}:{}),body:JSON.stringify(p)}).catch(()=>{})}catch(o){}},snapshot(){return{pageNodes:[...t.values()],capturedAt:Date.now()}},serialize(){return JSON.stringify({pageNodes:[...t.values()]})},restore:g,destroy(){if(typeof window!="undefined")try{localStorage.removeItem(r),localStorage.removeItem(We)}catch(i){}}}}var ze=["pricing","hero","social_proof","cta","features","faq","comparison","trust","navigation","generic"],Wt=[["pricing",/\b(pricing|price|plans?|subscriptions?|per month|\/mo|tier)\b/i],["faq",/\b(faq|frequently asked|common questions?)\b/i],["comparison",/\b(compare|comparison|versus|vs\.)\b/i],["social_proof",/\b(testimonial|reviews?|trusted by|loved by|customers?|logos|rated|case stud)\b/i],["trust",/\b(security|guarantee|privacy|compliance|secure|gdpr|soc ?2|encrypt)\b/i],["features",/\b(features?|how it works|benefits?|capabilit|what you get)\b/i]],qt=[["pricing",/(?:[$€£]\s?\d[\d,.]*\s*(?:\/|per\s)\s*(?:mo|month|yr|year|seat|user))|(?:\b(?:starter|basic|pro|growth|premium|enterprise)\b[^.]{0,60}[$€£]\s?\d)/i],["social_proof",/(?:★{2,})|(?:\b\d(?:\.\d)?\s*(?:out of|\/)\s*5\b)|(?:\brated\b)|(?:["“][^"”]{20,160}["”]\s*[—–-]\s*[A-Z][a-z]+)/],["trust",/\b(?:money[- ]back guarantee|refund(?:s)? within|encrypted|soc ?2|iso ?27001|gdpr[- ]compliant|cancel anytime)\b/i],["comparison",/\b(?:vs|versus)\b\.?[^.!?]{0,80}\b(?:compare|comparison|plans?|features?|alternative)\b|\bhow (?:we|it) compares?\b/i]];function zt(e){var n;let t=e.querySelector("h1, h2, h3");return((n=t==null?void 0:t.textContent)!=null?n:"").slice(0,160)}function Jt(e){if(e.tag==="nav"||e.tag==="footer")return{type:"navigation",strength:"weak"};let t=`${e.idClass} ${e.headingText}`.toLowerCase();for(let[n,r]of Wt)if(r.test(t))return{type:n,strength:"strong"};for(let[n,r]of qt)if(r.test(e.bodyText))return{type:n,strength:"strong"};return e.actionCount>=1&&e.textLength>0&&e.textLength<200?{type:"cta",strength:"weak"}:e.tag==="header"?{type:"hero",strength:"weak"}:/\b(hero|headline|banner)\b/i.test(t)?{type:"hero",strength:"weak"}:{type:"generic",strength:"weak"}}function Qt(e){var n,r;let t=((n=e.textContent)!=null?n:"").replace(/\s+/g," ").trim();return{tag:e.tagName.toLowerCase(),idClass:`${e.id} ${String((r=e.className)!=null?r:"")}`,headingText:zt(e),bodyText:t.slice(0,2e3),actionCount:e.querySelectorAll('a, button, [role="button"]').length,textLength:t.length}}function Je(e){return Jt(Qt(e)).type}var Vt=new Set(["SECTION","ARTICLE","MAIN","DIV"]),Yt="h1, h2, h3",Qe=30,Ht=1500,Xt={scan:async()=>({nodes:[],edges:[],scannedAt:0}),observe:()=>{},getProminenceScore:()=>0,destroy:()=>{}};function Ve(e,t,n){return n<=t?0:Math.max(0,Math.min(1,(e-t)/(n-t)))}function Zt(e){var t,n,r;try{let l=e;for(let g of Object.keys(l)){if(!g.startsWith("__reactFiber")&&!g.startsWith("__reactInternalInstance"))continue;let i=l[g],d=(r=(t=i==null?void 0:i.type)==null?void 0:t.displayName)!=null?r:(n=i==null?void 0:i.type)==null?void 0:n.name;if(d&&d.length>1)return d}}catch(l){}}function en(e){let t={};for(let n of Array.from(e.attributes))n.name.startsWith("data-")&&(t[n.name]=n.value);return t}var Ye=new Set(ze);function tn(e){let t=e.getAttribute("data-sentient-type");if(t&&Ye.has(t))return t;let n=e.getAttribute("role");return n&&Ye.has(n)?n:Je(e)}function nn(e){let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e.charCodeAt(n)&4294967295;return(t>>>0).toString(16).padStart(8,"0")}function on(e){let t=[],n=e;for(;n;){let r=n.parentElement;if(!r){t.push(n.tagName.toLowerCase());break}let l=Array.prototype.indexOf.call(r.children,n);t.push(`${n.tagName.toLowerCase()}[${l}]`),n=r}return t.reverse().join("/")}function rn(e){var n;let t=(n=e.getAttribute("data-sentient-id"))!=null?n:e.getAttribute("id");return t||`${e.tagName.toLowerCase()}-${nn(on(e))}`}function sn(e){let t=0,n=e.parentElement;for(;n;)t++,n=n.parentElement;return t}function Ce(e,t){var r,l,g;let n=e.querySelector(Yt);return{componentId:rn(e),semanticType:tn(e),ariaLabel:(r=e.getAttribute("aria-label"))!=null?r:void 0,headingText:(g=(l=n==null?void 0:n.textContent)==null?void 0:l.trim())!=null?g:void 0,isAboveFold:e.getBoundingClientRect().top<window.innerHeight,prominenceScore:t(e),depth:sn(e),reactComponentName:Zt(e),dataAttributes:en(e)}}function He(e){var g;let t=[],n=new Set,r="__root__",l=new Map;for(let[i,d]of e){let s=i.parentElement,o=r;for(;s;){if(e.has(s)){o=e.get(s);let c=e.get(i),m=`${o}->${c}`;!n.has(m)&&o!==c&&(n.add(m),t.push({fromComponentId:o,toComponentId:c,weight:.6}));break}s=s.parentElement}let a=(g=l.get(o))!=null?g:[];a.push(i),l.set(o,a)}for(let i of l.values()){if(i.length<2)continue;let d=i.length>Qe?i.slice(0,Qe):i;for(let s=0;s<d.length;s++)for(let o=s+1;o<d.length;o++){if(t.length>=Ht)return t;let a=e.get(d[s]),c=e.get(d[o]);if(a===c)continue;let m=`${a}->${c}::sib`,p=`${c}->${a}::sib`;n.has(m)||(n.add(m),t.push({fromComponentId:a,toComponentId:c,weight:.3})),n.has(p)||(n.add(p),t.push({fromComponentId:c,toComponentId:a,weight:.3}))}}return t}function an(e){let t=[],n=new Set,r=new Map;return document.querySelectorAll("[data-sentient-id]").forEach(i=>{if(i instanceof Element&&!n.has(i)){n.add(i);let d=Ce(i,e);t.push(d),r.set(i,d.componentId)}}),document.querySelectorAll("section, article, main, aside").forEach(i=>{if(!(i instanceof Element)||n.has(i))return;let d=i.hasAttribute("aria-label"),s=i.hasAttribute("data-sentient-id");if(!d&&!s)return;n.add(i);let o=Ce(i,e);t.push(o),r.set(i,o.componentId)}),{nodes:t,edges:He(r),elementToId:r}}function Xe(){if(typeof window=="undefined")return Xt;let e=null,t=0,n=null,r=new Map,l=s=>{try{let o=window.getComputedStyle(s),a=parseFloat(o.fontSize)||12,c=parseFloat(o.zIndex)||0,m=s.getBoundingClientRect(),p=Math.max(m.top,0),f=window.innerHeight||1,S=1/(p/f+1),C=Ve(a,12,48)*.4+S*.4+Ve(c,0,100)*.2;return Math.max(0,Math.min(1,C))}catch(o){return .5}};return{scan:()=>new Promise(s=>{let o=()=>{let{nodes:a,edges:c,elementToId:m}=an(l);r.clear();for(let[p,f]of m)r.set(p,f);s({nodes:a,edges:c,scannedAt:Date.now()})};try{typeof requestIdleCallback=="function"?t=requestIdleCallback(o,{timeout:100}):o()}catch(a){o()}}),observe:s=>{n=s;try{e=new MutationObserver(o=>{let a=[],c=new Set;for(let p of o)p.type==="childList"&&p.addedNodes.forEach(f=>{if(!(f instanceof Element)||!Vt.has(f.tagName))return;let S=f.hasAttribute("data-sentient-id"),C=f.hasAttribute("aria-label");if(!S&&!C)return;let E=Ce(f,l);a.push(E),c.add(E.componentId),r.set(f,E.componentId)});if(a.length===0||!n)return;for(let p of[...r.keys()])p.isConnected||r.delete(p);let m=He(r).filter(p=>c.has(p.fromComponentId)||c.has(p.toComponentId));n({nodes:a,edges:m,addedAt:Date.now()})}),e.observe(document.body,{childList:!0,subtree:!0})}catch(o){}},getProminenceScore:l,destroy:()=>{if(e&&(e.disconnect(),e=null),t&&typeof cancelIdleCallback=="function")try{cancelIdleCallback(t)}catch(s){}t=0,n=null,r.clear()}}}var cn="https://api.sentient-ui.com/v1/events";function dn(){try{let e=document.cookie.match(/(?:^|; )_snt_uid=([^;]*)/);return e?decodeURIComponent(e[1]):void 0}catch(e){return}}var fe=new Map;function ln(e){var c;let t=Fe(e),n=e.respectDoNotTrack!==!1&&Ee(),r=e.consent===!1||n;if(!e.graph||!e.apiKey||r||typeof window=="undefined")return t;let l=fe.get(e.apiKey);if(l)try{l()}catch(m){}let g=Xe(),i=(c=e.ingestUrl)!=null?c:cn,d=qe({syncUrl:i.replace(/\/events\/?$/,"/graph/sync"),apiKey:e.apiKey,projectId:e.apiKey,sessionId:dn()});g.scan().then(m=>{for(let p of m.nodes)d.addPageNode({id:p.componentId,componentId:p.componentId,semanticType:p.semanticType,answers:e.captureDomText&&p.headingText?[p.headingText]:[],prominenceScore:p.prominenceScore,depth:p.depth});for(let p of m.edges)d.addStructuralEdge(p);d.syncOnce()});let s=null,o=()=>{s!==null&&clearTimeout(s),s=setTimeout(()=>{s=null,d.syncOnce()},500)};g.observe(m=>{for(let p of m.nodes)d.addPageNode({id:p.componentId,componentId:p.componentId,semanticType:p.semanticType,answers:e.captureDomText&&p.headingText?[p.headingText]:[],prominenceScore:p.prominenceScore,depth:p.depth});for(let p of m.edges)d.addStructuralEdge(p);o()});let a=()=>{s!==null&&(clearTimeout(s),s=null),g.destroy(),d.destroy(),fe.get(e.apiKey)===a&&fe.delete(e.apiKey)};return fe.set(e.apiKey,a),j(I({},t),{getGraph:()=>d.snapshot(),dispose:()=>{a(),t.dispose()},destroy:()=>{a(),t.destroy()}})}0&&(module.exports={deriveSessionSegment,detectDeviceClass,detectTimeOfDay,detectTrafficSource,init,referrerDomainFromReferer,sanitizePageUrl});
2
2
  //# sourceMappingURL=index-graph.js.map