@firstflow/react 0.0.1 → 0.0.101

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.
@@ -0,0 +1,2157 @@
1
+ // src/analytics/events.ts
2
+ var ISSUE_SUBMITTED = "issue_submitted";
3
+ var FEEDBACK_SUBMITTED = "feedback_submitted";
4
+ var SURVEY_COMPLETED = "survey_completed";
5
+ var EXPERIENCE_SHOWN = "experience_shown";
6
+ var EXPERIENCE_CLICKED = "experience_clicked";
7
+ var CHAT_MESSAGE_SENT = "chat_message_sent";
8
+ var EXPERIENCE_ANALYTICS_TYPE = {
9
+ FLOW_MESSAGE: "flow_message",
10
+ FLOW_ANNOUNCEMENT: "flow_announcement",
11
+ SURVEY: "survey"
12
+ };
13
+
14
+ // src/issue/defaults.ts
15
+ var DEFAULT_PROMPT_TEXT = "Something wrong? Report an issue and we'll look into it.";
16
+ var DEFAULT_ISSUE_TITLE = "Report an issue";
17
+ var DEFAULT_ISSUE_TRIGGER_LABEL = "Report issue";
18
+ var DEFAULT_ISSUE_SUBMIT_LABEL = "Submit";
19
+ var DEFAULT_ISSUE_CANCEL_LABEL = "Cancel";
20
+ var DEFAULT_ISSUE_CANCEL_ENABLED = true;
21
+ var DEFAULT_FIELDS = [
22
+ { id: "name", label: "Name", type: "text", required: true },
23
+ { id: "email", label: "Email", type: "text", required: true },
24
+ { id: "subject", label: "Subject", type: "text", required: true },
25
+ { id: "description", label: "Description", type: "textarea", required: true },
26
+ { id: "session_id", label: "Session ID", type: "text", required: false }
27
+ ];
28
+
29
+ // src/issue/normalizer.ts
30
+ function normalizeField(raw) {
31
+ const id = raw.id?.trim();
32
+ if (!id) return null;
33
+ if (raw.allowed === false) return null;
34
+ const type = raw.type === "textarea" || raw.type === "select" ? raw.type : "text";
35
+ return {
36
+ id,
37
+ type,
38
+ label: raw.label?.trim() ?? id,
39
+ required: raw.required === true,
40
+ options: type === "select" && Array.isArray(raw.options) ? raw.options : void 0
41
+ };
42
+ }
43
+ function normalizeConfig(agentId, apiUrl, raw) {
44
+ const enabled = raw?.enabled === true;
45
+ const title = typeof raw?.title === "string" && raw.title.trim() ? raw.title.trim() : DEFAULT_ISSUE_TITLE;
46
+ const triggerLabel = typeof raw?.triggerLabel === "string" && raw.triggerLabel.trim() ? raw.triggerLabel.trim() : DEFAULT_ISSUE_TRIGGER_LABEL;
47
+ const submitLabel = typeof raw?.submitLabel === "string" && raw.submitLabel.trim() ? raw.submitLabel.trim() : DEFAULT_ISSUE_SUBMIT_LABEL;
48
+ const cancelLabel = typeof raw?.cancelLabel === "string" && raw.cancelLabel.trim() ? raw.cancelLabel.trim() : DEFAULT_ISSUE_CANCEL_LABEL;
49
+ const cancelEnabled = typeof raw?.cancelEnabled === "boolean" ? raw.cancelEnabled : DEFAULT_ISSUE_CANCEL_ENABLED;
50
+ const promptText = typeof raw?.promptText === "string" && raw.promptText.trim() ? raw.promptText.trim() : DEFAULT_PROMPT_TEXT;
51
+ let fields = DEFAULT_FIELDS;
52
+ if (Array.isArray(raw?.fields) && raw.fields.length > 0) {
53
+ const normalized = raw.fields.map(normalizeField).filter((f) => f !== null);
54
+ if (normalized.length > 0) fields = normalized;
55
+ }
56
+ return {
57
+ agentId,
58
+ apiUrl: apiUrl.replace(/\/$/, ""),
59
+ enabled,
60
+ title,
61
+ triggerLabel,
62
+ submitLabel,
63
+ cancelLabel,
64
+ cancelEnabled,
65
+ promptText,
66
+ fields
67
+ };
68
+ }
69
+
70
+ // src/issue/validate.ts
71
+ var CONTEXT_KEYS = /* @__PURE__ */ new Set([
72
+ "conversationId",
73
+ "messageId",
74
+ "sessionId",
75
+ "agentId",
76
+ "model"
77
+ ]);
78
+ function isContextKey(key3) {
79
+ return CONTEXT_KEYS.has(key3);
80
+ }
81
+ function validatePayload(data, fields) {
82
+ const errors = {};
83
+ const fieldIds = new Set(fields.map((f) => f.id));
84
+ for (const field of fields) {
85
+ const value = data[field.id];
86
+ const str = typeof value === "string" ? value.trim() : "";
87
+ if (field.required && !str) {
88
+ errors[field.id] = `${field.label ?? field.id} is required`;
89
+ continue;
90
+ }
91
+ if (!field.required && !str) continue;
92
+ if (field.type === "select" && field.options?.length) {
93
+ if (!field.options.includes(str)) {
94
+ errors[field.id] = `Please choose a valid option`;
95
+ }
96
+ }
97
+ }
98
+ return {
99
+ valid: Object.keys(errors).length === 0,
100
+ errors: Object.keys(errors).length > 0 ? errors : void 0
101
+ };
102
+ }
103
+ function getFormFieldsOnly(data, fields) {
104
+ const fieldIds = new Set(fields.map((f) => f.id));
105
+ const out = {};
106
+ for (const key3 of Object.keys(data)) {
107
+ if (isContextKey(key3)) continue;
108
+ if (fieldIds.has(key3)) out[key3] = data[key3];
109
+ }
110
+ return out;
111
+ }
112
+
113
+ // src/analytics/identity.ts
114
+ var ANON_KEY = "__ff_anon_id";
115
+ var SESSION_KEY = "ff_sess";
116
+ function uuid() {
117
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
118
+ const r = Math.random() * 16 | 0;
119
+ return (c === "x" ? r : r & 3 | 8).toString(16);
120
+ });
121
+ }
122
+ function getStorage(key3, storage) {
123
+ if (typeof window === "undefined" || !storage) return uuid();
124
+ try {
125
+ let value = storage.getItem(key3);
126
+ if (!value) {
127
+ value = uuid();
128
+ storage.setItem(key3, value);
129
+ }
130
+ return value;
131
+ } catch {
132
+ return uuid();
133
+ }
134
+ }
135
+ function getAnonymousId() {
136
+ return getStorage(ANON_KEY, typeof window !== "undefined" ? window.localStorage : null);
137
+ }
138
+ function getSessionId() {
139
+ return getStorage(SESSION_KEY, typeof window !== "undefined" ? window.sessionStorage : null);
140
+ }
141
+ function removeStorageKey(key3, storage) {
142
+ if (typeof window === "undefined" || !storage) return;
143
+ try {
144
+ storage.removeItem(key3);
145
+ } catch {
146
+ }
147
+ }
148
+ function clearAnonymousId() {
149
+ removeStorageKey(ANON_KEY, typeof window !== "undefined" ? window.localStorage : null);
150
+ }
151
+ function clearSessionId() {
152
+ removeStorageKey(SESSION_KEY, typeof window !== "undefined" ? window.sessionStorage : null);
153
+ }
154
+
155
+ // src/analytics/device.ts
156
+ function getDeviceType() {
157
+ if (typeof window === "undefined") return "desktop";
158
+ const w = window.innerWidth;
159
+ if (w <= 768) return "mobile";
160
+ if (w <= 1024) return "tablet";
161
+ return "desktop";
162
+ }
163
+
164
+ // src/analytics/jitsu.ts
165
+ var DEFAULT_JITSU_HOST = "https://cmmiyu46c00003b6unusxjf0j.analytics.firstflow.dev";
166
+ var DEFAULT_JITSU_KEY = "P5qcd1d4Bd98EdxS4EfnucLohE2Ch37K:U4OECTXofJVzR5jhjaCpaYj478871PFU";
167
+ function uuid2() {
168
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
169
+ const r = Math.random() * 16 | 0;
170
+ return (c === "x" ? r : r & 3 | 8).toString(16);
171
+ });
172
+ }
173
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
174
+ "anonymousId",
175
+ "agent_id",
176
+ "session_id",
177
+ "device_type",
178
+ "user_id",
179
+ "userId"
180
+ ]);
181
+ function sanitizeTraits(traits) {
182
+ if (!traits) return {};
183
+ const out = {};
184
+ for (const [key3, value] of Object.entries(traits)) {
185
+ if (RESERVED_KEYS.has(key3)) continue;
186
+ out[key3] = value;
187
+ }
188
+ return out;
189
+ }
190
+ function coerceTraitValueToString(value) {
191
+ if (value === null || value === void 0) return null;
192
+ if (typeof value === "string") return value;
193
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
194
+ if (typeof value === "boolean") return String(value);
195
+ try {
196
+ return JSON.stringify(value);
197
+ } catch {
198
+ return String(value);
199
+ }
200
+ }
201
+ function traitsRecordToStringMap(traits) {
202
+ const out = {};
203
+ for (const [k, v] of Object.entries(traits)) {
204
+ const s = coerceTraitValueToString(v);
205
+ if (s !== null) out[k] = s;
206
+ }
207
+ return Object.keys(out).length > 0 ? out : void 0;
208
+ }
209
+ function buildTraitsStringMap(config) {
210
+ const merged = { ...sanitizeTraits(config.traits) };
211
+ const uid = config.userId?.trim();
212
+ if (uid) merged.user_id = uid;
213
+ return traitsRecordToStringMap(merged);
214
+ }
215
+ function buildJitsuTrackPayload(config, event, properties = {}) {
216
+ const traits = buildTraitsStringMap(config);
217
+ const payload = {
218
+ messageId: uuid2(),
219
+ type: "track",
220
+ event,
221
+ anonymousId: getAnonymousId(),
222
+ ...config.userId ? { userId: config.userId } : {},
223
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
224
+ properties: {
225
+ agent_id: config.agentId,
226
+ session_id: getSessionId(),
227
+ device_type: getDeviceType(),
228
+ ...properties
229
+ }
230
+ };
231
+ if (traits) payload.traits = traits;
232
+ return payload;
233
+ }
234
+ function sendTrack(config, event, properties = {}) {
235
+ const host = config.jitsuHost || DEFAULT_JITSU_HOST;
236
+ const key3 = config.jitsuWriteKey || DEFAULT_JITSU_KEY;
237
+ const payload = buildJitsuTrackPayload(config, event, properties);
238
+ const auth = "Basic " + btoa(key3);
239
+ fetch(`${host.replace(/\/$/, "")}/api/s/track`, {
240
+ method: "POST",
241
+ headers: {
242
+ "Content-Type": "application/json",
243
+ Authorization: auth
244
+ },
245
+ body: JSON.stringify(payload),
246
+ keepalive: true
247
+ }).catch(() => {
248
+ });
249
+ }
250
+
251
+ // src/analytics/analytics.ts
252
+ var DEFAULT_JITSU_HOST2 = "https://cmmiyu46c00003b6unusxjf0j.analytics.firstflow.dev";
253
+ var DEFAULT_JITSU_KEY2 = "P5qcd1d4Bd98EdxS4EfnucLohE2Ch37K:U4OECTXofJVzR5jhjaCpaYj478871PFU";
254
+ function createAnalytics(config) {
255
+ const agentId = config.agentId;
256
+ const jitsuHost = config.jitsuHost ?? DEFAULT_JITSU_HOST2;
257
+ const jitsuWriteKey = config.jitsuWriteKey ?? DEFAULT_JITSU_KEY2;
258
+ const identity = {
259
+ userId: config.userId,
260
+ traits: config.traits ?? {}
261
+ };
262
+ function transportConfig() {
263
+ return {
264
+ jitsuHost,
265
+ jitsuWriteKey,
266
+ agentId,
267
+ userId: identity.userId,
268
+ traits: identity.traits
269
+ };
270
+ }
271
+ return {
272
+ track(eventName, properties = {}) {
273
+ sendTrack(transportConfig(), eventName, properties);
274
+ },
275
+ identify(userId, traits = {}) {
276
+ this.setUser(userId, traits);
277
+ if (!userId) return;
278
+ sendTrack(transportConfig(), "user_identified", {});
279
+ },
280
+ page(name, properties = {}) {
281
+ sendTrack(transportConfig(), "page_view", { page_name: name ?? void 0, ...properties });
282
+ },
283
+ setUser(userId, traits = {}) {
284
+ identity.userId = userId || void 0;
285
+ identity.traits = { ...traits ?? {} };
286
+ },
287
+ getUser() {
288
+ return {
289
+ userId: identity.userId,
290
+ traits: { ...identity.traits ?? {} }
291
+ };
292
+ }
293
+ };
294
+ }
295
+
296
+ // src/issue/IssueReporter.ts
297
+ function extractContextMetadata(data, formFieldIds) {
298
+ const ctx = {};
299
+ for (const key3 of Object.keys(data)) {
300
+ if (formFieldIds.has(key3)) continue;
301
+ const v = data[key3];
302
+ if (v !== void 0) ctx[key3] = v;
303
+ }
304
+ return ctx;
305
+ }
306
+ function buildPayload(formValues, contextMetadata) {
307
+ return { ...formValues, ...contextMetadata };
308
+ }
309
+ function createIssueReporter(options) {
310
+ const { agentId, apiUrl, config: rawConfig, analytics: analyticsOpt } = options;
311
+ const analytics = analyticsOpt ?? createAnalytics({ agentId });
312
+ let config = normalizeConfig(agentId, apiUrl, rawConfig);
313
+ let openHandler;
314
+ const instance = {
315
+ getConfig() {
316
+ return config;
317
+ },
318
+ setConfig(next) {
319
+ config = next;
320
+ },
321
+ async reportIssue(data) {
322
+ const formValues = getFormFieldsOnly(
323
+ data,
324
+ config.fields
325
+ );
326
+ const result = validatePayload(formValues, config.fields);
327
+ if (!result.valid) {
328
+ throw new Error(
329
+ result.errors ? Object.entries(result.errors).map(([k, v]) => `${k}: ${v}`).join(", ") : "Validation failed"
330
+ );
331
+ }
332
+ const formFieldIds = new Set(config.fields.map((f) => f.id));
333
+ const contextMetadata = extractContextMetadata(data, formFieldIds);
334
+ const payload = buildPayload(formValues, contextMetadata);
335
+ analytics.track(ISSUE_SUBMITTED, {
336
+ payload: JSON.stringify(payload),
337
+ metadata: JSON.stringify({})
338
+ });
339
+ },
340
+ open(options2) {
341
+ openHandler?.(options2);
342
+ },
343
+ setOpenHandler(handler) {
344
+ openHandler = handler;
345
+ },
346
+ destroy() {
347
+ openHandler = void 0;
348
+ }
349
+ };
350
+ return instance;
351
+ }
352
+
353
+ // src/types.ts
354
+ var EXPERIENCE_SETTINGS_DEFAULT = {
355
+ trigger: { show_on: "chat_opens", delay_seconds: 0 },
356
+ device: "all",
357
+ audience: { type: "all" },
358
+ schedule: { start_type: "now", expiration_type: "none" },
359
+ frequency: { type: "once" },
360
+ priority: 0
361
+ };
362
+
363
+ // src/experience/eligibility.ts
364
+ function makeKey(experienceId) {
365
+ return `ff_exp_${experienceId}_freq`;
366
+ }
367
+ var FREQ_KEY_PREFIX = "ff_exp_";
368
+ function getStorageSafely(scope) {
369
+ try {
370
+ if (typeof window === "undefined") return null;
371
+ return scope === "session" ? window.sessionStorage : window.localStorage;
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+ function createStorageAdapter() {
377
+ const memLocal = /* @__PURE__ */ new Map();
378
+ const memSession = /* @__PURE__ */ new Map();
379
+ return {
380
+ get(experienceId, scope) {
381
+ const key3 = makeKey(experienceId);
382
+ const mem = scope === "session" ? memSession : memLocal;
383
+ try {
384
+ const storage = getStorageSafely(scope);
385
+ const raw = storage ? storage.getItem(key3) : null;
386
+ if (!raw) return mem.get(key3) ?? null;
387
+ const parsed = JSON.parse(raw);
388
+ if (typeof parsed?.count !== "number" || typeof parsed?.window_start !== "string") {
389
+ return mem.get(key3) ?? null;
390
+ }
391
+ return parsed;
392
+ } catch {
393
+ return mem.get(key3) ?? null;
394
+ }
395
+ },
396
+ set(experienceId, rec, scope) {
397
+ const key3 = makeKey(experienceId);
398
+ const mem = scope === "session" ? memSession : memLocal;
399
+ try {
400
+ const storage = getStorageSafely(scope);
401
+ if (storage) storage.setItem(key3, JSON.stringify(rec));
402
+ else mem.set(key3, rec);
403
+ } catch {
404
+ mem.set(key3, rec);
405
+ }
406
+ },
407
+ clear(experienceId, scope) {
408
+ const key3 = makeKey(experienceId);
409
+ const mem = scope === "session" ? memSession : memLocal;
410
+ try {
411
+ const storage = getStorageSafely(scope);
412
+ if (storage) storage.removeItem(key3);
413
+ } catch {
414
+ }
415
+ mem.delete(key3);
416
+ }
417
+ };
418
+ }
419
+ function clearFrequencyRecords() {
420
+ if (typeof window === "undefined") return;
421
+ const clearStorage = (storage) => {
422
+ try {
423
+ const keys = [];
424
+ for (let i = 0; i < storage.length; i++) {
425
+ const key3 = storage.key(i);
426
+ if (key3 && key3.startsWith(FREQ_KEY_PREFIX)) keys.push(key3);
427
+ }
428
+ for (const key3 of keys) storage.removeItem(key3);
429
+ } catch {
430
+ }
431
+ };
432
+ clearStorage(window.localStorage);
433
+ clearStorage(window.sessionStorage);
434
+ }
435
+ function isStatusEligible(status) {
436
+ return status === "active";
437
+ }
438
+ function isDeviceEligible(device) {
439
+ if (device === "all") return true;
440
+ if (typeof navigator === "undefined") return true;
441
+ const ua = navigator.userAgent.toLowerCase();
442
+ const isMobile = /android|iphone|ipad|ipod|mobile/.test(ua);
443
+ return device === "mobile" ? isMobile : !isMobile;
444
+ }
445
+ function asMs(value) {
446
+ if (!value) return null;
447
+ const ms = Date.parse(value);
448
+ return Number.isFinite(ms) ? ms : null;
449
+ }
450
+ function isScheduleEligible(schedule, now = Date.now()) {
451
+ if (schedule.start_type === "scheduled") {
452
+ const startMs = asMs(schedule.starts_at);
453
+ if (startMs != null && now < startMs) return false;
454
+ }
455
+ if (schedule.expiration_type === "scheduled") {
456
+ const endMs = asMs(schedule.expires_at);
457
+ if (endMs != null && now >= endMs) return false;
458
+ }
459
+ return true;
460
+ }
461
+ function periodToMs(period) {
462
+ if (period === "day") return 864e5;
463
+ if (period === "week") return 6048e5;
464
+ return Infinity;
465
+ }
466
+ function getScopeForFrequency(frequency) {
467
+ if (frequency.type === "limited" && (frequency.period ?? "session") === "session") {
468
+ return "session";
469
+ }
470
+ return "local";
471
+ }
472
+ function isFrequencyEligible(experienceId, frequency, storage, now = Date.now()) {
473
+ if (frequency.type === "always") return true;
474
+ const scope = getScopeForFrequency(frequency);
475
+ const rec = storage.get(experienceId, scope);
476
+ if (!rec) return true;
477
+ if (frequency.type === "once") {
478
+ return rec.count === 0;
479
+ }
480
+ const limit = frequency.limit ?? 1;
481
+ const period = frequency.period ?? "session";
482
+ const windowMs = periodToMs(period);
483
+ if (windowMs !== Infinity) {
484
+ const windowStart = Date.parse(rec.window_start);
485
+ if (Number.isFinite(windowStart) && now - windowStart > windowMs) {
486
+ return true;
487
+ }
488
+ }
489
+ return rec.count < limit;
490
+ }
491
+ function markExperienceShown(experienceId, frequency, storage, now = Date.now()) {
492
+ if (frequency.type === "always") return;
493
+ const scope = getScopeForFrequency(frequency);
494
+ const rec = storage.get(experienceId, scope);
495
+ if (!rec) {
496
+ storage.set(
497
+ experienceId,
498
+ { count: 1, window_start: new Date(now).toISOString() },
499
+ scope
500
+ );
501
+ return;
502
+ }
503
+ const period = frequency.type === "limited" ? frequency.period ?? "session" : "lifetime";
504
+ const windowMs = periodToMs(period);
505
+ const windowStart = Date.parse(rec.window_start);
506
+ const windowExpired = windowMs !== Infinity && Number.isFinite(windowStart) && now - windowStart > windowMs;
507
+ if (windowExpired) {
508
+ storage.set(
509
+ experienceId,
510
+ { count: 1, window_start: new Date(now).toISOString() },
511
+ scope
512
+ );
513
+ return;
514
+ }
515
+ storage.set(
516
+ experienceId,
517
+ {
518
+ count: rec.count + 1,
519
+ window_start: rec.window_start || new Date(now).toISOString()
520
+ },
521
+ scope
522
+ );
523
+ }
524
+ function isTriggerCompatible(trigger) {
525
+ if (trigger.show_on === "chat_opens") return true;
526
+ return true;
527
+ }
528
+ function isAbsent(value) {
529
+ return value == null;
530
+ }
531
+ function resolveFromObject(input, path) {
532
+ if (!input) return void 0;
533
+ let current = input;
534
+ for (const seg of path) {
535
+ if (!current || typeof current !== "object" || Array.isArray(current))
536
+ return void 0;
537
+ current = current[seg];
538
+ }
539
+ return current;
540
+ }
541
+ function resolveTrait(trait, user) {
542
+ const key3 = String(trait ?? "").trim();
543
+ if (!key3) return void 0;
544
+ const segments = key3.split(".").slice(0, 3);
545
+ const fromRoot = resolveFromObject(
546
+ user,
547
+ segments
548
+ );
549
+ if (fromRoot !== void 0) return fromRoot;
550
+ const traitSegments = segments[0] === "traits" ? segments.slice(1) : segments;
551
+ return resolveFromObject(user.traits ?? {}, traitSegments);
552
+ }
553
+ function evaluateNumeric(actual, expected, operator) {
554
+ if (typeof actual !== "number" || typeof expected !== "number") return false;
555
+ if (!Number.isFinite(actual) || !Number.isFinite(expected)) return false;
556
+ switch (operator) {
557
+ case "gt":
558
+ return actual > expected;
559
+ case "gte":
560
+ return actual >= expected;
561
+ case "lt":
562
+ return actual < expected;
563
+ case "lte":
564
+ return actual <= expected;
565
+ default:
566
+ return false;
567
+ }
568
+ }
569
+ function evaluateRule(rule, user) {
570
+ const actual = resolveTrait(rule.trait, user);
571
+ switch (rule.operator) {
572
+ case "exists":
573
+ return !isAbsent(actual);
574
+ case "not_exists":
575
+ return isAbsent(actual);
576
+ case "equals":
577
+ return actual === rule.value;
578
+ case "not_equals":
579
+ return actual !== rule.value;
580
+ case "contains":
581
+ return typeof actual === "string" && typeof rule.value === "string" ? actual.includes(rule.value) : false;
582
+ case "not_contains":
583
+ return typeof actual === "string" && typeof rule.value === "string" ? !actual.includes(rule.value) : false;
584
+ case "in":
585
+ return Array.isArray(rule.value) ? rule.value.includes(actual) : false;
586
+ case "not_in":
587
+ return Array.isArray(rule.value) ? !rule.value.includes(actual) : false;
588
+ case "gt":
589
+ case "gte":
590
+ case "lt":
591
+ case "lte":
592
+ return evaluateNumeric(actual, rule.value, rule.operator);
593
+ default:
594
+ return false;
595
+ }
596
+ }
597
+ function evaluateAudience(audience, user, membershipMap = {}) {
598
+ if (audience.type === "all") return true;
599
+ if (audience.type === "segment") {
600
+ const seg = String(audience.segment_id ?? "").trim();
601
+ if (!seg) return false;
602
+ return membershipMap[seg] === true;
603
+ }
604
+ if (audience.type === "rules") {
605
+ if (!user) return false;
606
+ if (audience.operator === "AND") {
607
+ return audience.rules.every((rule) => evaluateRule(rule, user));
608
+ }
609
+ if (audience.operator === "OR") {
610
+ return audience.rules.some((rule) => evaluateRule(rule, user));
611
+ }
612
+ return false;
613
+ }
614
+ return false;
615
+ }
616
+ function sortByPriorityDescThenStable(items) {
617
+ return [...items].sort((a, b) => {
618
+ const pa = a.settings?.priority ?? 0;
619
+ const pb = b.settings?.priority ?? 0;
620
+ if (pb !== pa) return pb - pa;
621
+ const aKey = String(a.experienceId ?? a.id ?? "");
622
+ const bKey = String(b.experienceId ?? b.id ?? "");
623
+ return aKey.localeCompare(bKey);
624
+ });
625
+ }
626
+ function runEligibilityFilter(experiences, storage, context = {}) {
627
+ const blocked = new Set(context.overrides?.blockedExperiences ?? []);
628
+ const forced = new Set(context.overrides?.forcedExperiences ?? []);
629
+ const user = context.user ?? null;
630
+ const membershipMap = context.membershipMap ?? {};
631
+ const passing = experiences.filter((exp) => {
632
+ const settings = exp.settings ?? EXPERIENCE_SETTINGS_DEFAULT;
633
+ if (blocked.has(exp.experienceId)) return false;
634
+ if (forced.has(exp.experienceId)) {
635
+ return isFrequencyEligible(exp.experienceId, settings.frequency, storage);
636
+ }
637
+ return isStatusEligible(exp.status) && isDeviceEligible(settings.device) && isScheduleEligible(settings.schedule) && isFrequencyEligible(exp.experienceId, settings.frequency, storage) && evaluateAudience(settings.audience, user, membershipMap) && isTriggerCompatible(settings.trigger);
638
+ });
639
+ return sortByPriorityDescThenStable(passing);
640
+ }
641
+
642
+ // src/platform/auth.ts
643
+ function normalizeApiKey(apiKey) {
644
+ return String(apiKey ?? "").trim();
645
+ }
646
+ function getApiKeyFingerprint(apiKey) {
647
+ const key3 = normalizeApiKey(apiKey);
648
+ if (!key3) return "no-key";
649
+ return `key-${key3.length}-${key3.slice(-6)}`;
650
+ }
651
+ function getSdkAuthHeaders(apiKey) {
652
+ const key3 = normalizeApiKey(apiKey);
653
+ if (!key3) return {};
654
+ return {
655
+ Authorization: `Bearer ${key3}`
656
+ };
657
+ }
658
+
659
+ // src/platform/fetcher.ts
660
+ var CONFIG_TTL_MS = 5 * 60 * 1e3;
661
+ var configCache = /* @__PURE__ */ new Map();
662
+ var inflight = /* @__PURE__ */ new Map();
663
+ function cacheKey(apiUrl, agentId, apiKey) {
664
+ return `${apiUrl.replace(/\/$/, "")}::${agentId}::${getApiKeyFingerprint(apiKey)}`;
665
+ }
666
+ function extractAgentRecord(json) {
667
+ if (!json || typeof json !== "object") return null;
668
+ const o = json;
669
+ const data = o.data;
670
+ if (data?.agent && typeof data.agent === "object") {
671
+ return data.agent;
672
+ }
673
+ if (o.agent && typeof o.agent === "object") {
674
+ return o.agent;
675
+ }
676
+ if ("issues_config" in o || "feedback_config" in o) {
677
+ return o;
678
+ }
679
+ return null;
680
+ }
681
+ function normalizeWidgetUiFromAgent(agent) {
682
+ const nested = agent.widget_ui ?? agent.widgetUi;
683
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
684
+ const o = nested;
685
+ const c = o.primary_color ?? o.primaryColor;
686
+ if (typeof c === "string" && c.trim()) {
687
+ return { primaryColor: c.trim() };
688
+ }
689
+ }
690
+ const top = agent.primary_color ?? agent.primaryColor;
691
+ if (typeof top === "string" && top.trim()) {
692
+ return { primaryColor: top.trim() };
693
+ }
694
+ return null;
695
+ }
696
+ function extractExperiencesFromResponse(json) {
697
+ if (!json || typeof json !== "object") return [];
698
+ const data = json.data;
699
+ const raw = data?.experiences;
700
+ if (!Array.isArray(raw)) return [];
701
+ return raw.filter((x) => x != null && typeof x === "object");
702
+ }
703
+ async function fetchSdkAgentConfig(apiUrl, agentId, apiKey) {
704
+ const key3 = cacheKey(apiUrl, agentId, apiKey);
705
+ const now = Date.now();
706
+ const cached = configCache.get(key3);
707
+ if (cached && cached.expiresAt > now) {
708
+ return cached.value;
709
+ }
710
+ if (cached && cached.expiresAt <= now) {
711
+ void fetchSdkAgentConfigFresh(apiUrl, agentId, apiKey).catch(() => {
712
+ });
713
+ return cached.value;
714
+ }
715
+ return fetchSdkAgentConfigFresh(apiUrl, agentId, apiKey);
716
+ }
717
+ async function fetchSdkAgentConfigFresh(apiUrl, agentId, apiKey) {
718
+ const key3 = cacheKey(apiUrl, agentId, apiKey);
719
+ const existing = inflight.get(key3);
720
+ if (existing) return existing;
721
+ const req = (async () => {
722
+ const url = `${apiUrl.replace(/\/$/, "")}/agents/${agentId}/config`;
723
+ const res = await fetch(url, {
724
+ headers: getSdkAuthHeaders(apiKey)
725
+ });
726
+ if (!res.ok) throw new Error(`Failed to load config: ${res.status}`);
727
+ const json = await res.json();
728
+ const experiences = extractExperiencesFromResponse(json);
729
+ const agent = extractAgentRecord(json);
730
+ if (!agent) {
731
+ const value2 = experiences.length ? { experiences } : {};
732
+ configCache.set(key3, { value: value2, expiresAt: Date.now() + CONFIG_TTL_MS });
733
+ return value2;
734
+ }
735
+ const value = {
736
+ issues_config: agent.issues_config,
737
+ feedback_config: agent.feedback_config,
738
+ experiences,
739
+ widget_ui: normalizeWidgetUiFromAgent(agent)
740
+ };
741
+ configCache.set(key3, { value, expiresAt: Date.now() + CONFIG_TTL_MS });
742
+ return value;
743
+ })();
744
+ inflight.set(key3, req);
745
+ try {
746
+ return await req;
747
+ } finally {
748
+ inflight.delete(key3);
749
+ }
750
+ }
751
+ function clearSdkAgentConfigCache(apiUrl, agentId, apiKey) {
752
+ if (apiUrl && agentId) {
753
+ if (apiKey != null) {
754
+ configCache.delete(cacheKey(apiUrl, agentId, apiKey));
755
+ return;
756
+ }
757
+ const prefix = `${apiUrl.replace(/\/$/, "")}::${agentId}::`;
758
+ for (const key3 of configCache.keys()) {
759
+ if (key3.startsWith(prefix)) configCache.delete(key3);
760
+ }
761
+ return;
762
+ }
763
+ configCache.clear();
764
+ }
765
+
766
+ // src/platform/firstflow-public-api-url.ts
767
+ var FIRSTFLOW_PUBLIC_API_BASE_URL = "http://localhost:3002";
768
+
769
+ // src/feedback/feedbackNormalizer.ts
770
+ var DEFAULT_TAGS = ["Insightful", "Actionable", "Clear"];
771
+ var DEFAULT_PLACEHOLDER = "Anything else you'd like to add? (optional)";
772
+ var DEFAULT_CONFIRMATION_MESSAGE = "Thanks for your feedback!";
773
+ function normalizeSide(raw) {
774
+ const tags = Array.isArray(raw?.tags) && raw.tags.length > 0 ? [...new Set(raw.tags.map((t) => String(t).trim()).filter(Boolean))] : [...DEFAULT_TAGS];
775
+ return {
776
+ tags,
777
+ placeholder: typeof raw?.placeholder === "string" && raw.placeholder.trim() ? raw.placeholder.trim() : DEFAULT_PLACEHOLDER,
778
+ showTags: raw?.showTags === true,
779
+ showComment: raw?.showComment === true,
780
+ confirmationMessage: typeof raw?.confirmationMessage === "string" && raw.confirmationMessage.trim() ? raw.confirmationMessage.trim() : DEFAULT_CONFIRMATION_MESSAGE
781
+ };
782
+ }
783
+ function normalizeFeedbackConfig(raw) {
784
+ const enabled = raw?.enabled === true;
785
+ return {
786
+ enabled,
787
+ like: normalizeSide(raw?.like),
788
+ dislike: normalizeSide(raw?.dislike)
789
+ };
790
+ }
791
+ function defaultFeedbackConfig() {
792
+ return normalizeFeedbackConfig({ enabled: false });
793
+ }
794
+
795
+ // src/feedback/MessageFeedback.ts
796
+ var METADATA_MAX_BYTES = 32768;
797
+ function normalizeMetadata(meta) {
798
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
799
+ if (Object.keys(meta).length === 0) return void 0;
800
+ try {
801
+ const s = JSON.stringify(meta);
802
+ if (s.length > METADATA_MAX_BYTES) {
803
+ console.warn(
804
+ `[Firstflow] feedback metadata omitted (serialized length ${s.length} > ${METADATA_MAX_BYTES})`
805
+ );
806
+ return void 0;
807
+ }
808
+ return JSON.parse(s);
809
+ } catch {
810
+ console.warn("[Firstflow] feedback metadata must be JSON-serializable; omitted");
811
+ return void 0;
812
+ }
813
+ }
814
+ function buildAnalyticsFeedbackPayload(payload) {
815
+ const payloadObj = {
816
+ conversation_id: payload.conversationId.trim(),
817
+ message_id: payload.messageId.trim(),
818
+ rating: payload.rating,
819
+ submitted_at: (/* @__PURE__ */ new Date()).toISOString()
820
+ };
821
+ if (payload.comment != null && String(payload.comment).trim()) {
822
+ payloadObj.comment = String(payload.comment).trim();
823
+ }
824
+ if (payload.messagePreview != null && String(payload.messagePreview).trim()) {
825
+ payloadObj.message_preview = String(payload.messagePreview).trim().slice(0, 2e3);
826
+ }
827
+ if (payload.tags != null && payload.tags.length > 0) {
828
+ payloadObj.tags = payload.tags;
829
+ }
830
+ if (typeof payload.showTags === "boolean") {
831
+ payloadObj.showTags = payload.showTags;
832
+ }
833
+ if (typeof payload.showComment === "boolean") {
834
+ payloadObj.showComment = payload.showComment;
835
+ }
836
+ const metadata = normalizeMetadata(payload.metadata) ?? {};
837
+ return {
838
+ payload: JSON.stringify(payloadObj),
839
+ metadata: JSON.stringify(metadata),
840
+ rating: payload.rating
841
+ // top-level for ClickHouse summary queries
842
+ };
843
+ }
844
+ function createMessageFeedback(options) {
845
+ const { agentId, analytics: analyticsOpt, config: rawConfig } = options;
846
+ const analytics = analyticsOpt ?? createAnalytics({ agentId });
847
+ let config = normalizeFeedbackConfig(rawConfig ?? void 0);
848
+ return {
849
+ getConfig() {
850
+ return config;
851
+ },
852
+ setConfig(next) {
853
+ config = next;
854
+ },
855
+ isEnabled() {
856
+ return config.enabled === true;
857
+ },
858
+ async submit(payload) {
859
+ const cid = payload.conversationId?.trim();
860
+ const mid = payload.messageId?.trim();
861
+ if (!cid) throw new Error("conversationId is required");
862
+ if (!mid) throw new Error("messageId is required");
863
+ if (payload.rating !== "like" && payload.rating !== "dislike") {
864
+ throw new Error('rating must be "like" or "dislike"');
865
+ }
866
+ const sideCfg = payload.rating === "like" ? config.like : config.dislike;
867
+ const enforcedPayload = {
868
+ ...payload,
869
+ conversationId: cid,
870
+ messageId: mid,
871
+ showTags: sideCfg.showTags,
872
+ showComment: sideCfg.showComment,
873
+ tags: sideCfg.showTags ? payload.tags : void 0,
874
+ comment: sideCfg.showComment ? payload.comment : void 0
875
+ };
876
+ analytics.track(FEEDBACK_SUBMITTED, buildAnalyticsFeedbackPayload(enforcedPayload));
877
+ },
878
+ destroy() {
879
+ config = defaultFeedbackConfig();
880
+ }
881
+ };
882
+ }
883
+
884
+ // src/survey/surveyNormalizer.ts
885
+ var SURVEY_NODE_TYPES = /* @__PURE__ */ new Set([
886
+ "nps",
887
+ "multiple_choice",
888
+ "slider",
889
+ "opinion_scale",
890
+ "open_question",
891
+ "concept_test",
892
+ "interview_prompt",
893
+ "text_block",
894
+ "thank_you"
895
+ ]);
896
+ function asObject(value) {
897
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
898
+ return value;
899
+ }
900
+ function asString(value) {
901
+ return typeof value === "string" ? value : void 0;
902
+ }
903
+ function asNumber(value) {
904
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
905
+ }
906
+ function asStringArray(value) {
907
+ if (!Array.isArray(value)) return void 0;
908
+ const out = value.map((x) => String(x).trim()).filter(Boolean);
909
+ return out.length ? out : void 0;
910
+ }
911
+ function isRequired(type, config) {
912
+ if (type === "text_block" || type === "thank_you") return false;
913
+ return config.questionRequired === true || config.required === true;
914
+ }
915
+ function mapQuestion(node) {
916
+ const typeRaw = typeof node.type === "string" ? node.type : "";
917
+ if (!SURVEY_NODE_TYPES.has(typeRaw)) return null;
918
+ const type = typeRaw;
919
+ const config = asObject(node.data?.config) ?? {};
920
+ const id = asString(config.id) ?? asString(node.id) ?? "";
921
+ if (!id) return null;
922
+ const questionText = asString(config.questionText)?.trim() || (type === "text_block" ? "Text block" : type === "thank_you" ? "Thank you" : "");
923
+ return {
924
+ id,
925
+ nodeId: asString(node.id) ?? id,
926
+ type,
927
+ questionText,
928
+ required: isRequired(type, config),
929
+ options: asStringArray(config.surveyOptions),
930
+ minValue: asNumber(config.surveyMinValue),
931
+ maxValue: asNumber(config.surveyMaxValue),
932
+ minLabel: asString(config.surveyMinLabel),
933
+ maxLabel: asString(config.surveyMaxLabel),
934
+ step: asNumber(config.surveyStep),
935
+ scaleType: asString(config.surveyScaleType),
936
+ placeholder: asString(config.surveyPlaceholder),
937
+ multiline: config.surveyMultiline === true,
938
+ showConfetti: type === "thank_you" ? typeof config.showConfetti === "boolean" ? config.showConfetti : void 0 : void 0
939
+ };
940
+ }
941
+ function mapQuestionFromWorkflowOrLegacyNode(raw) {
942
+ const node = asObject(raw);
943
+ if (!node) return null;
944
+ const nodeId = asString(node.id)?.trim();
945
+ if (!nodeId) return null;
946
+ const data = asObject(node.data);
947
+ const icon = data && typeof data.icon === "string" ? data.icon.toLowerCase() : "";
948
+ const nodeTypeRaw = typeof node.type === "string" ? node.type.toLowerCase() : "";
949
+ let type = null;
950
+ if (icon === "survey") {
951
+ return null;
952
+ }
953
+ if (icon && SURVEY_NODE_TYPES.has(icon)) {
954
+ type = icon;
955
+ } else if (nodeTypeRaw && SURVEY_NODE_TYPES.has(nodeTypeRaw)) {
956
+ type = nodeTypeRaw;
957
+ }
958
+ if (!type) return null;
959
+ const config = asObject(data?.config) ?? {};
960
+ const id = asString(config.id)?.trim() ?? nodeId;
961
+ const questionText = asString(config.questionText)?.trim() || (type === "text_block" ? "Text block" : type === "thank_you" ? "Thank you" : "");
962
+ return {
963
+ id,
964
+ nodeId,
965
+ type,
966
+ questionText,
967
+ required: isRequired(type, config),
968
+ options: asStringArray(config.surveyOptions),
969
+ minValue: asNumber(config.surveyMinValue),
970
+ maxValue: asNumber(config.surveyMaxValue),
971
+ minLabel: asString(config.surveyMinLabel),
972
+ maxLabel: asString(config.surveyMaxLabel),
973
+ step: asNumber(config.surveyStep),
974
+ scaleType: asString(config.surveyScaleType),
975
+ placeholder: asString(config.surveyPlaceholder),
976
+ multiline: config.surveyMultiline === true,
977
+ showConfetti: type === "thank_you" ? typeof config.showConfetti === "boolean" ? config.showConfetti : void 0 : void 0
978
+ };
979
+ }
980
+ function isSurveyWorkflowNode(node) {
981
+ const data = asObject(node.data);
982
+ if (!data) return false;
983
+ const icon = typeof data.icon === "string" ? data.icon.toLowerCase() : "";
984
+ const title = typeof data.title === "string" ? data.title.toLowerCase() : "";
985
+ const config = asObject(data.config);
986
+ const hasList = Array.isArray(config?.surveyQuestions) && (config?.surveyQuestions).length > 0;
987
+ return icon === "survey" || title === "survey" || hasList;
988
+ }
989
+ function mapDashboardSurveyQuestion(raw, parentNodeId) {
990
+ const q = asObject(raw);
991
+ if (!q) return null;
992
+ const typeRaw = asString(q.type) ?? "";
993
+ if (!SURVEY_NODE_TYPES.has(typeRaw)) return null;
994
+ const type = typeRaw;
995
+ const id = asString(q.id) ?? "";
996
+ if (!id) return null;
997
+ const questionText = asString(q.questionText)?.trim() || (type === "text_block" ? "Text block" : type === "thank_you" ? "Thank you" : "");
998
+ const required = type === "text_block" || type === "thank_you" ? false : q.required === true || q.questionRequired === true;
999
+ return {
1000
+ id,
1001
+ nodeId: parentNodeId,
1002
+ type,
1003
+ questionText,
1004
+ required,
1005
+ options: asStringArray(q.options) ?? asStringArray(q.surveyOptions),
1006
+ minValue: asNumber(q.minValue) ?? asNumber(q.surveyMinValue),
1007
+ maxValue: asNumber(q.maxValue) ?? asNumber(q.surveyMaxValue),
1008
+ minLabel: asString(q.minLabel) ?? asString(q.surveyMinLabel),
1009
+ maxLabel: asString(q.maxLabel) ?? asString(q.surveyMaxLabel),
1010
+ step: asNumber(q.step) ?? asNumber(q.surveyStep),
1011
+ scaleType: asString(q.scaleType) ?? asString(q.surveyScaleType),
1012
+ placeholder: asString(q.placeholder) ?? asString(q.surveyPlaceholder),
1013
+ multiline: q.multiline === true || q.surveyMultiline === true,
1014
+ showConfetti: type === "thank_you" ? typeof q.showConfetti === "boolean" ? q.showConfetti : void 0 : void 0
1015
+ };
1016
+ }
1017
+ function extractSurveyQuestionsFromRuntimeFlow(flow) {
1018
+ if (!flow) return [];
1019
+ const nodesRaw = Array.isArray(flow.nodes) ? flow.nodes : [];
1020
+ for (const raw of nodesRaw) {
1021
+ const node = asObject(raw);
1022
+ if (!node) continue;
1023
+ if (!isSurveyWorkflowNode(node)) continue;
1024
+ const nodeId = asString(node.id);
1025
+ if (!nodeId) continue;
1026
+ const data = asObject(node.data);
1027
+ const config = asObject(data?.config);
1028
+ const list = Array.isArray(config?.surveyQuestions) ? config?.surveyQuestions : [];
1029
+ const batch = [];
1030
+ for (const item of list) {
1031
+ const mapped = mapDashboardSurveyQuestion(item, nodeId);
1032
+ if (mapped) batch.push(mapped);
1033
+ }
1034
+ if (batch.length > 0) return batch;
1035
+ }
1036
+ return [];
1037
+ }
1038
+ function normalizeSurveyExperiences(experiences) {
1039
+ if (!Array.isArray(experiences)) return [];
1040
+ const out = [];
1041
+ for (const raw of experiences) {
1042
+ const exp = asObject(raw);
1043
+ if (!exp) continue;
1044
+ if (asString(exp.type) !== "survey") continue;
1045
+ const experienceId = asString(exp.id)?.trim();
1046
+ if (!experienceId) continue;
1047
+ const flow = asObject(exp.flow);
1048
+ const fromSurveyNode = extractSurveyQuestionsFromRuntimeFlow(flow);
1049
+ const nodesRaw = Array.isArray(flow?.nodes) ? flow?.nodes : [];
1050
+ const fromWorkflowChain = nodesRaw.map((node) => mapQuestionFromWorkflowOrLegacyNode(node)).filter((q) => q != null);
1051
+ const questions = fromSurveyNode.length > 0 ? fromSurveyNode : fromWorkflowChain.length > 0 ? fromWorkflowChain : nodesRaw.map((node) => mapQuestion(asObject(node) ?? {})).filter((q) => q != null);
1052
+ out.push({
1053
+ experienceId,
1054
+ name: asString(exp.name) ?? null,
1055
+ status: asString(exp.status) ?? null,
1056
+ settings: asObject(exp.settings) ?? null,
1057
+ questions
1058
+ });
1059
+ }
1060
+ return out;
1061
+ }
1062
+
1063
+ // src/survey/Survey.ts
1064
+ var METADATA_MAX_BYTES2 = 32768;
1065
+ function normalizeMetadata2(meta) {
1066
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
1067
+ if (Object.keys(meta).length === 0) return void 0;
1068
+ try {
1069
+ const s = JSON.stringify(meta);
1070
+ if (s.length > METADATA_MAX_BYTES2) return void 0;
1071
+ return JSON.parse(s);
1072
+ } catch {
1073
+ return void 0;
1074
+ }
1075
+ }
1076
+ function createSurveyModule(options) {
1077
+ const { agentId, analytics: analyticsOpt, experiences } = options;
1078
+ const analytics = analyticsOpt ?? createAnalytics({ agentId });
1079
+ const storage = createStorageAdapter();
1080
+ const applyAll = (raw) => runEligibilityFilter(normalizeSurveyExperiences(raw ?? void 0), storage);
1081
+ let surveys = applyAll(experiences ?? void 0);
1082
+ return {
1083
+ getSurveys() {
1084
+ return surveys;
1085
+ },
1086
+ getSurvey(experienceId) {
1087
+ return surveys.find((s) => s.experienceId === experienceId) ?? null;
1088
+ },
1089
+ setExperiences(nextExperiences) {
1090
+ surveys = applyAll(nextExperiences);
1091
+ },
1092
+ isEnabled(experienceId) {
1093
+ if (!experienceId) return surveys.length > 0;
1094
+ return surveys.some((s) => s.experienceId === experienceId);
1095
+ },
1096
+ async submit(payload) {
1097
+ const expId = String(payload.experienceId ?? "").trim();
1098
+ if (!expId) throw new Error("experienceId is required");
1099
+ const metadata = normalizeMetadata2(payload.metadata) ?? {};
1100
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1101
+ analytics.track(SURVEY_COMPLETED, {
1102
+ agent_id: agentId,
1103
+ experience_id: expId,
1104
+ survey_id: expId,
1105
+ conversation_id: payload.conversationId?.trim() || void 0,
1106
+ submitted_at: completedAt,
1107
+ payload: JSON.stringify({
1108
+ experience_id: expId,
1109
+ survey_id: expId,
1110
+ conversation_id: payload.conversationId?.trim() || void 0,
1111
+ completed_at: completedAt,
1112
+ answers: payload.answers ?? {}
1113
+ }),
1114
+ metadata: JSON.stringify(metadata)
1115
+ });
1116
+ },
1117
+ destroy() {
1118
+ surveys = [];
1119
+ }
1120
+ };
1121
+ }
1122
+
1123
+ // src/platform/Firstflow.ts
1124
+ import deepEqual from "fast-deep-equal";
1125
+
1126
+ // src/platform/overrides.ts
1127
+ var OVERRIDES_TTL_MS = 6e4;
1128
+ var cache = /* @__PURE__ */ new Map();
1129
+ var inflight2 = /* @__PURE__ */ new Map();
1130
+ var EMPTY_OVERRIDES = {
1131
+ blockedExperiences: [],
1132
+ forcedExperiences: []
1133
+ };
1134
+ function key(apiUrl, agentId, userId, apiKey) {
1135
+ return `${apiUrl.replace(/\/$/, "")}::${agentId}::${userId}::${getApiKeyFingerprint(apiKey)}`;
1136
+ }
1137
+ function normalizeArray(value) {
1138
+ if (!Array.isArray(value)) return [];
1139
+ return value.filter((v) => typeof v === "string" && v.trim().length > 0);
1140
+ }
1141
+ function parseOverrides(json) {
1142
+ if (!json || typeof json !== "object") return EMPTY_OVERRIDES;
1143
+ const root = json;
1144
+ const data = root.data && typeof root.data === "object" ? root.data : root;
1145
+ return {
1146
+ blockedExperiences: normalizeArray(
1147
+ data.blockedExperiences ?? data.blocked_experiences
1148
+ ),
1149
+ forcedExperiences: normalizeArray(
1150
+ data.forcedExperiences ?? data.forced_experiences
1151
+ )
1152
+ };
1153
+ }
1154
+ async function fetchAudienceOverrides(apiUrl, agentId, userId, apiKey) {
1155
+ const uid = String(userId ?? "").trim();
1156
+ if (!uid) return EMPTY_OVERRIDES;
1157
+ const k = key(apiUrl, agentId, uid, apiKey);
1158
+ const now = Date.now();
1159
+ const cached = cache.get(k);
1160
+ if (cached && cached.expiresAt > now) return cached.value;
1161
+ if (cached && cached.expiresAt <= now) {
1162
+ void fetchAudienceOverridesFresh(apiUrl, agentId, uid, apiKey).catch(
1163
+ () => {
1164
+ }
1165
+ );
1166
+ return cached.value;
1167
+ }
1168
+ return fetchAudienceOverridesFresh(apiUrl, agentId, uid, apiKey);
1169
+ }
1170
+ async function fetchAudienceOverridesFresh(apiUrl, agentId, userId, apiKey) {
1171
+ const uid = String(userId).trim();
1172
+ if (!uid) return EMPTY_OVERRIDES;
1173
+ const k = key(apiUrl, agentId, uid, apiKey);
1174
+ const existing = inflight2.get(k);
1175
+ if (existing) return existing;
1176
+ const req = (async () => {
1177
+ try {
1178
+ const base = apiUrl.replace(/\/$/, "");
1179
+ const url = `${base}/agents/${agentId}/overrides?userId=${encodeURIComponent(uid)}`;
1180
+ const res = await fetch(url, {
1181
+ headers: getSdkAuthHeaders(apiKey)
1182
+ });
1183
+ if (!res.ok) throw new Error(`Failed to load overrides: ${res.status}`);
1184
+ const json = await res.json();
1185
+ const value = parseOverrides(json);
1186
+ cache.set(k, { value, expiresAt: Date.now() + OVERRIDES_TTL_MS });
1187
+ return value;
1188
+ } catch {
1189
+ return cache.get(k)?.value ?? EMPTY_OVERRIDES;
1190
+ }
1191
+ })();
1192
+ inflight2.set(k, req);
1193
+ try {
1194
+ return await req;
1195
+ } finally {
1196
+ inflight2.delete(k);
1197
+ }
1198
+ }
1199
+ function clearAudienceOverridesCache(apiUrl, agentId, userId, apiKey) {
1200
+ if (apiUrl && agentId && userId) {
1201
+ if (apiKey != null) {
1202
+ cache.delete(key(apiUrl, agentId, userId, apiKey));
1203
+ return;
1204
+ }
1205
+ const prefix = `${apiUrl.replace(/\/$/, "")}::${agentId}::${userId}::`;
1206
+ for (const entryKey of cache.keys()) {
1207
+ if (entryKey.startsWith(prefix)) cache.delete(entryKey);
1208
+ }
1209
+ return;
1210
+ }
1211
+ cache.clear();
1212
+ }
1213
+
1214
+ // src/platform/membership.ts
1215
+ var MEMBERSHIP_TTL_MS = 6e4;
1216
+ var cache2 = /* @__PURE__ */ new Map();
1217
+ var inflight3 = /* @__PURE__ */ new Map();
1218
+ function key2(apiUrl, agentId, userId, apiKey) {
1219
+ return `${apiUrl.replace(/\/$/, "")}::${agentId}::${userId}::${getApiKeyFingerprint(apiKey)}`;
1220
+ }
1221
+ function parseMembership(json) {
1222
+ if (!json || typeof json !== "object") return {};
1223
+ const root = json;
1224
+ const data = root.data && typeof root.data === "object" ? root.data : root;
1225
+ const out = {};
1226
+ for (const [segmentId, value] of Object.entries(data)) {
1227
+ const normalizedId = String(segmentId ?? "").trim();
1228
+ if (!normalizedId) continue;
1229
+ out[normalizedId] = value === true;
1230
+ }
1231
+ return out;
1232
+ }
1233
+ async function fetchAudienceMembership(apiUrl, agentId, userId, apiKey) {
1234
+ const uid = String(userId ?? "").trim();
1235
+ if (!uid) return {};
1236
+ const k = key2(apiUrl, agentId, uid, apiKey);
1237
+ const now = Date.now();
1238
+ const cached = cache2.get(k);
1239
+ if (cached && cached.expiresAt > now) return cached.value;
1240
+ if (cached && cached.expiresAt <= now) {
1241
+ void fetchAudienceMembershipFresh(apiUrl, agentId, uid, apiKey).catch(
1242
+ () => {
1243
+ }
1244
+ );
1245
+ return cached.value;
1246
+ }
1247
+ return fetchAudienceMembershipFresh(apiUrl, agentId, uid, apiKey);
1248
+ }
1249
+ async function fetchAudienceMembershipFresh(apiUrl, agentId, userId, apiKey) {
1250
+ const uid = String(userId).trim();
1251
+ if (!uid) return {};
1252
+ const k = key2(apiUrl, agentId, uid, apiKey);
1253
+ const existing = inflight3.get(k);
1254
+ if (existing) return existing;
1255
+ const req = (async () => {
1256
+ try {
1257
+ const base = apiUrl.replace(/\/$/, "");
1258
+ const url = `${base}/agents/${agentId}/audience-membership?userId=${encodeURIComponent(uid)}`;
1259
+ const res = await fetch(url, {
1260
+ headers: getSdkAuthHeaders(apiKey)
1261
+ });
1262
+ if (!res.ok) throw new Error(`Failed to load audience membership: ${res.status}`);
1263
+ const json = await res.json();
1264
+ const value = parseMembership(json);
1265
+ cache2.set(k, { value, expiresAt: Date.now() + MEMBERSHIP_TTL_MS });
1266
+ return value;
1267
+ } catch {
1268
+ return cache2.get(k)?.value ?? {};
1269
+ }
1270
+ })();
1271
+ inflight3.set(k, req);
1272
+ try {
1273
+ return await req;
1274
+ } finally {
1275
+ inflight3.delete(k);
1276
+ }
1277
+ }
1278
+ function clearAudienceMembershipCache(apiUrl, agentId, userId, apiKey) {
1279
+ if (apiUrl && agentId && userId) {
1280
+ if (apiKey != null) {
1281
+ cache2.delete(key2(apiUrl, agentId, userId, apiKey));
1282
+ return;
1283
+ }
1284
+ const prefix = `${apiUrl.replace(/\/$/, "")}::${agentId}::${userId}::`;
1285
+ for (const entryKey of cache2.keys()) {
1286
+ if (entryKey.startsWith(prefix)) cache2.delete(entryKey);
1287
+ }
1288
+ return;
1289
+ }
1290
+ cache2.clear();
1291
+ }
1292
+
1293
+ // src/experience/experienceNormalizer.ts
1294
+ function asObject2(value) {
1295
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1296
+ return value;
1297
+ }
1298
+ function asString2(value) {
1299
+ return typeof value === "string" ? value : void 0;
1300
+ }
1301
+ function normalizeAudience(value) {
1302
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1303
+ return { type: "all" };
1304
+ }
1305
+ const raw = value;
1306
+ const t = asString2(raw.type);
1307
+ if (t === "all") return { type: "all" };
1308
+ if (t === "segment") {
1309
+ return {
1310
+ type: "segment",
1311
+ segment_id: asString2(raw.segment_id) ?? void 0
1312
+ };
1313
+ }
1314
+ if (t === "rules") {
1315
+ const op = raw.operator === "OR" ? "OR" : "AND";
1316
+ const allowedOps = /* @__PURE__ */ new Set([
1317
+ "equals",
1318
+ "not_equals",
1319
+ "contains",
1320
+ "not_contains",
1321
+ "in",
1322
+ "not_in",
1323
+ "gt",
1324
+ "gte",
1325
+ "lt",
1326
+ "lte",
1327
+ "exists",
1328
+ "not_exists"
1329
+ ]);
1330
+ const rules = Array.isArray(raw.rules) ? raw.rules.filter((r) => {
1331
+ if (!r || typeof r !== "object") return false;
1332
+ const rr = r;
1333
+ return typeof rr.trait === "string" && typeof rr.operator === "string" && allowedOps.has(rr.operator);
1334
+ }) : [];
1335
+ return {
1336
+ type: "rules",
1337
+ operator: op,
1338
+ rules: rules.map((r) => ({
1339
+ trait: r.trait,
1340
+ operator: r.operator,
1341
+ value: r.value
1342
+ }))
1343
+ };
1344
+ }
1345
+ return { type: "all" };
1346
+ }
1347
+ function normalizeSettings(value) {
1348
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1349
+ const raw = value;
1350
+ return {
1351
+ ...EXPERIENCE_SETTINGS_DEFAULT,
1352
+ ...raw,
1353
+ audience: normalizeAudience(raw.audience)
1354
+ };
1355
+ }
1356
+ function normalizeExperienceRows(experiences) {
1357
+ if (!Array.isArray(experiences)) return [];
1358
+ const out = [];
1359
+ for (const raw of experiences) {
1360
+ const exp = asObject2(raw);
1361
+ if (!exp) continue;
1362
+ const experienceId = asString2(exp.id)?.trim();
1363
+ if (!experienceId) continue;
1364
+ out.push({
1365
+ experienceId,
1366
+ name: asString2(exp.name) ?? null,
1367
+ status: asString2(exp.status) ?? null,
1368
+ type: asString2(exp.type) ?? null,
1369
+ settings: normalizeSettings(exp.settings),
1370
+ flow: asObject2(exp.flow) ?? null
1371
+ });
1372
+ }
1373
+ return out;
1374
+ }
1375
+
1376
+ // src/platform/Firstflow.ts
1377
+ var internalIssueMap = /* @__PURE__ */ new WeakMap();
1378
+ var internalFeedbackMap = /* @__PURE__ */ new WeakMap();
1379
+ var internalSurveyMap = /* @__PURE__ */ new WeakMap();
1380
+ var internalRuntimeMap = /* @__PURE__ */ new WeakMap();
1381
+ var EMPTY_OVERRIDES2 = {
1382
+ blockedExperiences: [],
1383
+ forcedExperiences: []
1384
+ };
1385
+ function normalizeAudienceMembership(map) {
1386
+ if (!map) return {};
1387
+ const next = {};
1388
+ for (const [segmentId, value] of Object.entries(map)) {
1389
+ const normalizedId = String(segmentId ?? "").trim();
1390
+ if (!normalizedId) continue;
1391
+ next[normalizedId] = value === true;
1392
+ }
1393
+ return next;
1394
+ }
1395
+ function createIssueFacade(inner) {
1396
+ return {
1397
+ open(options) {
1398
+ inner.open(options);
1399
+ },
1400
+ async submit(data) {
1401
+ await inner.reportIssue(data);
1402
+ },
1403
+ getConfig() {
1404
+ return inner.getConfig();
1405
+ },
1406
+ destroy() {
1407
+ inner.destroy();
1408
+ }
1409
+ };
1410
+ }
1411
+ function createFeedbackFacade(inner) {
1412
+ return {
1413
+ submit(payload) {
1414
+ return inner.submit(payload);
1415
+ },
1416
+ getConfig() {
1417
+ return inner.getConfig();
1418
+ },
1419
+ isEnabled() {
1420
+ return inner.isEnabled();
1421
+ }
1422
+ };
1423
+ }
1424
+ function getInternalIssueReporter(firstflow) {
1425
+ const inner = internalIssueMap.get(firstflow);
1426
+ if (!inner) throw new Error("Invalid Firstflow instance");
1427
+ return inner;
1428
+ }
1429
+ function getInternalMessageFeedback(firstflow) {
1430
+ const inner = internalFeedbackMap.get(firstflow);
1431
+ if (!inner) throw new Error("Invalid Firstflow instance");
1432
+ return inner;
1433
+ }
1434
+ function getInternalSurvey(firstflow) {
1435
+ const inner = internalSurveyMap.get(firstflow);
1436
+ if (!inner) throw new Error("Invalid Firstflow instance");
1437
+ return inner;
1438
+ }
1439
+ function getInternalRuntimeState(firstflow) {
1440
+ const inner = internalRuntimeMap.get(firstflow);
1441
+ if (!inner) throw new Error("Invalid Firstflow instance");
1442
+ return inner;
1443
+ }
1444
+ function subscribeInternalRuntime(firstflow, listener) {
1445
+ const state = getInternalRuntimeState(firstflow);
1446
+ state.listeners.add(listener);
1447
+ return () => {
1448
+ state.listeners.delete(listener);
1449
+ };
1450
+ }
1451
+ function getRuntimeSnapshot(firstflow) {
1452
+ const state = getInternalRuntimeState(firstflow);
1453
+ const cached = state.runtimeSnapshotCache;
1454
+ if (cached && cached.version === state.version) {
1455
+ return cached.snapshot;
1456
+ }
1457
+ const snapshot = {
1458
+ version: state.version,
1459
+ user: state.user,
1460
+ audienceMembership: { ...state.audienceMembership },
1461
+ overrides: {
1462
+ blockedExperiences: [...state.overrides.blockedExperiences],
1463
+ forcedExperiences: [...state.overrides.forcedExperiences]
1464
+ },
1465
+ experienceRows: state.experienceRows
1466
+ };
1467
+ state.runtimeSnapshotCache = { version: state.version, snapshot };
1468
+ return snapshot;
1469
+ }
1470
+ function notifyRuntime(state) {
1471
+ state.version += 1;
1472
+ for (const listener of state.listeners) listener();
1473
+ }
1474
+ function syncExperiencesFromRaw(firstflow, rawExperiences, options = {}) {
1475
+ const runtime = getInternalRuntimeState(firstflow);
1476
+ const survey = getInternalSurvey(firstflow);
1477
+ const nextRows = normalizeExperienceRows(rawExperiences);
1478
+ if (deepEqual(nextRows, runtime.experienceRows)) return false;
1479
+ runtime.experienceRows = nextRows;
1480
+ survey.setExperiences(rawExperiences);
1481
+ if (options.notify !== false) notifyRuntime(runtime);
1482
+ return true;
1483
+ }
1484
+ function applySdkAgentPayloadInternal(firstflow, sdkPayload, options = {}) {
1485
+ const runtime = getInternalRuntimeState(firstflow);
1486
+ const issue = getInternalIssueReporter(firstflow);
1487
+ const feedback = getInternalMessageFeedback(firstflow);
1488
+ const nextIssueConfig = normalizeConfig(
1489
+ firstflow.agentId,
1490
+ firstflow.apiUrl,
1491
+ sdkPayload.issues_config
1492
+ );
1493
+ const nextFeedbackConfig = normalizeFeedbackConfig(
1494
+ sdkPayload.feedback_config
1495
+ );
1496
+ let didChange = false;
1497
+ if (!deepEqual(nextIssueConfig, runtime.issueConfigSnapshot)) {
1498
+ runtime.issueConfigSnapshot = nextIssueConfig;
1499
+ issue.setConfig(nextIssueConfig);
1500
+ didChange = true;
1501
+ }
1502
+ if (!deepEqual(nextFeedbackConfig, runtime.feedbackConfigSnapshot)) {
1503
+ runtime.feedbackConfigSnapshot = nextFeedbackConfig;
1504
+ feedback.setConfig(nextFeedbackConfig);
1505
+ didChange = true;
1506
+ }
1507
+ if (syncExperiencesFromRaw(firstflow, sdkPayload.experiences, { notify: false })) {
1508
+ didChange = true;
1509
+ }
1510
+ const nextWidgetUi = sdkPayload.widget_ui ?? null;
1511
+ if (!deepEqual(nextWidgetUi, runtime.widgetUi)) {
1512
+ runtime.widgetUi = nextWidgetUi;
1513
+ didChange = true;
1514
+ }
1515
+ if (didChange && options.notify !== false) {
1516
+ notifyRuntime(runtime);
1517
+ }
1518
+ return didChange;
1519
+ }
1520
+ function applySdkAgentPayload(firstflow, sdkPayload) {
1521
+ applySdkAgentPayloadInternal(firstflow, sdkPayload);
1522
+ }
1523
+ function createFirstflow(options) {
1524
+ const apiUrl = FIRSTFLOW_PUBLIC_API_BASE_URL.replace(/\/$/, "");
1525
+ const { agentId, apiKey, user, autoFetchAudienceMembership = true } = options;
1526
+ const analytics = createAnalytics({
1527
+ agentId,
1528
+ userId: user?.id,
1529
+ traits: user?.traits
1530
+ });
1531
+ const inner = createIssueReporter({
1532
+ agentId,
1533
+ apiUrl,
1534
+ analytics
1535
+ });
1536
+ const feedbackInner = createMessageFeedback({
1537
+ agentId,
1538
+ analytics
1539
+ });
1540
+ const surveyInner = createSurveyModule({ agentId, analytics });
1541
+ const initialIssueConfig = inner.getConfig();
1542
+ const initialFeedbackConfig = feedbackInner.getConfig();
1543
+ const runtime = {
1544
+ user: user ?? null,
1545
+ audienceMembership: {},
1546
+ overrides: EMPTY_OVERRIDES2,
1547
+ experienceRows: [],
1548
+ issueConfigSnapshot: initialIssueConfig,
1549
+ feedbackConfigSnapshot: initialFeedbackConfig,
1550
+ widgetUi: null,
1551
+ listeners: /* @__PURE__ */ new Set(),
1552
+ version: 0,
1553
+ audienceFetchTimer: null,
1554
+ runtimeSnapshotCache: null
1555
+ };
1556
+ const notify = () => {
1557
+ notifyRuntime(runtime);
1558
+ };
1559
+ const fetchAudienceRuntimeForUser = async (userId) => {
1560
+ const uid = String(userId ?? "").trim();
1561
+ const [overrides, membership] = await Promise.all([
1562
+ fetchAudienceOverrides(apiUrl, agentId, uid, apiKey),
1563
+ autoFetchAudienceMembership ? fetchAudienceMembership(apiUrl, agentId, uid, apiKey) : Promise.resolve({})
1564
+ ]);
1565
+ let didChange = false;
1566
+ if (!deepEqual(overrides, runtime.overrides)) {
1567
+ runtime.overrides = overrides;
1568
+ didChange = true;
1569
+ }
1570
+ if (autoFetchAudienceMembership) {
1571
+ const nextMembership = normalizeAudienceMembership(membership);
1572
+ if (!deepEqual(nextMembership, runtime.audienceMembership)) {
1573
+ runtime.audienceMembership = nextMembership;
1574
+ didChange = true;
1575
+ }
1576
+ }
1577
+ if (didChange) {
1578
+ notify();
1579
+ }
1580
+ };
1581
+ const scheduleAudienceRuntimeFetch = (userId, delayMs = 300) => {
1582
+ if (runtime.audienceFetchTimer) clearTimeout(runtime.audienceFetchTimer);
1583
+ runtime.audienceFetchTimer = setTimeout(() => {
1584
+ runtime.audienceFetchTimer = null;
1585
+ void fetchAudienceRuntimeForUser(userId);
1586
+ }, delayMs);
1587
+ };
1588
+ const firstflow = {
1589
+ get agentId() {
1590
+ return agentId;
1591
+ },
1592
+ get apiUrl() {
1593
+ return apiUrl;
1594
+ },
1595
+ getWidgetUi() {
1596
+ const w = runtime.widgetUi;
1597
+ if (!w) return null;
1598
+ return Object.freeze({ ...w });
1599
+ },
1600
+ analytics,
1601
+ issue: createIssueFacade(inner),
1602
+ feedback: createFeedbackFacade(feedbackInner),
1603
+ survey: {
1604
+ getSurveys() {
1605
+ return surveyInner.getSurveys();
1606
+ },
1607
+ getSurvey(experienceId) {
1608
+ return surveyInner.getSurvey(experienceId);
1609
+ },
1610
+ setExperiences(experiences) {
1611
+ syncExperiencesFromRaw(firstflow, experiences);
1612
+ },
1613
+ isEnabled(experienceId) {
1614
+ return surveyInner.isEnabled(experienceId);
1615
+ },
1616
+ submit(payload) {
1617
+ return surveyInner.submit(payload);
1618
+ }
1619
+ },
1620
+ getUser() {
1621
+ return runtime.user ? { ...runtime.user, traits: { ...runtime.user.traits ?? {} } } : null;
1622
+ },
1623
+ setUser(nextUser) {
1624
+ const previousUserId = runtime.user?.id ?? null;
1625
+ runtime.user = nextUser ?? null;
1626
+ if (nextUser?.id) {
1627
+ analytics.identify(nextUser.id, nextUser.traits ?? {});
1628
+ if (previousUserId !== nextUser.id) {
1629
+ scheduleAudienceRuntimeFetch(nextUser.id);
1630
+ }
1631
+ } else {
1632
+ analytics.setUser(void 0, {});
1633
+ runtime.overrides = EMPTY_OVERRIDES2;
1634
+ if (autoFetchAudienceMembership) {
1635
+ runtime.audienceMembership = {};
1636
+ }
1637
+ }
1638
+ notify();
1639
+ },
1640
+ setAudienceMembership(map) {
1641
+ runtime.audienceMembership = {
1642
+ ...runtime.audienceMembership,
1643
+ ...normalizeAudienceMembership(map)
1644
+ };
1645
+ notify();
1646
+ },
1647
+ getAudienceMembership() {
1648
+ return { ...runtime.audienceMembership };
1649
+ },
1650
+ async refreshConfig() {
1651
+ const uid = runtime.user?.id;
1652
+ const [sdk, overrides, membership] = await Promise.all([
1653
+ fetchSdkAgentConfigFresh(apiUrl, agentId, apiKey),
1654
+ fetchAudienceOverrides(apiUrl, agentId, uid, apiKey),
1655
+ autoFetchAudienceMembership ? fetchAudienceMembership(apiUrl, agentId, uid, apiKey) : Promise.resolve({})
1656
+ ]);
1657
+ let didChange = applySdkAgentPayloadInternal(firstflow, sdk, {
1658
+ notify: false
1659
+ });
1660
+ if (!deepEqual(overrides, runtime.overrides)) {
1661
+ runtime.overrides = overrides;
1662
+ didChange = true;
1663
+ }
1664
+ if (autoFetchAudienceMembership) {
1665
+ const nextMembership = normalizeAudienceMembership(membership);
1666
+ if (!deepEqual(nextMembership, runtime.audienceMembership)) {
1667
+ runtime.audienceMembership = nextMembership;
1668
+ didChange = true;
1669
+ }
1670
+ }
1671
+ if (didChange) {
1672
+ notify();
1673
+ }
1674
+ },
1675
+ reset() {
1676
+ clearAnonymousId();
1677
+ clearSessionId();
1678
+ clearFrequencyRecords();
1679
+ runtime.user = null;
1680
+ runtime.audienceMembership = {};
1681
+ runtime.overrides = EMPTY_OVERRIDES2;
1682
+ runtime.widgetUi = null;
1683
+ analytics.setUser(void 0, {});
1684
+ clearAudienceOverridesCache();
1685
+ clearAudienceMembershipCache();
1686
+ clearSdkAgentConfigCache();
1687
+ notify();
1688
+ }
1689
+ };
1690
+ if (runtime.user?.id) {
1691
+ analytics.identify(runtime.user.id, runtime.user.traits ?? {});
1692
+ scheduleAudienceRuntimeFetch(runtime.user.id, 0);
1693
+ }
1694
+ internalIssueMap.set(firstflow, inner);
1695
+ internalFeedbackMap.set(firstflow, feedbackInner);
1696
+ internalSurveyMap.set(firstflow, surveyInner);
1697
+ internalRuntimeMap.set(firstflow, runtime);
1698
+ return firstflow;
1699
+ }
1700
+
1701
+ // src/issue/hooks/useFirstflowIssueForm.ts
1702
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState2 } from "react";
1703
+
1704
+ // src/issue/IssueReporterContext.tsx
1705
+ import {
1706
+ createContext,
1707
+ useCallback,
1708
+ useContext,
1709
+ useEffect as useEffect2,
1710
+ useRef,
1711
+ useState
1712
+ } from "react";
1713
+
1714
+ // src/components/IssueForm.tsx
1715
+ import { useEffect } from "react";
1716
+
1717
+ // #style-inject:#style-inject
1718
+ function styleInject(css, { insertAt } = {}) {
1719
+ if (!css || typeof document === "undefined") return;
1720
+ const head = document.head || document.getElementsByTagName("head")[0];
1721
+ const style = document.createElement("style");
1722
+ style.type = "text/css";
1723
+ if (insertAt === "top") {
1724
+ if (head.firstChild) {
1725
+ head.insertBefore(style, head.firstChild);
1726
+ } else {
1727
+ head.appendChild(style);
1728
+ }
1729
+ } else {
1730
+ head.appendChild(style);
1731
+ }
1732
+ if (style.styleSheet) {
1733
+ style.styleSheet.cssText = css;
1734
+ } else {
1735
+ style.appendChild(document.createTextNode(css));
1736
+ }
1737
+ }
1738
+
1739
+ // src/components/IssueForm.css
1740
+ styleInject(".ffIssueBackdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: grid;\n place-items: center;\n z-index: 1000;\n}\n.ffIssuePanel {\n width: min(560px, calc(100vw - 24px));\n border-radius: 14px;\n background: #fff;\n border: 1px solid #e6e6e6;\n padding: 16px;\n color: #121212;\n}\n.ffIssueTitle {\n margin: 0 0 8px;\n font-size: 18px;\n}\n.ffIssuePrompt {\n margin: 0 0 12px;\n font-size: 14px;\n color: #525252;\n}\n.ffIssueField {\n margin-bottom: 10px;\n}\n.ffIssueLabel {\n display: block;\n font-size: 12px;\n margin-bottom: 4px;\n color: #3f3f3f;\n}\n.ffIssueInput,\n.ffIssueTextarea,\n.ffIssueSelect {\n width: 100%;\n border: 1px solid #d4d4d4;\n border-radius: 8px;\n padding: 8px 10px;\n font-size: 14px;\n}\n.ffIssueTextarea {\n min-height: 84px;\n resize: vertical;\n}\n.ffIssueErrorText {\n margin-top: 4px;\n font-size: 12px;\n color: #b91c1c;\n}\n.ffIssueActions {\n margin-top: 12px;\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.ffIssueButton {\n border-radius: 8px;\n border: 1px solid #d4d4d4;\n padding: 8px 12px;\n background: #fff;\n color: #111;\n cursor: pointer;\n}\n.ffIssuePrimary {\n background: #111;\n color: #fff;\n border-color: #111;\n}\n.ffIssueStatusError {\n margin-top: 8px;\n font-size: 12px;\n color: #b91c1c;\n}\n.ffIssueStatusSuccess {\n margin-top: 8px;\n font-size: 12px;\n color: #166534;\n}\n");
1741
+
1742
+ // src/components/IssueForm.tsx
1743
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1744
+ var styles = {
1745
+ backdrop: "ffIssueBackdrop",
1746
+ panel: "ffIssuePanel",
1747
+ title: "ffIssueTitle",
1748
+ prompt: "ffIssuePrompt",
1749
+ field: "ffIssueField",
1750
+ label: "ffIssueLabel",
1751
+ input: "ffIssueInput",
1752
+ textarea: "ffIssueTextarea",
1753
+ select: "ffIssueSelect",
1754
+ errorText: "ffIssueErrorText",
1755
+ actions: "ffIssueActions",
1756
+ button: "ffIssueButton",
1757
+ primary: "ffIssuePrimary",
1758
+ statusError: "ffIssueStatusError",
1759
+ statusSuccess: "ffIssueStatusSuccess"
1760
+ };
1761
+ function renderField(field, value, error, onChange) {
1762
+ if (field.type === "textarea") {
1763
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1764
+ /* @__PURE__ */ jsx(
1765
+ "textarea",
1766
+ {
1767
+ className: styles.textarea,
1768
+ value,
1769
+ onChange: (event) => onChange(field.id, event.target.value)
1770
+ }
1771
+ ),
1772
+ error ? /* @__PURE__ */ jsx("div", { className: styles.errorText, children: error }) : null
1773
+ ] });
1774
+ }
1775
+ if (field.type === "select") {
1776
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1777
+ /* @__PURE__ */ jsxs(
1778
+ "select",
1779
+ {
1780
+ className: styles.select,
1781
+ value,
1782
+ onChange: (event) => onChange(field.id, event.target.value),
1783
+ children: [
1784
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select an option" }),
1785
+ (field.options ?? []).map((option) => /* @__PURE__ */ jsx("option", { value: option, children: option }, `${field.id}:${option}`))
1786
+ ]
1787
+ }
1788
+ ),
1789
+ error ? /* @__PURE__ */ jsx("div", { className: styles.errorText, children: error }) : null
1790
+ ] });
1791
+ }
1792
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1793
+ /* @__PURE__ */ jsx(
1794
+ "input",
1795
+ {
1796
+ className: styles.input,
1797
+ value,
1798
+ onChange: (event) => onChange(field.id, event.target.value)
1799
+ }
1800
+ ),
1801
+ error ? /* @__PURE__ */ jsx("div", { className: styles.errorText, children: error }) : null
1802
+ ] });
1803
+ }
1804
+ function IssueReporter({ showTrigger = true }) {
1805
+ const { isModalOpen, openModal, closeModal } = useIssueReporterContext();
1806
+ const {
1807
+ fields,
1808
+ values,
1809
+ errors,
1810
+ setValue,
1811
+ submit,
1812
+ submitting,
1813
+ submitted,
1814
+ submitError,
1815
+ clearSubmitError,
1816
+ reset,
1817
+ title,
1818
+ promptText,
1819
+ triggerLabel,
1820
+ submitLabel,
1821
+ cancelLabel,
1822
+ cancelEnabled,
1823
+ isEnabled
1824
+ } = useFirstflowIssueForm();
1825
+ useEffect(() => {
1826
+ if (!isModalOpen) {
1827
+ reset();
1828
+ clearSubmitError();
1829
+ }
1830
+ }, [isModalOpen, reset, clearSubmitError]);
1831
+ if (!isEnabled) return null;
1832
+ const handleSubmit = async (event) => {
1833
+ event.preventDefault();
1834
+ await submit();
1835
+ };
1836
+ const body = isModalOpen ? /* @__PURE__ */ jsx("div", { className: styles.backdrop, role: "presentation", onClick: closeModal, children: /* @__PURE__ */ jsxs(
1837
+ "section",
1838
+ {
1839
+ className: styles.panel,
1840
+ role: "dialog",
1841
+ "aria-modal": "true",
1842
+ "aria-label": title,
1843
+ onClick: (event) => event.stopPropagation(),
1844
+ children: [
1845
+ /* @__PURE__ */ jsx("h2", { className: styles.title, children: title }),
1846
+ promptText ? /* @__PURE__ */ jsx("p", { className: styles.prompt, children: promptText }) : null,
1847
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, children: [
1848
+ fields.map((field) => /* @__PURE__ */ jsxs("div", { className: styles.field, children: [
1849
+ /* @__PURE__ */ jsx("label", { className: styles.label, htmlFor: `ff-issue-${field.id}`, children: field.label }),
1850
+ renderField(
1851
+ field,
1852
+ values[field.id] ?? "",
1853
+ errors[field.id],
1854
+ (fieldId, next) => setValue(fieldId, next)
1855
+ )
1856
+ ] }, field.id)),
1857
+ /* @__PURE__ */ jsxs("div", { className: styles.actions, children: [
1858
+ cancelEnabled ? /* @__PURE__ */ jsx("button", { type: "button", className: styles.button, onClick: closeModal, children: cancelLabel }) : null,
1859
+ /* @__PURE__ */ jsx(
1860
+ "button",
1861
+ {
1862
+ type: "submit",
1863
+ className: `${styles.button} ${styles.primary}`,
1864
+ disabled: submitting,
1865
+ children: submitting ? "Submitting..." : submitLabel
1866
+ }
1867
+ )
1868
+ ] }),
1869
+ submitError ? /* @__PURE__ */ jsx("div", { className: styles.statusError, children: submitError }) : null,
1870
+ submitted ? /* @__PURE__ */ jsx("div", { className: styles.statusSuccess, children: "Thanks for your report." }) : null
1871
+ ] })
1872
+ ]
1873
+ }
1874
+ ) }) : null;
1875
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1876
+ showTrigger ? /* @__PURE__ */ jsx("button", { type: "button", className: styles.button, onClick: () => openModal(), children: triggerLabel }) : null,
1877
+ body
1878
+ ] });
1879
+ }
1880
+
1881
+ // src/issue/IssueReporterContext.tsx
1882
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1883
+ var IssueReporterContext = createContext(
1884
+ null
1885
+ );
1886
+ function IssueReporterProvider({
1887
+ reporter,
1888
+ children
1889
+ }) {
1890
+ const [isModalOpen, setIsModalOpen] = useState(false);
1891
+ const openOptionsRef = useRef(void 0);
1892
+ const openModal = useCallback((options) => {
1893
+ openOptionsRef.current = options;
1894
+ setIsModalOpen(true);
1895
+ }, []);
1896
+ const closeModal = useCallback(() => {
1897
+ setIsModalOpen(false);
1898
+ openOptionsRef.current = void 0;
1899
+ }, []);
1900
+ useEffect2(() => {
1901
+ reporter.setOpenHandler(openModal);
1902
+ return () => {
1903
+ reporter.setOpenHandler(void 0);
1904
+ };
1905
+ }, [reporter, openModal]);
1906
+ const value = {
1907
+ reporter,
1908
+ isModalOpen,
1909
+ openModal,
1910
+ closeModal,
1911
+ openOptionsRef
1912
+ };
1913
+ return /* @__PURE__ */ jsxs2(IssueReporterContext.Provider, { value, children: [
1914
+ children,
1915
+ /* @__PURE__ */ jsx2(IssueReporter, { showTrigger: false })
1916
+ ] });
1917
+ }
1918
+ function useIssueReporterContext() {
1919
+ const ctx = useContext(IssueReporterContext);
1920
+ if (ctx == null) {
1921
+ throw new Error(
1922
+ "useIssueReporterContext must be used within IssueReporterProvider"
1923
+ );
1924
+ }
1925
+ return ctx;
1926
+ }
1927
+
1928
+ // src/issue/hooks/useFirstflowIssueForm.ts
1929
+ function emptyValues(fields) {
1930
+ const init = {};
1931
+ fields.forEach((f) => {
1932
+ init[f.id] = "";
1933
+ });
1934
+ return init;
1935
+ }
1936
+ function useFirstflowIssueForm() {
1937
+ const { reporter, openOptionsRef } = useIssueReporterContext();
1938
+ const config = reporter.getConfig();
1939
+ const fields = config.fields;
1940
+ const [values, setValuesState] = useState2(() => emptyValues(fields));
1941
+ const [errors, setErrors] = useState2({});
1942
+ const [submitting, setSubmitting] = useState2(false);
1943
+ const [submitted, setSubmitted] = useState2(false);
1944
+ const [submitError, setSubmitError] = useState2(null);
1945
+ const fieldsKey = fields.map((f) => f.id).join("\0");
1946
+ useEffect3(() => {
1947
+ setValuesState(emptyValues(fields));
1948
+ setErrors({});
1949
+ setSubmitted(false);
1950
+ setSubmitError(null);
1951
+ }, [fieldsKey, reporter]);
1952
+ const setValue = useCallback2((fieldId, value) => {
1953
+ setValuesState((prev) => ({ ...prev, [fieldId]: value }));
1954
+ setErrors((prev) => prev[fieldId] ? { ...prev, [fieldId]: "" } : prev);
1955
+ }, []);
1956
+ const setValues = useCallback2((next) => {
1957
+ setValuesState(next);
1958
+ }, []);
1959
+ const validate = useCallback2(() => {
1960
+ const result = validatePayload(
1961
+ values,
1962
+ fields
1963
+ );
1964
+ if (!result.valid) {
1965
+ setErrors(result.errors ?? {});
1966
+ return false;
1967
+ }
1968
+ setErrors({});
1969
+ return true;
1970
+ }, [values, fields]);
1971
+ const submit = useCallback2(async () => {
1972
+ if (!validate()) return;
1973
+ setSubmitting(true);
1974
+ setSubmitError(null);
1975
+ const context = openOptionsRef.current ?? {};
1976
+ try {
1977
+ await reporter.reportIssue({ ...values, ...context });
1978
+ setSubmitted(true);
1979
+ } catch (e) {
1980
+ const msg = e instanceof Error ? e.message : String(e);
1981
+ setSubmitError(msg);
1982
+ throw e;
1983
+ } finally {
1984
+ setSubmitting(false);
1985
+ }
1986
+ }, [validate, values, reporter, openOptionsRef]);
1987
+ const reset = useCallback2(() => {
1988
+ setValuesState(emptyValues(fields));
1989
+ setErrors({});
1990
+ setSubmitted(false);
1991
+ setSubmitError(null);
1992
+ }, [fields]);
1993
+ const clearSubmitError = useCallback2(() => setSubmitError(null), []);
1994
+ return {
1995
+ values,
1996
+ setValue,
1997
+ setValues,
1998
+ errors,
1999
+ submitting,
2000
+ submitted,
2001
+ submitError,
2002
+ validate,
2003
+ submit,
2004
+ reset,
2005
+ clearSubmitError,
2006
+ isEnabled: config.enabled,
2007
+ config,
2008
+ title: config.title,
2009
+ triggerLabel: config.triggerLabel,
2010
+ submitLabel: config.submitLabel,
2011
+ cancelLabel: config.cancelLabel,
2012
+ cancelEnabled: config.cancelEnabled,
2013
+ uiText: {
2014
+ title: config.title,
2015
+ triggerLabel: config.triggerLabel,
2016
+ submitLabel: config.submitLabel,
2017
+ cancelLabel: config.cancelLabel,
2018
+ cancelEnabled: config.cancelEnabled,
2019
+ promptText: config.promptText
2020
+ },
2021
+ fields,
2022
+ promptText: config.promptText
2023
+ };
2024
+ }
2025
+
2026
+ // src/platform/FirstflowContext.tsx
2027
+ import {
2028
+ createContext as createContext2,
2029
+ useContext as useContext2,
2030
+ useEffect as useEffect4,
2031
+ useLayoutEffect,
2032
+ useRef as useRef2,
2033
+ useState as useState3
2034
+ } from "react";
2035
+ import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
2036
+ var FirstflowContext = createContext2(null);
2037
+ function FirstflowProvider({
2038
+ agentId,
2039
+ apiKey,
2040
+ children,
2041
+ fallback = null,
2042
+ onError,
2043
+ initialSdkPayload = null,
2044
+ user = null,
2045
+ audienceMembership,
2046
+ autoFetchAudienceMembership = true
2047
+ }) {
2048
+ const firstflowRef = useRef2(null);
2049
+ if (!firstflowRef.current && agentId.trim()) {
2050
+ firstflowRef.current = createFirstflow({
2051
+ agentId,
2052
+ apiKey,
2053
+ user: user ?? void 0,
2054
+ autoFetchAudienceMembership
2055
+ });
2056
+ }
2057
+ const [childrenReady, setChildrenReady] = useState3(initialSdkPayload == null);
2058
+ const [bootstrapError, setBootstrapError] = useState3(null);
2059
+ const firstflow = firstflowRef.current;
2060
+ const initialAppliedRef = useRef2(false);
2061
+ useLayoutEffect(() => {
2062
+ if (!firstflow) return;
2063
+ if (!initialSdkPayload || initialAppliedRef.current) {
2064
+ setChildrenReady(true);
2065
+ return;
2066
+ }
2067
+ applySdkAgentPayload(firstflow, initialSdkPayload);
2068
+ initialAppliedRef.current = true;
2069
+ setChildrenReady(true);
2070
+ }, [firstflow, initialSdkPayload]);
2071
+ useEffect4(() => {
2072
+ if (!firstflow) return;
2073
+ if (audienceMembership) {
2074
+ firstflow.setAudienceMembership(audienceMembership);
2075
+ }
2076
+ }, [firstflow, audienceMembership]);
2077
+ useEffect4(() => {
2078
+ if (!firstflow) {
2079
+ return;
2080
+ }
2081
+ if (!apiKey?.trim()) {
2082
+ const err = new Error(
2083
+ "Missing `apiKey` in FirstflowProvider; remote config requires an API key."
2084
+ );
2085
+ setBootstrapError(err);
2086
+ onError?.(err);
2087
+ return;
2088
+ }
2089
+ const controller = new AbortController();
2090
+ let cancelled = false;
2091
+ setBootstrapError(null);
2092
+ void (async () => {
2093
+ try {
2094
+ const payload = await fetchSdkAgentConfig(
2095
+ firstflow.apiUrl,
2096
+ agentId,
2097
+ apiKey.trim()
2098
+ );
2099
+ if (cancelled || controller.signal.aborted) return;
2100
+ applySdkAgentPayload(firstflow, payload);
2101
+ } catch (error) {
2102
+ if (cancelled || controller.signal.aborted) return;
2103
+ const err = error instanceof Error ? error : new Error(String(error));
2104
+ setBootstrapError(err);
2105
+ onError?.(err);
2106
+ } finally {
2107
+ if (!cancelled) {
2108
+ setChildrenReady(true);
2109
+ }
2110
+ }
2111
+ })();
2112
+ return () => {
2113
+ cancelled = true;
2114
+ controller.abort();
2115
+ };
2116
+ }, [agentId, apiKey, firstflow, onError]);
2117
+ if (!firstflow || bootstrapError || !childrenReady) {
2118
+ return /* @__PURE__ */ jsx3(Fragment2, { children: fallback });
2119
+ }
2120
+ const internalIssue = getInternalIssueReporter(firstflow);
2121
+ return /* @__PURE__ */ jsx3(FirstflowContext.Provider, { value: firstflow, children: /* @__PURE__ */ jsx3(IssueReporterProvider, { reporter: internalIssue, children }) });
2122
+ }
2123
+ function useFirstflow() {
2124
+ const ctx = useContext2(FirstflowContext);
2125
+ if (ctx == null) {
2126
+ throw new Error("useFirstflow must be used within FirstflowProvider");
2127
+ }
2128
+ return ctx;
2129
+ }
2130
+
2131
+ export {
2132
+ ISSUE_SUBMITTED,
2133
+ FEEDBACK_SUBMITTED,
2134
+ SURVEY_COMPLETED,
2135
+ EXPERIENCE_SHOWN,
2136
+ EXPERIENCE_CLICKED,
2137
+ CHAT_MESSAGE_SENT,
2138
+ EXPERIENCE_ANALYTICS_TYPE,
2139
+ createIssueReporter,
2140
+ EXPERIENCE_SETTINGS_DEFAULT,
2141
+ createStorageAdapter,
2142
+ clearFrequencyRecords,
2143
+ markExperienceShown,
2144
+ runEligibilityFilter,
2145
+ fetchSdkAgentConfig,
2146
+ FIRSTFLOW_PUBLIC_API_BASE_URL,
2147
+ subscribeInternalRuntime,
2148
+ getRuntimeSnapshot,
2149
+ createFirstflow,
2150
+ useFirstflowIssueForm,
2151
+ styleInject,
2152
+ IssueReporter,
2153
+ IssueReporterProvider,
2154
+ useIssueReporterContext,
2155
+ FirstflowProvider,
2156
+ useFirstflow
2157
+ };