@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.
package/dist/index.mjs CHANGED
@@ -1,1072 +1,618 @@
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";
1
+ import {
2
+ CHAT_MESSAGE_SENT,
3
+ EXPERIENCE_ANALYTICS_TYPE,
4
+ EXPERIENCE_CLICKED,
5
+ EXPERIENCE_SETTINGS_DEFAULT,
6
+ EXPERIENCE_SHOWN,
7
+ FIRSTFLOW_PUBLIC_API_BASE_URL,
8
+ FirstflowProvider,
9
+ SURVEY_COMPLETED,
10
+ clearFrequencyRecords,
11
+ createFirstflow,
12
+ createStorageAdapter,
13
+ fetchSdkAgentConfig,
14
+ getRuntimeSnapshot,
15
+ markExperienceShown,
16
+ runEligibilityFilter,
17
+ styleInject,
18
+ subscribeInternalRuntime,
19
+ useFirstflow
20
+ } from "./chunk-WTTDFCFD.mjs";
8
21
 
9
- // src/analytics/identity.ts
10
- var ANON_KEY = "ff_anon";
11
- var SESSION_KEY = "ff_sess";
12
- function uuid() {
13
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
14
- const r = Math.random() * 16 | 0;
15
- return (c === "x" ? r : r & 3 | 8).toString(16);
16
- });
22
+ // src/platform/hooks/useFirstflowSelector.ts
23
+ import { useSyncExternalStore } from "react";
24
+ function useFirstflowSelector(selector) {
25
+ const firstflow = useFirstflow();
26
+ return useSyncExternalStore(
27
+ (listener) => subscribeInternalRuntime(firstflow, listener),
28
+ () => selector(getRuntimeSnapshot(firstflow)),
29
+ () => selector(getRuntimeSnapshot(firstflow))
30
+ );
17
31
  }
18
- function getStorage(key, storage) {
19
- if (typeof window === "undefined" || !storage) return uuid();
20
- try {
21
- let value = storage.getItem(key);
22
- if (!value) {
23
- value = uuid();
24
- storage.setItem(key, value);
25
- }
26
- return value;
27
- } catch {
28
- return uuid();
32
+
33
+ // src/survey/hooks/useFirstflowSurvey.ts
34
+ import { useCallback, useMemo, useState } from "react";
35
+ function hasValidAnswer(question, value) {
36
+ if (question.type === "text_block" || question.type === "thank_you") return true;
37
+ if (value == null) return false;
38
+ if (Array.isArray(value)) return value.length > 0;
39
+ if (typeof value === "number") return Number.isFinite(value);
40
+ if (typeof value === "string") return value.trim().length > 0;
41
+ return false;
42
+ }
43
+ function requiredQuestionsAreAnswered(questions, answers) {
44
+ for (const q of questions) {
45
+ if (!q.required) continue;
46
+ if (!hasValidAnswer(q, answers[q.id] ?? null)) return false;
29
47
  }
48
+ return true;
30
49
  }
31
- function getAnonymousId() {
32
- return getStorage(ANON_KEY, typeof window !== "undefined" ? window.localStorage : null);
50
+ function isScopedMode(options) {
51
+ return !!options && typeof options.experienceId === "string" && options.experienceId.trim().length > 0;
33
52
  }
34
- function getSessionId() {
35
- return getStorage(SESSION_KEY, typeof window !== "undefined" ? window.sessionStorage : null);
53
+ function useFirstflowSurvey(options) {
54
+ const firstflow = useFirstflow();
55
+ const scoped = isScopedMode(options);
56
+ const [answers, setAnswers] = useState({});
57
+ const [currentIndex, setCurrentIndex] = useState(0);
58
+ const [submitting, setSubmitting] = useState(false);
59
+ const [submitted, setSubmitted] = useState(false);
60
+ const [error, setError] = useState(null);
61
+ if (!scoped) return firstflow.survey;
62
+ const survey = firstflow.survey.getSurvey(options.experienceId);
63
+ const questions = survey?.questions ?? [];
64
+ const currentQuestion = questions[currentIndex] ?? null;
65
+ const canSubmit = requiredQuestionsAreAnswered(questions, answers);
66
+ const setAnswer = useCallback((questionId, value) => {
67
+ setAnswers((prev2) => ({ ...prev2, [questionId]: value }));
68
+ setError(null);
69
+ setSubmitted(false);
70
+ }, []);
71
+ const next = useCallback(() => {
72
+ setCurrentIndex((i) => Math.min(i + 1, Math.max(0, questions.length - 1)));
73
+ }, [questions.length]);
74
+ const prev = useCallback(() => {
75
+ setCurrentIndex((i) => Math.max(0, i - 1));
76
+ }, []);
77
+ const goTo = useCallback(
78
+ (index) => {
79
+ setCurrentIndex(Math.max(0, Math.min(index, Math.max(0, questions.length - 1))));
80
+ },
81
+ [questions.length]
82
+ );
83
+ const submit = useCallback(async () => {
84
+ if (!canSubmit) {
85
+ setError("Please answer all required questions");
86
+ return;
87
+ }
88
+ setSubmitting(true);
89
+ setError(null);
90
+ try {
91
+ const basePayload = {
92
+ experienceId: options.experienceId,
93
+ conversationId: options.conversationId,
94
+ answers,
95
+ metadata: options.metadata
96
+ };
97
+ const payload = options.mapSubmit ? options.mapSubmit(basePayload) : basePayload;
98
+ const expId = String(payload.experienceId ?? "").trim();
99
+ if (!expId) throw new Error("experienceId is required");
100
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
101
+ firstflow.analytics.track(SURVEY_COMPLETED, {
102
+ agent_id: firstflow.agentId,
103
+ experience_id: expId,
104
+ survey_id: expId,
105
+ conversation_id: payload.conversationId?.trim() || void 0,
106
+ submitted_at: completedAt,
107
+ payload: JSON.stringify({
108
+ experience_id: expId,
109
+ survey_id: expId,
110
+ conversation_id: payload.conversationId?.trim() || void 0,
111
+ completed_at: completedAt,
112
+ answers: payload.answers ?? {}
113
+ }),
114
+ metadata: JSON.stringify(payload.metadata ?? {})
115
+ });
116
+ setSubmitted(true);
117
+ } catch (e) {
118
+ setError(e instanceof Error ? e.message : "Failed to submit survey");
119
+ } finally {
120
+ setSubmitting(false);
121
+ }
122
+ }, [answers, canSubmit, firstflow, options]);
123
+ const result = useMemo(
124
+ () => ({
125
+ surveyId: options.experienceId,
126
+ questions,
127
+ currentIndex,
128
+ currentQuestion,
129
+ answers,
130
+ setAnswer,
131
+ next,
132
+ prev,
133
+ goTo,
134
+ canSubmit,
135
+ isEnabled: firstflow.survey.isEnabled(options.experienceId),
136
+ submitting,
137
+ submitted,
138
+ error,
139
+ submit,
140
+ clearError: () => setError(null)
141
+ }),
142
+ [
143
+ options.experienceId,
144
+ questions,
145
+ currentIndex,
146
+ currentQuestion,
147
+ answers,
148
+ setAnswer,
149
+ next,
150
+ prev,
151
+ goTo,
152
+ canSubmit,
153
+ firstflow,
154
+ submitting,
155
+ submitted,
156
+ error,
157
+ submit
158
+ ]
159
+ );
160
+ return result;
36
161
  }
37
162
 
38
- // src/analytics/device.ts
39
- function getDeviceType() {
40
- if (typeof window === "undefined") return "desktop";
41
- const w = window.innerWidth;
42
- if (w <= 768) return "mobile";
43
- if (w <= 1024) return "tablet";
44
- return "desktop";
45
- }
163
+ // src/experience/hooks/useExperienceEligibility.ts
164
+ import { useMemo as useMemo2 } from "react";
46
165
 
47
- // src/analytics/jitsu.ts
48
- var DEFAULT_JITSU_HOST = "https://cmmiyu46c00003b6unusxjf0j.analytics.firstflow.dev";
49
- var DEFAULT_JITSU_KEY = "P5qcd1d4Bd98EdxS4EfnucLohE2Ch37K:U4OECTXofJVzR5jhjaCpaYj478871PFU";
50
- function uuid2() {
51
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
52
- const r = Math.random() * 16 | 0;
53
- return (c === "x" ? r : r & 3 | 8).toString(16);
54
- });
55
- }
56
- function sendTrack(config, event, properties = {}) {
57
- const host = config.jitsuHost || DEFAULT_JITSU_HOST;
58
- const key = config.jitsuWriteKey || DEFAULT_JITSU_KEY;
59
- const payload = {
60
- messageId: uuid2(),
61
- type: "track",
62
- event,
63
- anonymousId: getAnonymousId(),
64
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
65
- properties: {
66
- agent_id: config.agentId,
67
- session_id: getSessionId(),
68
- device_type: getDeviceType(),
69
- ...properties
70
- }
71
- };
72
- const auth = "Basic " + btoa(key);
73
- fetch(`${host.replace(/\/$/, "")}/api/s/track`, {
74
- method: "POST",
75
- headers: {
76
- "Content-Type": "application/json",
77
- Authorization: auth
78
- },
79
- body: JSON.stringify(payload),
80
- keepalive: true
81
- }).catch(() => {
82
- });
166
+ // src/platform/hooks/useFirstflowRuntimeSnapshot.ts
167
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
168
+ function useFirstflowRuntimeSnapshot(firstflow) {
169
+ return useSyncExternalStore2(
170
+ (listener) => subscribeInternalRuntime(firstflow, listener),
171
+ () => getRuntimeSnapshot(firstflow),
172
+ () => getRuntimeSnapshot(firstflow)
173
+ );
83
174
  }
84
175
 
85
- // src/analytics/analytics.ts
86
- var DEFAULT_JITSU_HOST2 = "https://cmmiyu46c00003b6unusxjf0j.analytics.firstflow.dev";
87
- var DEFAULT_JITSU_KEY2 = "P5qcd1d4Bd98EdxS4EfnucLohE2Ch37K:U4OECTXofJVzR5jhjaCpaYj478871PFU";
88
- function createAnalytics(config) {
89
- const agentId = config.agentId;
90
- const jitsuHost = config.jitsuHost ?? DEFAULT_JITSU_HOST2;
91
- const jitsuWriteKey = config.jitsuWriteKey ?? DEFAULT_JITSU_KEY2;
92
- const transportConfig = { jitsuHost, jitsuWriteKey, agentId };
93
- return {
94
- track(eventName, properties = {}) {
95
- sendTrack(transportConfig, eventName, properties);
96
- },
97
- identify(userId, traits = {}) {
98
- if (userId) {
99
- sendTrack(transportConfig, "user_identified", { user_id: userId, ...traits });
100
- }
101
- },
102
- page(name, properties = {}) {
103
- sendTrack(transportConfig, "page_view", { page_name: name ?? void 0, ...properties });
104
- }
105
- };
176
+ // src/experience/hooks/useExperienceEligibility.ts
177
+ var storage = createStorageAdapter();
178
+ function useExperienceEligibility(experienceId) {
179
+ const firstflow = useFirstflow();
180
+ const runtime = useFirstflowRuntimeSnapshot(firstflow);
181
+ return useMemo2(() => {
182
+ const eligible = runEligibilityFilter(runtime.experienceRows, storage, {
183
+ user: runtime.user,
184
+ overrides: runtime.overrides,
185
+ membershipMap: runtime.audienceMembership
186
+ });
187
+ return eligible.some((exp) => exp.experienceId === experienceId);
188
+ }, [runtime.experienceRows, experienceId, runtime.version]);
106
189
  }
107
190
 
108
- // src/config/defaults.ts
109
- var DEFAULT_PROMPT_TEXT = "Something wrong? Report an issue and we'll look into it.";
110
- var DEFAULT_FIELDS = [
111
- { id: "name", label: "Name", type: "text", required: true },
112
- { id: "email", label: "Email", type: "text", required: true },
113
- { id: "subject", label: "Subject", type: "text", required: true },
114
- { id: "description", label: "Description", type: "textarea", required: true },
115
- { id: "session_id", label: "Session ID", type: "text", required: false }
116
- ];
191
+ // src/experience/hooks/useFirstflowResolveExperience.ts
192
+ import { useMemo as useMemo4 } from "react";
117
193
 
118
- // src/config/normalizer.ts
119
- function normalizeField(raw) {
120
- const id = raw.id?.trim();
121
- if (!id) return null;
122
- if (raw.allowed === false) return null;
123
- const type = raw.type === "textarea" || raw.type === "select" ? raw.type : "text";
124
- return {
125
- id,
126
- type,
127
- label: raw.label?.trim() ?? id,
128
- required: raw.required === true,
129
- options: type === "select" && Array.isArray(raw.options) ? raw.options : void 0
130
- };
194
+ // src/experience/hooks/useFirstflowSlotCandidates.ts
195
+ import { useMemo as useMemo3 } from "react";
196
+
197
+ // src/experience/flowMessageNode.ts
198
+ function normalizeFlowMessageStyle(style) {
199
+ if (style === "inline") return "message";
200
+ return style;
201
+ }
202
+ function normalizeFlowQuickReplyItem(entry) {
203
+ if (typeof entry === "string") return { label: entry };
204
+ if (entry && typeof entry === "object" && "label" in entry && typeof entry.label === "string") {
205
+ const o = entry;
206
+ return { label: o.label, prompt: o.prompt };
207
+ }
208
+ return { label: "" };
209
+ }
210
+ function normalizeFlowQuickReplyList(raw) {
211
+ if (!Array.isArray(raw)) return [];
212
+ return raw.map((x) => normalizeFlowQuickReplyItem(x));
131
213
  }
132
- function normalizeConfig(agentId, apiUrl, raw) {
133
- const enabled = raw?.enabled === true;
134
- const promptText = typeof raw?.promptText === "string" && raw.promptText.trim() ? raw.promptText.trim() : DEFAULT_PROMPT_TEXT;
135
- let fields = DEFAULT_FIELDS;
136
- if (Array.isArray(raw?.fields) && raw.fields.length > 0) {
137
- const normalized = raw.fields.map(normalizeField).filter((f) => f !== null);
138
- if (normalized.length > 0) fields = normalized;
214
+ var EXPERIENCE_MESSAGE_ANALYTICS_ACTION = {
215
+ cta_primary: "cta_primary",
216
+ cta_dismiss: "cta_dismiss",
217
+ quick_reply: "quick_reply",
218
+ carousel_cta: "carousel_cta"
219
+ };
220
+ var EXPERIENCE_MESSAGE_BLOCKS_VERSION = 4;
221
+ function buildExperienceMessageBlocks(config) {
222
+ const blocks = [];
223
+ const content = config.messageContent?.trim();
224
+ if (content) {
225
+ blocks.push({ type: "text", body: content });
226
+ }
227
+ const style = config.messageStyle;
228
+ if (style === "carousel" && config.carouselCards && config.carouselCards.length > 0) {
229
+ blocks.push({ type: "carousel", cards: config.carouselCards });
230
+ }
231
+ if (style === "quick_replies") {
232
+ const qr = normalizeFlowQuickReplyList(config.quickReplies).filter((q) => q.label.trim());
233
+ if (qr.length > 0) {
234
+ blocks.push({ type: "quick_replies", options: qr });
235
+ }
236
+ }
237
+ const ctaType = config.ctaType ?? "none";
238
+ const ctaText = config.ctaText?.trim();
239
+ const skipCtaBlocks = style === "quick_replies";
240
+ if (!skipCtaBlocks && (ctaType === "button" || ctaType === "link" || ctaType === "button_dismiss") && ctaText) {
241
+ const promptTrim = config.ctaPrompt?.trim();
242
+ blocks.push({
243
+ type: "cta_primary",
244
+ label: ctaText,
245
+ ctaType,
246
+ url: config.ctaUrl?.trim() || void 0,
247
+ promptText: ctaType === "button" || ctaType === "button_dismiss" ? promptTrim || void 0 : void 0
248
+ });
249
+ }
250
+ const dismissText = config.dismissText?.trim();
251
+ if (!skipCtaBlocks && (ctaType === "dismiss" || ctaType === "button_dismiss") && dismissText) {
252
+ blocks.push({ type: "cta_dismiss", label: dismissText });
139
253
  }
254
+ return blocks;
255
+ }
256
+ function buildExperienceMessageUi(blocks, messageStyle) {
140
257
  return {
141
- agentId,
142
- apiUrl: apiUrl.replace(/\/$/, ""),
143
- enabled,
144
- promptText,
145
- fields
258
+ hasText: blocks.some((b) => b.type === "text"),
259
+ hasCarousel: blocks.some((b) => b.type === "carousel"),
260
+ hasQuickReplies: blocks.some((b) => b.type === "quick_replies"),
261
+ hasPrimaryCta: blocks.some((b) => b.type === "cta_primary"),
262
+ hasDismissCta: blocks.some((b) => b.type === "cta_dismiss"),
263
+ messageStyle: normalizeFlowMessageStyle(messageStyle)
146
264
  };
147
265
  }
148
-
149
- // src/validation/validate.ts
150
- var CONTEXT_KEYS = /* @__PURE__ */ new Set([
151
- "conversationId",
152
- "messageId",
153
- "sessionId",
154
- "agentId",
155
- "model"
266
+ var FLOW_MESSAGE_STYLES = /* @__PURE__ */ new Set([
267
+ "message",
268
+ "inline",
269
+ "card",
270
+ "carousel",
271
+ "quick_replies",
272
+ "rich_card"
156
273
  ]);
157
- function isContextKey(key) {
158
- return CONTEXT_KEYS.has(key);
159
- }
160
- function validatePayload(data, fields) {
161
- const errors = {};
162
- const fieldIds = new Set(fields.map((f) => f.id));
163
- for (const field of fields) {
164
- const value = data[field.id];
165
- const str = typeof value === "string" ? value.trim() : "";
166
- if (field.required && !str) {
167
- errors[field.id] = `${field.label ?? field.id} is required`;
168
- continue;
169
- }
170
- if (!field.required && !str) continue;
171
- if (field.type === "select" && field.options?.length) {
172
- if (!field.options.includes(str)) {
173
- errors[field.id] = `Please choose a valid option`;
174
- }
175
- }
274
+ var FLOW_MESSAGE_CTA_TYPES = /* @__PURE__ */ new Set([
275
+ "none",
276
+ "button",
277
+ "link",
278
+ "dismiss",
279
+ "button_dismiss"
280
+ ]);
281
+ var FLOW_CAROUSEL_BUTTON_ACTIONS = /* @__PURE__ */ new Set([
282
+ "prompt",
283
+ "link"
284
+ ]);
285
+ function asTrimmedString(v) {
286
+ if (typeof v !== "string") return void 0;
287
+ const t = v.trim();
288
+ return t || void 0;
289
+ }
290
+ function parseFlowCarouselCardEntry(entry, index) {
291
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
292
+ return { id: `carousel-${index}`, title: "", description: "" };
176
293
  }
294
+ const o = entry;
295
+ const buttonAction = typeof o.buttonAction === "string" && FLOW_CAROUSEL_BUTTON_ACTIONS.has(o.buttonAction) ? o.buttonAction : void 0;
296
+ const secondaryButtonAction = typeof o.secondaryButtonAction === "string" && FLOW_CAROUSEL_BUTTON_ACTIONS.has(o.secondaryButtonAction) ? o.secondaryButtonAction : void 0;
177
297
  return {
178
- valid: Object.keys(errors).length === 0,
179
- errors: Object.keys(errors).length > 0 ? errors : void 0
298
+ id: typeof o.id === "string" ? o.id : `carousel-${index}`,
299
+ title: typeof o.title === "string" ? o.title : "",
300
+ description: typeof o.description === "string" ? o.description : "",
301
+ buttonText: asTrimmedString(o.buttonText),
302
+ buttonAction,
303
+ buttonUrl: asTrimmedString(o.buttonUrl),
304
+ buttonPrompt: asTrimmedString(o.buttonPrompt),
305
+ showCardCta: typeof o.showCardCta === "boolean" ? o.showCardCta : void 0,
306
+ secondaryButtonText: asTrimmedString(o.secondaryButtonText),
307
+ secondaryButtonAction,
308
+ secondaryButtonUrl: asTrimmedString(o.secondaryButtonUrl),
309
+ secondaryButtonPrompt: asTrimmedString(o.secondaryButtonPrompt),
310
+ emoji: asTrimmedString(o.emoji),
311
+ mediaType: o.mediaType === "emoji" || o.mediaType === "image" || o.mediaType === "video" ? o.mediaType : void 0,
312
+ imageUrl: asTrimmedString(o.imageUrl),
313
+ videoUrl: asTrimmedString(o.videoUrl)
180
314
  };
181
315
  }
182
- function getFormFieldsOnly(data, fields) {
183
- const fieldIds = new Set(fields.map((f) => f.id));
184
- const out = {};
185
- for (const key of Object.keys(data)) {
186
- if (isContextKey(key)) continue;
187
- if (fieldIds.has(key)) out[key] = data[key];
316
+ function parseRichCardTags(raw) {
317
+ if (!Array.isArray(raw)) return void 0;
318
+ const out = [];
319
+ for (const item of raw) {
320
+ if (!item || typeof item !== "object") continue;
321
+ const o = item;
322
+ const label = typeof o.label === "string" ? o.label.trim() : "";
323
+ if (!label) continue;
324
+ out.push({
325
+ label,
326
+ bg: typeof o.bg === "string" ? o.bg : void 0,
327
+ color: typeof o.color === "string" ? o.color : void 0
328
+ });
329
+ }
330
+ return out.length ? out : void 0;
331
+ }
332
+ function parseRichCardMeta(raw) {
333
+ if (!Array.isArray(raw)) return void 0;
334
+ const out = [];
335
+ for (const item of raw) {
336
+ if (!item || typeof item !== "object") continue;
337
+ const o = item;
338
+ const label = typeof o.label === "string" ? o.label.trim() : "";
339
+ if (!label) continue;
340
+ out.push({
341
+ label,
342
+ icon: typeof o.icon === "string" ? o.icon : void 0
343
+ });
188
344
  }
189
- return out;
345
+ return out.length ? out : void 0;
190
346
  }
191
-
192
- // src/api/IssueReporter.ts
193
- function extractContextMetadata(data, formFieldIds) {
194
- const ctx = {};
195
- for (const key of Object.keys(data)) {
196
- if (formFieldIds.has(key)) continue;
197
- const v = data[key];
198
- if (v !== void 0) ctx[key] = v;
347
+ function tryParseHttpImageUrl(raw) {
348
+ if (!raw || typeof raw !== "string") return void 0;
349
+ const t = raw.trim();
350
+ if (!/^https?:\/\//i.test(t)) return void 0;
351
+ try {
352
+ const u = new URL(t);
353
+ if (u.protocol !== "http:" && u.protocol !== "https:") return void 0;
354
+ return u.href;
355
+ } catch {
356
+ return void 0;
199
357
  }
200
- return ctx;
201
- }
202
- function buildPayload(formValues, contextMetadata) {
203
- return { ...formValues, ...contextMetadata };
204
- }
205
- function createIssueReporter(options) {
206
- const { agentId, apiUrl, config: rawConfig, analytics: analyticsOpt } = options;
207
- const analytics = analyticsOpt ?? createAnalytics({ agentId });
208
- let config = normalizeConfig(agentId, apiUrl, rawConfig);
209
- let openHandler;
210
- const instance = {
211
- getConfig() {
212
- return config;
213
- },
214
- setConfig(next) {
215
- config = next;
216
- },
217
- async reportIssue(data) {
218
- const formValues = getFormFieldsOnly(
219
- data,
220
- config.fields
221
- );
222
- const result = validatePayload(formValues, config.fields);
223
- if (!result.valid) {
224
- throw new Error(
225
- result.errors ? Object.entries(result.errors).map(([k, v]) => `${k}: ${v}`).join(", ") : "Validation failed"
226
- );
227
- }
228
- const formFieldIds = new Set(config.fields.map((f) => f.id));
229
- const contextMetadata = extractContextMetadata(data, formFieldIds);
230
- const payload = buildPayload(formValues, contextMetadata);
231
- analytics.track(ISSUE_SUBMITTED, {
232
- payload: JSON.stringify(payload),
233
- metadata: JSON.stringify({})
234
- });
235
- },
236
- open(options2) {
237
- openHandler?.(options2);
238
- },
239
- setOpenHandler(handler) {
240
- openHandler = handler;
241
- },
242
- destroy() {
243
- openHandler = void 0;
244
- }
245
- };
246
- return instance;
247
358
  }
248
-
249
- // src/config/feedbackNormalizer.ts
250
- var DEFAULT_TAGS = ["Insightful", "Actionable", "Clear"];
251
- var DEFAULT_HEADING = "Tell us more";
252
- var DEFAULT_PLACEHOLDER = "Anything else you'd like to add? (optional)";
253
- function normalizeSide(raw) {
254
- const tags = Array.isArray(raw?.tags) && raw.tags.length > 0 ? [...new Set(raw.tags.map((t) => String(t).trim()).filter(Boolean))] : [...DEFAULT_TAGS];
359
+ function parseFlowMessageNodeConfig(raw) {
360
+ const messageStyle = typeof raw.messageStyle === "string" && FLOW_MESSAGE_STYLES.has(raw.messageStyle) ? raw.messageStyle : void 0;
361
+ const ctaType = typeof raw.ctaType === "string" && FLOW_MESSAGE_CTA_TYPES.has(raw.ctaType) ? raw.ctaType : void 0;
362
+ const carouselRaw = raw.carouselCards;
363
+ const carouselCards = Array.isArray(carouselRaw) ? carouselRaw.map((c, i) => parseFlowCarouselCardEntry(c, i)) : void 0;
364
+ const quickReplies = Array.isArray(raw.quickReplies) ? raw.quickReplies : void 0;
365
+ const richCardMediaType = raw.richCardMediaType === "emoji" || raw.richCardMediaType === "image" ? raw.richCardMediaType : void 0;
255
366
  return {
256
- tags,
257
- heading: typeof raw?.heading === "string" && raw.heading.trim() ? raw.heading.trim() : DEFAULT_HEADING,
258
- placeholder: typeof raw?.placeholder === "string" && raw.placeholder.trim() ? raw.placeholder.trim() : DEFAULT_PLACEHOLDER,
259
- showTags: raw?.showTags === true,
260
- showComment: raw?.showComment === true
367
+ messageContent: typeof raw.messageContent === "string" ? raw.messageContent : void 0,
368
+ messageStyle,
369
+ ctaType,
370
+ ctaText: typeof raw.ctaText === "string" ? raw.ctaText : void 0,
371
+ ctaUrl: typeof raw.ctaUrl === "string" ? raw.ctaUrl : void 0,
372
+ ctaPrompt: typeof raw.ctaPrompt === "string" ? raw.ctaPrompt : void 0,
373
+ dismissText: typeof raw.dismissText === "string" ? raw.dismissText : void 0,
374
+ quickReplies,
375
+ carouselCards,
376
+ richCardImageUrl: typeof raw.richCardImageUrl === "string" ? raw.richCardImageUrl : void 0,
377
+ richCardTitle: typeof raw.richCardTitle === "string" ? raw.richCardTitle : void 0,
378
+ richCardEmoji: typeof raw.richCardEmoji === "string" ? raw.richCardEmoji : void 0,
379
+ richCardMediaType,
380
+ richCardTags: parseRichCardTags(raw.richCardTags),
381
+ richCardMeta: parseRichCardMeta(raw.richCardMeta)
261
382
  };
262
383
  }
263
- function normalizeFeedbackConfig(raw) {
264
- const enabled = raw?.enabled === true;
384
+ function buildExperienceCarouselCtaAction(config, cardId, actionId) {
385
+ const id = actionId === "secondary" ? "secondary" : "primary";
386
+ const card = config.carouselCards?.find((c) => c.id === cardId);
387
+ if (!card) {
388
+ return { kind: "carousel_cta", cardId, actionId: id };
389
+ }
390
+ const isSecondary = id === "secondary";
391
+ const label = isSecondary ? card.secondaryButtonText?.trim() : card.buttonText?.trim();
392
+ const buttonAction = isSecondary ? card.secondaryButtonAction ?? "prompt" : card.buttonAction ?? "prompt";
393
+ const linkUrl = isSecondary ? card.secondaryButtonUrl?.trim() || void 0 : card.buttonUrl?.trim() || void 0;
394
+ const promptText = isSecondary ? card.secondaryButtonPrompt?.trim() || void 0 : card.buttonPrompt?.trim() || void 0;
265
395
  return {
266
- enabled,
267
- like: normalizeSide(raw?.like),
268
- dislike: normalizeSide(raw?.dislike)
396
+ kind: "carousel_cta",
397
+ cardId,
398
+ actionId: id,
399
+ buttonAction,
400
+ label,
401
+ linkUrl,
402
+ promptText
269
403
  };
270
404
  }
271
- function defaultFeedbackConfig() {
272
- return normalizeFeedbackConfig({ enabled: false });
273
- }
274
405
 
275
- // src/api/MessageFeedback.ts
276
- var METADATA_MAX_BYTES = 32768;
277
- function normalizeMetadata(meta) {
278
- if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
279
- if (Object.keys(meta).length === 0) return void 0;
280
- try {
281
- const s = JSON.stringify(meta);
282
- if (s.length > METADATA_MAX_BYTES) {
283
- console.warn(
284
- `[Firstflow] feedback metadata omitted (serialized length ${s.length} > ${METADATA_MAX_BYTES})`
285
- );
286
- return void 0;
406
+ // src/experience/hooks/useFirstflowSlotCandidates.ts
407
+ function asRecord(value) {
408
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
409
+ return value;
410
+ }
411
+ function isMessageNode(node) {
412
+ const data = asRecord(node.data);
413
+ const config = asRecord(data?.config);
414
+ const icon = typeof data?.icon === "string" ? data.icon.toLowerCase() : "";
415
+ const title = typeof data?.title === "string" ? data.title.toLowerCase() : "";
416
+ return icon === "message" || title === "message" || typeof config?.messageStyle === "string" || typeof config?.messageContent === "string" || Array.isArray(config?.quickReplies);
417
+ }
418
+ function isAnnouncementNode(node) {
419
+ const data = asRecord(node.data);
420
+ const config = asRecord(data?.config);
421
+ const icon = typeof data?.icon === "string" ? data.icon.toLowerCase() : "";
422
+ const title = typeof data?.title === "string" ? data.title.toLowerCase() : "";
423
+ return icon === "announcement" || title === "announcement" || typeof config?.announcementStyle === "string" || typeof config?.announcementTitle === "string" || typeof config?.announcementBody === "string";
424
+ }
425
+ function toNodeConfig(node) {
426
+ const cfg = node.data?.config;
427
+ return cfg && typeof cfg === "object" ? cfg : {};
428
+ }
429
+ function messageNodeTier(config) {
430
+ const rawStyle = typeof config.messageStyle === "string" ? config.messageStyle : void 0;
431
+ const normalized = normalizeFlowMessageStyle(rawStyle) ?? rawStyle;
432
+ const carouselCards = config.carouselCards;
433
+ const hasCarousel = normalized === "carousel" && Array.isArray(carouselCards) && carouselCards.length > 0;
434
+ if (hasCarousel) return 4;
435
+ if (normalized === "rich_card") return 3;
436
+ if (normalized === "card") return 2;
437
+ if (normalized === "quick_replies") return 1;
438
+ return 0;
439
+ }
440
+ function pickMessageNodeForSlot(typedNodes) {
441
+ const candidates = typedNodes.filter(
442
+ (n) => isMessageNode(n) && typeof n.id === "string"
443
+ );
444
+ if (candidates.length === 0) return null;
445
+ let best = candidates[0];
446
+ let bestTier = messageNodeTier(toNodeConfig(best));
447
+ for (let i = 1; i < candidates.length; i++) {
448
+ const c = candidates[i];
449
+ const t = messageNodeTier(toNodeConfig(c));
450
+ if (t > bestTier) {
451
+ bestTier = t;
452
+ best = c;
287
453
  }
288
- return JSON.parse(s);
289
- } catch {
290
- console.warn("[Firstflow] feedback metadata must be JSON-serializable; omitted");
291
- return void 0;
292
- }
293
- }
294
- function buildJitsuPayload(payload) {
295
- const payloadObj = {
296
- conversation_id: payload.conversationId.trim(),
297
- message_id: payload.messageId.trim(),
298
- rating: payload.rating,
299
- submitted_at: (/* @__PURE__ */ new Date()).toISOString()
300
- };
301
- if (payload.comment != null && String(payload.comment).trim()) {
302
- payloadObj.comment = String(payload.comment).trim();
303
454
  }
304
- if (payload.messagePreview != null && String(payload.messagePreview).trim()) {
305
- payloadObj.message_preview = String(payload.messagePreview).trim().slice(0, 2e3);
455
+ return best;
456
+ }
457
+ function buildTourMessageSteps(flow) {
458
+ const nodes = Array.isArray(flow?.nodes) ? flow.nodes : [];
459
+ const edges = Array.isArray(flow?.edges) ? flow.edges : [];
460
+ const typedNodes = nodes.filter((n) => !!n && typeof n === "object");
461
+ const nodeById = /* @__PURE__ */ new Map();
462
+ for (const n of typedNodes) {
463
+ if (typeof n.id === "string") nodeById.set(n.id, n);
306
464
  }
307
- if (payload.tags != null && payload.tags.length > 0) {
308
- payloadObj.tags = payload.tags;
465
+ const outgoing = /* @__PURE__ */ new Map();
466
+ for (const e of edges) {
467
+ const s = typeof e.source === "string" ? e.source : null;
468
+ const t = typeof e.target === "string" ? e.target : null;
469
+ if (!s || !t) continue;
470
+ if (!outgoing.has(s)) outgoing.set(s, []);
471
+ outgoing.get(s).push(t);
309
472
  }
310
- if (typeof payload.showTags === "boolean") {
311
- payloadObj.showTags = payload.showTags;
473
+ let startId = null;
474
+ if (nodeById.has("1")) {
475
+ startId = "1";
476
+ } else {
477
+ const incoming = /* @__PURE__ */ new Set();
478
+ for (const e of edges) {
479
+ if (typeof e.target === "string") incoming.add(e.target);
480
+ }
481
+ for (const id of nodeById.keys()) {
482
+ if (!incoming.has(id)) {
483
+ startId = id;
484
+ break;
485
+ }
486
+ }
312
487
  }
313
- if (typeof payload.showComment === "boolean") {
314
- payloadObj.showComment = payload.showComment;
488
+ if (!startId && nodeById.size > 0) {
489
+ startId = [...nodeById.keys()][0];
315
490
  }
316
- const metadata = normalizeMetadata(payload.metadata) ?? {};
317
- return {
318
- payload: JSON.stringify(payloadObj),
319
- metadata: JSON.stringify(metadata),
320
- rating: payload.rating
321
- // top-level for ClickHouse summary queries
322
- };
323
- }
324
- function createMessageFeedback(options) {
325
- const { agentId, analytics: analyticsOpt, config: rawConfig } = options;
326
- const analytics = analyticsOpt ?? createAnalytics({ agentId });
327
- let config = normalizeFeedbackConfig(rawConfig ?? void 0);
328
- return {
329
- getConfig() {
330
- return config;
331
- },
332
- setConfig(next) {
333
- config = next;
334
- },
335
- isEnabled() {
336
- return config.enabled === true;
337
- },
338
- async submit(payload) {
339
- const cid = payload.conversationId?.trim();
340
- const mid = payload.messageId?.trim();
341
- if (!cid) throw new Error("conversationId is required");
342
- if (!mid) throw new Error("messageId is required");
343
- if (payload.rating !== "like" && payload.rating !== "dislike") {
344
- throw new Error('rating must be "like" or "dislike"');
491
+ if (!startId) return null;
492
+ const orderedIds = [];
493
+ const visited = /* @__PURE__ */ new Set();
494
+ function visit(id) {
495
+ if (visited.has(id)) return;
496
+ visited.add(id);
497
+ const node = nodeById.get(id);
498
+ if (node && isMessageNode(node)) {
499
+ orderedIds.push(id);
500
+ }
501
+ const next = outgoing.get(id);
502
+ if (next) {
503
+ for (const tid of next) {
504
+ visit(tid);
345
505
  }
346
- analytics.track(
347
- FEEDBACK_SUBMITTED,
348
- buildJitsuPayload({ ...payload, conversationId: cid, messageId: mid })
349
- );
350
- },
351
- destroy() {
352
- config = defaultFeedbackConfig();
353
506
  }
354
- };
355
- }
356
-
357
- // src/api/Firstflow.ts
358
- var internalIssueMap = /* @__PURE__ */ new WeakMap();
359
- var internalFeedbackMap = /* @__PURE__ */ new WeakMap();
360
- function createIssueFacade(inner) {
361
- return {
362
- open(options) {
363
- inner.open(options);
364
- },
365
- async submit(data) {
366
- await inner.reportIssue(data);
367
- },
368
- getConfig() {
369
- return inner.getConfig();
370
- },
371
- destroy() {
372
- inner.destroy();
507
+ }
508
+ visit(startId);
509
+ for (const n of typedNodes) {
510
+ if (typeof n.id !== "string" || !isMessageNode(n)) continue;
511
+ if (!visited.has(n.id)) {
512
+ visit(n.id);
373
513
  }
374
- };
514
+ }
515
+ if (orderedIds.length === 0) return null;
516
+ return orderedIds.map((id) => {
517
+ const node = nodeById.get(id);
518
+ return { nodeId: id, config: toNodeConfig(node) };
519
+ });
375
520
  }
376
- function createFeedbackFacade(inner) {
377
- return {
378
- submit(payload) {
379
- return inner.submit(payload);
380
- },
381
- getConfig() {
382
- return inner.getConfig();
383
- },
384
- isEnabled() {
385
- return inner.isEnabled();
386
- }
387
- };
521
+ function deriveCandidate(row) {
522
+ const { experienceId, type, settings, flow } = row;
523
+ if (type === "survey") {
524
+ return { type: "survey", experienceId, settings: settings ?? null };
525
+ }
526
+ const nodes = Array.isArray(flow?.nodes) ? flow.nodes : [];
527
+ const typedNodes = nodes.filter((n) => !!n && typeof n === "object");
528
+ const messageNode = pickMessageNodeForSlot(typedNodes);
529
+ const announcementNode = typedNodes.find(isAnnouncementNode) ?? null;
530
+ if (type === "tour") {
531
+ const steps = buildTourMessageSteps(flow);
532
+ if (!steps?.length) return null;
533
+ return {
534
+ type: "message",
535
+ experienceId,
536
+ nodeId: steps[0].nodeId,
537
+ config: steps[0].config,
538
+ settings: settings ?? null,
539
+ tourSteps: steps
540
+ };
541
+ }
542
+ if (type === "message") {
543
+ if (!messageNode || typeof messageNode.id !== "string") return null;
544
+ return {
545
+ type: "message",
546
+ experienceId,
547
+ nodeId: messageNode.id,
548
+ config: toNodeConfig(messageNode),
549
+ settings: settings ?? null
550
+ };
551
+ }
552
+ if (type === "announcement") {
553
+ if (!announcementNode || typeof announcementNode.id !== "string") return null;
554
+ return {
555
+ type: "announcement",
556
+ experienceId,
557
+ nodeId: announcementNode.id,
558
+ config: toNodeConfig(announcementNode),
559
+ settings: settings ?? null
560
+ };
561
+ }
562
+ if (messageNode && typeof messageNode.id === "string") {
563
+ return {
564
+ type: "message",
565
+ experienceId,
566
+ nodeId: messageNode.id,
567
+ config: toNodeConfig(messageNode),
568
+ settings: settings ?? null
569
+ };
570
+ }
571
+ if (announcementNode && typeof announcementNode.id === "string") {
572
+ return {
573
+ type: "announcement",
574
+ experienceId,
575
+ nodeId: announcementNode.id,
576
+ config: toNodeConfig(announcementNode),
577
+ settings: settings ?? null
578
+ };
579
+ }
580
+ return null;
388
581
  }
389
- function getInternalIssueReporter(firstflow) {
390
- const inner = internalIssueMap.get(firstflow);
391
- if (!inner) throw new Error("Invalid Firstflow instance");
392
- return inner;
393
- }
394
- function getInternalMessageFeedback(firstflow) {
395
- const inner = internalFeedbackMap.get(firstflow);
396
- if (!inner) throw new Error("Invalid Firstflow instance");
397
- return inner;
398
- }
399
- function createFirstflow(options) {
400
- const { agentId, apiUrl, jitsuHost, jitsuWriteKey, feedbackConfig } = options;
401
- const analytics = createAnalytics({ agentId, jitsuHost, jitsuWriteKey });
402
- const inner = createIssueReporter({ agentId, apiUrl, analytics });
403
- const feedbackInner = createMessageFeedback({
404
- agentId,
405
- analytics,
406
- config: feedbackConfig ?? void 0
407
- });
408
- const firstflow = {
409
- get agentId() {
410
- return agentId;
411
- },
412
- get apiUrl() {
413
- return apiUrl;
414
- },
415
- analytics,
416
- issue: createIssueFacade(inner),
417
- feedback: createFeedbackFacade(feedbackInner)
418
- };
419
- internalIssueMap.set(firstflow, inner);
420
- internalFeedbackMap.set(firstflow, feedbackInner);
421
- return firstflow;
582
+ function useFirstflowSlotCandidates(typeFilter) {
583
+ const firstflow = useFirstflow();
584
+ const runtime = useFirstflowRuntimeSnapshot(firstflow);
585
+ const storage2 = useMemo3(() => createStorageAdapter(), []);
586
+ return useMemo3(() => {
587
+ const eligible = runEligibilityFilter(runtime.experienceRows, storage2, {
588
+ user: runtime.user,
589
+ overrides: runtime.overrides,
590
+ membershipMap: runtime.audienceMembership
591
+ });
592
+ return eligible.map((row) => deriveCandidate(row)).filter((candidate) => candidate != null).filter(
593
+ (candidate) => !typeFilter || typeFilter.includes(candidate.type)
594
+ );
595
+ }, [runtime.experienceRows, storage2, typeFilter, runtime.version]);
422
596
  }
423
597
 
424
- // src/context/FirstflowContext.tsx
425
- import { createContext as createContext2, useContext as useContext2 } from "react";
426
-
427
- // src/context/IssueReporterContext.tsx
428
- import {
429
- createContext,
430
- useCallback as useCallback3,
431
- useContext,
432
- useEffect as useEffect3,
433
- useRef,
434
- useState as useState2
435
- } from "react";
436
-
437
- // src/components/IssueModal.tsx
438
- import { useEffect as useEffect2 } from "react";
439
- import { createPortal } from "react-dom";
440
-
441
- // src/components/FormEngine.tsx
442
- import { useCallback as useCallback2 } from "react";
598
+ // src/experience/hooks/useFirstflowResolveExperience.ts
599
+ function useFirstflowResolveExperience(experienceId) {
600
+ const candidates = useFirstflowSlotCandidates();
601
+ return useMemo4(
602
+ () => candidates.find((candidate) => candidate.experienceId === experienceId) ?? null,
603
+ [candidates, experienceId]
604
+ );
605
+ }
443
606
 
444
- // src/hooks/useIssueForm.ts
445
- import { useCallback, useEffect, useState } from "react";
446
- function emptyValues(fields) {
447
- const init = {};
448
- fields.forEach((f) => {
449
- init[f.id] = "";
450
- });
451
- return init;
452
- }
453
- function useIssueForm() {
454
- const { reporter, openOptionsRef } = useIssueReporterContext();
455
- const config = reporter.getConfig();
456
- const fields = config.fields;
457
- const [values, setValuesState] = useState(() => emptyValues(fields));
458
- const [errors, setErrors] = useState({});
459
- const [submitting, setSubmitting] = useState(false);
460
- const [submitted, setSubmitted] = useState(false);
461
- const [submitError, setSubmitError] = useState(null);
462
- const fieldsKey = fields.map((f) => f.id).join("\0");
463
- useEffect(() => {
464
- setValuesState(emptyValues(fields));
465
- setErrors({});
466
- setSubmitted(false);
467
- setSubmitError(null);
468
- }, [fieldsKey, reporter]);
469
- const setValue = useCallback((fieldId, value) => {
470
- setValuesState((prev) => ({ ...prev, [fieldId]: value }));
471
- setErrors((prev) => prev[fieldId] ? { ...prev, [fieldId]: "" } : prev);
472
- }, []);
473
- const setValues = useCallback((next) => {
474
- setValuesState(next);
475
- }, []);
476
- const validate = useCallback(() => {
477
- const result = validatePayload(
478
- values,
479
- fields
480
- );
481
- if (!result.valid) {
482
- setErrors(result.errors ?? {});
483
- return false;
484
- }
485
- setErrors({});
486
- return true;
487
- }, [values, fields]);
488
- const submit = useCallback(async () => {
489
- if (!validate()) return;
490
- setSubmitting(true);
491
- setSubmitError(null);
492
- const context = openOptionsRef.current ?? {};
493
- try {
494
- await reporter.reportIssue({ ...values, ...context });
495
- setSubmitted(true);
496
- } catch (e) {
497
- const msg = e instanceof Error ? e.message : String(e);
498
- setSubmitError(msg);
499
- throw e;
500
- } finally {
501
- setSubmitting(false);
502
- }
503
- }, [validate, values, reporter, openOptionsRef]);
504
- const reset = useCallback(() => {
505
- setValuesState(emptyValues(fields));
506
- setErrors({});
507
- setSubmitted(false);
508
- setSubmitError(null);
509
- }, [fields]);
510
- const clearSubmitError = useCallback(() => setSubmitError(null), []);
511
- return {
512
- values,
513
- setValue,
514
- setValues,
515
- errors,
516
- submitting,
517
- submitted,
518
- submitError,
519
- validate,
520
- submit,
521
- reset,
522
- clearSubmitError,
523
- isEnabled: config.enabled,
524
- config,
525
- fields,
526
- promptText: config.promptText
527
- };
528
- }
529
-
530
- // src/components/FormEngine.module.css
531
- var FormEngine_default = {};
532
-
533
- // src/components/FormEngine.tsx
534
- import { jsx, jsxs } from "react/jsx-runtime";
535
- function FormEngine({
536
- onSubmitSuccess,
537
- onCancel,
538
- submitLabel = "Submit",
539
- cancelLabel = "Cancel"
540
- }) {
541
- const {
542
- values,
543
- setValue,
544
- errors,
545
- submitting,
546
- submit,
547
- submitError,
548
- isEnabled,
549
- fields,
550
- promptText
551
- } = useIssueForm();
552
- const handleSubmit = useCallback2(
553
- async (e) => {
554
- e.preventDefault();
555
- try {
556
- await submit();
557
- onSubmitSuccess?.();
558
- } catch {
559
- }
560
- },
561
- [submit, onSubmitSuccess]
562
- );
563
- if (!isEnabled) return null;
564
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: FormEngine_default.form, children: [
565
- promptText ? /* @__PURE__ */ jsx("p", { className: FormEngine_default.prompt, children: promptText }) : null,
566
- fields.map((field) => /* @__PURE__ */ jsxs("div", { className: FormEngine_default.field, children: [
567
- /* @__PURE__ */ jsxs("label", { htmlFor: field.id, className: FormEngine_default.label, children: [
568
- field.label,
569
- field.required ? " *" : ""
570
- ] }),
571
- field.type === "textarea" ? /* @__PURE__ */ jsx(
572
- "textarea",
573
- {
574
- id: field.id,
575
- className: errors[field.id] ? `${FormEngine_default.textarea} ${FormEngine_default.textareaError}` : FormEngine_default.textarea,
576
- value: values[field.id] ?? "",
577
- onChange: (e) => setValue(field.id, e.target.value),
578
- required: field.required
579
- }
580
- ) : field.type === "select" ? /* @__PURE__ */ jsxs(
581
- "select",
582
- {
583
- id: field.id,
584
- className: errors[field.id] ? `${FormEngine_default.select} ${FormEngine_default.selectError}` : FormEngine_default.select,
585
- value: values[field.id] ?? "",
586
- onChange: (e) => setValue(field.id, e.target.value),
587
- required: field.required,
588
- children: [
589
- /* @__PURE__ */ jsx("option", { value: "", children: "Select\u2026" }),
590
- (field.options ?? []).map((opt) => /* @__PURE__ */ jsx("option", { value: opt, children: opt }, opt))
591
- ]
592
- }
593
- ) : /* @__PURE__ */ jsx(
594
- "input",
595
- {
596
- id: field.id,
597
- type: "text",
598
- className: errors[field.id] ? `${FormEngine_default.input} ${FormEngine_default.inputError}` : FormEngine_default.input,
599
- value: values[field.id] ?? "",
600
- onChange: (e) => setValue(field.id, e.target.value),
601
- required: field.required
602
- }
603
- ),
604
- errors[field.id] ? /* @__PURE__ */ jsx("span", { className: FormEngine_default.errorText, children: errors[field.id] }) : null
605
- ] }, field.id)),
606
- submitError ? /* @__PURE__ */ jsx("p", { className: FormEngine_default.submitError, children: submitError }) : null,
607
- /* @__PURE__ */ jsxs("div", { className: FormEngine_default.actions, children: [
608
- onCancel ? /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, disabled: submitting, children: cancelLabel }) : null,
609
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: submitting, children: submitting ? "Sending\u2026" : submitLabel })
610
- ] })
611
- ] });
612
- }
613
-
614
- // src/components/IssueModal.module.css
615
- var IssueModal_default = {};
616
-
617
- // src/components/IssueModal.tsx
618
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
619
- function IssueModal() {
620
- const { isModalOpen, closeModal } = useIssueReporterContext();
621
- useEffect2(() => {
622
- if (!isModalOpen) return;
623
- const onKeyDown = (e) => {
624
- if (e.key === "Escape") closeModal();
625
- };
626
- document.addEventListener("keydown", onKeyDown);
627
- return () => document.removeEventListener("keydown", onKeyDown);
628
- }, [isModalOpen, closeModal]);
629
- if (!isModalOpen) return null;
630
- const content = /* @__PURE__ */ jsx2(
631
- "div",
632
- {
633
- className: IssueModal_default.overlay,
634
- role: "dialog",
635
- "aria-modal": "true",
636
- "aria-labelledby": "ff-issue-modal-title",
637
- onClick: (e) => e.target === e.currentTarget && closeModal(),
638
- children: /* @__PURE__ */ jsxs2("div", { className: IssueModal_default.panel, onClick: (e) => e.stopPropagation(), children: [
639
- /* @__PURE__ */ jsx2(
640
- "button",
641
- {
642
- type: "button",
643
- className: IssueModal_default.close,
644
- onClick: closeModal,
645
- "aria-label": "Close",
646
- children: "\xD7"
647
- }
648
- ),
649
- /* @__PURE__ */ jsx2("h2", { id: "ff-issue-modal-title", className: IssueModal_default.title, children: "Report an issue" }),
650
- /* @__PURE__ */ jsx2(
651
- FormEngine,
652
- {
653
- onSubmitSuccess: closeModal,
654
- onCancel: closeModal,
655
- submitLabel: "Submit",
656
- cancelLabel: "Cancel"
657
- }
658
- )
659
- ] })
660
- }
661
- );
662
- return createPortal(content, document.body);
663
- }
664
-
665
- // src/context/IssueReporterContext.tsx
666
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
667
- var IssueReporterContext = createContext(
668
- null
669
- );
670
- function IssueReporterProvider({
671
- reporter,
672
- children
673
- }) {
674
- const [isModalOpen, setIsModalOpen] = useState2(false);
675
- const openOptionsRef = useRef(void 0);
676
- const openModal = useCallback3((options) => {
677
- openOptionsRef.current = options;
678
- setIsModalOpen(true);
679
- }, []);
680
- const closeModal = useCallback3(() => {
681
- setIsModalOpen(false);
682
- openOptionsRef.current = void 0;
683
- }, []);
684
- useEffect3(() => {
685
- reporter.setOpenHandler(openModal);
686
- return () => {
687
- reporter.setOpenHandler(void 0);
688
- };
689
- }, [reporter, openModal]);
690
- const value = {
691
- reporter,
692
- isModalOpen,
693
- openModal,
694
- closeModal,
695
- openOptionsRef
696
- };
697
- return /* @__PURE__ */ jsxs3(IssueReporterContext.Provider, { value, children: [
698
- children,
699
- /* @__PURE__ */ jsx3(IssueModal, {})
700
- ] });
701
- }
702
- function useIssueReporterContext() {
703
- const ctx = useContext(IssueReporterContext);
704
- if (ctx == null) {
705
- throw new Error(
706
- "useIssueReporterContext must be used within IssueReporterProvider"
707
- );
708
- }
709
- return ctx;
710
- }
711
-
712
- // src/context/FirstflowContext.tsx
713
- import { jsx as jsx4 } from "react/jsx-runtime";
714
- var FirstflowContext = createContext2(null);
715
- var FirstflowConfigContext = createContext2(null);
716
- function FirstflowProvider({ firstflow, children, config = null }) {
717
- const internalIssue = getInternalIssueReporter(firstflow);
718
- return /* @__PURE__ */ jsx4(FirstflowConfigContext.Provider, { value: config ?? null, children: /* @__PURE__ */ jsx4(FirstflowContext.Provider, { value: firstflow, children: /* @__PURE__ */ jsx4(IssueReporterProvider, { reporter: internalIssue, children }) }) });
719
- }
720
- function useFirstflow() {
721
- const ctx = useContext2(FirstflowContext);
722
- if (ctx == null) {
723
- throw new Error("useFirstflow must be used within FirstflowProvider");
724
- }
725
- return ctx;
726
- }
727
- function useFirstflowConfig() {
728
- return useContext2(FirstflowConfigContext);
729
- }
730
-
731
- // src/hooks/useCreateFirstflow.ts
732
- import { useEffect as useEffect4, useState as useState3 } from "react";
733
-
734
- // src/config/fetcher.ts
735
- function extractAgentRecord(json) {
736
- if (!json || typeof json !== "object") return null;
737
- const o = json;
738
- const data = o.data;
739
- if (data?.agent && typeof data.agent === "object") {
740
- return data.agent;
741
- }
742
- if (o.agent && typeof o.agent === "object") {
743
- return o.agent;
744
- }
745
- if ("issues_config" in o || "feedback_config" in o) {
746
- return o;
747
- }
748
- return null;
749
- }
750
- function extractExperiencesFromResponse(json) {
751
- if (!json || typeof json !== "object") return [];
752
- const data = json.data;
753
- const raw = data?.experiences;
754
- if (!Array.isArray(raw)) return [];
755
- return raw.filter((x) => x != null && typeof x === "object");
756
- }
757
- async function fetchSdkAgentConfig(apiUrl, agentId) {
758
- const url = `${apiUrl.replace(/\/$/, "")}/agents/${agentId}/config`;
759
- const res = await fetch(url);
760
- if (!res.ok) throw new Error(`Failed to load config: ${res.status}`);
761
- const json = await res.json();
762
- const experiences = extractExperiencesFromResponse(json);
763
- const agent = extractAgentRecord(json);
764
- if (!agent) {
765
- return experiences.length ? { experiences } : {};
766
- }
767
- return {
768
- issues_config: agent.issues_config,
769
- feedback_config: agent.feedback_config,
770
- experiences
771
- };
772
- }
773
-
774
- // src/hooks/useCreateFirstflow.ts
775
- function useCreateFirstflow(agentId, apiUrl, options = {}) {
776
- const { fetchConfig = false } = options;
777
- const [firstflow, setFirstflow] = useState3(null);
778
- const [loading, setLoading] = useState3(true);
779
- const [error, setError] = useState3(null);
780
- const [config, setConfig] = useState3(null);
781
- useEffect4(() => {
782
- if (!agentId || !apiUrl) {
783
- setFirstflow(createFirstflow({ agentId: agentId || "", apiUrl: apiUrl || "" }));
784
- setConfig(null);
785
- setLoading(false);
786
- return;
787
- }
788
- if (!fetchConfig) {
789
- setFirstflow(createFirstflow({ agentId, apiUrl }));
790
- setConfig(null);
791
- setLoading(false);
792
- return;
793
- }
794
- let cancelled = false;
795
- setLoading(true);
796
- setError(null);
797
- setConfig(null);
798
- (async () => {
799
- try {
800
- const sdk = await fetchSdkAgentConfig(apiUrl, agentId);
801
- if (cancelled) return;
802
- const inst = createFirstflow({ agentId, apiUrl });
803
- getInternalIssueReporter(inst).setConfig(normalizeConfig(agentId, apiUrl, sdk.issues_config));
804
- getInternalMessageFeedback(inst).setConfig(normalizeFeedbackConfig(sdk.feedback_config));
805
- setFirstflow(inst);
806
- setConfig(sdk);
807
- } catch (e) {
808
- if (!cancelled) {
809
- setError(e instanceof Error ? e : new Error(String(e)));
810
- setConfig(null);
811
- }
812
- } finally {
813
- if (!cancelled) setLoading(false);
814
- }
815
- })();
816
- return () => {
817
- cancelled = true;
818
- };
819
- }, [agentId, apiUrl, fetchConfig]);
820
- return { firstflow, loading, error, config };
821
- }
822
-
823
- // src/hooks/useFeedback.ts
824
- import { useCallback as useCallback4, useEffect as useEffect5, useMemo, useRef as useRef2, useState as useState4 } from "react";
825
- function storageKey(agentId, messageId) {
826
- return `ff_feedback_${agentId}_${messageId}`;
827
- }
828
- function isMessageScope(o) {
829
- return o != null && typeof o.conversationId === "string" && typeof o.messageId === "string" && typeof o.messagePreview === "string";
830
- }
831
- function useFeedback(options) {
832
- const firstflow = useFirstflow();
833
- const scoped = isMessageScope(options);
834
- const [rating, setRatingState] = useState4(null);
835
- const [selectedTags, setSelectedTags] = useState4([]);
836
- const [comment, setCommentState] = useState4("");
837
- const [submitting, setSubmitting] = useState4(false);
838
- const [error, setError] = useState4(null);
839
- const [submitted, setSubmitted] = useState4(false);
840
- const isEnabled = firstflow.feedback.isEnabled();
841
- const config = isEnabled ? firstflow.feedback.getConfig() : null;
842
- const sideConfig = useMemo(() => {
843
- if (!scoped || !config || !rating) return null;
844
- return rating === "like" ? config.like : config.dislike;
845
- }, [scoped, config, rating]);
846
- const ratingRef = useRef2(rating);
847
- ratingRef.current = rating;
848
- const conversationId = scoped ? options.conversationId : "";
849
- const messageId = scoped ? options.messageId : "";
850
- const messagePreview = scoped ? options.messagePreview : "";
851
- const metadata = scoped ? options.metadata : void 0;
852
- useEffect5(() => {
853
- if (!scoped) return;
854
- setRatingState(null);
855
- setSelectedTags([]);
856
- setCommentState("");
857
- setError(null);
858
- setSubmitted(false);
859
- if (!isEnabled) return;
860
- try {
861
- const raw = localStorage.getItem(storageKey(firstflow.agentId, messageId));
862
- if (!raw) return;
863
- const parsed = JSON.parse(raw);
864
- if (parsed.rating === "like" || parsed.rating === "dislike") {
865
- setRatingState(parsed.rating);
866
- setSelectedTags(Array.isArray(parsed.tags) ? parsed.tags : []);
867
- setCommentState(typeof parsed.comment === "string" ? parsed.comment : "");
868
- }
869
- } catch {
870
- }
871
- }, [scoped, firstflow.agentId, isEnabled, messageId]);
872
- const setRating = useCallback4(
873
- (r) => {
874
- if (!scoped) return;
875
- if (r !== ratingRef.current) {
876
- setSelectedTags([]);
877
- setCommentState("");
878
- }
879
- setRatingState(r);
880
- setError(null);
881
- setSubmitted(false);
882
- },
883
- [scoped]
884
- );
885
- const toggleTag = useCallback4(
886
- (tag) => {
887
- if (!scoped) return;
888
- setSelectedTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]);
889
- setError(null);
890
- },
891
- [scoped]
892
- );
893
- const setComment = useCallback4(
894
- (c) => {
895
- if (!scoped) return;
896
- setCommentState(c);
897
- },
898
- [scoped]
899
- );
900
- const clearError = useCallback4(() => setError(null), []);
901
- const submit = useCallback4(async () => {
902
- if (!scoped || !isEnabled || !rating) return;
903
- setSubmitting(true);
904
- setError(null);
905
- const cfg = firstflow.feedback.getConfig();
906
- const sideCfg = rating === "like" ? cfg.like : cfg.dislike;
907
- const showTags = sideCfg.showTags === true;
908
- const showComment = sideCfg.showComment === true;
909
- const payload = {
910
- conversationId,
911
- messageId,
912
- rating,
913
- showTags,
914
- showComment,
915
- tags: showTags && selectedTags.length ? [...new Set(selectedTags)] : void 0,
916
- comment: showComment && comment.trim() ? comment.trim() : void 0,
917
- messagePreview: messagePreview.slice(0, 2e3),
918
- metadata
919
- };
920
- try {
921
- await firstflow.feedback.submit(payload);
922
- try {
923
- localStorage.setItem(
924
- storageKey(firstflow.agentId, messageId),
925
- JSON.stringify({ rating, tags: selectedTags, comment })
926
- );
927
- } catch {
928
- }
929
- setSubmitted(true);
930
- } catch (e) {
931
- setError(e instanceof Error ? e.message : "Failed to send feedback");
932
- } finally {
933
- setSubmitting(false);
934
- }
935
- }, [
936
- scoped,
937
- isEnabled,
938
- rating,
939
- conversationId,
940
- messageId,
941
- messagePreview,
942
- metadata,
943
- selectedTags,
944
- comment,
945
- firstflow,
946
- isEnabled
947
- ]);
948
- if (!scoped) {
949
- return firstflow.feedback;
950
- }
951
- return {
952
- rating,
953
- selectedTags,
954
- comment,
955
- submitting,
956
- submitted,
957
- error,
958
- setRating,
959
- toggleTag,
960
- setComment,
961
- submit,
962
- clearError,
963
- isEnabled,
964
- config,
965
- sideConfig
966
- };
967
- }
968
-
969
- // src/hooks/useExperienceMessageNode.ts
970
- import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo2, useRef as useRef3 } from "react";
971
-
972
- // src/experience/flowMessageNode.ts
973
- function normalizeFlowMessageStyle(style) {
974
- if (style === "inline") return "message";
975
- return style;
976
- }
977
- function normalizeFlowQuickReplyItem(entry) {
978
- if (typeof entry === "string") return { label: entry };
979
- if (entry && typeof entry === "object" && "label" in entry && typeof entry.label === "string") {
980
- const o = entry;
981
- return { label: o.label, prompt: o.prompt };
982
- }
983
- return { label: "" };
984
- }
985
- function normalizeFlowQuickReplyList(raw) {
986
- if (!Array.isArray(raw)) return [];
987
- return raw.map((x) => normalizeFlowQuickReplyItem(x));
988
- }
989
- var EXPERIENCE_MESSAGE_ANALYTICS_ACTION = {
990
- cta_primary: "cta_primary",
991
- cta_dismiss: "cta_dismiss",
992
- quick_reply: "quick_reply",
993
- carousel_cta: "carousel_cta"
994
- };
995
- var EXPERIENCE_MESSAGE_BLOCKS_VERSION = 4;
996
- function buildExperienceMessageBlocks(config) {
997
- const blocks = [];
998
- const content = config.messageContent?.trim();
999
- if (content) {
1000
- blocks.push({ type: "text", body: content });
1001
- }
1002
- const style = config.messageStyle;
1003
- if (style === "carousel" && config.carouselCards && config.carouselCards.length > 0) {
1004
- blocks.push({ type: "carousel", cards: config.carouselCards });
1005
- }
1006
- if (style === "quick_replies") {
1007
- const qr = normalizeFlowQuickReplyList(config.quickReplies).filter((q) => q.label.trim());
1008
- if (qr.length > 0) {
1009
- blocks.push({ type: "quick_replies", options: qr });
1010
- }
1011
- }
1012
- const ctaType = config.ctaType ?? "none";
1013
- const ctaText = config.ctaText?.trim();
1014
- const skipCtaBlocks = style === "quick_replies";
1015
- if (!skipCtaBlocks && (ctaType === "button" || ctaType === "link" || ctaType === "button_dismiss") && ctaText) {
1016
- const promptTrim = config.ctaPrompt?.trim();
1017
- blocks.push({
1018
- type: "cta_primary",
1019
- label: ctaText,
1020
- ctaType,
1021
- url: config.ctaUrl?.trim() || void 0,
1022
- promptText: ctaType === "button" || ctaType === "button_dismiss" ? promptTrim || void 0 : void 0
1023
- });
1024
- }
1025
- const dismissText = config.dismissText?.trim();
1026
- if (!skipCtaBlocks && (ctaType === "dismiss" || ctaType === "button_dismiss") && dismissText) {
1027
- blocks.push({ type: "cta_dismiss", label: dismissText });
1028
- }
1029
- return blocks;
1030
- }
1031
- function buildExperienceMessageUi(blocks, messageStyle) {
1032
- return {
1033
- hasText: blocks.some((b) => b.type === "text"),
1034
- hasCarousel: blocks.some((b) => b.type === "carousel"),
1035
- hasQuickReplies: blocks.some((b) => b.type === "quick_replies"),
1036
- hasPrimaryCta: blocks.some((b) => b.type === "cta_primary"),
1037
- hasDismissCta: blocks.some((b) => b.type === "cta_dismiss"),
1038
- messageStyle: normalizeFlowMessageStyle(messageStyle)
1039
- };
1040
- }
1041
- function buildExperienceCarouselCtaAction(config, cardId, actionId) {
1042
- const id = actionId === "secondary" ? "secondary" : "primary";
1043
- const card = config.carouselCards?.find((c) => c.id === cardId);
1044
- if (!card) {
1045
- return { kind: "carousel_cta", cardId, actionId: id };
1046
- }
1047
- const isSecondary = id === "secondary";
1048
- const label = isSecondary ? card.secondaryButtonText?.trim() : card.buttonText?.trim();
1049
- const buttonAction = isSecondary ? card.secondaryButtonAction ?? "prompt" : card.buttonAction ?? "prompt";
1050
- const linkUrl = isSecondary ? card.secondaryButtonUrl?.trim() || void 0 : card.buttonUrl?.trim() || void 0;
1051
- const promptText = isSecondary ? card.secondaryButtonPrompt?.trim() || void 0 : card.buttonPrompt?.trim() || void 0;
1052
- return {
1053
- kind: "carousel_cta",
1054
- cardId,
1055
- actionId: id,
1056
- buttonAction,
1057
- label,
1058
- linkUrl,
1059
- promptText
1060
- };
1061
- }
1062
-
1063
- // src/hooks/useExperienceMessageNode.ts
607
+ // src/experience/hooks/useExperienceMessageNode.ts
608
+ import { useCallback as useCallback2, useEffect, useMemo as useMemo5, useRef } from "react";
1064
609
  var shownImpressionKeys = /* @__PURE__ */ new Set();
1065
610
  function buildClickAnalyticsBase(firstflowAgentId, experienceId, nodeId, conversationId, messageStyle, metadata, action) {
1066
611
  const base = {
1067
612
  ...metadata ?? {},
1068
613
  agent_id: firstflowAgentId,
1069
614
  experience_id: experienceId,
615
+ experience_type: EXPERIENCE_ANALYTICS_TYPE.FLOW_MESSAGE,
1070
616
  node_id: nodeId,
1071
617
  message_style: normalizeFlowMessageStyle(messageStyle) ?? null,
1072
618
  action: EXPERIENCE_MESSAGE_ANALYTICS_ACTION[action.kind]
@@ -1098,6 +644,7 @@ function buildShownAnalyticsBase(firstflowAgentId, experienceId, nodeId, convers
1098
644
  ...metadata ?? {},
1099
645
  agent_id: firstflowAgentId,
1100
646
  experience_id: experienceId,
647
+ experience_type: EXPERIENCE_ANALYTICS_TYPE.FLOW_MESSAGE,
1101
648
  node_id: nodeId,
1102
649
  message_style: normalizeFlowMessageStyle(messageStyle) ?? null,
1103
650
  ...conversationId ? { conversation_id: conversationId } : {}
@@ -1117,13 +664,13 @@ function useExperienceMessageNode(options) {
1117
664
  onAction,
1118
665
  mapAnalytics
1119
666
  } = options;
1120
- const blocks = useMemo2(() => buildExperienceMessageBlocks(config), [config]);
1121
- const ui = useMemo2(
667
+ const blocks = useMemo5(() => buildExperienceMessageBlocks(config), [config]);
668
+ const ui = useMemo5(
1122
669
  () => buildExperienceMessageUi(blocks, config.messageStyle),
1123
670
  [blocks, config.messageStyle]
1124
671
  );
1125
672
  const impressionKey = impressionKeyOpt ?? `${firstflow.agentId}\0${experienceId}\0${nodeId}\0${conversationId ?? ""}`;
1126
- const reportShown = useCallback5(() => {
673
+ const reportShown = useCallback2(() => {
1127
674
  if (dedupeShown && shownImpressionKeys.has(impressionKey)) {
1128
675
  return;
1129
676
  }
@@ -1161,13 +708,13 @@ function useExperienceMessageNode(options) {
1161
708
  metadata,
1162
709
  mapAnalytics
1163
710
  ]);
1164
- const reportShownRef = useRef3(reportShown);
711
+ const reportShownRef = useRef(reportShown);
1165
712
  reportShownRef.current = reportShown;
1166
- useEffect6(() => {
713
+ useEffect(() => {
1167
714
  if (!trackShownOnMount) return;
1168
715
  reportShownRef.current();
1169
716
  }, [trackShownOnMount, impressionKey]);
1170
- const emitClick = useCallback5(
717
+ const emitClick = useCallback2(
1171
718
  (action) => {
1172
719
  try {
1173
720
  onAction?.(action);
@@ -1207,7 +754,7 @@ function useExperienceMessageNode(options) {
1207
754
  mapAnalytics
1208
755
  ]
1209
756
  );
1210
- const handlers = useMemo2(() => {
757
+ const handlers = useMemo5(() => {
1211
758
  return {
1212
759
  onPrimaryCta: () => {
1213
760
  const b = blocks.find((x) => x.type === "cta_primary");
@@ -1250,8 +797,8 @@ function useExperienceMessageNode(options) {
1250
797
  };
1251
798
  }
1252
799
 
1253
- // src/hooks/useExperienceAnnouncementNode.ts
1254
- import { useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo3, useRef as useRef4 } from "react";
800
+ // src/experience/hooks/useExperienceAnnouncementNode.ts
801
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo6, useRef as useRef2 } from "react";
1255
802
 
1256
803
  // src/experience/flowAnnouncementNode.ts
1257
804
  var EXPERIENCE_ANNOUNCEMENT_ANALYTICS_ACTION = {
@@ -1307,6 +854,9 @@ function normalizeFlowAnnouncementNodeConfig(raw) {
1307
854
  announcementTitle: r.announcementTitle != null ? String(r.announcementTitle) : void 0,
1308
855
  announcementBody: r.announcementBody != null ? String(r.announcementBody) : void 0,
1309
856
  announcementEmoji: r.announcementEmoji != null ? String(r.announcementEmoji) : void 0,
857
+ announcementImageUrl: r.announcementImageUrl != null ? String(r.announcementImageUrl) : void 0,
858
+ announcementBadge: r.announcementBadge != null ? String(r.announcementBadge) : void 0,
859
+ announcementBadgeIcon: r.announcementBadgeIcon != null ? String(r.announcementBadgeIcon) : void 0,
1310
860
  ctaText: r.ctaText != null ? String(r.ctaText) : void 0,
1311
861
  ctaUrl: r.ctaUrl != null ? String(r.ctaUrl) : void 0,
1312
862
  announcementDismissible: toBool(r.announcementDismissible),
@@ -1339,7 +889,8 @@ function buildExperienceAnnouncementUi(config, content) {
1339
889
  hasBody: Boolean(content.body),
1340
890
  hasEmoji: Boolean(content.emoji),
1341
891
  hasCta: Boolean(content.ctaLabel),
1342
- isDismissible: Boolean(config.announcementDismissible),
892
+ /** `undefined` / missing → dismissible (matches dashboard defaults). */
893
+ isDismissible: config.announcementDismissible !== false,
1343
894
  autoHide,
1344
895
  autoHideDelay: autoHide ? delay : 0,
1345
896
  position: config.announcementPosition,
@@ -1348,13 +899,14 @@ function buildExperienceAnnouncementUi(config, content) {
1348
899
  };
1349
900
  }
1350
901
 
1351
- // src/hooks/useExperienceAnnouncementNode.ts
902
+ // src/experience/hooks/useExperienceAnnouncementNode.ts
1352
903
  var shownImpressionKeys2 = /* @__PURE__ */ new Set();
1353
904
  function buildClickAnalyticsBase2(firstflowAgentId, experienceId, nodeId, conversationId, config, metadata, action) {
1354
905
  const base = {
1355
906
  ...metadata ?? {},
1356
907
  agent_id: firstflowAgentId,
1357
908
  experience_id: experienceId,
909
+ experience_type: EXPERIENCE_ANALYTICS_TYPE.FLOW_ANNOUNCEMENT,
1358
910
  node_id: nodeId,
1359
911
  announcement_style: config.announcementStyle ?? null,
1360
912
  announcement_theme: config.announcementTheme ?? null,
@@ -1372,6 +924,7 @@ function buildShownAnalyticsBase2(firstflowAgentId, experienceId, nodeId, conver
1372
924
  ...metadata ?? {},
1373
925
  agent_id: firstflowAgentId,
1374
926
  experience_id: experienceId,
927
+ experience_type: EXPERIENCE_ANALYTICS_TYPE.FLOW_ANNOUNCEMENT,
1375
928
  node_id: nodeId,
1376
929
  announcement_style: config.announcementStyle ?? null,
1377
930
  announcement_theme: config.announcementTheme ?? null,
@@ -1392,21 +945,21 @@ function useExperienceAnnouncementNode(options) {
1392
945
  onAction,
1393
946
  mapAnalytics
1394
947
  } = options;
1395
- const config = useMemo3(
948
+ const config = useMemo6(
1396
949
  () => normalizeFlowAnnouncementNodeConfig(configInput),
1397
950
  [configInput]
1398
951
  );
1399
- const content = useMemo3(() => buildExperienceAnnouncementContent(config), [config]);
1400
- const ui = useMemo3(() => buildExperienceAnnouncementUi(config, content), [config, content]);
952
+ const content = useMemo6(() => buildExperienceAnnouncementContent(config), [config]);
953
+ const ui = useMemo6(() => buildExperienceAnnouncementUi(config, content), [config, content]);
1401
954
  const impressionKey = impressionKeyOpt ?? `${firstflow.agentId}\0${experienceId}\0${nodeId}\0${conversationId ?? ""}`;
1402
- const timerRef = useRef4(null);
1403
- const clearAutoHideTimer = useCallback6(() => {
955
+ const timerRef = useRef2(null);
956
+ const clearAutoHideTimer = useCallback3(() => {
1404
957
  if (timerRef.current != null) {
1405
958
  clearTimeout(timerRef.current);
1406
959
  timerRef.current = null;
1407
960
  }
1408
961
  }, []);
1409
- const emitClick = useCallback6(
962
+ const emitClick = useCallback3(
1410
963
  (action) => {
1411
964
  try {
1412
965
  onAction?.(action);
@@ -1443,9 +996,9 @@ function useExperienceAnnouncementNode(options) {
1443
996
  },
1444
997
  [onAction, firstflow, experienceId, nodeId, conversationId, config, metadata, mapAnalytics]
1445
998
  );
1446
- const emitClickRef = useRef4(emitClick);
999
+ const emitClickRef = useRef2(emitClick);
1447
1000
  emitClickRef.current = emitClick;
1448
- useEffect7(() => {
1001
+ useEffect2(() => {
1449
1002
  if (!ui.autoHide || ui.autoHideDelay <= 0) return;
1450
1003
  timerRef.current = setTimeout(() => {
1451
1004
  timerRef.current = null;
@@ -1457,298 +1010,2715 @@ function useExperienceAnnouncementNode(options) {
1457
1010
  timerRef.current = null;
1458
1011
  }
1459
1012
  };
1460
- }, [ui.autoHide, ui.autoHideDelay, impressionKey]);
1461
- const reportShown = useCallback6(() => {
1462
- if (dedupeShown && shownImpressionKeys2.has(impressionKey)) {
1013
+ }, [ui.autoHide, ui.autoHideDelay, impressionKey]);
1014
+ const reportShown = useCallback3(() => {
1015
+ if (dedupeShown && shownImpressionKeys2.has(impressionKey)) {
1016
+ return;
1017
+ }
1018
+ const base = buildShownAnalyticsBase2(
1019
+ firstflow.agentId,
1020
+ experienceId,
1021
+ nodeId,
1022
+ conversationId,
1023
+ config,
1024
+ metadata
1025
+ );
1026
+ let finalProps = base;
1027
+ try {
1028
+ finalProps = mapAnalytics?.(EXPERIENCE_SHOWN, { ...base }) ?? base;
1029
+ } catch (e) {
1030
+ console.warn(
1031
+ "[Firstflow] useExperienceAnnouncementNode mapAnalytics (experience_shown)",
1032
+ e
1033
+ );
1034
+ finalProps = base;
1035
+ }
1036
+ try {
1037
+ firstflow.analytics.track(EXPERIENCE_SHOWN, finalProps);
1038
+ if (dedupeShown) {
1039
+ shownImpressionKeys2.add(impressionKey);
1040
+ }
1041
+ } catch (e) {
1042
+ console.warn(
1043
+ "[Firstflow] useExperienceAnnouncementNode analytics.track (experience_shown)",
1044
+ e
1045
+ );
1046
+ }
1047
+ }, [
1048
+ dedupeShown,
1049
+ impressionKey,
1050
+ firstflow,
1051
+ experienceId,
1052
+ nodeId,
1053
+ conversationId,
1054
+ config,
1055
+ metadata,
1056
+ mapAnalytics
1057
+ ]);
1058
+ const reportShownRef = useRef2(reportShown);
1059
+ reportShownRef.current = reportShown;
1060
+ useEffect2(() => {
1061
+ if (!trackShownOnMount) return;
1062
+ reportShownRef.current();
1063
+ }, [trackShownOnMount, impressionKey]);
1064
+ const dismiss = useCallback3(() => {
1065
+ clearAutoHideTimer();
1066
+ emitClick({ kind: "dismiss" });
1067
+ }, [clearAutoHideTimer, emitClick]);
1068
+ const ctaClick = useCallback3(() => {
1069
+ if (!content.ctaLabel) return;
1070
+ clearAutoHideTimer();
1071
+ emitClick({
1072
+ kind: "cta_click",
1073
+ label: content.ctaLabel,
1074
+ url: content.ctaUrl
1075
+ });
1076
+ }, [content.ctaLabel, content.ctaUrl, clearAutoHideTimer, emitClick]);
1077
+ const cancelAutoHide = useCallback3(() => {
1078
+ clearAutoHideTimer();
1079
+ }, [clearAutoHideTimer]);
1080
+ const handlers = useMemo6(
1081
+ () => ({
1082
+ dismiss,
1083
+ ctaClick,
1084
+ cancelAutoHide,
1085
+ reportShown
1086
+ }),
1087
+ [dismiss, ctaClick, cancelAutoHide, reportShown]
1088
+ );
1089
+ return {
1090
+ config,
1091
+ content,
1092
+ ui,
1093
+ blocksVersion: EXPERIENCE_ANNOUNCEMENT_BLOCKS_VERSION,
1094
+ handlers
1095
+ };
1096
+ }
1097
+
1098
+ // src/platform/mount.tsx
1099
+ import { createRoot } from "react-dom/client";
1100
+ import { Fragment, jsx } from "react/jsx-runtime";
1101
+ function mount(options, target) {
1102
+ const root = createRoot(target);
1103
+ root.render(
1104
+ /* @__PURE__ */ jsx(
1105
+ FirstflowProvider,
1106
+ {
1107
+ agentId: options.agentId,
1108
+ apiKey: options.apiKey,
1109
+ initialSdkPayload: options.initialSdkPayload,
1110
+ children: /* @__PURE__ */ jsx(Fragment, {})
1111
+ }
1112
+ )
1113
+ );
1114
+ return () => root.unmount();
1115
+ }
1116
+
1117
+ // src/components/ExperienceSlot.tsx
1118
+ import {
1119
+ useCallback as useCallback10,
1120
+ useEffect as useEffect9,
1121
+ useLayoutEffect as useLayoutEffect2,
1122
+ useMemo as useMemo13,
1123
+ useRef as useRef9,
1124
+ useState as useState8
1125
+ } from "react";
1126
+
1127
+ // src/components/classNames.ts
1128
+ function classNames(...parts) {
1129
+ return parts.filter(Boolean).join(" ");
1130
+ }
1131
+
1132
+ // src/components/Survey.tsx
1133
+ import React, {
1134
+ useCallback as useCallback4,
1135
+ useEffect as useEffect3,
1136
+ useMemo as useMemo7,
1137
+ useRef as useRef3,
1138
+ useState as useState2
1139
+ } from "react";
1140
+
1141
+ // src/components/Survey.css
1142
+ styleInject('.ffSurveyRoot[data-appearance=dark] {\n --ff-survey-surface: #1a1a1a;\n --ff-survey-shell-bg: #1e1e1e;\n --ff-survey-bg-muted: #2a2a2a;\n --ff-survey-border: #3a3a3a;\n --ff-survey-border-subtle: #2f2f2f;\n --ff-survey-row-divider: #333333;\n --ff-survey-text: #f3f3f3;\n --ff-survey-text-muted: #a8a8a8;\n --ff-survey-row-hover: rgba(255, 255, 255, 0.06);\n --ff-survey-row-active: rgba(255, 255, 255, 0.1);\n --ff-survey-row-selected: rgba(255, 255, 255, 0.08);\n --ff-survey-num-bg: #141414;\n --ff-survey-num-text: #c8c8c8;\n --ff-survey-num-border: #404040;\n --ff-survey-accent: #ffffff;\n --ff-survey-accent-contrast: #121212;\n --ff-survey-footer-bg: #252525;\n --ff-survey-nav-btn-bg: #1a1a1a;\n --ff-survey-nav-btn-border: #404040;\n --ff-survey-nav-btn-hover: #2a2a2a;\n --ff-survey-shortcuts: #8a8a8a;\n --ff-survey-shortcuts-dot: #555;\n --ff-survey-placeholder: #8f8f8f;\n --ff-survey-range-track-mute: #3a3a3a;\n --ff-survey-bubble-bg: #f3f3f3;\n --ff-survey-bubble-text: #1a1a1a;\n --ff-survey-shell-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), 0 1px 4px rgba(0, 0, 0, 0.28);\n}\n.ffSurveyRoot[data-appearance=light] {\n --ff-survey-surface: #ffffff;\n --ff-survey-shell-bg: #ffffff;\n --ff-survey-bg-muted: #f5f5f5;\n --ff-survey-border: #e5e5e5;\n --ff-survey-border-subtle: #eeeeee;\n --ff-survey-row-divider: #eeeeee;\n --ff-survey-text: #1a1a1a;\n --ff-survey-text-muted: #888888;\n --ff-survey-row-hover: rgba(0, 0, 0, 0.04);\n --ff-survey-row-active: #f0f0f0;\n --ff-survey-row-selected: #f0f0f0;\n --ff-survey-num-bg: #f5f5f5;\n --ff-survey-num-text: #888888;\n --ff-survey-num-border: #e5e5e5;\n --ff-survey-accent: #1a1a1a;\n --ff-survey-accent-contrast: #ffffff;\n --ff-survey-footer-bg: #f5f5f5;\n --ff-survey-nav-btn-bg: #ffffff;\n --ff-survey-nav-btn-border: #e0e0e0;\n --ff-survey-nav-btn-hover: #f0f0f0;\n --ff-survey-shortcuts: #bbbbbb;\n --ff-survey-shortcuts-dot: #cccccc;\n --ff-survey-placeholder: #737373;\n --ff-survey-range-track-mute: #e5e5e5;\n --ff-survey-bubble-bg: #1a1a1a;\n --ff-survey-bubble-text: #ffffff;\n --ff-survey-shell-shadow: 0 2px 10px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n.ffSurveyRoot {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n width: 100%;\n box-sizing: border-box;\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n background: transparent;\n border: none;\n border-radius: 0;\n color: var(--ff-survey-text);\n outline: none;\n}\n.ffSurveyShell {\n width: 100%;\n border: 1px solid var(--ff-survey-border);\n border-radius: 16px;\n overflow: hidden;\n background: var(--ff-survey-shell-bg);\n color: var(--ff-survey-text);\n box-shadow: var(--ff-survey-shell-shadow, 0 2px 12px rgba(0, 0, 0, 0.08));\n}\n.ffSurveyTopBar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 16px 24px 10px;\n}\n.ffSurveyQuestionTitle {\n margin: 0;\n flex: 1;\n min-width: 0;\n font-size: 16px;\n font-weight: 500;\n font-family: inherit;\n line-height: 1.4;\n color: var(--ff-survey-text);\n}\n.ffSurveyHeaderControls {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n flex-shrink: 0;\n}\n.ffSurveyHeaderIconBtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 36px;\n height: 36px;\n min-width: 36px;\n min-height: 36px;\n padding: 0;\n border: 1px solid var(--ff-survey-nav-btn-border);\n border-radius: 10px;\n background: var(--ff-survey-nav-btn-bg);\n color: var(--ff-survey-text-muted);\n cursor: pointer;\n transition:\n background 0.12s ease,\n color 0.12s ease,\n opacity 0.12s ease;\n}\n.ffSurveyHeaderIconBtn svg {\n display: block;\n flex-shrink: 0;\n}\n.ffSurveyHeaderIconBtn:hover:not(:disabled) {\n background: var(--ff-survey-nav-btn-hover);\n color: var(--ff-survey-text);\n}\n.ffSurveyHeaderIconBtn:disabled {\n opacity: 0.3;\n cursor: default;\n}\n.ffSurveyHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 12px;\n}\n.ffSurveyTitle {\n margin: 0;\n font-size: 18px;\n font-family:\n Georgia,\n "Times New Roman",\n serif;\n line-height: 1.2;\n}\n.ffSurveyStep {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 36px;\n color: var(--ff-survey-text-muted);\n font-size: 13px;\n line-height: 1;\n padding: 0 8px;\n white-space: nowrap;\n}\n.ffSurveyQuestionPanel {\n animation: ffSurveyPanelIn 0.24s ease-out;\n min-height: 1px;\n}\n.ffSurveyQuestionPanelPadded {\n padding: 0 24px 20px;\n}\n@keyframes ffSurveyPanelIn {\n from {\n opacity: 0;\n transform: translateY(6px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .ffSurveyQuestionPanel {\n animation: none;\n }\n}\n.ffSurveyQuestion {\n margin: 0 0 10px;\n font-size: 14px;\n line-height: 1.4;\n}\n.ffSurveyChoices {\n display: flex;\n flex-direction: column;\n}\n.ffSurveyChoiceRow {\n display: flex;\n align-items: center;\n gap: 14px;\n width: 100%;\n margin: 0;\n padding: 15px 24px;\n border: none;\n border-bottom: 1px solid var(--ff-survey-row-divider);\n border-radius: 0;\n background: transparent;\n color: var(--ff-survey-text);\n text-align: left;\n font-family: inherit;\n font-size: 15px;\n line-height: 1.35;\n cursor: pointer;\n transition: background 0.12s ease;\n}\n.ffSurveyChoiceRow:last-child {\n border-bottom: none;\n}\n.ffSurveyChoiceRow:hover {\n background: var(--ff-survey-row-hover);\n}\n.ffSurveyChoiceNum {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: 34px;\n height: 34px;\n min-height: 34px;\n padding: 0 10px;\n border-radius: 999px;\n border: 1px solid var(--ff-survey-num-border);\n background: var(--ff-survey-num-bg);\n color: var(--ff-survey-num-text);\n font-size: 14px;\n font-weight: 500;\n line-height: 0;\n flex-shrink: 0;\n transition:\n background 0.12s ease,\n color 0.12s ease,\n border-color 0.12s ease;\n}\n.ffSurveyChoiceLabel {\n flex: 1;\n min-width: 0;\n}\n.ffSurveyChoiceActive:not(.ffSurveyChoiceSelected) {\n background: var(--ff-survey-row-hover);\n}\n.ffSurveyChoiceSelected {\n background: var(--ff-survey-row-selected);\n}\n.ffSurveyChoiceSelected .ffSurveyChoiceLabel {\n font-weight: 500;\n}\n.ffSurveyChoiceSelected .ffSurveyChoiceNum {\n background: var(--ff-survey-accent);\n color: var(--ff-survey-accent-contrast);\n border-color: var(--ff-survey-accent);\n}\n.ffSurveyScaleBody {\n padding: 8px 24px 24px;\n}\n.ffSurveyScaleSliderWrap {\n position: relative;\n padding-top: 28px;\n margin-bottom: 10px;\n}\n.ffSurveyScaleBubble {\n position: absolute;\n top: 0;\n left: 0;\n transform: translateX(-50%);\n background: var(--ff-survey-bubble-bg);\n color: var(--ff-survey-bubble-text);\n font-size: 12px;\n font-weight: 500;\n padding: 3px 8px;\n border-radius: 6px;\n white-space: nowrap;\n pointer-events: none;\n line-height: 1.2;\n}\n.ffSurveyScaleBubble::after {\n content: "";\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n border: 4px solid transparent;\n border-top-color: var(--ff-survey-bubble-bg);\n}\n.ffSurveyScaleBigNum {\n font-size: 48px;\n font-weight: 500;\n color: var(--ff-survey-text);\n text-align: center;\n line-height: 1;\n margin: 4px 0 6px;\n}\n.ffSurveyScaleMood {\n text-align: center;\n font-size: 13px;\n color: var(--ff-survey-text-muted);\n margin-bottom: 20px;\n min-height: 18px;\n}\n.ffSurveyScaleEmojiRow {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 16px;\n padding: 0 2px;\n}\n.ffSurveyScaleEmoji {\n font-size: 26px;\n line-height: 1;\n opacity: 0.22;\n transition: opacity 0.15s ease, transform 0.15s ease;\n}\n.ffSurveyScaleEmojiActive {\n opacity: 1;\n transform: scale(1.22);\n}\n.ffSurveyScaleStarRow {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 16px;\n padding: 0 4px;\n gap: 4px;\n}\n.ffSurveyScaleStar {\n font-size: 22px;\n line-height: 1;\n color: var(--ff-survey-text-muted);\n opacity: 0.28;\n transition:\n opacity 0.15s ease,\n transform 0.15s ease,\n color 0.15s ease;\n}\n.ffSurveyScaleStarActive {\n opacity: 1;\n color: var(--ff-survey-accent);\n transform: scale(1.08);\n}\n.ffSurveyScaleRange {\n width: 100%;\n height: 4px;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 2px;\n outline: none;\n cursor: pointer;\n box-sizing: border-box;\n}\n.ffSurveyScaleRange::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n background: var(--ff-survey-accent);\n border: 3px solid var(--ff-survey-shell-bg);\n box-shadow: 0 0 0 1.5px var(--ff-survey-accent);\n cursor: grab;\n transition: transform 0.1s ease;\n}\n.ffSurveyScaleRange:active::-webkit-slider-thumb {\n transform: scale(1.12);\n cursor: grabbing;\n}\n.ffSurveyScaleRange::-moz-range-thumb {\n width: 22px;\n height: 22px;\n border-radius: 50%;\n background: var(--ff-survey-accent);\n border: 3px solid var(--ff-survey-shell-bg);\n box-shadow: 0 0 0 1.5px var(--ff-survey-accent);\n cursor: grab;\n}\n.ffSurveyScaleRange::-moz-range-track {\n height: 4px;\n border-radius: 2px;\n background: transparent;\n}\n.ffSurveyScaleRangeLabels {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: var(--ff-survey-text-muted);\n margin-top: 8px;\n gap: 12px;\n}\n.ffSurveyScaleRangeLabels span {\n min-width: 0;\n}\n@media (prefers-reduced-motion: reduce) {\n .ffSurveyScaleEmoji,\n .ffSurveyScaleStar,\n .ffSurveyScaleRange::-webkit-slider-thumb {\n transition: none;\n }\n}\n.ffSurveyInput {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--ff-survey-border);\n border-radius: 10px;\n background: var(--ff-survey-bg-muted);\n color: var(--ff-survey-text);\n padding: 12px 14px;\n font-size: 14px;\n font-family: inherit;\n}\n.ffSurveyInput::placeholder {\n color: var(--ff-survey-placeholder);\n}\n.ffSurveyRow {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n margin-top: 12px;\n}\n.ffSurveyFooterBar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 11px 16px 11px 20px;\n background: var(--ff-survey-footer-bg);\n border-top: 1px solid var(--ff-survey-row-divider);\n}\n.ffSurveyFooterBarStart {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 10px;\n min-height: 36px;\n}\n.ffSurveyFooterBarEnd {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n flex-shrink: 0;\n}\n.ffSurveySkipButton {\n background: var(--ff-survey-nav-btn-bg);\n border: 1px solid var(--ff-survey-nav-btn-border);\n border-radius: 8px;\n padding: 7px 16px;\n font-size: 13px;\n font-weight: 400;\n font-family: inherit;\n color: var(--ff-survey-text);\n cursor: pointer;\n white-space: nowrap;\n flex-shrink: 0;\n transition: background 0.12s ease;\n}\n.ffSurveySkipButton:hover {\n background: var(--ff-survey-nav-btn-hover);\n}\n.ffSurveyButton {\n border: 1px solid var(--ff-survey-border);\n border-radius: 10px;\n background: transparent;\n color: var(--ff-survey-text);\n padding: 8px 16px;\n cursor: pointer;\n font-size: 14px;\n font-family: inherit;\n}\n.ffSurveyButtonPrimary {\n background: var(--ff-survey-accent);\n color: var(--ff-survey-accent-contrast);\n border-color: var(--ff-survey-accent);\n border-radius: 8px;\n padding: 7px 16px;\n font-size: 13px;\n font-weight: 500;\n}\n.ffSurveyButtonPrimary:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ffSurveyButtonMuted {\n color: var(--ff-survey-text-muted);\n border-color: var(--ff-survey-border);\n background: transparent;\n}\n.ffSurveyShortcuts {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-wrap: wrap;\n gap: 6px;\n margin: 0;\n padding: 0;\n font-size: 12px;\n line-height: 1.4;\n color: var(--ff-survey-shortcuts);\n}\n.ffSurveyShortcutsDot {\n color: var(--ff-survey-shortcuts-dot);\n user-select: none;\n}\n.ffSurveyMessage {\n font-size: 13px;\n margin-top: 10px;\n max-width: 660px;\n width: 100%;\n text-align: center;\n}\n.ffSurveyError {\n color: #c44;\n}\n.ffSurveyRoot[data-appearance=dark] .ffSurveyError {\n color: #f49a9a;\n}\n.ffSurveySuccess {\n color: #2a7a2a;\n}\n.ffSurveyRoot[data-appearance=dark] .ffSurveySuccess {\n color: #a3e3a3;\n}\n.ffSurveyTopBarSpacer {\n flex: 1;\n min-width: 0;\n}\n.ffSurveyQuestionPanelThankYou {\n padding: 0;\n}\n.ffSurveyTyRoot {\n position: relative;\n overflow: hidden;\n min-height: 220px;\n animation: ffSurveyTyEnter 0.35s ease both;\n}\n@keyframes ffSurveyTyEnter {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ffSurveyTyCanvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n.ffSurveyTyBody {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 40px 24px 32px;\n text-align: center;\n position: relative;\n z-index: 1;\n}\n.ffSurveyTyIconCircle {\n width: 68px;\n height: 68px;\n border-radius: 50%;\n background: var(--ff-survey-num-bg);\n border: 1px solid var(--ff-survey-num-border);\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 22px;\n font-size: 30px;\n line-height: 1;\n animation: ffSurveyTyPop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s both;\n}\n@keyframes ffSurveyTyPop {\n from {\n transform: scale(0);\n opacity: 0;\n }\n to {\n transform: scale(1);\n opacity: 1;\n }\n}\n@keyframes ffSurveyTyFadeUp {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ffSurveyTyTitle {\n font-size: 20px;\n font-weight: 500;\n color: var(--ff-survey-text);\n margin: 0 0 10px;\n line-height: 1.3;\n max-width: 420px;\n animation: ffSurveyTyFadeUp 0.3s ease 0.2s both;\n}\n.ffSurveyTySubtitle {\n font-size: 14px;\n color: var(--ff-survey-text-muted);\n line-height: 1.6;\n max-width: 320px;\n margin: 0;\n animation: ffSurveyTyFadeUp 0.3s ease 0.25s both;\n}\n.ffSurveyTyFooter {\n border-top: 1px solid var(--ff-survey-row-divider);\n padding: 12px 20px;\n background: var(--ff-survey-footer-bg);\n display: flex;\n justify-content: center;\n position: relative;\n z-index: 1;\n}\n.ffSurveyTyDoneBtn {\n background: var(--ff-survey-accent);\n color: var(--ff-survey-accent-contrast);\n border: none;\n border-radius: 8px;\n padding: 9px 32px;\n font-size: 14px;\n font-weight: 400;\n font-family: inherit;\n cursor: pointer;\n white-space: nowrap;\n transition: opacity 0.12s ease;\n animation: ffSurveyTyFadeUp 0.3s ease 0.3s both;\n}\n.ffSurveyTyDoneBtn:hover:not(:disabled) {\n opacity: 0.88;\n}\n.ffSurveyTyDoneBtn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n@media (prefers-reduced-motion: reduce) {\n .ffSurveyTyRoot {\n animation: none;\n }\n .ffSurveyTyIconCircle,\n .ffSurveyTyTitle,\n .ffSurveyTySubtitle,\n .ffSurveyTyDoneBtn {\n animation: none;\n }\n}\n');
1143
+
1144
+ // src/components/Survey.tsx
1145
+ import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
1146
+ var shownImpressionKeys3 = /* @__PURE__ */ new Set();
1147
+ var styles = {
1148
+ root: "ffSurveyRoot",
1149
+ shell: "ffSurveyShell",
1150
+ header: "ffSurveyHeader",
1151
+ title: "ffSurveyTitle",
1152
+ step: "ffSurveyStep",
1153
+ topBar: "ffSurveyTopBar",
1154
+ questionTitle: "ffSurveyQuestionTitle",
1155
+ headerControls: "ffSurveyHeaderControls",
1156
+ headerIconBtn: "ffSurveyHeaderIconBtn",
1157
+ questionPanel: "ffSurveyQuestionPanel",
1158
+ questionPanelPadded: "ffSurveyQuestionPanelPadded",
1159
+ question: "ffSurveyQuestion",
1160
+ choices: "ffSurveyChoices",
1161
+ choiceRow: "ffSurveyChoiceRow",
1162
+ choiceNum: "ffSurveyChoiceNum",
1163
+ choiceLabel: "ffSurveyChoiceLabel",
1164
+ choiceActive: "ffSurveyChoiceActive",
1165
+ choiceSelected: "ffSurveyChoiceSelected",
1166
+ scaleBody: "ffSurveyScaleBody",
1167
+ scaleSliderWrap: "ffSurveyScaleSliderWrap",
1168
+ scaleBubble: "ffSurveyScaleBubble",
1169
+ scaleBigNum: "ffSurveyScaleBigNum",
1170
+ scaleMood: "ffSurveyScaleMood",
1171
+ scaleEmojiRow: "ffSurveyScaleEmojiRow",
1172
+ scaleEmoji: "ffSurveyScaleEmoji",
1173
+ scaleEmojiActive: "ffSurveyScaleEmojiActive",
1174
+ scaleStarRow: "ffSurveyScaleStarRow",
1175
+ scaleStar: "ffSurveyScaleStar",
1176
+ scaleStarActive: "ffSurveyScaleStarActive",
1177
+ scaleRange: "ffSurveyScaleRange",
1178
+ scaleRangeLabels: "ffSurveyScaleRangeLabels",
1179
+ input: "ffSurveyInput",
1180
+ row: "ffSurveyRow",
1181
+ footerBar: "ffSurveyFooterBar",
1182
+ footerBarStart: "ffSurveyFooterBarStart",
1183
+ footerBarEnd: "ffSurveyFooterBarEnd",
1184
+ button: "ffSurveyButton",
1185
+ buttonPrimary: "ffSurveyButtonPrimary",
1186
+ buttonMuted: "ffSurveyButtonMuted",
1187
+ skipButton: "ffSurveySkipButton",
1188
+ shortcuts: "ffSurveyShortcuts",
1189
+ shortcutsDot: "ffSurveyShortcutsDot",
1190
+ message: "ffSurveyMessage",
1191
+ error: "ffSurveyError",
1192
+ success: "ffSurveySuccess",
1193
+ topBarSpacer: "ffSurveyTopBarSpacer",
1194
+ questionPanelThankYou: "ffSurveyQuestionPanelThankYou",
1195
+ tyRoot: "ffSurveyTyRoot",
1196
+ tyCanvas: "ffSurveyTyCanvas",
1197
+ tyBody: "ffSurveyTyBody",
1198
+ tyIconCircle: "ffSurveyTyIconCircle",
1199
+ tyTitle: "ffSurveyTyTitle",
1200
+ tySubtitle: "ffSurveyTySubtitle",
1201
+ tyFooter: "ffSurveyTyFooter",
1202
+ tyDoneBtn: "ffSurveyTyDoneBtn"
1203
+ };
1204
+ var THANK_YOU_DEFAULT_SUBTITLE = "Your feedback means a lot. We'll use it to make things better.";
1205
+ var THANK_YOU_CONFETTI_COLORS = [
1206
+ "#1a1a1a",
1207
+ "#888888",
1208
+ "#c4c4c4",
1209
+ "#5dcaa5",
1210
+ "#7f77dd",
1211
+ "#ef9f27",
1212
+ "#d4537e"
1213
+ ];
1214
+ function launchThankYouConfetti(canvas, colors) {
1215
+ const ctx = canvas.getContext("2d");
1216
+ if (!ctx) return () => {
1217
+ };
1218
+ const c = ctx;
1219
+ const W = canvas.offsetWidth;
1220
+ const H = canvas.offsetHeight;
1221
+ if (W < 16 || H < 16) return () => {
1222
+ };
1223
+ canvas.width = W;
1224
+ canvas.height = H;
1225
+ const pieces = Array.from({ length: 80 }, () => ({
1226
+ x: W / 2 + (Math.random() - 0.5) * 60,
1227
+ y: H / 2,
1228
+ vx: (Math.random() - 0.5) * 9,
1229
+ vy: -Math.random() * 11 - 4,
1230
+ size: Math.random() * 7 + 3,
1231
+ color: colors[Math.floor(Math.random() * colors.length)] ?? colors[0],
1232
+ rot: Math.random() * 360,
1233
+ vrot: (Math.random() - 0.5) * 14,
1234
+ gravity: 0.3,
1235
+ alpha: 1,
1236
+ shape: Math.random() > 0.5 ? "rect" : "circle"
1237
+ }));
1238
+ let rafId = 0;
1239
+ function draw() {
1240
+ c.clearRect(0, 0, W, H);
1241
+ let anyAlive = false;
1242
+ for (const p of pieces) {
1243
+ p.x += p.vx;
1244
+ p.y += p.vy;
1245
+ p.vy += p.gravity;
1246
+ p.rot += p.vrot;
1247
+ p.alpha -= 0.011;
1248
+ if (p.alpha > 0) anyAlive = true;
1249
+ c.save();
1250
+ c.globalAlpha = Math.max(0, p.alpha);
1251
+ c.translate(p.x, p.y);
1252
+ c.rotate(p.rot * Math.PI / 180);
1253
+ c.fillStyle = p.color;
1254
+ if (p.shape === "rect") {
1255
+ c.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
1256
+ } else {
1257
+ c.beginPath();
1258
+ c.arc(0, 0, p.size / 2, 0, Math.PI * 2);
1259
+ c.fill();
1260
+ }
1261
+ c.restore();
1262
+ }
1263
+ if (anyAlive) {
1264
+ rafId = requestAnimationFrame(draw);
1265
+ } else {
1266
+ c.clearRect(0, 0, W, H);
1267
+ }
1268
+ }
1269
+ rafId = requestAnimationFrame(draw);
1270
+ return () => cancelAnimationFrame(rafId);
1271
+ }
1272
+ function ThankYouStep({
1273
+ question,
1274
+ showDone,
1275
+ onDone,
1276
+ disabled
1277
+ }) {
1278
+ const canvasRef = useRef3(null);
1279
+ const cleanupRef = useRef3(null);
1280
+ const title = question.questionText?.trim() || "Thank you!";
1281
+ const subtitle = question.placeholder?.trim() || THANK_YOU_DEFAULT_SUBTITLE;
1282
+ const confettiOn = question.showConfetti !== false;
1283
+ const fireConfetti = useCallback4(() => {
1284
+ if (!confettiOn || !canvasRef.current) return;
1285
+ cleanupRef.current?.();
1286
+ cleanupRef.current = launchThankYouConfetti(
1287
+ canvasRef.current,
1288
+ THANK_YOU_CONFETTI_COLORS
1289
+ );
1290
+ }, [confettiOn]);
1291
+ useEffect3(() => {
1292
+ if (!confettiOn) return;
1293
+ if (typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
1294
+ return;
1295
+ }
1296
+ const t = window.setTimeout(fireConfetti, 300);
1297
+ return () => {
1298
+ window.clearTimeout(t);
1299
+ cleanupRef.current?.();
1300
+ cleanupRef.current = null;
1301
+ };
1302
+ }, [confettiOn, fireConfetti, question.id]);
1303
+ const handleDone = () => {
1304
+ fireConfetti();
1305
+ onDone();
1306
+ };
1307
+ return /* @__PURE__ */ jsxs("div", { className: styles.tyRoot, children: [
1308
+ /* @__PURE__ */ jsx2("canvas", { ref: canvasRef, className: styles.tyCanvas, "aria-hidden": true }),
1309
+ /* @__PURE__ */ jsxs("div", { className: styles.tyBody, children: [
1310
+ /* @__PURE__ */ jsx2("div", { className: styles.tyIconCircle, "aria-hidden": true, children: "\u{1F389}" }),
1311
+ /* @__PURE__ */ jsx2("p", { className: styles.tyTitle, children: title }),
1312
+ /* @__PURE__ */ jsx2("p", { className: styles.tySubtitle, children: subtitle })
1313
+ ] }),
1314
+ showDone ? /* @__PURE__ */ jsx2("div", { className: styles.tyFooter, children: /* @__PURE__ */ jsx2(
1315
+ "button",
1316
+ {
1317
+ type: "button",
1318
+ className: styles.tyDoneBtn,
1319
+ onClick: handleDone,
1320
+ disabled,
1321
+ children: "Done"
1322
+ }
1323
+ ) }) : null
1324
+ ] });
1325
+ }
1326
+ function isChoiceStep(question) {
1327
+ if (!question) return false;
1328
+ return question.type === "multiple_choice" || question.type === "concept_test";
1329
+ }
1330
+ function isScaleQuestionType(q) {
1331
+ if (!q) return false;
1332
+ return q.type === "nps" || q.type === "opinion_scale" || q.type === "slider";
1333
+ }
1334
+ var SCALE_MOODS = [
1335
+ "Terrible",
1336
+ "Very bad",
1337
+ "Bad",
1338
+ "Poor",
1339
+ "Below average",
1340
+ "Okay",
1341
+ "Decent",
1342
+ "Good",
1343
+ "Great",
1344
+ "Excellent",
1345
+ "Outstanding"
1346
+ ];
1347
+ var SCALE_EMOJIS = ["\u{1F61E}", "\u{1F615}", "\u{1F610}", "\u{1F642}", "\u{1F604}"];
1348
+ var SCALE_STAR_COUNT = 5;
1349
+ function readScaleStep(question) {
1350
+ const s = question.step;
1351
+ if (typeof s === "number" && Number.isFinite(s) && s > 0) return s;
1352
+ return 1;
1353
+ }
1354
+ function snapScaleValue(raw, min, max, step) {
1355
+ const steps = Math.round((raw - min) / step);
1356
+ let v = min + steps * step;
1357
+ if (!Number.isFinite(v)) v = min;
1358
+ const p = 10 ** Math.min(6, decimalsForStep(step));
1359
+ v = Math.round(v * p) / p;
1360
+ return Math.min(max, Math.max(min, v));
1361
+ }
1362
+ function decimalsForStep(step) {
1363
+ const t = String(step);
1364
+ if (!t.includes(".")) return 0;
1365
+ return t.split(".")[1]?.length ?? 0;
1366
+ }
1367
+ function formatScaleDisplay(value, step) {
1368
+ if (step >= 1 && Number.isInteger(step)) return String(Math.round(value));
1369
+ const d = Math.min(4, decimalsForStep(step) || 2);
1370
+ return value.toFixed(d);
1371
+ }
1372
+ function scaleUiVariant(question) {
1373
+ if (question.scaleType === "emojis") return "emoji";
1374
+ if (question.scaleType === "stars") return "stars";
1375
+ if (question.type === "slider") return "bubble";
1376
+ return "bignumber";
1377
+ }
1378
+ function moodForScale(value, min, max) {
1379
+ if (max === min) return SCALE_MOODS[5];
1380
+ const t = (value - min) / (max - min);
1381
+ const i = Math.round(t * (SCALE_MOODS.length - 1));
1382
+ return SCALE_MOODS[Math.min(SCALE_MOODS.length - 1, Math.max(0, i))] ?? SCALE_MOODS[5];
1383
+ }
1384
+ function emojiSlotIndex(value, min, max, len) {
1385
+ if (len <= 0) return 0;
1386
+ if (max === min) return 0;
1387
+ const t = (value - min) / (max - min);
1388
+ return Math.min(len - 1, Math.floor(t * len));
1389
+ }
1390
+ function starFillUpto(value, min, max) {
1391
+ if (max === min) return 0;
1392
+ const t = (value - min) / (max - min);
1393
+ return Math.min(
1394
+ SCALE_STAR_COUNT - 1,
1395
+ Math.round(t * (SCALE_STAR_COUNT - 1))
1396
+ );
1397
+ }
1398
+ function IconArrowLeft() {
1399
+ return /* @__PURE__ */ jsxs(
1400
+ "svg",
1401
+ {
1402
+ xmlns: "http://www.w3.org/2000/svg",
1403
+ width: 12,
1404
+ height: 12,
1405
+ viewBox: "0 0 24 24",
1406
+ fill: "none",
1407
+ stroke: "currentColor",
1408
+ strokeWidth: 2,
1409
+ strokeLinecap: "round",
1410
+ strokeLinejoin: "round",
1411
+ "aria-hidden": true,
1412
+ children: [
1413
+ /* @__PURE__ */ jsx2("path", { d: "m12 19-7-7 7-7" }),
1414
+ /* @__PURE__ */ jsx2("path", { d: "M19 12H5" })
1415
+ ]
1416
+ }
1417
+ );
1418
+ }
1419
+ function IconArrowRight() {
1420
+ return /* @__PURE__ */ jsxs(
1421
+ "svg",
1422
+ {
1423
+ xmlns: "http://www.w3.org/2000/svg",
1424
+ width: 12,
1425
+ height: 12,
1426
+ viewBox: "0 0 24 24",
1427
+ fill: "none",
1428
+ stroke: "currentColor",
1429
+ strokeWidth: 2,
1430
+ strokeLinecap: "round",
1431
+ strokeLinejoin: "round",
1432
+ "aria-hidden": true,
1433
+ children: [
1434
+ /* @__PURE__ */ jsx2("path", { d: "M5 12h14" }),
1435
+ /* @__PURE__ */ jsx2("path", { d: "m12 5 7 7-7 7" })
1436
+ ]
1437
+ }
1438
+ );
1439
+ }
1440
+ function hasValidAnswer2(question, value) {
1441
+ if (question.type === "text_block" || question.type === "thank_you")
1442
+ return true;
1443
+ if (value == null) return false;
1444
+ if (Array.isArray(value)) return value.length > 0;
1445
+ if (typeof value === "number") return Number.isFinite(value);
1446
+ if (typeof value === "string") return value.trim().length > 0;
1447
+ return false;
1448
+ }
1449
+ function asText(value) {
1450
+ if (typeof value === "string") return value;
1451
+ if (typeof value === "number") return String(value);
1452
+ return "";
1453
+ }
1454
+ function readScale(question) {
1455
+ const min = Number.isFinite(question.minValue) ? Number(question.minValue) : 0;
1456
+ const max = Number.isFinite(question.maxValue) ? Number(question.maxValue) : 10;
1457
+ return { min, max };
1458
+ }
1459
+ function isTextEntryElement(target) {
1460
+ if (!(target instanceof HTMLElement)) return false;
1461
+ const tag = target.tagName;
1462
+ if (tag === "TEXTAREA") return true;
1463
+ if (tag === "INPUT") {
1464
+ const type = target.type?.toLowerCase() ?? "";
1465
+ if (type === "checkbox" || type === "radio" || type === "button" || type === "submit")
1466
+ return false;
1467
+ return true;
1468
+ }
1469
+ return target.isContentEditable;
1470
+ }
1471
+ function renderScaleInput(question, answer, setAnswer) {
1472
+ const { min, max } = readScale(question);
1473
+ const step = readScaleStep(question);
1474
+ const value = typeof answer === "number" && Number.isFinite(answer) ? snapScaleValue(answer, min, max, step) : min;
1475
+ const pct = max === min ? 100 : (value - min) / (max - min) * 100;
1476
+ const trackBackground = `linear-gradient(to right, var(--ff-survey-accent) 0%, var(--ff-survey-accent) ${pct}%, var(--ff-survey-range-track-mute) ${pct}%, var(--ff-survey-range-track-mute) 100%)`;
1477
+ const variant = scaleUiVariant(question);
1478
+ const minCap = (question.minLabel?.trim() || "").length > 0 ? question.minLabel : String(min);
1479
+ const maxCap = (question.maxLabel?.trim() || "").length > 0 ? question.maxLabel : String(max);
1480
+ const rangeInput = /* @__PURE__ */ jsx2(
1481
+ "input",
1482
+ {
1483
+ className: styles.scaleRange,
1484
+ type: "range",
1485
+ min,
1486
+ max,
1487
+ step,
1488
+ value,
1489
+ "aria-valuemin": min,
1490
+ "aria-valuemax": max,
1491
+ "aria-valuenow": value,
1492
+ style: { background: trackBackground },
1493
+ onChange: (e) => {
1494
+ const v = snapScaleValue(Number(e.target.value), min, max, step);
1495
+ setAnswer(question.id, v);
1496
+ }
1497
+ }
1498
+ );
1499
+ let body;
1500
+ if (variant === "bubble") {
1501
+ body = /* @__PURE__ */ jsxs(Fragment2, { children: [
1502
+ /* @__PURE__ */ jsxs("div", { className: styles.scaleSliderWrap, children: [
1503
+ /* @__PURE__ */ jsx2("div", { className: styles.scaleBubble, style: { left: `${pct}%` }, children: formatScaleDisplay(value, step) }),
1504
+ rangeInput
1505
+ ] }),
1506
+ /* @__PURE__ */ jsxs("div", { className: styles.scaleRangeLabels, children: [
1507
+ /* @__PURE__ */ jsx2("span", { children: minCap }),
1508
+ /* @__PURE__ */ jsx2("span", { children: maxCap })
1509
+ ] })
1510
+ ] });
1511
+ } else if (variant === "bignumber") {
1512
+ body = /* @__PURE__ */ jsxs(Fragment2, { children: [
1513
+ /* @__PURE__ */ jsx2("div", { className: styles.scaleBigNum, children: formatScaleDisplay(value, step) }),
1514
+ /* @__PURE__ */ jsx2("div", { className: styles.scaleMood, children: moodForScale(value, min, max) }),
1515
+ rangeInput,
1516
+ /* @__PURE__ */ jsxs("div", { className: styles.scaleRangeLabels, children: [
1517
+ /* @__PURE__ */ jsx2("span", { children: minCap }),
1518
+ /* @__PURE__ */ jsx2("span", { children: maxCap })
1519
+ ] })
1520
+ ] });
1521
+ } else if (variant === "emoji") {
1522
+ const ei = emojiSlotIndex(value, min, max, SCALE_EMOJIS.length);
1523
+ body = /* @__PURE__ */ jsxs(Fragment2, { children: [
1524
+ /* @__PURE__ */ jsx2("div", { className: styles.scaleEmojiRow, children: SCALE_EMOJIS.map((em, i) => /* @__PURE__ */ jsx2(
1525
+ "span",
1526
+ {
1527
+ className: classNames(
1528
+ styles.scaleEmoji,
1529
+ i === ei ? styles.scaleEmojiActive : void 0
1530
+ ),
1531
+ "aria-hidden": true,
1532
+ children: em
1533
+ },
1534
+ em
1535
+ )) }),
1536
+ rangeInput,
1537
+ /* @__PURE__ */ jsxs("div", { className: styles.scaleRangeLabels, children: [
1538
+ /* @__PURE__ */ jsx2("span", { children: minCap }),
1539
+ /* @__PURE__ */ jsx2("span", { children: maxCap })
1540
+ ] })
1541
+ ] });
1542
+ } else {
1543
+ const upto = starFillUpto(value, min, max);
1544
+ body = /* @__PURE__ */ jsxs(Fragment2, { children: [
1545
+ /* @__PURE__ */ jsx2("div", { className: styles.scaleStarRow, children: Array.from({ length: SCALE_STAR_COUNT }, (_, i) => /* @__PURE__ */ jsx2(
1546
+ "span",
1547
+ {
1548
+ className: classNames(
1549
+ styles.scaleStar,
1550
+ i <= upto ? styles.scaleStarActive : void 0
1551
+ ),
1552
+ "aria-hidden": true,
1553
+ children: "\u2605"
1554
+ },
1555
+ i
1556
+ )) }),
1557
+ rangeInput,
1558
+ /* @__PURE__ */ jsxs("div", { className: styles.scaleRangeLabels, children: [
1559
+ /* @__PURE__ */ jsx2("span", { children: minCap }),
1560
+ /* @__PURE__ */ jsx2("span", { children: maxCap })
1561
+ ] })
1562
+ ] });
1563
+ }
1564
+ return /* @__PURE__ */ jsx2("div", { className: styles.scaleBody, children: body });
1565
+ }
1566
+ function renderQuestionInput(question, answer, focusedIndex, onFocusedIndexChange, setAnswer, onAfterChoiceSelect) {
1567
+ if (question.type === "text_block" || question.type === "thank_you") {
1568
+ return null;
1569
+ }
1570
+ if (question.type === "multiple_choice" || question.type === "concept_test") {
1571
+ const options = question.options ?? [];
1572
+ return /* @__PURE__ */ jsx2("div", { className: styles.choices, role: "listbox", "aria-label": "Choices", children: options.map((opt, index) => {
1573
+ const selected = answer === opt;
1574
+ const active = focusedIndex === index;
1575
+ return /* @__PURE__ */ jsxs(
1576
+ "button",
1577
+ {
1578
+ type: "button",
1579
+ role: "option",
1580
+ "aria-selected": selected,
1581
+ className: classNames(
1582
+ styles.choiceRow,
1583
+ active ? styles.choiceActive : void 0,
1584
+ selected ? styles.choiceSelected : void 0
1585
+ ),
1586
+ onMouseEnter: () => onFocusedIndexChange(index),
1587
+ onClick: () => {
1588
+ if (answer === opt) return;
1589
+ setAnswer(question.id, opt);
1590
+ onAfterChoiceSelect?.();
1591
+ },
1592
+ children: [
1593
+ /* @__PURE__ */ jsx2("span", { className: styles.choiceNum, "aria-hidden": true, children: index + 1 }),
1594
+ /* @__PURE__ */ jsx2("span", { className: styles.choiceLabel, children: opt })
1595
+ ]
1596
+ },
1597
+ `${question.id}:${opt}`
1598
+ );
1599
+ }) });
1600
+ }
1601
+ if (question.type === "nps" || question.type === "opinion_scale" || question.type === "slider") {
1602
+ return renderScaleInput(question, answer, setAnswer);
1603
+ }
1604
+ const multiline = question.multiline === true || question.type === "open_question";
1605
+ if (multiline) {
1606
+ return /* @__PURE__ */ jsx2(
1607
+ "textarea",
1608
+ {
1609
+ className: styles.input,
1610
+ rows: 4,
1611
+ value: asText(answer),
1612
+ placeholder: question.placeholder ?? "Type your answer",
1613
+ onChange: (e) => setAnswer(question.id, e.target.value)
1614
+ }
1615
+ );
1616
+ }
1617
+ return /* @__PURE__ */ jsx2(
1618
+ "input",
1619
+ {
1620
+ className: styles.input,
1621
+ value: asText(answer),
1622
+ placeholder: question.placeholder ?? "Type your answer",
1623
+ onChange: (e) => setAnswer(question.id, e.target.value)
1624
+ }
1625
+ );
1626
+ }
1627
+ function Survey({
1628
+ experienceId,
1629
+ layout = "paginated",
1630
+ appearance = "auto",
1631
+ conversationId,
1632
+ metadata,
1633
+ onSubmit,
1634
+ onSubmitted,
1635
+ onError,
1636
+ onDismissed,
1637
+ className,
1638
+ classNames: classNameMap
1639
+ }) {
1640
+ const firstflow = useFirstflow();
1641
+ const survey = useFirstflowSurvey({
1642
+ experienceId,
1643
+ conversationId,
1644
+ metadata
1645
+ });
1646
+ const {
1647
+ questions,
1648
+ currentIndex,
1649
+ currentQuestion,
1650
+ answers,
1651
+ setAnswer,
1652
+ next,
1653
+ prev,
1654
+ canSubmit,
1655
+ submit,
1656
+ submitted,
1657
+ submitting,
1658
+ error,
1659
+ clearError
1660
+ } = survey;
1661
+ const [focusedChoiceIndex, setFocusedChoiceIndex] = useState2(0);
1662
+ const [resolvedAppearance, setResolvedAppearance] = useState2("dark");
1663
+ const hadSubmittedRef = useRef3(false);
1664
+ const rootRef = useRef3(null);
1665
+ useEffect3(() => {
1666
+ setFocusedChoiceIndex(0);
1667
+ }, [currentQuestion?.id]);
1668
+ useEffect3(() => {
1669
+ if (appearance !== "auto") {
1670
+ setResolvedAppearance(appearance);
1671
+ return;
1672
+ }
1673
+ if (typeof window === "undefined") return;
1674
+ const mq = window.matchMedia("(prefers-color-scheme: light)");
1675
+ const apply = () => setResolvedAppearance(mq.matches ? "light" : "dark");
1676
+ apply();
1677
+ mq.addEventListener("change", apply);
1678
+ return () => mq.removeEventListener("change", apply);
1679
+ }, [appearance]);
1680
+ const agentId = firstflow.agentId;
1681
+ const metadataJson = JSON.stringify(metadata ?? {});
1682
+ useEffect3(() => {
1683
+ const impressionKey = `${agentId}\0${experienceId}\0${conversationId ?? ""}`;
1684
+ if (shownImpressionKeys3.has(impressionKey)) return;
1685
+ shownImpressionKeys3.add(impressionKey);
1686
+ const shownAt = (/* @__PURE__ */ new Date()).toISOString();
1687
+ firstflow.analytics.track(EXPERIENCE_SHOWN, {
1688
+ agent_id: agentId,
1689
+ experience_id: experienceId,
1690
+ experience_type: EXPERIENCE_ANALYTICS_TYPE.SURVEY,
1691
+ survey_id: experienceId,
1692
+ conversation_id: conversationId?.trim() || void 0,
1693
+ shown_at: shownAt,
1694
+ payload: JSON.stringify({
1695
+ experience_id: experienceId,
1696
+ experience_type: EXPERIENCE_ANALYTICS_TYPE.SURVEY,
1697
+ survey_id: experienceId,
1698
+ conversation_id: conversationId?.trim() || void 0,
1699
+ shown_at: shownAt
1700
+ }),
1701
+ metadata: metadataJson
1702
+ });
1703
+ }, [firstflow.analytics, agentId, experienceId, conversationId, metadataJson]);
1704
+ useEffect3(() => {
1705
+ if (error) onError?.(error);
1706
+ }, [error, onError]);
1707
+ useEffect3(() => {
1708
+ if (!submitted || hadSubmittedRef.current) return;
1709
+ hadSubmittedRef.current = true;
1710
+ onSubmit?.(answers);
1711
+ onSubmitted?.();
1712
+ }, [submitted, answers, onSubmit, onSubmitted]);
1713
+ useEffect3(() => {
1714
+ if (!submitted) hadSubmittedRef.current = false;
1715
+ }, [submitted]);
1716
+ const hasQuestions = questions.length > 0;
1717
+ const isLastQuestion = currentIndex >= questions.length - 1;
1718
+ const activeQuestion = currentQuestion;
1719
+ const choiceCount = useMemo7(() => {
1720
+ if (!activeQuestion) return 0;
1721
+ if (activeQuestion.type === "multiple_choice" || activeQuestion.type === "concept_test") {
1722
+ return activeQuestion.options?.length ?? 0;
1723
+ }
1724
+ return 0;
1725
+ }, [activeQuestion]);
1726
+ const canLeaveCurrentStep = activeQuestion != null && (!activeQuestion.required || hasValidAnswer2(activeQuestion, answers[activeQuestion.id] ?? null));
1727
+ const showSkip = layout === "paginated" && !isLastQuestion && !submitted && activeQuestion?.type !== "thank_you" && activeQuestion != null && !activeQuestion.required;
1728
+ const showNextArrow = layout === "paginated" && !isLastQuestion && canLeaveCurrentStep && !submitted;
1729
+ const handleSubmit = useCallback4(async () => {
1730
+ clearError();
1731
+ await submit();
1732
+ }, [clearError, submit]);
1733
+ const skipCurrentQuestion = useCallback4(() => {
1734
+ if (submitted || isLastQuestion || !canLeaveCurrentStep) return;
1735
+ next();
1736
+ }, [submitted, isLastQuestion, canLeaveCurrentStep, next]);
1737
+ const advanceAfterChoiceSelect = useCallback4(() => {
1738
+ if (layout !== "paginated" || isLastQuestion || submitting || submitted) return;
1739
+ next();
1740
+ }, [layout, isLastQuestion, submitting, submitted, next]);
1741
+ const thankYouDone = useCallback4(() => {
1742
+ if (submitted || submitting) return;
1743
+ if (isLastQuestion) void handleSubmit();
1744
+ else next();
1745
+ }, [submitted, submitting, isLastQuestion, handleSubmit, next]);
1746
+ const handleGlobalKeyDown = useCallback4(
1747
+ (event) => {
1748
+ if (submitted) return;
1749
+ if (event.key === "Escape") {
1750
+ if (!showSkip) return;
1751
+ event.preventDefault();
1752
+ event.stopPropagation();
1753
+ skipCurrentQuestion();
1754
+ return;
1755
+ }
1756
+ if (layout !== "paginated" || !activeQuestion) return;
1757
+ const inTextEntry = isTextEntryElement(event.target);
1758
+ if (isScaleQuestionType(activeQuestion) && !inTextEntry) {
1759
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
1760
+ const { min, max } = readScale(activeQuestion);
1761
+ const step = readScaleStep(activeQuestion);
1762
+ const curRaw = answers[activeQuestion.id];
1763
+ const cur = typeof curRaw === "number" && Number.isFinite(curRaw) ? snapScaleValue(curRaw, min, max, step) : min;
1764
+ const delta = event.key === "ArrowLeft" ? -step : step;
1765
+ const nv = snapScaleValue(cur + delta, min, max, step);
1766
+ event.preventDefault();
1767
+ event.stopPropagation();
1768
+ if (nv !== cur || curRaw == null) {
1769
+ setAnswer(activeQuestion.id, nv);
1770
+ }
1771
+ return;
1772
+ }
1773
+ }
1774
+ if (event.key === "ArrowLeft" && !inTextEntry) {
1775
+ if (currentIndex <= 0) return;
1776
+ event.preventDefault();
1777
+ event.stopPropagation();
1778
+ prev();
1779
+ return;
1780
+ }
1781
+ if (event.key === "ArrowRight" && !inTextEntry) {
1782
+ if (!showNextArrow) return;
1783
+ event.preventDefault();
1784
+ event.stopPropagation();
1785
+ next();
1786
+ return;
1787
+ }
1788
+ if (inTextEntry && event.key !== "Enter") return;
1789
+ if (event.key === "ArrowDown" && choiceCount > 0) {
1790
+ event.preventDefault();
1791
+ event.stopPropagation();
1792
+ setFocusedChoiceIndex((prevIndex) => (prevIndex + 1) % choiceCount);
1793
+ return;
1794
+ }
1795
+ if (event.key === "ArrowUp" && choiceCount > 0) {
1796
+ event.preventDefault();
1797
+ event.stopPropagation();
1798
+ setFocusedChoiceIndex(
1799
+ (prevIndex) => (prevIndex - 1 + choiceCount) % choiceCount
1800
+ );
1801
+ return;
1802
+ }
1803
+ if (event.key !== "Enter") return;
1804
+ if (inTextEntry) return;
1805
+ if ((activeQuestion.type === "multiple_choice" || activeQuestion.type === "concept_test") && (activeQuestion.options?.length ?? 0) > 0) {
1806
+ event.preventDefault();
1807
+ event.stopPropagation();
1808
+ setAnswer(
1809
+ activeQuestion.id,
1810
+ activeQuestion.options?.[focusedChoiceIndex] ?? null
1811
+ );
1812
+ advanceAfterChoiceSelect();
1813
+ return;
1814
+ }
1815
+ event.preventDefault();
1816
+ event.stopPropagation();
1817
+ if (isLastQuestion) void handleSubmit();
1818
+ else next();
1819
+ },
1820
+ [
1821
+ submitted,
1822
+ layout,
1823
+ activeQuestion,
1824
+ showSkip,
1825
+ skipCurrentQuestion,
1826
+ currentIndex,
1827
+ prev,
1828
+ showNextArrow,
1829
+ next,
1830
+ choiceCount,
1831
+ focusedChoiceIndex,
1832
+ setAnswer,
1833
+ answers,
1834
+ isLastQuestion,
1835
+ handleSubmit,
1836
+ advanceAfterChoiceSelect
1837
+ ]
1838
+ );
1839
+ useEffect3(() => {
1840
+ const el = rootRef.current;
1841
+ if (!el) return;
1842
+ el.addEventListener("keydown", handleGlobalKeyDown, true);
1843
+ return () => el.removeEventListener("keydown", handleGlobalKeyDown, true);
1844
+ }, [handleGlobalKeyDown]);
1845
+ if (!hasQuestions) {
1846
+ return /* @__PURE__ */ jsx2(
1847
+ "div",
1848
+ {
1849
+ ref: rootRef,
1850
+ className: classNames(styles.root, className, classNameMap?.root),
1851
+ "data-appearance": resolvedAppearance,
1852
+ children: /* @__PURE__ */ jsx2("p", { className: styles.question, children: "No survey questions are available for this experience yet." })
1853
+ }
1854
+ );
1855
+ }
1856
+ const showContinue = layout === "paginated" && !isLastQuestion && !submitted && activeQuestion?.type !== "thank_you";
1857
+ const renderPaginationFooter = () => /* @__PURE__ */ jsxs("div", { className: classNames(styles.footerBar, classNameMap?.nav), children: [
1858
+ /* @__PURE__ */ jsx2("div", { className: styles.footerBarStart, children: showSkip ? /* @__PURE__ */ jsx2(
1859
+ "button",
1860
+ {
1861
+ type: "button",
1862
+ className: styles.skipButton,
1863
+ onClick: skipCurrentQuestion,
1864
+ children: "Skip"
1865
+ }
1866
+ ) : null }),
1867
+ /* @__PURE__ */ jsxs("div", { className: styles.footerBarEnd, children: [
1868
+ showContinue ? /* @__PURE__ */ jsx2(
1869
+ "button",
1870
+ {
1871
+ type: "button",
1872
+ className: classNames(styles.button, styles.buttonPrimary),
1873
+ onClick: next,
1874
+ disabled: !canLeaveCurrentStep || submitting,
1875
+ children: "Continue"
1876
+ }
1877
+ ) : null,
1878
+ isLastQuestion ? /* @__PURE__ */ jsx2(
1879
+ "button",
1880
+ {
1881
+ type: "button",
1882
+ className: classNames(styles.button, styles.buttonPrimary),
1883
+ onClick: () => void handleSubmit(),
1884
+ disabled: !canSubmit || submitting,
1885
+ children: submitting ? "Submitting..." : "Submit"
1886
+ }
1887
+ ) : null
1888
+ ] })
1889
+ ] });
1890
+ const renderShortcuts = () => {
1891
+ if (layout !== "paginated" || submitted) return null;
1892
+ const onThankYou = activeQuestion != null && activeQuestion.type === "thank_you";
1893
+ const onScale = activeQuestion != null && isScaleQuestionType(activeQuestion);
1894
+ const segments = onThankYou ? ["Enter to continue"] : onScale ? ["\u2190 \u2192 to adjust", "Enter to continue"] : ["\u2191\u2193 to navigate", "Enter to select"];
1895
+ if (showSkip) segments.push("Esc to skip");
1896
+ return /* @__PURE__ */ jsx2("div", { className: styles.shortcuts, "aria-hidden": true, children: segments.map((text, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
1897
+ i > 0 ? /* @__PURE__ */ jsx2("span", { className: styles.shortcutsDot, children: "\xB7" }) : null,
1898
+ /* @__PURE__ */ jsx2("span", { children: text })
1899
+ ] }, text)) });
1900
+ };
1901
+ const renderQuestion = (question, index) => /* @__PURE__ */ jsxs("section", { children: [
1902
+ /* @__PURE__ */ jsxs("div", { className: styles.header, children: [
1903
+ /* @__PURE__ */ jsx2("h3", { className: styles.title, children: "Survey" }),
1904
+ /* @__PURE__ */ jsxs("span", { className: styles.step, children: [
1905
+ index + 1,
1906
+ " of ",
1907
+ questions.length
1908
+ ] })
1909
+ ] }),
1910
+ question.type !== "thank_you" ? /* @__PURE__ */ jsx2("p", { className: classNames(styles.question, classNameMap?.question), children: question.questionText || "Untitled question" }) : null,
1911
+ question.type === "thank_you" ? /* @__PURE__ */ jsx2(
1912
+ ThankYouStep,
1913
+ {
1914
+ question,
1915
+ showDone: false,
1916
+ onDone: () => {
1917
+ }
1918
+ }
1919
+ ) : renderQuestionInput(
1920
+ question,
1921
+ answers[question.id] ?? null,
1922
+ focusedChoiceIndex,
1923
+ setFocusedChoiceIndex,
1924
+ setAnswer
1925
+ )
1926
+ ] }, question.id);
1927
+ const paginatedHeader = activeQuestion ? /* @__PURE__ */ jsxs("div", { className: styles.topBar, children: [
1928
+ activeQuestion.type === "thank_you" ? /* @__PURE__ */ jsx2("div", { className: styles.topBarSpacer, "aria-hidden": true }) : /* @__PURE__ */ jsx2("h3", { className: styles.questionTitle, children: activeQuestion.questionText || "Untitled question" }),
1929
+ /* @__PURE__ */ jsxs("div", { className: styles.headerControls, children: [
1930
+ /* @__PURE__ */ jsx2(
1931
+ "button",
1932
+ {
1933
+ type: "button",
1934
+ className: styles.headerIconBtn,
1935
+ onClick: prev,
1936
+ disabled: currentIndex === 0 || submitting,
1937
+ "aria-label": "Previous question",
1938
+ children: /* @__PURE__ */ jsx2(IconArrowLeft, {})
1939
+ }
1940
+ ),
1941
+ /* @__PURE__ */ jsxs("span", { className: styles.step, children: [
1942
+ currentIndex + 1,
1943
+ " of ",
1944
+ questions.length
1945
+ ] }),
1946
+ /* @__PURE__ */ jsx2(
1947
+ "button",
1948
+ {
1949
+ type: "button",
1950
+ className: styles.headerIconBtn,
1951
+ onClick: next,
1952
+ disabled: isLastQuestion || !showNextArrow || submitting,
1953
+ "aria-label": "Next question",
1954
+ children: /* @__PURE__ */ jsx2(IconArrowRight, {})
1955
+ }
1956
+ )
1957
+ ] })
1958
+ ] }) : null;
1959
+ return /* @__PURE__ */ jsxs(
1960
+ "div",
1961
+ {
1962
+ ref: rootRef,
1963
+ className: classNames(styles.root, className, classNameMap?.root),
1964
+ "data-appearance": resolvedAppearance,
1965
+ tabIndex: -1,
1966
+ "aria-label": "Firstflow survey",
1967
+ children: [
1968
+ layout === "all" ? questions.map((question, index) => renderQuestion(question, index)) : activeQuestion ? /* @__PURE__ */ jsxs(Fragment2, { children: [
1969
+ /* @__PURE__ */ jsxs("div", { className: styles.shell, children: [
1970
+ paginatedHeader,
1971
+ /* @__PURE__ */ jsx2(
1972
+ "div",
1973
+ {
1974
+ className: classNames(
1975
+ styles.questionPanel,
1976
+ activeQuestion.type === "thank_you" ? styles.questionPanelThankYou : isChoiceStep(activeQuestion) ? void 0 : styles.questionPanelPadded
1977
+ ),
1978
+ children: activeQuestion.type === "thank_you" ? /* @__PURE__ */ jsx2(
1979
+ ThankYouStep,
1980
+ {
1981
+ question: activeQuestion,
1982
+ showDone: true,
1983
+ onDone: thankYouDone,
1984
+ disabled: submitting
1985
+ }
1986
+ ) : renderQuestionInput(
1987
+ activeQuestion,
1988
+ answers[activeQuestion.id] ?? null,
1989
+ focusedChoiceIndex,
1990
+ setFocusedChoiceIndex,
1991
+ setAnswer,
1992
+ advanceAfterChoiceSelect
1993
+ )
1994
+ },
1995
+ activeQuestion.id
1996
+ ),
1997
+ activeQuestion.type === "thank_you" ? null : renderPaginationFooter()
1998
+ ] }),
1999
+ renderShortcuts()
2000
+ ] }) : null,
2001
+ layout === "all" ? /* @__PURE__ */ jsxs("div", { className: classNames(styles.row, classNameMap?.nav), children: [
2002
+ onDismissed ? /* @__PURE__ */ jsx2(
2003
+ "button",
2004
+ {
2005
+ type: "button",
2006
+ className: classNames(styles.button, styles.buttonMuted),
2007
+ onClick: onDismissed,
2008
+ children: "Skip"
2009
+ }
2010
+ ) : null,
2011
+ /* @__PURE__ */ jsx2(
2012
+ "button",
2013
+ {
2014
+ type: "button",
2015
+ className: classNames(styles.button, styles.buttonPrimary),
2016
+ onClick: () => void handleSubmit(),
2017
+ disabled: !canSubmit || submitting,
2018
+ children: submitting ? "Submitting..." : "Submit"
2019
+ }
2020
+ )
2021
+ ] }) : null,
2022
+ error ? /* @__PURE__ */ jsx2(
2023
+ "p",
2024
+ {
2025
+ className: classNames(
2026
+ styles.message,
2027
+ styles.error,
2028
+ classNameMap?.error
2029
+ ),
2030
+ children: error
2031
+ }
2032
+ ) : null,
2033
+ submitted ? /* @__PURE__ */ jsx2(
2034
+ "p",
2035
+ {
2036
+ className: classNames(
2037
+ styles.message,
2038
+ styles.success,
2039
+ classNameMap?.success
2040
+ ),
2041
+ children: "Thanks for your feedback."
2042
+ }
2043
+ ) : null
2044
+ ]
2045
+ }
2046
+ );
2047
+ }
2048
+
2049
+ // src/components/ExperienceMessageCard.tsx
2050
+ import { useCallback as useCallback5, useEffect as useEffect4, useMemo as useMemo8, useRef as useRef4, useState as useState3 } from "react";
2051
+
2052
+ // src/components/ExperienceMessageCard.css
2053
+ styleInject('.ffMessageCardRoot[data-appearance=dark] {\n --ff-msg-shell-bg: #1e1e1e;\n --ff-msg-border: #3a3a3a;\n --ff-msg-border-subtle: #2f2f2f;\n --ff-msg-text: #f3f3f3;\n --ff-msg-text-muted: #a8a8a8;\n --ff-msg-footer-bg: #252525;\n --ff-msg-accent: #ffffff;\n --ff-msg-accent-contrast: #121212;\n --ff-msg-icon-border: #404040;\n --ff-msg-dismiss-hover: #2a2a2a;\n --ff-msg-ghost-hover: #2a2a2a;\n --ff-msg-icon-bg: color-mix(in srgb, var(--ff-msg-accent) 16%, #1e1e1e);\n --ff-msg-shell-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), 0 1px 4px rgba(0, 0, 0, 0.28);\n}\n.ffMessageCardRoot[data-appearance=light] {\n --ff-msg-shell-bg: #ffffff;\n --ff-msg-border: #e5e5e5;\n --ff-msg-border-subtle: #eeeeee;\n --ff-msg-text: #1a1a1a;\n --ff-msg-text-muted: #888888;\n --ff-msg-footer-bg: #f5f5f5;\n --ff-msg-accent: #1a1a1a;\n --ff-msg-accent-contrast: #ffffff;\n --ff-msg-icon-border: #e5e5e5;\n --ff-msg-dismiss-hover: #f0f0f0;\n --ff-msg-ghost-hover: #f0f0f0;\n --ff-msg-icon-bg: color-mix(in srgb, var(--ff-msg-accent) 10%, #ffffff);\n --ff-msg-shell-shadow: 0 2px 10px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n.ffMessageCardRoot {\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n width: 100%;\n box-sizing: border-box;\n color: var(--ff-msg-text);\n}\n.ffMessageCardShell {\n background: var(--ff-msg-shell-bg);\n border: 1px solid var(--ff-msg-border);\n border-radius: 14px;\n overflow: hidden;\n box-shadow: var(--ff-msg-shell-shadow);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.ffMessageCardShell.ffMessageCardDismissed {\n opacity: 0;\n transform: scale(0.97);\n pointer-events: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .ffMessageCardShell {\n transition: none;\n }\n .ffMessageCardShell.ffMessageCardDismissed {\n transform: none;\n }\n}\n.ffMessageCardHead {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 16px 0;\n gap: 12px;\n}\n.ffMessageCardHeadLeft {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n.ffMessageCardIcon {\n width: 40px;\n height: 40px;\n border-radius: 10px;\n border: 1px solid var(--ff-msg-icon-border);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n line-height: 1;\n flex-shrink: 0;\n background: var(--ff-msg-icon-bg);\n}\n.ffMessageCardHeadText {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n.ffMessageCardTitle {\n font-size: 14px;\n font-weight: 500;\n color: var(--ff-msg-text);\n margin: 0;\n line-height: 1.3;\n}\n.ffMessageCardEyebrow {\n font-size: 12px;\n color: var(--ff-msg-text-muted);\n margin: 0;\n}\n.ffMessageCardDismiss {\n width: 26px;\n height: 26px;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--ff-msg-text-muted);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n flex-shrink: 0;\n transition: background 0.12s ease, color 0.12s ease;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n margin: -4px -4px 0 0;\n}\n.ffMessageCardDismiss:hover {\n background: var(--ff-msg-dismiss-hover);\n color: var(--ff-msg-text);\n}\n.ffMessageCardBody {\n padding: 10px 16px 0;\n}\n.ffMessageCardDescription {\n font-size: 13px;\n color: var(--ff-msg-text-muted);\n line-height: 1.6;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ffMessageCardFooter {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px 16px 12px;\n margin-top: 12px;\n border-top: 1px solid var(--ff-msg-border-subtle);\n background: var(--ff-msg-footer-bg);\n}\n.ffMessageCardFooter.ffMessageCardFooter--singleCta {\n justify-content: center;\n}\n.ffMessageCardBtnPrimary {\n background: var(--ff-msg-accent);\n color: var(--ff-msg-accent-contrast);\n border: none;\n border-radius: 8px;\n padding: 7px 16px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n transition: opacity 0.12s ease;\n white-space: nowrap;\n}\n.ffMessageCardBtnPrimary:hover {\n opacity: 0.88;\n}\n.ffMessageCardBtnPrimary:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.ffMessageCardBtnGhost {\n background: transparent;\n border: 1px solid var(--ff-msg-border);\n border-radius: 8px;\n padding: 7px 14px;\n font-size: 13px;\n font-weight: 400;\n cursor: pointer;\n font-family: inherit;\n color: var(--ff-msg-text);\n transition: background 0.12s ease;\n white-space: nowrap;\n}\n.ffMessageCardBtnGhost:hover {\n background: var(--ff-msg-ghost-hover);\n}\n');
2054
+
2055
+ // src/components/ExperienceMessageCard.tsx
2056
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2057
+ var DISMISS_MS = 260;
2058
+ var styles2 = {
2059
+ root: "ffMessageCardRoot",
2060
+ shell: "ffMessageCardShell",
2061
+ shellDismissed: "ffMessageCardDismissed",
2062
+ head: "ffMessageCardHead",
2063
+ headLeft: "ffMessageCardHeadLeft",
2064
+ icon: "ffMessageCardIcon",
2065
+ headText: "ffMessageCardHeadText",
2066
+ title: "ffMessageCardTitle",
2067
+ eyebrow: "ffMessageCardEyebrow",
2068
+ dismissBtn: "ffMessageCardDismiss",
2069
+ body: "ffMessageCardBody",
2070
+ description: "ffMessageCardDescription",
2071
+ footer: "ffMessageCardFooter",
2072
+ footerSingleCta: "ffMessageCardFooter--singleCta",
2073
+ btnPrimary: "ffMessageCardBtnPrimary",
2074
+ btnGhost: "ffMessageCardBtnGhost"
2075
+ };
2076
+ function splitCardContent(messageContent) {
2077
+ const raw = messageContent?.trim() ?? "";
2078
+ if (!raw) {
2079
+ return { title: "Message", eyebrow: "", description: "" };
2080
+ }
2081
+ const lines = raw.split("\n");
2082
+ const first = lines[0]?.trim() || "Message";
2083
+ if (lines.length === 1) {
2084
+ return { title: first, eyebrow: "", description: "" };
2085
+ }
2086
+ const title = first.length > 72 ? `${first.slice(0, 69)}\u2026` : first;
2087
+ const rest = lines.slice(1);
2088
+ const eyebrow = rest[0]?.trim() ?? "";
2089
+ const description = rest.slice(1).join("\n").trim();
2090
+ return { title, eyebrow, description };
2091
+ }
2092
+ function findPrimaryBlock(blocks) {
2093
+ return blocks.find((b) => b.type === "cta_primary");
2094
+ }
2095
+ function findDismissBlock(blocks) {
2096
+ return blocks.find((b) => b.type === "cta_dismiss");
2097
+ }
2098
+ function ExperienceMessageCard({
2099
+ experienceId,
2100
+ nodeId,
2101
+ config,
2102
+ conversationId,
2103
+ metadata,
2104
+ onComplete,
2105
+ appearance = "auto",
2106
+ className,
2107
+ onMessageAction
2108
+ }) {
2109
+ const [resolvedAppearance, setResolvedAppearance] = useState3("light");
2110
+ const [dismissed, setDismissed] = useState3(false);
2111
+ const finishingRef = useRef4(false);
2112
+ useEffect4(() => {
2113
+ if (appearance !== "auto") {
2114
+ setResolvedAppearance(appearance);
2115
+ return;
2116
+ }
2117
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
2118
+ const apply = () => setResolvedAppearance(mq.matches ? "dark" : "light");
2119
+ apply();
2120
+ mq.addEventListener("change", apply);
2121
+ return () => mq.removeEventListener("change", apply);
2122
+ }, [appearance]);
2123
+ const { blocks, ui, handlers } = useExperienceMessageNode({
2124
+ config,
2125
+ nodeId,
2126
+ experienceId,
2127
+ conversationId,
2128
+ metadata,
2129
+ onAction: onMessageAction
2130
+ });
2131
+ const { title, eyebrow, description } = useMemo8(
2132
+ () => splitCardContent(config.messageContent),
2133
+ [config.messageContent]
2134
+ );
2135
+ const primaryBlock = useMemo8(() => findPrimaryBlock(blocks), [blocks]);
2136
+ const dismissBlock = useMemo8(() => findDismissBlock(blocks), [blocks]);
2137
+ const primaryAsDismissOnly = !ui.hasPrimaryCta && ui.hasDismissCta;
2138
+ const primaryLabel = primaryAsDismissOnly ? dismissBlock?.label ?? "Dismiss" : primaryBlock?.label ?? "Continue";
2139
+ const ghostLabel = dismissBlock?.label ?? "Dismiss";
2140
+ const finish = useCallback5(() => {
2141
+ if (finishingRef.current) return;
2142
+ finishingRef.current = true;
2143
+ setDismissed(true);
2144
+ window.setTimeout(() => {
2145
+ onComplete();
2146
+ }, DISMISS_MS);
2147
+ }, [onComplete]);
2148
+ const handleHeaderDismiss = useCallback5(() => {
2149
+ if (ui.hasDismissCta) {
2150
+ handlers.onDismissCta();
2151
+ }
2152
+ finish();
2153
+ }, [ui.hasDismissCta, handlers, finish]);
2154
+ const handlePrimary = useCallback5(() => {
2155
+ if (primaryAsDismissOnly) {
2156
+ handlers.onDismissCta();
2157
+ finish();
2158
+ return;
2159
+ }
2160
+ if (!primaryBlock) {
2161
+ finish();
2162
+ return;
2163
+ }
2164
+ handlers.onPrimaryCta();
2165
+ if (primaryBlock.ctaType === "link" && primaryBlock.url) {
2166
+ try {
2167
+ window.open(primaryBlock.url, "_blank", "noopener,noreferrer");
2168
+ } catch {
2169
+ }
2170
+ }
2171
+ finish();
2172
+ }, [primaryAsDismissOnly, primaryBlock, handlers, finish]);
2173
+ const handleGhost = useCallback5(() => {
2174
+ if (ui.hasDismissCta) {
2175
+ handlers.onDismissCta();
2176
+ }
2177
+ finish();
2178
+ }, [ui.hasDismissCta, handlers, finish]);
2179
+ const showFooter = ui.hasPrimaryCta || ui.hasDismissCta;
2180
+ const showGhost = ui.hasPrimaryCta && ui.hasDismissCta;
2181
+ return /* @__PURE__ */ jsx3(
2182
+ "div",
2183
+ {
2184
+ className: classNames(styles2.root, className),
2185
+ "data-appearance": resolvedAppearance,
2186
+ children: /* @__PURE__ */ jsxs2(
2187
+ "div",
2188
+ {
2189
+ className: classNames(
2190
+ styles2.shell,
2191
+ dismissed && styles2.shellDismissed
2192
+ ),
2193
+ children: [
2194
+ /* @__PURE__ */ jsxs2("div", { className: styles2.head, children: [
2195
+ /* @__PURE__ */ jsxs2("div", { className: styles2.headLeft, children: [
2196
+ /* @__PURE__ */ jsx3("div", { className: styles2.icon, "aria-hidden": true, children: "\u{1F4AC}" }),
2197
+ /* @__PURE__ */ jsxs2("div", { className: styles2.headText, children: [
2198
+ /* @__PURE__ */ jsx3("p", { className: styles2.title, children: title }),
2199
+ eyebrow ? /* @__PURE__ */ jsx3("p", { className: styles2.eyebrow, children: eyebrow }) : null
2200
+ ] })
2201
+ ] }),
2202
+ /* @__PURE__ */ jsx3(
2203
+ "button",
2204
+ {
2205
+ type: "button",
2206
+ className: styles2.dismissBtn,
2207
+ onClick: handleHeaderDismiss,
2208
+ "aria-label": "Dismiss",
2209
+ children: "\u2715"
2210
+ }
2211
+ )
2212
+ ] }),
2213
+ description ? /* @__PURE__ */ jsx3("div", { className: styles2.body, children: /* @__PURE__ */ jsx3("p", { className: styles2.description, children: description }) }) : null,
2214
+ showFooter ? /* @__PURE__ */ jsxs2(
2215
+ "div",
2216
+ {
2217
+ className: classNames(
2218
+ styles2.footer,
2219
+ !showGhost && styles2.footerSingleCta
2220
+ ),
2221
+ children: [
2222
+ /* @__PURE__ */ jsx3(
2223
+ "button",
2224
+ {
2225
+ type: "button",
2226
+ className: styles2.btnPrimary,
2227
+ onClick: handlePrimary,
2228
+ children: primaryLabel
2229
+ }
2230
+ ),
2231
+ showGhost ? /* @__PURE__ */ jsx3(
2232
+ "button",
2233
+ {
2234
+ type: "button",
2235
+ className: styles2.btnGhost,
2236
+ onClick: handleGhost,
2237
+ children: ghostLabel
2238
+ }
2239
+ ) : null
2240
+ ]
2241
+ }
2242
+ ) : null
2243
+ ]
2244
+ }
2245
+ )
2246
+ }
2247
+ );
2248
+ }
2249
+
2250
+ // src/components/ExperienceMessageRichCard.tsx
2251
+ import React3, { useCallback as useCallback6, useEffect as useEffect5, useMemo as useMemo9, useRef as useRef5, useState as useState4 } from "react";
2252
+
2253
+ // src/components/ExperienceMessageRichCard.css
2254
+ styleInject('.ffRichCardRoot[data-appearance=dark] {\n --ff-rc-shell-bg: #1e1e1e;\n --ff-rc-border: #3a3a3a;\n --ff-rc-border-hover: #555555;\n --ff-rc-border-subtle: #2f2f2f;\n --ff-rc-text: #f3f3f3;\n --ff-rc-text-muted: #a8a8a8;\n --ff-rc-meta-muted: #888888;\n --ff-rc-footer-bg: #252525;\n --ff-rc-footer-border: #2e2e2e;\n --ff-rc-accent: #ffffff;\n --ff-rc-accent-contrast: #121212;\n --ff-rc-placeholder-bg: #2a2a2a;\n --ff-rc-image-border: #333333;\n --ff-rc-pill-bg: #333333;\n --ff-rc-pill-text: #e8e8e8;\n --ff-rc-meta-dot: #444444;\n --ff-rc-ghost-hover: #2a2a2a;\n --ff-rc-icon-btn-border: #444444;\n --ff-rc-icon-btn-color: #888888;\n --ff-rc-icon-btn-hover-bg: #2a2a2a;\n --ff-rc-shell-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), 0 1px 4px rgba(0, 0, 0, 0.28);\n}\n.ffRichCardRoot[data-appearance=light] {\n --ff-rc-shell-bg: #ffffff;\n --ff-rc-border: #e5e5e5;\n --ff-rc-border-hover: #c8c8c8;\n --ff-rc-border-subtle: #eeeeee;\n --ff-rc-text: #1a1a1a;\n --ff-rc-text-muted: #888888;\n --ff-rc-meta-muted: #888888;\n --ff-rc-footer-bg: #f5f5f5;\n --ff-rc-footer-border: #eeeeee;\n --ff-rc-accent: #1a1a1a;\n --ff-rc-accent-contrast: #ffffff;\n --ff-rc-placeholder-bg: #f0f0f0;\n --ff-rc-image-border: #eeeeee;\n --ff-rc-pill-bg: #f0f0f0;\n --ff-rc-pill-text: #444444;\n --ff-rc-meta-dot: #d0d0d0;\n --ff-rc-ghost-hover: #f0f0f0;\n --ff-rc-icon-btn-border: #e0e0e0;\n --ff-rc-icon-btn-color: #888888;\n --ff-rc-icon-btn-hover-bg: #f0f0f0;\n --ff-rc-shell-shadow: 0 2px 10px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n.ffRichCardRoot {\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n width: 100%;\n box-sizing: border-box;\n color: var(--ff-rc-text);\n}\n.ffRichCardShell {\n background: var(--ff-rc-shell-bg);\n border: 1px solid var(--ff-rc-border);\n border-radius: 14px;\n overflow: hidden;\n box-shadow: var(--ff-rc-shell-shadow);\n transition:\n border-color 0.15s ease,\n opacity 0.25s ease,\n transform 0.25s ease;\n}\n.ffRichCardShell:hover {\n border-color: var(--ff-rc-border-hover);\n}\n.ffRichCardShell.ffRichCardDismissed {\n opacity: 0;\n transform: scale(0.97);\n pointer-events: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .ffRichCardShell {\n transition: border-color 0.15s ease, opacity 0.15s ease;\n }\n .ffRichCardShell.ffRichCardDismissed {\n transform: none;\n }\n}\n.ffRichCardImage {\n width: 100%;\n height: 140px;\n object-fit: cover;\n display: block;\n border: none;\n border-bottom: 1px solid var(--ff-rc-image-border);\n}\n.ffRichCardImagePlaceholder {\n width: 100%;\n height: 140px;\n background: var(--ff-rc-placeholder-bg);\n border-bottom: 1px solid var(--ff-rc-image-border);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 44px;\n line-height: 1;\n}\n.ffRichCardBody {\n padding: 14px 14px 0;\n}\n.ffRichCardTags {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n margin-bottom: 8px;\n}\n.ffRichCardPill {\n display: inline-flex;\n align-items: center;\n font-size: 11px;\n font-weight: 500;\n padding: 3px 8px;\n border-radius: 999px;\n white-space: nowrap;\n line-height: 1.4;\n background: var(--ff-rc-pill-bg);\n color: var(--ff-rc-pill-text);\n}\n.ffRichCardTitle {\n font-size: 15px;\n font-weight: 500;\n color: var(--ff-rc-text);\n margin: 0 0 6px;\n line-height: 1.35;\n}\n.ffRichCardDescription {\n font-size: 13px;\n color: var(--ff-rc-text-muted);\n line-height: 1.6;\n margin: 0 0 10px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ffRichCardMeta {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n.ffRichCardMetaItem {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n color: var(--ff-rc-meta-muted);\n}\n.ffRichCardMetaDot {\n width: 3px;\n height: 3px;\n border-radius: 50%;\n background: var(--ff-rc-meta-dot);\n flex-shrink: 0;\n}\n.ffRichCardFooter {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px 14px 12px;\n margin-top: 12px;\n border-top: 1px solid var(--ff-rc-footer-border);\n background: var(--ff-rc-footer-bg);\n}\n.ffRichCardFooter.ffRichCardFooter--singleCta {\n justify-content: center;\n position: relative;\n}\n.ffRichCardFooter.ffRichCardFooter--singleCta .ffRichCardBtnIconDismiss {\n position: absolute;\n right: 14px;\n top: 50%;\n transform: translateY(-50%);\n margin-left: 0;\n}\n.ffRichCardBtnPrimary {\n background: var(--ff-rc-accent);\n color: var(--ff-rc-accent-contrast);\n border: none;\n border-radius: 8px;\n padding: 7px 16px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n transition: opacity 0.12s ease;\n white-space: nowrap;\n}\n.ffRichCardBtnPrimary:hover {\n opacity: 0.88;\n}\n.ffRichCardBtnGhost {\n background: transparent;\n border: 1px solid var(--ff-rc-border);\n border-radius: 8px;\n padding: 7px 14px;\n font-size: 13px;\n font-weight: 400;\n cursor: pointer;\n font-family: inherit;\n color: var(--ff-rc-text);\n transition: background 0.12s ease;\n white-space: nowrap;\n}\n.ffRichCardBtnGhost:hover {\n background: var(--ff-rc-ghost-hover);\n}\n.ffRichCardBtnIconDismiss {\n background: transparent;\n border: 1px solid var(--ff-rc-icon-btn-border);\n border-radius: 8px;\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n flex-shrink: 0;\n font-size: 13px;\n line-height: 1;\n color: var(--ff-rc-icon-btn-color);\n margin-left: auto;\n padding: 0;\n}\n.ffRichCardBtnIconDismiss:hover {\n background: var(--ff-rc-icon-btn-hover-bg);\n color: var(--ff-rc-text);\n}\n');
2255
+
2256
+ // src/components/ExperienceMessageRichCard.tsx
2257
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2258
+ var DISMISS_MS2 = 260;
2259
+ var DEFAULT_HERO_EMOJI = "\u{1F5BC}\uFE0F";
2260
+ var styles3 = {
2261
+ root: "ffRichCardRoot",
2262
+ shell: "ffRichCardShell",
2263
+ shellDismissed: "ffRichCardDismissed",
2264
+ image: "ffRichCardImage",
2265
+ imagePlaceholder: "ffRichCardImagePlaceholder",
2266
+ body: "ffRichCardBody",
2267
+ tags: "ffRichCardTags",
2268
+ pill: "ffRichCardPill",
2269
+ title: "ffRichCardTitle",
2270
+ description: "ffRichCardDescription",
2271
+ meta: "ffRichCardMeta",
2272
+ metaItem: "ffRichCardMetaItem",
2273
+ metaDot: "ffRichCardMetaDot",
2274
+ footer: "ffRichCardFooter",
2275
+ footerSingleCta: "ffRichCardFooter--singleCta",
2276
+ btnPrimary: "ffRichCardBtnPrimary",
2277
+ btnGhost: "ffRichCardBtnGhost",
2278
+ btnIconDismiss: "ffRichCardBtnIconDismiss"
2279
+ };
2280
+ function richCardTitleAndDescription(config) {
2281
+ const raw = config.messageContent?.trim() ?? "";
2282
+ const explicit = config.richCardTitle?.trim();
2283
+ if (explicit) {
2284
+ return { title: explicit, description: raw };
2285
+ }
2286
+ if (!raw) {
2287
+ return { title: "Message", description: "" };
2288
+ }
2289
+ const lines = raw.split("\n");
2290
+ const first = lines[0]?.trim() || "Message";
2291
+ return {
2292
+ title: first,
2293
+ description: lines.slice(1).join("\n").trim()
2294
+ };
2295
+ }
2296
+ function findPrimaryBlock2(blocks) {
2297
+ return blocks.find((b) => b.type === "cta_primary");
2298
+ }
2299
+ function findDismissBlock2(blocks) {
2300
+ return blocks.find((b) => b.type === "cta_dismiss");
2301
+ }
2302
+ function ExperienceMessageRichCard({
2303
+ experienceId,
2304
+ nodeId,
2305
+ config,
2306
+ conversationId,
2307
+ metadata,
2308
+ onComplete,
2309
+ appearance = "auto",
2310
+ className,
2311
+ onMessageAction
2312
+ }) {
2313
+ const [resolvedAppearance, setResolvedAppearance] = useState4("light");
2314
+ const [dismissed, setDismissed] = useState4(false);
2315
+ const finishingRef = useRef5(false);
2316
+ useEffect5(() => {
2317
+ if (appearance !== "auto") {
2318
+ setResolvedAppearance(appearance);
2319
+ return;
2320
+ }
2321
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
2322
+ const apply = () => setResolvedAppearance(mq.matches ? "dark" : "light");
2323
+ apply();
2324
+ mq.addEventListener("change", apply);
2325
+ return () => mq.removeEventListener("change", apply);
2326
+ }, [appearance]);
2327
+ const { blocks, ui, handlers } = useExperienceMessageNode({
2328
+ config,
2329
+ nodeId,
2330
+ experienceId,
2331
+ conversationId,
2332
+ metadata,
2333
+ onAction: onMessageAction
2334
+ });
2335
+ const { title, description } = useMemo9(
2336
+ () => richCardTitleAndDescription(config),
2337
+ [config]
2338
+ );
2339
+ const hero = useMemo9(() => {
2340
+ const mediaType = config.richCardMediaType ?? "image";
2341
+ const href = tryParseHttpImageUrl(config.richCardImageUrl);
2342
+ const useImage = mediaType !== "emoji" && !!href;
2343
+ const emoji = config.richCardEmoji?.trim() || DEFAULT_HERO_EMOJI;
2344
+ return { useImage, imageSrc: href, emoji };
2345
+ }, [config.richCardImageUrl, config.richCardMediaType, config.richCardEmoji]);
2346
+ const tags = config.richCardTags ?? [];
2347
+ const meta = config.richCardMeta ?? [];
2348
+ const primaryBlock = useMemo9(() => findPrimaryBlock2(blocks), [blocks]);
2349
+ const dismissBlock = useMemo9(() => findDismissBlock2(blocks), [blocks]);
2350
+ const primaryAsDismissOnly = !ui.hasPrimaryCta && ui.hasDismissCta;
2351
+ const primaryLabel = primaryAsDismissOnly ? dismissBlock?.label ?? "Dismiss" : primaryBlock?.label ?? "Continue";
2352
+ const ghostLabel = dismissBlock?.label ?? "Dismiss";
2353
+ const finish = useCallback6(() => {
2354
+ if (finishingRef.current) return;
2355
+ finishingRef.current = true;
2356
+ setDismissed(true);
2357
+ window.setTimeout(() => {
2358
+ onComplete();
2359
+ }, DISMISS_MS2);
2360
+ }, [onComplete]);
2361
+ const emitDismissIfConfigured = useCallback6(() => {
2362
+ if (ui.hasDismissCta) {
2363
+ handlers.onDismissCta();
2364
+ }
2365
+ }, [ui.hasDismissCta, handlers]);
2366
+ const handleFooterDismiss = useCallback6(() => {
2367
+ emitDismissIfConfigured();
2368
+ finish();
2369
+ }, [emitDismissIfConfigured, finish]);
2370
+ const handlePrimary = useCallback6(() => {
2371
+ if (primaryAsDismissOnly) {
2372
+ handlers.onDismissCta();
2373
+ finish();
2374
+ return;
2375
+ }
2376
+ if (!primaryBlock) {
2377
+ finish();
2378
+ return;
2379
+ }
2380
+ handlers.onPrimaryCta();
2381
+ if (primaryBlock.ctaType === "link" && primaryBlock.url) {
2382
+ try {
2383
+ window.open(primaryBlock.url, "_blank", "noopener,noreferrer");
2384
+ } catch {
2385
+ }
2386
+ }
2387
+ finish();
2388
+ }, [primaryAsDismissOnly, primaryBlock, handlers, finish]);
2389
+ const handleGhost = useCallback6(() => {
2390
+ emitDismissIfConfigured();
2391
+ finish();
2392
+ }, [emitDismissIfConfigured, finish]);
2393
+ const showPrimary = ui.hasPrimaryCta || primaryAsDismissOnly;
2394
+ const showGhost = ui.hasPrimaryCta && ui.hasDismissCta;
2395
+ return /* @__PURE__ */ jsx4(
2396
+ "div",
2397
+ {
2398
+ className: classNames(styles3.root, className),
2399
+ "data-appearance": resolvedAppearance,
2400
+ children: /* @__PURE__ */ jsxs3(
2401
+ "div",
2402
+ {
2403
+ className: classNames(
2404
+ styles3.shell,
2405
+ dismissed && styles3.shellDismissed
2406
+ ),
2407
+ children: [
2408
+ hero.useImage && hero.imageSrc ? /* @__PURE__ */ jsx4(
2409
+ "img",
2410
+ {
2411
+ className: styles3.image,
2412
+ src: hero.imageSrc,
2413
+ alt: title ? title : "Card",
2414
+ decoding: "async"
2415
+ }
2416
+ ) : /* @__PURE__ */ jsx4("div", { className: styles3.imagePlaceholder, "aria-hidden": true, children: hero.emoji }),
2417
+ /* @__PURE__ */ jsxs3("div", { className: styles3.body, children: [
2418
+ tags.length > 0 ? /* @__PURE__ */ jsx4("div", { className: styles3.tags, children: tags.map((tag, i) => /* @__PURE__ */ jsx4(
2419
+ "span",
2420
+ {
2421
+ className: styles3.pill,
2422
+ style: tag.bg || tag.color ? { background: tag.bg, color: tag.color } : void 0,
2423
+ children: tag.label
2424
+ },
2425
+ `${tag.label}-${i}`
2426
+ )) }) : null,
2427
+ title ? /* @__PURE__ */ jsx4("p", { className: styles3.title, children: title }) : null,
2428
+ description ? /* @__PURE__ */ jsx4("p", { className: styles3.description, children: description }) : null,
2429
+ meta.length > 0 ? /* @__PURE__ */ jsx4("div", { className: styles3.meta, children: meta.map((item, i) => /* @__PURE__ */ jsxs3(React3.Fragment, { children: [
2430
+ i > 0 ? /* @__PURE__ */ jsx4("span", { className: styles3.metaDot, "aria-hidden": true }) : null,
2431
+ /* @__PURE__ */ jsxs3("span", { className: styles3.metaItem, children: [
2432
+ item.icon ? /* @__PURE__ */ jsx4("span", { style: { fontSize: 12 }, "aria-hidden": true, children: item.icon }) : null,
2433
+ item.label
2434
+ ] })
2435
+ ] }, `${item.label}-${i}`)) }) : null
2436
+ ] }),
2437
+ /* @__PURE__ */ jsxs3(
2438
+ "div",
2439
+ {
2440
+ className: classNames(
2441
+ styles3.footer,
2442
+ showPrimary && !showGhost && styles3.footerSingleCta
2443
+ ),
2444
+ children: [
2445
+ showPrimary ? /* @__PURE__ */ jsx4(
2446
+ "button",
2447
+ {
2448
+ type: "button",
2449
+ className: styles3.btnPrimary,
2450
+ onClick: handlePrimary,
2451
+ children: primaryLabel
2452
+ }
2453
+ ) : null,
2454
+ showGhost ? /* @__PURE__ */ jsx4(
2455
+ "button",
2456
+ {
2457
+ type: "button",
2458
+ className: styles3.btnGhost,
2459
+ onClick: handleGhost,
2460
+ children: ghostLabel
2461
+ }
2462
+ ) : null,
2463
+ /* @__PURE__ */ jsx4(
2464
+ "button",
2465
+ {
2466
+ type: "button",
2467
+ className: styles3.btnIconDismiss,
2468
+ onClick: handleFooterDismiss,
2469
+ "aria-label": "Dismiss",
2470
+ children: "\u2715"
2471
+ }
2472
+ )
2473
+ ]
2474
+ }
2475
+ )
2476
+ ]
2477
+ }
2478
+ )
2479
+ }
2480
+ );
2481
+ }
2482
+
2483
+ // src/components/ExperienceMessageCarousel.tsx
2484
+ import {
2485
+ useCallback as useCallback7,
2486
+ useEffect as useEffect6,
2487
+ useLayoutEffect,
2488
+ useMemo as useMemo10,
2489
+ useRef as useRef6,
2490
+ useState as useState5
2491
+ } from "react";
2492
+
2493
+ // src/components/ExperienceMessageCarousel.css
2494
+ styleInject('.ffMsgCarouselRoot[data-appearance=dark] {\n --ff-cc-shell-bg: #1e1e1e;\n --ff-cc-border: #3a3a3a;\n --ff-cc-border-hover: #555555;\n --ff-cc-text: #f3f3f3;\n --ff-cc-text-muted: #a8a8a8;\n --ff-cc-footer-bg: #252525;\n --ff-cc-footer-border: #2e2e2e;\n --ff-cc-accent: #ffffff;\n --ff-cc-accent-contrast: #121212;\n --ff-cc-placeholder-bg: #2a2a2a;\n --ff-cc-image-border: #333333;\n --ff-cc-nav-bg: #2a2a2a;\n --ff-cc-nav-border: #404040;\n --ff-cc-nav-hover: #333333;\n --ff-cc-dot: #555555;\n --ff-cc-dot-active: #f0f0f0;\n --ff-cc-ghost-hover: #2a2a2a;\n --ff-cc-icon-dismiss-border: #444444;\n --ff-cc-icon-dismiss-color: #888888;\n --ff-cc-icon-dismiss-hover-bg: #2a2a2a;\n --ff-cc-shell-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), 0 1px 4px rgba(0, 0, 0, 0.28);\n}\n.ffMsgCarouselRoot[data-appearance=light] {\n --ff-cc-shell-bg: #ffffff;\n --ff-cc-border: #e5e5e5;\n --ff-cc-border-hover: #c8c8c8;\n --ff-cc-text: #1a1a1a;\n --ff-cc-text-muted: #888888;\n --ff-cc-footer-bg: #f5f5f5;\n --ff-cc-footer-border: #eeeeee;\n --ff-cc-accent: #1a1a1a;\n --ff-cc-accent-contrast: #ffffff;\n --ff-cc-placeholder-bg: #f0f0f0;\n --ff-cc-image-border: #eeeeee;\n --ff-cc-nav-bg: #ffffff;\n --ff-cc-nav-border: #e0e0e0;\n --ff-cc-nav-hover: #f0f0f0;\n --ff-cc-dot: #cccccc;\n --ff-cc-dot-active: #1a1a1a;\n --ff-cc-ghost-hover: #f0f0f0;\n --ff-cc-icon-dismiss-border: #e0e0e0;\n --ff-cc-icon-dismiss-color: #888888;\n --ff-cc-icon-dismiss-hover-bg: #f0f0f0;\n --ff-cc-shell-shadow: 0 2px 10px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n.ffMsgCarouselRoot {\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n width: 100%;\n box-sizing: border-box;\n color: var(--ff-cc-text);\n}\n.ffMsgCarouselIntro {\n font-size: 13px;\n line-height: 1.55;\n color: var(--ff-cc-text-muted);\n margin: 0 0 10px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ffMsgCarouselShell {\n background: var(--ff-cc-shell-bg);\n border: 1px solid var(--ff-cc-border);\n border-radius: 14px;\n overflow: hidden;\n box-shadow: var(--ff-cc-shell-shadow);\n transition:\n border-color 0.15s ease,\n opacity 0.25s ease,\n transform 0.25s ease;\n}\n.ffMsgCarouselShell:hover {\n border-color: var(--ff-cc-border-hover);\n}\n.ffMsgCarouselShell.ffMsgCarouselDismissed {\n opacity: 0;\n transform: scale(0.97);\n pointer-events: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .ffMsgCarouselShell {\n transition: border-color 0.15s ease, opacity 0.15s ease;\n }\n .ffMsgCarouselShell.ffMsgCarouselDismissed {\n transform: none;\n }\n .ffMsgCarouselTrack {\n transition: none !important;\n }\n}\n.ffMsgCarouselTopBar {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n padding: 14px 16px 8px;\n gap: 8px;\n}\n.ffMsgCarouselDismissTop {\n width: 32px;\n height: 32px;\n border: 1px solid var(--ff-cc-icon-dismiss-border);\n border-radius: 8px;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n font-size: 13px;\n line-height: 1;\n color: var(--ff-cc-icon-dismiss-color);\n padding: 0;\n transition: background 0.12s ease, color 0.12s ease;\n}\n.ffMsgCarouselDismissTop:hover {\n background: var(--ff-cc-icon-dismiss-hover-bg);\n color: var(--ff-cc-text);\n}\n.ffMsgCarouselRow {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 0 8px 8px;\n}\n.ffMsgCarouselNav {\n flex-shrink: 0;\n width: 32px;\n height: 32px;\n min-width: 32px;\n min-height: 32px;\n align-self: center;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--ff-cc-nav-border);\n border-radius: 999px;\n background: var(--ff-cc-nav-bg);\n color: var(--ff-cc-text-muted);\n font-size: 17px;\n font-weight: 500;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n transition:\n background 0.12s ease,\n color 0.12s ease,\n border-color 0.12s ease;\n}\n.ffMsgCarouselNav:hover:not(:disabled) {\n background: var(--ff-cc-nav-hover);\n color: var(--ff-cc-text);\n border-color: var(--ff-cc-border-hover);\n}\n.ffMsgCarouselNav:disabled {\n opacity: 0.35;\n cursor: not-allowed;\n}\n.ffMsgCarouselViewport {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n border-radius: 10px;\n touch-action: pan-x;\n}\n.ffMsgCarouselTrack {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n align-items: stretch;\n transition: transform 0.32s cubic-bezier(0.25, 0.8, 0.25, 1);\n will-change: transform;\n}\n.ffMsgCarouselSlide {\n flex: 0 0 auto;\n align-self: stretch;\n min-width: 0;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--ff-cc-border);\n border-radius: 10px;\n overflow: hidden;\n background: var(--ff-cc-shell-bg);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);\n}\n.ffMsgCarouselImage {\n width: 100%;\n height: 140px;\n object-fit: cover;\n display: block;\n border-bottom: 1px solid var(--ff-cc-image-border);\n}\n.ffMsgCarouselImagePlaceholder {\n width: 100%;\n height: 140px;\n background: var(--ff-cc-placeholder-bg);\n border-bottom: 1px solid var(--ff-cc-image-border);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 44px;\n line-height: 1;\n}\n.ffMsgCarouselBody {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 12px 12px 0;\n}\n.ffMsgCarouselTitle {\n font-size: 15px;\n font-weight: 500;\n color: var(--ff-cc-text);\n margin: 0 0 6px;\n line-height: 1.35;\n}\n.ffMsgCarouselDescription {\n font-size: 13px;\n color: var(--ff-cc-text-muted);\n line-height: 1.6;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ffMsgCarouselDots {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 4px 12px 8px;\n}\n.ffMsgCarouselDot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n border: none;\n padding: 0;\n cursor: pointer;\n background: var(--ff-cc-dot);\n transition: transform 0.15s ease, background 0.15s ease;\n}\n.ffMsgCarouselDot:hover {\n transform: scale(1.15);\n}\n.ffMsgCarouselDot.ffMsgCarouselDotActive {\n background: var(--ff-cc-dot-active);\n transform: scale(1.2);\n}\n.ffMsgCarouselSlideFooter {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px 12px 12px;\n margin-top: auto;\n flex-shrink: 0;\n border-top: 1px solid var(--ff-cc-footer-border);\n background: var(--ff-cc-footer-bg);\n}\n.ffMsgCarouselSlideFooter.ffMsgCarouselSlideFooter--singleCta {\n justify-content: center;\n}\n.ffMsgCarouselBtnPrimary {\n background: var(--ff-cc-accent);\n color: var(--ff-cc-accent-contrast);\n border: none;\n border-radius: 8px;\n padding: 7px 16px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n transition: opacity 0.12s ease;\n white-space: nowrap;\n}\n.ffMsgCarouselBtnPrimary:hover {\n opacity: 0.88;\n}\n.ffMsgCarouselBtnGhost {\n background: transparent;\n border: 1px solid var(--ff-cc-border);\n border-radius: 8px;\n padding: 7px 14px;\n font-size: 13px;\n font-weight: 400;\n cursor: pointer;\n font-family: inherit;\n color: var(--ff-cc-text);\n transition: background 0.12s ease;\n white-space: nowrap;\n}\n.ffMsgCarouselBtnGhost:hover {\n background: var(--ff-cc-ghost-hover);\n}\n.ffMsgCarouselGlobalFooter {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 10px;\n padding-bottom: 10px;\n padding-top: 10px;\n background: var(--ff-cc-footer-bg);\n border-top: 1px solid var(--ff-cc-footer-border);\n}\n.ffMsgCarouselGlobalFooter.ffMsgCarouselGlobalFooter--singleCta {\n justify-content: center;\n}\n');
2495
+
2496
+ // src/components/ExperienceMessageCarousel.tsx
2497
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2498
+ var DISMISS_MS3 = 260;
2499
+ var SWIPE_PX = 48;
2500
+ var CAROUSEL_CARD_GAP = 8;
2501
+ var DEFAULT_HERO_EMOJI2 = "\u{1F5BC}\uFE0F";
2502
+ var styles4 = {
2503
+ root: "ffMsgCarouselRoot",
2504
+ intro: "ffMsgCarouselIntro",
2505
+ shell: "ffMsgCarouselShell",
2506
+ shellDismissed: "ffMsgCarouselDismissed",
2507
+ topBar: "ffMsgCarouselTopBar",
2508
+ dismissTop: "ffMsgCarouselDismissTop",
2509
+ row: "ffMsgCarouselRow",
2510
+ nav: "ffMsgCarouselNav",
2511
+ viewport: "ffMsgCarouselViewport",
2512
+ track: "ffMsgCarouselTrack",
2513
+ slide: "ffMsgCarouselSlide",
2514
+ image: "ffMsgCarouselImage",
2515
+ imagePlaceholder: "ffMsgCarouselImagePlaceholder",
2516
+ body: "ffMsgCarouselBody",
2517
+ title: "ffMsgCarouselTitle",
2518
+ description: "ffMsgCarouselDescription",
2519
+ dots: "ffMsgCarouselDots",
2520
+ dot: "ffMsgCarouselDot",
2521
+ dotActive: "ffMsgCarouselDotActive",
2522
+ slideFooter: "ffMsgCarouselSlideFooter",
2523
+ btnPrimary: "ffMsgCarouselBtnPrimary",
2524
+ btnGhost: "ffMsgCarouselBtnGhost",
2525
+ globalFooter: "ffMsgCarouselGlobalFooter",
2526
+ globalFooterSingleCta: "ffMsgCarouselGlobalFooter--singleCta",
2527
+ slideFooterSingleCta: "ffMsgCarouselSlideFooter--singleCta"
2528
+ };
2529
+ function carouselCardHero(card) {
2530
+ const mt = card.mediaType ?? "image";
2531
+ const href = tryParseHttpImageUrl(card.imageUrl);
2532
+ if (mt === "video") {
2533
+ return {
2534
+ kind: "placeholder",
2535
+ emoji: card.emoji?.trim() || "\u{1F3AC}"
2536
+ };
2537
+ }
2538
+ if (mt !== "emoji" && href) {
2539
+ return { kind: "image", src: href };
2540
+ }
2541
+ return {
2542
+ kind: "placeholder",
2543
+ emoji: card.emoji?.trim() || DEFAULT_HERO_EMOJI2
2544
+ };
2545
+ }
2546
+ function openCarouselButtonLink(card, which) {
2547
+ const isSecondary = which === "secondary";
2548
+ const action = isSecondary ? card.secondaryButtonAction ?? "prompt" : card.buttonAction ?? "prompt";
2549
+ if (action !== "link") return;
2550
+ const raw = isSecondary ? card.secondaryButtonUrl : card.buttonUrl;
2551
+ const url = typeof raw === "string" ? raw.trim() : "";
2552
+ const safe = tryParseHttpImageUrl(url);
2553
+ if (!safe) return;
2554
+ try {
2555
+ window.open(safe, "_blank", "noopener,noreferrer");
2556
+ } catch {
2557
+ }
2558
+ }
2559
+ function findTextBlock(blocks) {
2560
+ return blocks.find(
2561
+ (b) => b.type === "text"
2562
+ );
2563
+ }
2564
+ function findPrimaryBlock3(blocks) {
2565
+ return blocks.find(
2566
+ (b) => b.type === "cta_primary"
2567
+ );
2568
+ }
2569
+ function findDismissBlock3(blocks) {
2570
+ return blocks.find(
2571
+ (b) => b.type === "cta_dismiss"
2572
+ );
2573
+ }
2574
+ function cardPrimaryVisible(card) {
2575
+ return card.showCardCta !== false && !!card.buttonText?.trim();
2576
+ }
2577
+ function cardSecondaryVisible(card) {
2578
+ return card.showCardCta !== false && !!card.secondaryButtonText?.trim();
2579
+ }
2580
+ function ExperienceMessageCarousel({
2581
+ experienceId,
2582
+ nodeId,
2583
+ config,
2584
+ conversationId,
2585
+ metadata,
2586
+ onComplete,
2587
+ appearance = "auto",
2588
+ className,
2589
+ onMessageAction
2590
+ }) {
2591
+ const cards = config.carouselCards ?? [];
2592
+ const n = cards.length;
2593
+ const [resolvedAppearance, setResolvedAppearance] = useState5("light");
2594
+ const [index, setIndex] = useState5(0);
2595
+ const [dismissed, setDismissed] = useState5(false);
2596
+ const finishingRef = useRef6(false);
2597
+ const dragRef = useRef6(null);
2598
+ const rootRef = useRef6(null);
2599
+ const viewportRef = useRef6(null);
2600
+ const [slideW, setSlideW] = useState5(0);
2601
+ const visibleCount = n >= 2 ? 2 : 1;
2602
+ const maxIndex = Math.max(0, n - visibleCount);
2603
+ useLayoutEffect(() => {
2604
+ const el = viewportRef.current;
2605
+ if (!el || n === 0) return;
2606
+ const measure = () => {
2607
+ const w = el.clientWidth;
2608
+ if (w <= 0) return;
2609
+ const vc = n >= 2 ? 2 : 1;
2610
+ setSlideW((w - CAROUSEL_CARD_GAP * (vc - 1)) / vc);
2611
+ };
2612
+ measure();
2613
+ const ro = new ResizeObserver(() => measure());
2614
+ ro.observe(el);
2615
+ return () => ro.disconnect();
2616
+ }, [n]);
2617
+ useEffect6(() => {
2618
+ if (appearance !== "auto") {
2619
+ setResolvedAppearance(appearance);
2620
+ return;
2621
+ }
2622
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
2623
+ const apply = () => setResolvedAppearance(mq.matches ? "dark" : "light");
2624
+ apply();
2625
+ mq.addEventListener("change", apply);
2626
+ return () => mq.removeEventListener("change", apply);
2627
+ }, [appearance]);
2628
+ useEffect6(() => {
2629
+ const vis = n >= 2 ? 2 : 1;
2630
+ const maxI = Math.max(0, n - vis);
2631
+ setIndex((i) => Math.min(i, maxI));
2632
+ }, [n]);
2633
+ const { blocks, ui, handlers } = useExperienceMessageNode({
2634
+ config,
2635
+ nodeId,
2636
+ experienceId,
2637
+ conversationId,
2638
+ metadata,
2639
+ onAction: onMessageAction
2640
+ });
2641
+ const intro = useMemo10(() => {
2642
+ const b = findTextBlock(blocks);
2643
+ return b?.body.trim() ?? "";
2644
+ }, [blocks]);
2645
+ const primaryBlock = useMemo10(() => findPrimaryBlock3(blocks), [blocks]);
2646
+ const dismissBlock = useMemo10(() => findDismissBlock3(blocks), [blocks]);
2647
+ const primaryAsDismissOnly = !ui.hasPrimaryCta && ui.hasDismissCta;
2648
+ const globalPrimaryLabel = primaryAsDismissOnly ? dismissBlock?.label ?? "Dismiss" : primaryBlock?.label ?? "Continue";
2649
+ const globalGhostLabel = dismissBlock?.label ?? "Dismiss";
2650
+ const showGlobalPrimary = ui.hasPrimaryCta || primaryAsDismissOnly;
2651
+ const showGlobalGhost = ui.hasPrimaryCta && ui.hasDismissCta;
2652
+ const showGlobalFooter = showGlobalPrimary || showGlobalGhost;
2653
+ const finish = useCallback7(() => {
2654
+ if (finishingRef.current) return;
2655
+ finishingRef.current = true;
2656
+ setDismissed(true);
2657
+ window.setTimeout(() => {
2658
+ onComplete();
2659
+ }, DISMISS_MS3);
2660
+ }, [onComplete]);
2661
+ const emitDismissIfConfigured = useCallback7(() => {
2662
+ if (ui.hasDismissCta) {
2663
+ handlers.onDismissCta();
2664
+ }
2665
+ }, [ui.hasDismissCta, handlers]);
2666
+ const handleShellDismiss = useCallback7(() => {
2667
+ emitDismissIfConfigured();
2668
+ finish();
2669
+ }, [emitDismissIfConfigured, finish]);
2670
+ const handleGlobalPrimary = useCallback7(() => {
2671
+ if (primaryAsDismissOnly) {
2672
+ handlers.onDismissCta();
2673
+ finish();
2674
+ return;
2675
+ }
2676
+ if (!primaryBlock) {
2677
+ finish();
2678
+ return;
2679
+ }
2680
+ handlers.onPrimaryCta();
2681
+ if (primaryBlock.ctaType === "link" && primaryBlock.url) {
2682
+ const safe = tryParseHttpImageUrl(primaryBlock.url);
2683
+ if (safe) {
2684
+ try {
2685
+ window.open(safe, "_blank", "noopener,noreferrer");
2686
+ } catch {
2687
+ }
2688
+ }
2689
+ }
2690
+ finish();
2691
+ }, [primaryAsDismissOnly, primaryBlock, handlers, finish]);
2692
+ const handleGlobalGhost = useCallback7(() => {
2693
+ emitDismissIfConfigured();
2694
+ finish();
2695
+ }, [emitDismissIfConfigured, finish]);
2696
+ const goPrev = useCallback7(() => {
2697
+ setIndex((i) => Math.max(0, i - 1));
2698
+ }, []);
2699
+ const goNext = useCallback7(() => {
2700
+ setIndex((i) => Math.min(maxIndex, i + 1));
2701
+ }, [maxIndex]);
2702
+ const handleCardPrimary = useCallback7(
2703
+ (card) => {
2704
+ handlers.onCarouselCta(card.id, "primary");
2705
+ openCarouselButtonLink(card, "primary");
2706
+ finish();
2707
+ },
2708
+ [handlers, finish]
2709
+ );
2710
+ const handleCardSecondary = useCallback7(
2711
+ (card) => {
2712
+ handlers.onCarouselCta(card.id, "secondary");
2713
+ openCarouselButtonLink(card, "secondary");
2714
+ finish();
2715
+ },
2716
+ [handlers, finish]
2717
+ );
2718
+ useEffect6(() => {
2719
+ const el = rootRef.current;
2720
+ if (!el) return;
2721
+ const onKey = (e) => {
2722
+ if (e.key === "ArrowLeft") {
2723
+ e.preventDefault();
2724
+ goPrev();
2725
+ } else if (e.key === "ArrowRight") {
2726
+ e.preventDefault();
2727
+ goNext();
2728
+ }
2729
+ };
2730
+ el.addEventListener("keydown", onKey);
2731
+ return () => el.removeEventListener("keydown", onKey);
2732
+ }, [goPrev, goNext]);
2733
+ const onPointerDown = useCallback7((e) => {
2734
+ dragRef.current = { x: e.clientX, pid: e.pointerId };
2735
+ }, []);
2736
+ const endDrag = useCallback7(
2737
+ (e) => {
2738
+ const d = dragRef.current;
2739
+ dragRef.current = null;
2740
+ if (!d || e.pointerId !== d.pid) return;
2741
+ const dx = e.clientX - d.x;
2742
+ if (dx < -SWIPE_PX) goNext();
2743
+ else if (dx > SWIPE_PX) goPrev();
2744
+ },
2745
+ [goNext, goPrev]
2746
+ );
2747
+ const trackWidthPx = slideW > 0 ? n * slideW + Math.max(0, n - 1) * CAROUSEL_CARD_GAP : void 0;
2748
+ const translateXPx = slideW > 0 ? index * (slideW + CAROUSEL_CARD_GAP) : 0;
2749
+ if (n === 0) {
2750
+ return null;
2751
+ }
2752
+ return /* @__PURE__ */ jsxs4(
2753
+ "div",
2754
+ {
2755
+ ref: rootRef,
2756
+ className: classNames(styles4.root, className),
2757
+ "data-appearance": resolvedAppearance,
2758
+ tabIndex: 0,
2759
+ role: "region",
2760
+ "aria-roledescription": "carousel",
2761
+ "aria-label": "Promotional carousel",
2762
+ children: [
2763
+ intro ? /* @__PURE__ */ jsx5("p", { className: styles4.intro, children: intro }) : null,
2764
+ /* @__PURE__ */ jsxs4(
2765
+ "div",
2766
+ {
2767
+ className: classNames(styles4.shell, dismissed && styles4.shellDismissed),
2768
+ children: [
2769
+ /* @__PURE__ */ jsx5("div", { className: styles4.topBar, children: /* @__PURE__ */ jsx5(
2770
+ "button",
2771
+ {
2772
+ type: "button",
2773
+ className: styles4.dismissTop,
2774
+ onClick: handleShellDismiss,
2775
+ "aria-label": "Dismiss",
2776
+ children: "\u2715"
2777
+ }
2778
+ ) }),
2779
+ /* @__PURE__ */ jsxs4("div", { className: styles4.row, children: [
2780
+ maxIndex > 0 ? /* @__PURE__ */ jsx5(
2781
+ "button",
2782
+ {
2783
+ type: "button",
2784
+ className: styles4.nav,
2785
+ onClick: goPrev,
2786
+ disabled: index <= 0,
2787
+ "aria-label": "Previous slide",
2788
+ children: "\u2039"
2789
+ }
2790
+ ) : null,
2791
+ /* @__PURE__ */ jsx5(
2792
+ "div",
2793
+ {
2794
+ ref: viewportRef,
2795
+ className: styles4.viewport,
2796
+ onPointerDown,
2797
+ onPointerUp: endDrag,
2798
+ onPointerCancel: endDrag,
2799
+ onPointerLeave: (e) => {
2800
+ if (dragRef.current?.pid === e.pointerId) {
2801
+ dragRef.current = null;
2802
+ }
2803
+ },
2804
+ children: /* @__PURE__ */ jsx5(
2805
+ "div",
2806
+ {
2807
+ className: styles4.track,
2808
+ style: {
2809
+ gap: CAROUSEL_CARD_GAP,
2810
+ ...trackWidthPx != null ? {
2811
+ width: trackWidthPx,
2812
+ transform: `translateX(-${translateXPx}px)`
2813
+ } : { width: "100%" }
2814
+ },
2815
+ children: cards.map((card, i) => {
2816
+ const hero = carouselCardHero(card);
2817
+ const title = card.title?.trim() || "Card";
2818
+ const desc = card.description?.trim() ?? "";
2819
+ const inView = i >= index && i < index + visibleCount;
2820
+ return /* @__PURE__ */ jsxs4(
2821
+ "div",
2822
+ {
2823
+ className: styles4.slide,
2824
+ style: slideW > 0 ? { width: slideW, flex: "0 0 auto" } : { flex: "1 1 0", minWidth: 0 },
2825
+ "aria-hidden": !inView,
2826
+ children: [
2827
+ hero.kind === "image" ? /* @__PURE__ */ jsx5(
2828
+ "img",
2829
+ {
2830
+ className: styles4.image,
2831
+ src: hero.src,
2832
+ alt: title,
2833
+ decoding: "async"
2834
+ }
2835
+ ) : /* @__PURE__ */ jsx5("div", { className: styles4.imagePlaceholder, "aria-hidden": true, children: hero.emoji }),
2836
+ /* @__PURE__ */ jsxs4("div", { className: styles4.body, children: [
2837
+ /* @__PURE__ */ jsx5("p", { className: styles4.title, children: title }),
2838
+ desc ? /* @__PURE__ */ jsx5("p", { className: styles4.description, children: desc }) : null
2839
+ ] }),
2840
+ cardPrimaryVisible(card) || cardSecondaryVisible(card) ? /* @__PURE__ */ jsxs4(
2841
+ "div",
2842
+ {
2843
+ className: classNames(
2844
+ styles4.slideFooter,
2845
+ cardPrimaryVisible(card) !== cardSecondaryVisible(card) && styles4.slideFooterSingleCta
2846
+ ),
2847
+ children: [
2848
+ cardPrimaryVisible(card) ? /* @__PURE__ */ jsx5(
2849
+ "button",
2850
+ {
2851
+ type: "button",
2852
+ className: styles4.btnPrimary,
2853
+ onClick: () => handleCardPrimary(card),
2854
+ children: card.buttonText.trim()
2855
+ }
2856
+ ) : null,
2857
+ cardSecondaryVisible(card) ? /* @__PURE__ */ jsx5(
2858
+ "button",
2859
+ {
2860
+ type: "button",
2861
+ className: styles4.btnGhost,
2862
+ onClick: () => handleCardSecondary(card),
2863
+ children: card.secondaryButtonText.trim()
2864
+ }
2865
+ ) : null
2866
+ ]
2867
+ }
2868
+ ) : null
2869
+ ]
2870
+ },
2871
+ card.id
2872
+ );
2873
+ })
2874
+ }
2875
+ )
2876
+ }
2877
+ ),
2878
+ maxIndex > 0 ? /* @__PURE__ */ jsx5(
2879
+ "button",
2880
+ {
2881
+ type: "button",
2882
+ className: styles4.nav,
2883
+ onClick: goNext,
2884
+ disabled: index >= maxIndex,
2885
+ "aria-label": "Next slide",
2886
+ children: "\u203A"
2887
+ }
2888
+ ) : null
2889
+ ] }),
2890
+ maxIndex > 0 ? /* @__PURE__ */ jsx5("div", { className: styles4.dots, role: "tablist", "aria-label": "Slides", children: Array.from({ length: maxIndex + 1 }, (_, page) => /* @__PURE__ */ jsx5(
2891
+ "button",
2892
+ {
2893
+ type: "button",
2894
+ role: "tab",
2895
+ "aria-selected": page === index,
2896
+ "aria-label": `Go to slide group ${page + 1}`,
2897
+ className: classNames(
2898
+ styles4.dot,
2899
+ page === index && styles4.dotActive
2900
+ ),
2901
+ onClick: () => setIndex(page)
2902
+ },
2903
+ page
2904
+ )) }) : null,
2905
+ showGlobalFooter ? /* @__PURE__ */ jsxs4(
2906
+ "div",
2907
+ {
2908
+ className: classNames(
2909
+ styles4.globalFooter,
2910
+ showGlobalPrimary && !showGlobalGhost && styles4.globalFooterSingleCta
2911
+ ),
2912
+ children: [
2913
+ showGlobalPrimary ? /* @__PURE__ */ jsx5(
2914
+ "button",
2915
+ {
2916
+ type: "button",
2917
+ className: styles4.btnPrimary,
2918
+ onClick: handleGlobalPrimary,
2919
+ children: globalPrimaryLabel
2920
+ }
2921
+ ) : null,
2922
+ showGlobalGhost ? /* @__PURE__ */ jsx5(
2923
+ "button",
2924
+ {
2925
+ type: "button",
2926
+ className: styles4.btnGhost,
2927
+ onClick: handleGlobalGhost,
2928
+ children: globalGhostLabel
2929
+ }
2930
+ ) : null
2931
+ ]
2932
+ }
2933
+ ) : null
2934
+ ]
2935
+ }
2936
+ )
2937
+ ]
2938
+ }
2939
+ );
2940
+ }
2941
+
2942
+ // src/components/ExperienceAnnouncementCard.tsx
2943
+ import {
2944
+ useCallback as useCallback8,
2945
+ useEffect as useEffect7,
2946
+ useMemo as useMemo11,
2947
+ useRef as useRef7,
2948
+ useState as useState6
2949
+ } from "react";
2950
+
2951
+ // src/components/ExperienceAnnouncementCard.css
2952
+ styleInject('.ffAnnRoot[data-appearance=light] {\n --ff-ann-shell-bg: #ffffff;\n --ff-ann-border: #e5e5e5;\n --ff-ann-text: #1a1a1a;\n --ff-ann-text-muted: #888888;\n --ff-ann-footer-bg: #f5f5f5;\n --ff-ann-footer-border: #eeeeee;\n --ff-ann-dismiss-hover: #f0f0f0;\n --ff-ann-ghost-hover: #f0f0f0;\n --ff-ann-image-border: #eeeeee;\n --ff-ann-btn-fg: #ffffff;\n --ff-ann-shell-shadow: 0 2px 10px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n.ffAnnRoot[data-appearance=dark] {\n --ff-ann-shell-bg: #1e1e1e;\n --ff-ann-border: #3a3a3a;\n --ff-ann-text: #f3f3f3;\n --ff-ann-text-muted: #a8a8a8;\n --ff-ann-footer-bg: #252525;\n --ff-ann-footer-border: #2e2e2e;\n --ff-ann-dismiss-hover: #2a2a2a;\n --ff-ann-ghost-hover: #2a2a2a;\n --ff-ann-image-border: #333333;\n --ff-ann-btn-fg: #121212;\n --ff-ann-shell-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), 0 1px 4px rgba(0, 0, 0, 0.28);\n}\n.ffAnnRoot[data-announcement-theme=info] {\n --ff-ann-theme-primary: #2563eb;\n}\n.ffAnnRoot[data-announcement-theme=success] {\n --ff-ann-theme-primary: #059669;\n}\n.ffAnnRoot[data-announcement-theme=warning] {\n --ff-ann-theme-primary: #d97706;\n}\n.ffAnnRoot[data-announcement-theme=promo] {\n --ff-ann-theme-primary: #db2777;\n}\n.ffAnnRoot[data-announcement-theme=neutral] {\n --ff-ann-theme-primary: #1a1a1a;\n}\n.ffAnnRoot[data-appearance=dark][data-announcement-theme=info] {\n --ff-ann-theme-primary: #3b82f6;\n}\n.ffAnnRoot[data-appearance=dark][data-announcement-theme=success] {\n --ff-ann-theme-primary: #34d399;\n}\n.ffAnnRoot[data-appearance=dark][data-announcement-theme=warning] {\n --ff-ann-theme-primary: #fbbf24;\n}\n.ffAnnRoot[data-appearance=dark][data-announcement-theme=promo] {\n --ff-ann-theme-primary: #f472b6;\n}\n.ffAnnRoot[data-appearance=dark][data-announcement-theme=neutral] {\n --ff-ann-theme-primary: #f3f3f3;\n}\n.ffAnnRoot {\n --ff-ann-accent: var(--ff-agent-primary, var(--ff-ann-theme-primary));\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n width: 100%;\n box-sizing: border-box;\n color: var(--ff-ann-text);\n}\n.ffAnnShell {\n background: var(--ff-ann-shell-bg);\n border: 1px solid var(--ff-ann-border);\n border-radius: 14px;\n overflow: hidden;\n box-shadow: var(--ff-ann-shell-shadow);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.ffAnnShell.ffAnnDismissed {\n opacity: 0;\n transform: scale(0.98);\n pointer-events: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .ffAnnShell {\n transition: opacity 0.15s ease;\n }\n .ffAnnShell.ffAnnDismissed {\n transform: none;\n }\n}\n.ffAnnImage {\n width: 100%;\n height: 100px;\n object-fit: cover;\n display: block;\n border-bottom: 1px solid var(--ff-ann-image-border);\n}\n.ffAnnImagePlaceholder {\n width: 100%;\n height: 100px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 36px;\n line-height: 1;\n border-bottom: 1px solid var(--ff-ann-image-border);\n background: color-mix(in srgb, var(--ff-ann-accent) 12%, var(--ff-ann-shell-bg));\n}\n.ffAnnTop {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n padding: 14px 16px 0;\n}\n.ffAnnBadge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-size: 11px;\n font-weight: 500;\n padding: 3px 9px;\n border-radius: 999px;\n white-space: nowrap;\n line-height: 1.4;\n background: color-mix(in srgb, var(--ff-ann-accent) 16%, var(--ff-ann-shell-bg));\n color: var(--ff-ann-accent);\n}\n.ffAnnDismiss {\n width: 26px;\n height: 26px;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--ff-ann-text-muted);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n flex-shrink: 0;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n transition: background 0.12s ease, color 0.12s ease;\n margin: -4px -4px 0 0;\n}\n.ffAnnDismiss:hover {\n background: var(--ff-ann-dismiss-hover);\n color: var(--ff-ann-text);\n}\n.ffAnnDismiss.ffAnnDismissSolo {\n margin-left: auto;\n}\n.ffAnnBody {\n padding: 10px 16px 0;\n}\n.ffAnnTitle {\n font-size: 16px;\n font-weight: 500;\n color: var(--ff-ann-text);\n margin: 0 0 6px;\n line-height: 1.35;\n}\n.ffAnnDescription {\n font-size: 13px;\n color: var(--ff-ann-text-muted);\n line-height: 1.65;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ffAnnFooter {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px 16px 14px;\n margin-top: 14px;\n border-top: 1px solid var(--ff-ann-footer-border);\n background: var(--ff-ann-footer-bg);\n}\n.ffAnnBtnPrimary {\n background: var(--ff-ann-accent);\n color: var(--ff-ann-btn-fg);\n border: none;\n border-radius: 8px;\n padding: 7px 16px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n transition: opacity 0.12s ease;\n white-space: nowrap;\n}\n.ffAnnBtnPrimary:hover {\n opacity: 0.88;\n}\n.ffAnnBtnGhost {\n background: transparent;\n border: 1px solid var(--ff-ann-border);\n border-radius: 8px;\n padding: 7px 14px;\n font-size: 13px;\n font-weight: 400;\n cursor: pointer;\n font-family: inherit;\n color: var(--ff-ann-text);\n transition: background 0.12s ease;\n white-space: nowrap;\n}\n.ffAnnBtnGhost:hover {\n background: var(--ff-ann-ghost-hover);\n}\n');
2953
+
2954
+ // src/components/ExperienceAnnouncementCard.tsx
2955
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2956
+ var DISMISS_MS4 = 260;
2957
+ var GHOST_LABEL = "Maybe later";
2958
+ var DEFAULT_EMOJI = "\u{1F389}";
2959
+ var DEFAULT_BADGE_ICON = "\u2605";
2960
+ var THEME_BADGE_LABEL = {
2961
+ info: "What's new",
2962
+ success: "Success",
2963
+ warning: "Important",
2964
+ promo: "Featured",
2965
+ neutral: "Update"
2966
+ };
2967
+ var styles5 = {
2968
+ root: "ffAnnRoot",
2969
+ shell: "ffAnnShell",
2970
+ shellDismissed: "ffAnnDismissed",
2971
+ image: "ffAnnImage",
2972
+ imagePlaceholder: "ffAnnImagePlaceholder",
2973
+ top: "ffAnnTop",
2974
+ badge: "ffAnnBadge",
2975
+ dismiss: "ffAnnDismiss",
2976
+ dismissSolo: "ffAnnDismissSolo",
2977
+ body: "ffAnnBody",
2978
+ title: "ffAnnTitle",
2979
+ description: "ffAnnDescription",
2980
+ footer: "ffAnnFooter",
2981
+ btnPrimary: "ffAnnBtnPrimary",
2982
+ btnGhost: "ffAnnBtnGhost"
2983
+ };
2984
+ function ExperienceAnnouncementCard({
2985
+ experienceId,
2986
+ nodeId,
2987
+ config: configInput,
2988
+ conversationId,
2989
+ metadata,
2990
+ onComplete,
2991
+ appearance = "auto",
2992
+ className,
2993
+ onAnnouncementAction
2994
+ }) {
2995
+ const firstflow = useFirstflow();
2996
+ const [resolvedAppearance, setResolvedAppearance] = useState6("light");
2997
+ const [dismissed, setDismissed] = useState6(false);
2998
+ const finishingRef = useRef7(false);
2999
+ const finishAnimated = useCallback8(() => {
3000
+ if (finishingRef.current) return;
3001
+ finishingRef.current = true;
3002
+ setDismissed(true);
3003
+ window.setTimeout(() => onComplete(), DISMISS_MS4);
3004
+ }, [onComplete]);
3005
+ const { config, content, ui, handlers } = useExperienceAnnouncementNode({
3006
+ config: configInput,
3007
+ nodeId,
3008
+ experienceId,
3009
+ conversationId,
3010
+ metadata,
3011
+ onAction: (e) => {
3012
+ onAnnouncementAction?.(e);
3013
+ if (e.kind === "auto_hidden") {
3014
+ finishAnimated();
3015
+ }
3016
+ }
3017
+ });
3018
+ useEffect7(() => {
3019
+ if (appearance !== "auto") {
3020
+ setResolvedAppearance(appearance);
1463
3021
  return;
1464
3022
  }
1465
- const base = buildShownAnalyticsBase2(
1466
- firstflow.agentId,
1467
- experienceId,
1468
- nodeId,
1469
- conversationId,
1470
- config,
1471
- metadata
1472
- );
1473
- let finalProps = base;
1474
- try {
1475
- finalProps = mapAnalytics?.(EXPERIENCE_SHOWN, { ...base }) ?? base;
1476
- } catch (e) {
1477
- console.warn(
1478
- "[Firstflow] useExperienceAnnouncementNode mapAnalytics (experience_shown)",
1479
- e
1480
- );
1481
- finalProps = base;
3023
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
3024
+ const apply = () => setResolvedAppearance(mq.matches ? "dark" : "light");
3025
+ apply();
3026
+ mq.addEventListener("change", apply);
3027
+ return () => mq.removeEventListener("change", apply);
3028
+ }, [appearance]);
3029
+ const theme = config.announcementTheme ?? "neutral";
3030
+ const agentPrimary = firstflow.getWidgetUi()?.primaryColor?.trim();
3031
+ const hero = useMemo11(() => {
3032
+ const href = tryParseHttpImageUrl(config.announcementImageUrl);
3033
+ if (href) {
3034
+ return { kind: "image", src: href };
1482
3035
  }
1483
- try {
1484
- firstflow.analytics.track(EXPERIENCE_SHOWN, finalProps);
1485
- if (dedupeShown) {
1486
- shownImpressionKeys2.add(impressionKey);
3036
+ return {
3037
+ kind: "emoji",
3038
+ emoji: content.emoji?.trim() || DEFAULT_EMOJI
3039
+ };
3040
+ }, [config.announcementImageUrl, content.emoji]);
3041
+ const badgeLabel = useMemo11(() => {
3042
+ const custom = config.announcementBadge?.trim();
3043
+ if (custom) return custom;
3044
+ return THEME_BADGE_LABEL[theme] ?? THEME_BADGE_LABEL.neutral;
3045
+ }, [config.announcementBadge, theme]);
3046
+ const badgeIcon = config.announcementBadgeIcon?.trim() || DEFAULT_BADGE_ICON;
3047
+ const showBadge = Boolean(badgeLabel);
3048
+ const title = content.title?.trim() || "What's new";
3049
+ const description = content.body?.trim() ?? "";
3050
+ const handleDismiss = useCallback8(() => {
3051
+ handlers.dismiss();
3052
+ finishAnimated();
3053
+ }, [handlers, finishAnimated]);
3054
+ const handlePrimary = useCallback8(() => {
3055
+ handlers.ctaClick();
3056
+ const url = content.ctaUrl?.trim();
3057
+ const safe = url ? tryParseHttpImageUrl(url) : void 0;
3058
+ if (safe) {
3059
+ try {
3060
+ window.open(safe, "_blank", "noopener,noreferrer");
3061
+ } catch {
1487
3062
  }
1488
- } catch (e) {
1489
- console.warn(
1490
- "[Firstflow] useExperienceAnnouncementNode analytics.track (experience_shown)",
1491
- e
1492
- );
1493
3063
  }
1494
- }, [
1495
- dedupeShown,
1496
- impressionKey,
1497
- firstflow,
1498
- experienceId,
3064
+ finishAnimated();
3065
+ }, [handlers, content.ctaUrl, finishAnimated]);
3066
+ const handleGhost = useCallback8(() => {
3067
+ handlers.dismiss();
3068
+ finishAnimated();
3069
+ }, [handlers, finishAnimated]);
3070
+ const showFooter = ui.hasCta || ui.isDismissible;
3071
+ return /* @__PURE__ */ jsx6(
3072
+ "div",
3073
+ {
3074
+ className: classNames(styles5.root, className),
3075
+ "data-appearance": resolvedAppearance,
3076
+ "data-announcement-theme": theme,
3077
+ style: agentPrimary ? {
3078
+ ["--ff-agent-primary"]: agentPrimary
3079
+ } : void 0,
3080
+ children: /* @__PURE__ */ jsxs5(
3081
+ "div",
3082
+ {
3083
+ className: classNames(styles5.shell, dismissed && styles5.shellDismissed),
3084
+ children: [
3085
+ hero.kind === "image" ? /* @__PURE__ */ jsx6(
3086
+ "img",
3087
+ {
3088
+ className: styles5.image,
3089
+ src: hero.src,
3090
+ alt: title,
3091
+ decoding: "async"
3092
+ }
3093
+ ) : /* @__PURE__ */ jsx6("div", { className: styles5.imagePlaceholder, "aria-hidden": true, children: hero.emoji }),
3094
+ /* @__PURE__ */ jsxs5("div", { className: styles5.top, children: [
3095
+ showBadge ? /* @__PURE__ */ jsxs5("span", { className: styles5.badge, children: [
3096
+ badgeIcon ? /* @__PURE__ */ jsx6("span", { style: { fontSize: 11 }, "aria-hidden": true, children: badgeIcon }) : null,
3097
+ badgeLabel
3098
+ ] }) : null,
3099
+ ui.isDismissible ? /* @__PURE__ */ jsx6(
3100
+ "button",
3101
+ {
3102
+ type: "button",
3103
+ className: classNames(
3104
+ styles5.dismiss,
3105
+ !showBadge && styles5.dismissSolo
3106
+ ),
3107
+ onClick: handleDismiss,
3108
+ "aria-label": "Dismiss",
3109
+ children: "\u2715"
3110
+ }
3111
+ ) : null
3112
+ ] }),
3113
+ /* @__PURE__ */ jsxs5("div", { className: styles5.body, children: [
3114
+ /* @__PURE__ */ jsx6("p", { className: styles5.title, children: title }),
3115
+ description ? /* @__PURE__ */ jsx6("p", { className: styles5.description, children: description }) : null
3116
+ ] }),
3117
+ showFooter ? /* @__PURE__ */ jsxs5("div", { className: styles5.footer, children: [
3118
+ ui.hasCta && content.ctaLabel ? /* @__PURE__ */ jsx6(
3119
+ "button",
3120
+ {
3121
+ type: "button",
3122
+ className: styles5.btnPrimary,
3123
+ onClick: handlePrimary,
3124
+ children: content.ctaLabel
3125
+ }
3126
+ ) : null,
3127
+ ui.isDismissible ? /* @__PURE__ */ jsx6(
3128
+ "button",
3129
+ {
3130
+ type: "button",
3131
+ className: styles5.btnGhost,
3132
+ onClick: handleGhost,
3133
+ children: GHOST_LABEL
3134
+ }
3135
+ ) : null
3136
+ ] }) : null
3137
+ ]
3138
+ }
3139
+ )
3140
+ }
3141
+ );
3142
+ }
3143
+
3144
+ // src/components/ExperienceMessageInline.tsx
3145
+ import { useCallback as useCallback9, useEffect as useEffect8, useMemo as useMemo12, useRef as useRef8, useState as useState7 } from "react";
3146
+
3147
+ // src/components/ExperienceMessageInline.css
3148
+ styleInject('.ffMsgInlineRoot[data-appearance=dark] {\n --ff-inl-shell-bg: #1e1e1e;\n --ff-inl-border: #3a3a3a;\n --ff-inl-text: #f3f3f3;\n --ff-inl-text-muted: #a8a8a8;\n --ff-inl-footer-bg: #252525;\n --ff-inl-footer-border: #2e2e2e;\n --ff-inl-accent: #ffffff;\n --ff-inl-accent-contrast: #121212;\n --ff-inl-dismiss-hover: #2a2a2a;\n --ff-inl-ghost-hover: #2a2a2a;\n --ff-inl-chip-bg: #2a2a2a;\n --ff-inl-chip-border: #404040;\n --ff-inl-chip-hover: #333333;\n --ff-inl-shell-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), 0 1px 4px rgba(0, 0, 0, 0.28);\n}\n.ffMsgInlineRoot[data-appearance=light] {\n --ff-inl-shell-bg: #ffffff;\n --ff-inl-border: #e5e5e5;\n --ff-inl-text: #1a1a1a;\n --ff-inl-text-muted: #888888;\n --ff-inl-footer-bg: #f5f5f5;\n --ff-inl-footer-border: #eeeeee;\n --ff-inl-accent: #1a1a1a;\n --ff-inl-accent-contrast: #ffffff;\n --ff-inl-dismiss-hover: #f0f0f0;\n --ff-inl-ghost-hover: #f0f0f0;\n --ff-inl-chip-bg: #f5f5f5;\n --ff-inl-chip-border: #e0e0e0;\n --ff-inl-chip-hover: #eeeeee;\n --ff-inl-shell-shadow: 0 2px 10px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n.ffMsgInlineRoot {\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n width: 100%;\n box-sizing: border-box;\n color: var(--ff-inl-text);\n}\n.ffMsgInlineShell {\n background: var(--ff-inl-shell-bg);\n border: 1px solid var(--ff-inl-border);\n border-radius: 16px;\n overflow: hidden;\n box-shadow: var(--ff-inl-shell-shadow);\n transition: opacity 0.25s ease, transform 0.25s ease;\n}\n.ffMsgInlineShell.ffMsgInlineDismissed {\n opacity: 0;\n transform: scale(0.97);\n pointer-events: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .ffMsgInlineShell {\n transition: opacity 0.15s ease;\n }\n .ffMsgInlineShell.ffMsgInlineDismissed {\n transform: none;\n }\n}\n.ffMsgInlineHead {\n display: flex;\n align-items: flex-start;\n justify-content: flex-end;\n padding: 10px 12px 0;\n}\n.ffMsgInlineDismiss {\n width: 26px;\n height: 26px;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--ff-inl-text-muted);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n flex-shrink: 0;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n transition: background 0.12s ease, color 0.12s ease;\n margin: -4px -4px 0 0;\n}\n.ffMsgInlineDismiss:hover {\n background: var(--ff-inl-dismiss-hover);\n color: var(--ff-inl-text);\n}\n.ffMsgInlineBody {\n padding: 4px 16px 12px;\n}\n.ffMsgInlineText {\n font-size: 14px;\n line-height: 1.55;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--ff-inl-text);\n}\n.ffMsgInlineChips {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 4px 16px 12px;\n}\n.ffMsgInlineChip {\n display: inline-flex;\n align-items: center;\n border: 1px solid var(--ff-inl-chip-border);\n background: var(--ff-inl-chip-bg);\n color: var(--ff-inl-text);\n border-radius: 999px;\n padding: 6px 14px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n transition: background 0.12s ease;\n max-width: 100%;\n}\n.ffMsgInlineChip:hover {\n background: var(--ff-inl-chip-hover);\n}\n.ffMsgInlineFooter {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px 16px 12px;\n border-top: 1px solid var(--ff-inl-footer-border);\n background: var(--ff-inl-footer-bg);\n}\n.ffMsgInlineFooter.ffMsgInlineFooter--singleCta {\n justify-content: center;\n}\n.ffMsgInlineBtnPrimary {\n background: var(--ff-inl-accent);\n color: var(--ff-inl-accent-contrast);\n border: none;\n border-radius: 8px;\n padding: 7px 16px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n transition: opacity 0.12s ease;\n white-space: nowrap;\n}\n.ffMsgInlineBtnPrimary:hover {\n opacity: 0.88;\n}\n.ffMsgInlineBtnGhost {\n background: transparent;\n border: 1px solid var(--ff-inl-border);\n border-radius: 8px;\n padding: 7px 14px;\n font-size: 13px;\n font-weight: 400;\n cursor: pointer;\n font-family: inherit;\n color: var(--ff-inl-text);\n transition: background 0.12s ease;\n white-space: nowrap;\n}\n.ffMsgInlineBtnGhost:hover {\n background: var(--ff-inl-ghost-hover);\n}\n');
3149
+
3150
+ // src/components/ExperienceMessageInline.tsx
3151
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3152
+ var DISMISS_MS5 = 260;
3153
+ var styles6 = {
3154
+ root: "ffMsgInlineRoot",
3155
+ shell: "ffMsgInlineShell",
3156
+ shellDismissed: "ffMsgInlineDismissed",
3157
+ head: "ffMsgInlineHead",
3158
+ dismissBtn: "ffMsgInlineDismiss",
3159
+ body: "ffMsgInlineBody",
3160
+ text: "ffMsgInlineText",
3161
+ chips: "ffMsgInlineChips",
3162
+ chip: "ffMsgInlineChip",
3163
+ footer: "ffMsgInlineFooter",
3164
+ footerSingleCta: "ffMsgInlineFooter--singleCta",
3165
+ btnPrimary: "ffMsgInlineBtnPrimary",
3166
+ btnGhost: "ffMsgInlineBtnGhost"
3167
+ };
3168
+ function findPrimaryBlock4(blocks) {
3169
+ return blocks.find((b) => b.type === "cta_primary");
3170
+ }
3171
+ function findDismissBlock4(blocks) {
3172
+ return blocks.find((b) => b.type === "cta_dismiss");
3173
+ }
3174
+ function findQuickRepliesBlock(blocks) {
3175
+ return blocks.find(
3176
+ (b) => b.type === "quick_replies"
3177
+ );
3178
+ }
3179
+ function ExperienceMessageInline({
3180
+ experienceId,
3181
+ nodeId,
3182
+ config,
3183
+ conversationId,
3184
+ metadata,
3185
+ onComplete,
3186
+ appearance = "auto",
3187
+ className,
3188
+ onMessageAction
3189
+ }) {
3190
+ const [resolvedAppearance, setResolvedAppearance] = useState7("light");
3191
+ const [dismissed, setDismissed] = useState7(false);
3192
+ const finishingRef = useRef8(false);
3193
+ useEffect8(() => {
3194
+ if (appearance !== "auto") {
3195
+ setResolvedAppearance(appearance);
3196
+ return;
3197
+ }
3198
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
3199
+ const apply = () => setResolvedAppearance(mq.matches ? "dark" : "light");
3200
+ apply();
3201
+ mq.addEventListener("change", apply);
3202
+ return () => mq.removeEventListener("change", apply);
3203
+ }, [appearance]);
3204
+ const { blocks, ui, handlers } = useExperienceMessageNode({
3205
+ config,
1499
3206
  nodeId,
3207
+ experienceId,
1500
3208
  conversationId,
1501
- config,
1502
3209
  metadata,
1503
- mapAnalytics
1504
- ]);
1505
- const reportShownRef = useRef4(reportShown);
1506
- reportShownRef.current = reportShown;
1507
- useEffect7(() => {
1508
- if (!trackShownOnMount) return;
1509
- reportShownRef.current();
1510
- }, [trackShownOnMount, impressionKey]);
1511
- const dismiss = useCallback6(() => {
1512
- clearAutoHideTimer();
1513
- emitClick({ kind: "dismiss" });
1514
- }, [clearAutoHideTimer, emitClick]);
1515
- const ctaClick = useCallback6(() => {
1516
- if (!content.ctaLabel) return;
1517
- clearAutoHideTimer();
1518
- emitClick({
1519
- kind: "cta_click",
1520
- label: content.ctaLabel,
1521
- url: content.ctaUrl
1522
- });
1523
- }, [content.ctaLabel, content.ctaUrl, clearAutoHideTimer, emitClick]);
1524
- const cancelAutoHide = useCallback6(() => {
1525
- clearAutoHideTimer();
1526
- }, [clearAutoHideTimer]);
1527
- const handlers = useMemo3(
1528
- () => ({
1529
- dismiss,
1530
- ctaClick,
1531
- cancelAutoHide,
1532
- reportShown
1533
- }),
1534
- [dismiss, ctaClick, cancelAutoHide, reportShown]
3210
+ onAction: onMessageAction
3211
+ });
3212
+ const quickBlock = useMemo12(() => findQuickRepliesBlock(blocks), [blocks]);
3213
+ const primaryBlock = useMemo12(() => findPrimaryBlock4(blocks), [blocks]);
3214
+ const dismissBlock = useMemo12(() => findDismissBlock4(blocks), [blocks]);
3215
+ const finish = useCallback9(() => {
3216
+ if (finishingRef.current) return;
3217
+ finishingRef.current = true;
3218
+ setDismissed(true);
3219
+ window.setTimeout(() => {
3220
+ onComplete();
3221
+ }, DISMISS_MS5);
3222
+ }, [onComplete]);
3223
+ const primaryAsDismissOnly = !ui.hasPrimaryCta && ui.hasDismissCta;
3224
+ const primaryLabel = primaryAsDismissOnly ? dismissBlock?.label ?? "Dismiss" : primaryBlock?.label ?? "Continue";
3225
+ const ghostLabel = dismissBlock?.label ?? "Dismiss";
3226
+ const handleHeaderDismiss = useCallback9(() => {
3227
+ if (ui.hasDismissCta) {
3228
+ handlers.onDismissCta();
3229
+ }
3230
+ finish();
3231
+ }, [ui.hasDismissCta, handlers, finish]);
3232
+ const handlePrimary = useCallback9(() => {
3233
+ if (primaryAsDismissOnly) {
3234
+ handlers.onDismissCta();
3235
+ finish();
3236
+ return;
3237
+ }
3238
+ if (!primaryBlock) {
3239
+ finish();
3240
+ return;
3241
+ }
3242
+ handlers.onPrimaryCta();
3243
+ if (primaryBlock.ctaType === "link" && primaryBlock.url) {
3244
+ const safe = tryParseHttpImageUrl(primaryBlock.url);
3245
+ if (safe) {
3246
+ try {
3247
+ window.open(safe, "_blank", "noopener,noreferrer");
3248
+ } catch {
3249
+ }
3250
+ }
3251
+ }
3252
+ finish();
3253
+ }, [primaryAsDismissOnly, primaryBlock, handlers, finish]);
3254
+ const handleGhost = useCallback9(() => {
3255
+ if (ui.hasDismissCta) {
3256
+ handlers.onDismissCta();
3257
+ }
3258
+ finish();
3259
+ }, [ui.hasDismissCta, handlers, finish]);
3260
+ const handleQuickReply = useCallback9(
3261
+ (label, index) => {
3262
+ handlers.onQuickReply(label, index);
3263
+ finish();
3264
+ },
3265
+ [handlers, finish]
3266
+ );
3267
+ const showFooter = ui.hasPrimaryCta || ui.hasDismissCta;
3268
+ const showGhost = ui.hasPrimaryCta && ui.hasDismissCta;
3269
+ const showQuick = ui.hasQuickReplies && quickBlock?.type === "quick_replies";
3270
+ const bodyText = config.messageContent?.trim() ?? "";
3271
+ return /* @__PURE__ */ jsx7(
3272
+ "div",
3273
+ {
3274
+ className: classNames(styles6.root, className),
3275
+ "data-appearance": resolvedAppearance,
3276
+ children: /* @__PURE__ */ jsxs6(
3277
+ "div",
3278
+ {
3279
+ className: classNames(
3280
+ styles6.shell,
3281
+ dismissed && styles6.shellDismissed
3282
+ ),
3283
+ children: [
3284
+ /* @__PURE__ */ jsx7("div", { className: styles6.head, children: /* @__PURE__ */ jsx7(
3285
+ "button",
3286
+ {
3287
+ type: "button",
3288
+ className: styles6.dismissBtn,
3289
+ onClick: handleHeaderDismiss,
3290
+ "aria-label": "Dismiss",
3291
+ children: "\u2715"
3292
+ }
3293
+ ) }),
3294
+ bodyText ? /* @__PURE__ */ jsx7("div", { className: classNames(styles6.body), children: /* @__PURE__ */ jsx7("p", { className: styles6.text, children: bodyText }) }) : null,
3295
+ showQuick && quickBlock ? /* @__PURE__ */ jsx7("div", { className: styles6.chips, role: "group", "aria-label": "Quick replies", children: quickBlock.options.map((opt, index) => /* @__PURE__ */ jsx7(
3296
+ "button",
3297
+ {
3298
+ type: "button",
3299
+ className: styles6.chip,
3300
+ onClick: () => handleQuickReply(opt.label, index),
3301
+ children: opt.label
3302
+ },
3303
+ `${opt.label}-${index}`
3304
+ )) }) : null,
3305
+ showFooter ? /* @__PURE__ */ jsxs6(
3306
+ "div",
3307
+ {
3308
+ className: classNames(
3309
+ styles6.footer,
3310
+ !showGhost && styles6.footerSingleCta
3311
+ ),
3312
+ children: [
3313
+ /* @__PURE__ */ jsx7(
3314
+ "button",
3315
+ {
3316
+ type: "button",
3317
+ className: styles6.btnPrimary,
3318
+ onClick: handlePrimary,
3319
+ children: primaryLabel
3320
+ }
3321
+ ),
3322
+ showGhost ? /* @__PURE__ */ jsx7(
3323
+ "button",
3324
+ {
3325
+ type: "button",
3326
+ className: styles6.btnGhost,
3327
+ onClick: handleGhost,
3328
+ children: ghostLabel
3329
+ }
3330
+ ) : null
3331
+ ]
3332
+ }
3333
+ ) : null
3334
+ ]
3335
+ }
3336
+ )
3337
+ }
1535
3338
  );
1536
- return {
1537
- config,
1538
- content,
1539
- ui,
1540
- blocksVersion: EXPERIENCE_ANNOUNCEMENT_BLOCKS_VERSION,
1541
- handlers
1542
- };
1543
3339
  }
1544
3340
 
1545
- // src/components/MessageFeedback.module.css
1546
- var MessageFeedback_default = {};
3341
+ // src/components/ExperienceSlot.css
3342
+ styleInject(".ffExperienceSlotRoot {\n width: 100%;\n}\n.ffExperienceSlotHidden {\n display: none;\n}\n");
1547
3343
 
1548
- // src/components/MessageFeedback.tsx
1549
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1550
- function ThumbsIcon({ up, active }) {
1551
- if (up) {
1552
- return /* @__PURE__ */ jsx5("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": true, children: /* @__PURE__ */ jsx5("path", { d: "M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" }) });
3344
+ // src/components/ExperienceSlot.tsx
3345
+ import { jsx as jsx8 } from "react/jsx-runtime";
3346
+ var styles7 = {
3347
+ root: "ffExperienceSlotRoot",
3348
+ hidden: "ffExperienceSlotHidden"
3349
+ };
3350
+ function createSessionSet(previous, experienceId) {
3351
+ const next = new Set(previous);
3352
+ next.add(experienceId);
3353
+ return next;
3354
+ }
3355
+ function isInlineMessageUnrenderable(flowConfig) {
3356
+ const messageStyle = normalizeFlowMessageStyle(flowConfig.messageStyle);
3357
+ const blocks = buildExperienceMessageBlocks(flowConfig);
3358
+ const ui = buildExperienceMessageUi(blocks, flowConfig.messageStyle);
3359
+ if (messageStyle === "carousel" && (flowConfig.carouselCards?.length ?? 0) === 0) {
3360
+ return true;
3361
+ }
3362
+ if (messageStyle === "quick_replies" && !ui.hasQuickReplies) {
3363
+ return true;
1553
3364
  }
1554
- return /* @__PURE__ */ jsx5("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": true, children: /* @__PURE__ */ jsx5("path", { d: "M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7.13-14h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.34 2H17" }) });
3365
+ const hasText = Boolean(flowConfig.messageContent?.trim());
3366
+ const hasFooter = ui.hasPrimaryCta || ui.hasDismissCta;
3367
+ return !hasText && !ui.hasQuickReplies && !hasFooter;
1555
3368
  }
1556
- function MessageFeedback({
3369
+ function ExperienceSlot({
3370
+ mode = "single",
3371
+ typeFilter,
1557
3372
  conversationId,
1558
- messageId,
1559
- messagePreview,
1560
- className,
1561
3373
  metadata,
1562
- variant = "default",
1563
- inlineFormSurface = "light"
3374
+ onShow,
3375
+ onQueueEmpty,
3376
+ renderCandidate,
3377
+ className,
3378
+ appearance = "auto",
3379
+ onExperienceMessageAction,
3380
+ onExperienceAnnouncementAction
1564
3381
  }) {
1565
- const inline = variant === "inline";
1566
- const {
1567
- rating,
1568
- selectedTags,
1569
- comment,
1570
- submitting,
1571
- submitted,
1572
- error,
1573
- setRating,
1574
- toggleTag,
1575
- setComment,
1576
- submit,
1577
- isEnabled,
1578
- sideConfig
1579
- } = useFeedback({
1580
- conversationId,
1581
- messageId,
1582
- messagePreview,
1583
- metadata
1584
- });
1585
- if (!isEnabled) return null;
1586
- const thumbs = /* @__PURE__ */ jsxs4("div", { className: MessageFeedback_default.thumbs, children: [
1587
- /* @__PURE__ */ jsx5(
1588
- "button",
1589
- {
1590
- type: "button",
1591
- className: `${MessageFeedback_default.thumbBtn} ${MessageFeedback_default.thumbBtnLike} ${rating === "like" ? MessageFeedback_default.thumbBtnActiveLike : ""}`,
1592
- onClick: () => setRating("like"),
1593
- "aria-pressed": rating === "like",
1594
- "aria-label": "Thumbs up",
1595
- children: /* @__PURE__ */ jsx5(ThumbsIcon, { up: true, active: rating === "like" })
3382
+ const storage2 = useMemo13(() => createStorageAdapter(), []);
3383
+ const [sessionConsumed, setSessionConsumed] = useState8(() => /* @__PURE__ */ new Set());
3384
+ const [tourStepIndex, setTourStepIndex] = useState8(0);
3385
+ const shownIdRef = useRef9(null);
3386
+ const queueEmptyRef = useRef9(false);
3387
+ const candidates = useFirstflowSlotCandidates(typeFilter);
3388
+ const remaining = useMemo13(
3389
+ () => candidates.filter((candidate) => !sessionConsumed.has(candidate.experienceId)),
3390
+ [candidates, sessionConsumed]
3391
+ );
3392
+ const current = remaining[0] ?? null;
3393
+ const tourExperienceIdRef = useRef9(null);
3394
+ useEffect9(() => {
3395
+ if (!current || current.type !== "message") {
3396
+ tourExperienceIdRef.current = null;
3397
+ return;
3398
+ }
3399
+ const msg = current;
3400
+ if (!msg.tourSteps?.length) return;
3401
+ if (tourExperienceIdRef.current !== current.experienceId) {
3402
+ tourExperienceIdRef.current = current.experienceId;
3403
+ setTourStepIndex(0);
3404
+ }
3405
+ }, [current]);
3406
+ const resolvedMessageStep = useMemo13(() => {
3407
+ if (!current || current.type !== "message") return null;
3408
+ const msg = current;
3409
+ if (msg.tourSteps?.length) {
3410
+ const i = Math.min(tourStepIndex, msg.tourSteps.length - 1);
3411
+ return msg.tourSteps[i];
3412
+ }
3413
+ return { nodeId: msg.nodeId, config: msg.config };
3414
+ }, [current, tourStepIndex]);
3415
+ const advanceCandidate = useCallback10(
3416
+ (candidate) => {
3417
+ setSessionConsumed((prev) => createSessionSet(prev, candidate.experienceId));
3418
+ markExperienceShown(
3419
+ candidate.experienceId,
3420
+ candidate.settings?.frequency ?? EXPERIENCE_SETTINGS_DEFAULT.frequency,
3421
+ storage2
3422
+ );
3423
+ },
3424
+ [storage2]
3425
+ );
3426
+ useEffect9(() => {
3427
+ if (!current) {
3428
+ shownIdRef.current = null;
3429
+ return;
3430
+ }
3431
+ const shownKey = `${current.type}:${current.experienceId}`;
3432
+ if (shownIdRef.current === shownKey) return;
3433
+ shownIdRef.current = shownKey;
3434
+ onShow?.(current.experienceId, current.type);
3435
+ }, [current, onShow]);
3436
+ useEffect9(() => {
3437
+ if (remaining.length > 0) {
3438
+ queueEmptyRef.current = false;
3439
+ return;
3440
+ }
3441
+ if (queueEmptyRef.current) return;
3442
+ queueEmptyRef.current = true;
3443
+ onQueueEmpty?.();
3444
+ }, [remaining.length, onQueueEmpty]);
3445
+ const messageSafetyAdvancedRef = useRef9(/* @__PURE__ */ new Set());
3446
+ useLayoutEffect2(() => {
3447
+ if (!current || current.type !== "message" || !resolvedMessageStep) return;
3448
+ const flowConfig = parseFlowMessageNodeConfig(resolvedMessageStep.config);
3449
+ const messageStyle = normalizeFlowMessageStyle(flowConfig.messageStyle);
3450
+ if (messageStyle === "card" || messageStyle === "rich_card" || messageStyle === "carousel" && (flowConfig.carouselCards?.length ?? 0) > 0) {
3451
+ return;
3452
+ }
3453
+ if (!isInlineMessageUnrenderable(flowConfig)) return;
3454
+ const key = `${current.experienceId}:${resolvedMessageStep.nodeId}`;
3455
+ if (messageSafetyAdvancedRef.current.has(key)) return;
3456
+ messageSafetyAdvancedRef.current.add(key);
3457
+ const msg = current;
3458
+ if (msg.tourSteps?.length) {
3459
+ const i = Math.min(tourStepIndex, msg.tourSteps.length - 1);
3460
+ if (i < msg.tourSteps.length - 1) {
3461
+ setTourStepIndex(i + 1);
3462
+ } else {
3463
+ advanceCandidate(current);
1596
3464
  }
1597
- ),
1598
- /* @__PURE__ */ jsx5(
1599
- "button",
3465
+ } else {
3466
+ advanceCandidate(current);
3467
+ }
3468
+ }, [current, resolvedMessageStep, tourStepIndex, advanceCandidate]);
3469
+ const advance = useCallback10(() => {
3470
+ if (!current) return;
3471
+ const msg = current.type === "message" ? current : null;
3472
+ if (msg?.tourSteps?.length) {
3473
+ const steps = msg.tourSteps;
3474
+ setTourStepIndex((i) => {
3475
+ if (i < steps.length - 1) {
3476
+ return i + 1;
3477
+ }
3478
+ queueMicrotask(() => advanceCandidate(current));
3479
+ return i;
3480
+ });
3481
+ return;
3482
+ }
3483
+ advanceCandidate(current);
3484
+ }, [current, advanceCandidate]);
3485
+ if (!current) return null;
3486
+ if (renderCandidate) {
3487
+ return /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, className), children: renderCandidate(current, advance) });
3488
+ }
3489
+ if (current.type === "survey") {
3490
+ return /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, className), children: /* @__PURE__ */ jsx8(
3491
+ Survey,
1600
3492
  {
1601
- type: "button",
1602
- className: `${MessageFeedback_default.thumbBtn} ${rating === "dislike" ? MessageFeedback_default.thumbBtnActiveDislike : ""}`,
1603
- onClick: () => setRating("dislike"),
1604
- "aria-pressed": rating === "dislike",
1605
- "aria-label": "Thumbs down",
1606
- children: /* @__PURE__ */ jsx5(ThumbsIcon, { up: false, active: rating === "dislike" })
3493
+ experienceId: current.experienceId,
3494
+ conversationId,
3495
+ metadata,
3496
+ layout: "paginated",
3497
+ appearance,
3498
+ onSubmitted: advance
1607
3499
  }
1608
- )
1609
- ] });
1610
- const formBlock = rating && sideConfig ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1611
- /* @__PURE__ */ jsx5("p", { className: MessageFeedback_default.sectionHeading, children: sideConfig.heading.toUpperCase() }),
1612
- sideConfig.showTags ? /* @__PURE__ */ jsx5("div", { className: MessageFeedback_default.tags, children: sideConfig.tags.map((tag) => /* @__PURE__ */ jsx5(
1613
- "button",
3500
+ ) });
3501
+ }
3502
+ if (current.type === "message") {
3503
+ if (!resolvedMessageStep) return mode === "queue" ? /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, styles7.hidden) }) : null;
3504
+ const flowConfig = parseFlowMessageNodeConfig(resolvedMessageStep.config);
3505
+ const messageStyle = normalizeFlowMessageStyle(flowConfig.messageStyle);
3506
+ const messageNodeId = resolvedMessageStep.nodeId;
3507
+ if (messageStyle === "card") {
3508
+ return /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, className), children: /* @__PURE__ */ jsx8(
3509
+ ExperienceMessageCard,
3510
+ {
3511
+ experienceId: current.experienceId,
3512
+ nodeId: messageNodeId,
3513
+ config: flowConfig,
3514
+ conversationId,
3515
+ metadata,
3516
+ appearance,
3517
+ onMessageAction: onExperienceMessageAction,
3518
+ onComplete: advance
3519
+ }
3520
+ ) });
3521
+ }
3522
+ if (messageStyle === "rich_card") {
3523
+ return /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, className), children: /* @__PURE__ */ jsx8(
3524
+ ExperienceMessageRichCard,
3525
+ {
3526
+ experienceId: current.experienceId,
3527
+ nodeId: messageNodeId,
3528
+ config: flowConfig,
3529
+ conversationId,
3530
+ metadata,
3531
+ appearance,
3532
+ onMessageAction: onExperienceMessageAction,
3533
+ onComplete: advance
3534
+ }
3535
+ ) });
3536
+ }
3537
+ if (messageStyle === "carousel" && (flowConfig.carouselCards?.length ?? 0) > 0) {
3538
+ return /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, className), children: /* @__PURE__ */ jsx8(
3539
+ ExperienceMessageCarousel,
3540
+ {
3541
+ experienceId: current.experienceId,
3542
+ nodeId: messageNodeId,
3543
+ config: flowConfig,
3544
+ conversationId,
3545
+ metadata,
3546
+ appearance,
3547
+ onMessageAction: onExperienceMessageAction,
3548
+ onComplete: advance
3549
+ }
3550
+ ) });
3551
+ }
3552
+ if (isInlineMessageUnrenderable(flowConfig)) {
3553
+ return mode === "queue" ? /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, styles7.hidden) }) : null;
3554
+ }
3555
+ return /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, className), children: /* @__PURE__ */ jsx8(
3556
+ ExperienceMessageInline,
1614
3557
  {
1615
- type: "button",
1616
- className: `${MessageFeedback_default.tag} ${selectedTags.includes(tag) ? MessageFeedback_default.tagSelected : ""}`,
1617
- onClick: () => toggleTag(tag),
1618
- children: tag
1619
- },
1620
- tag
1621
- )) }) : null,
1622
- sideConfig.showComment ? /* @__PURE__ */ jsx5(
1623
- "textarea",
3558
+ experienceId: current.experienceId,
3559
+ nodeId: messageNodeId,
3560
+ config: flowConfig,
3561
+ conversationId,
3562
+ metadata,
3563
+ appearance,
3564
+ onMessageAction: onExperienceMessageAction,
3565
+ onComplete: advance
3566
+ }
3567
+ ) });
3568
+ }
3569
+ if (current.type === "announcement") {
3570
+ const annConfig = normalizeFlowAnnouncementNodeConfig(current.config);
3571
+ return /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, className), children: /* @__PURE__ */ jsx8(
3572
+ ExperienceAnnouncementCard,
1624
3573
  {
1625
- className: MessageFeedback_default.textarea,
1626
- placeholder: sideConfig.placeholder,
1627
- value: comment,
1628
- onChange: (e) => setComment(e.target.value),
1629
- "aria-label": "Optional comment"
3574
+ experienceId: current.experienceId,
3575
+ nodeId: current.nodeId,
3576
+ config: annConfig,
3577
+ conversationId,
3578
+ metadata,
3579
+ appearance,
3580
+ onAnnouncementAction: onExperienceAnnouncementAction,
3581
+ onComplete: advance
1630
3582
  }
1631
- ) : null,
1632
- /* @__PURE__ */ jsx5("button", { type: "button", className: MessageFeedback_default.submitBtn, disabled: submitting, onClick: () => void submit(), children: submitting ? "Sending\u2026" : "Send feedback" }),
1633
- submitted && !error && /* @__PURE__ */ jsx5("p", { className: MessageFeedback_default.hint, children: "Thanks \u2014 you can change your choice above and send again anytime." }),
1634
- error && /* @__PURE__ */ jsx5("p", { className: MessageFeedback_default.error, children: error })
1635
- ] }) : null;
1636
- if (inline) {
1637
- const thumbCardClass = [MessageFeedback_default.card, MessageFeedback_default.cardInline, className].filter(Boolean).join(" ");
1638
- const formClass = [
1639
- MessageFeedback_default.inlineFormBelow,
1640
- inlineFormSurface === "dark" ? MessageFeedback_default.inlineFormBelowDark : ""
1641
- ].filter(Boolean).join(" ");
1642
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
1643
- /* @__PURE__ */ jsx5("div", { className: thumbCardClass, "data-ff-feedback-variant": "inline", "data-ff-inline-thumbs": "", children: thumbs }),
1644
- formBlock && /* @__PURE__ */ jsx5("div", { className: formClass, "data-ff-inline-form": "", children: formBlock })
1645
- ] });
3583
+ ) });
1646
3584
  }
1647
- const cardClass = [MessageFeedback_default.card, className].filter(Boolean).join(" ");
1648
- return /* @__PURE__ */ jsxs4("div", { className: cardClass, "data-ff-feedback-variant": "default", children: [
1649
- /* @__PURE__ */ jsx5("p", { className: MessageFeedback_default.previewLabel, children: "PREVIEW" }),
1650
- /* @__PURE__ */ jsx5("div", { className: MessageFeedback_default.previewBox, children: messagePreview || "\u2014" }),
1651
- thumbs,
1652
- formBlock
1653
- ] });
3585
+ return mode === "queue" ? /* @__PURE__ */ jsx8("div", { className: classNames(styles7.root, styles7.hidden) }) : null;
1654
3586
  }
1655
3587
 
1656
- // src/mount.tsx
1657
- import { createRoot } from "react-dom/client";
1658
-
1659
- // src/components/InlineIssueForm.module.css
1660
- var InlineIssueForm_default = {};
1661
-
1662
- // src/components/InlineIssueForm.tsx
1663
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1664
- function InlineIssueForm() {
1665
- const { reporter } = useIssueReporterContext();
1666
- const config = reporter.getConfig();
1667
- if (!config.enabled) return null;
1668
- return /* @__PURE__ */ jsxs5("div", { className: InlineIssueForm_default.wrapper, children: [
1669
- /* @__PURE__ */ jsx6("h3", { className: InlineIssueForm_default.title, children: "Report an issue" }),
1670
- /* @__PURE__ */ jsx6(FormEngine, {})
1671
- ] });
1672
- }
3588
+ // src/components/FirstflowWidget.tsx
3589
+ import { useCallback as useCallback11, useState as useState9 } from "react";
1673
3590
 
1674
- // src/mount.tsx
1675
- import { jsx as jsx7 } from "react/jsx-runtime";
1676
- function mount(firstflow, target) {
1677
- const root = createRoot(target);
1678
- root.render(
1679
- /* @__PURE__ */ jsx7(FirstflowProvider, { firstflow, children: /* @__PURE__ */ jsx7(InlineIssueForm, {}) })
1680
- );
1681
- return () => root.unmount();
1682
- }
3591
+ // src/components/FirstflowWidget.css
3592
+ styleInject(".ffWidgetRoot {\n position: relative;\n width: 100%;\n}\n.ffWidgetChat {\n position: relative;\n z-index: 1;\n}\n.ffWidgetSlotStackWrap {\n position: absolute;\n left: -1%;\n right: 0;\n bottom: -1%;\n z-index: 2;\n pointer-events: none;\n width: 102%;\n}\n.ffWidgetSlotStackWrap.ffWidgetSlotStackWrap--survey {\n bottom: -20px;\n}\n.ffWidgetSlotStackWrap > * {\n pointer-events: auto;\n}\n.ffWidgetSlotStack {\n margin-bottom: 0;\n}\n.ffWidgetOverlayLayer {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-end;\n justify-content: center;\n padding: 12px;\n pointer-events: none;\n z-index: 2;\n}\n.ffWidgetOverlayCard {\n width: 100%;\n max-width: 740px;\n pointer-events: auto;\n}\n");
1683
3593
 
1684
- // src/hooks/useIssueReporter.ts
1685
- import { useCallback as useCallback7 } from "react";
1686
- function useIssueReporter() {
1687
- const { reporter, isModalOpen, openModal, closeModal } = useIssueReporterContext();
1688
- const open = useCallback7(
1689
- (options) => {
1690
- openModal(options);
3594
+ // src/components/FirstflowWidget.tsx
3595
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3596
+ var styles8 = {
3597
+ root: "ffWidgetRoot",
3598
+ chat: "ffWidgetChat",
3599
+ slotStackWrap: "ffWidgetSlotStackWrap",
3600
+ slotStackWrapSurvey: "ffWidgetSlotStackWrap--survey",
3601
+ slotStack: "ffWidgetSlotStack",
3602
+ overlayLayer: "ffWidgetOverlayLayer",
3603
+ overlayCard: "ffWidgetOverlayCard"
3604
+ };
3605
+ function FirstflowWidget({
3606
+ children,
3607
+ conversationId,
3608
+ metadata,
3609
+ experiencePlacement = "stack",
3610
+ className,
3611
+ classNames: classNameMap,
3612
+ experienceSlotProps
3613
+ }) {
3614
+ const [stackSurveyLift, setStackSurveyLift] = useState9(false);
3615
+ const mergedOnShow = useCallback11(
3616
+ (experienceId, type) => {
3617
+ setStackSurveyLift(type === "survey");
3618
+ experienceSlotProps?.onShow?.(experienceId, type);
1691
3619
  },
1692
- [openModal]
3620
+ [experienceSlotProps?.onShow]
1693
3621
  );
1694
- const reportIssue = useCallback7(
1695
- async (data) => {
1696
- await reporter.reportIssue(data);
1697
- },
1698
- [reporter]
3622
+ const mergedOnQueueEmpty = useCallback11(() => {
3623
+ setStackSurveyLift(false);
3624
+ experienceSlotProps?.onQueueEmpty?.();
3625
+ }, [experienceSlotProps?.onQueueEmpty]);
3626
+ const slotClassName = classNames(
3627
+ experienceSlotProps?.className,
3628
+ experiencePlacement === "stack" ? styles8.slotStack : styles8.overlayCard,
3629
+ experiencePlacement === "overlay" ? classNameMap?.overlayCard : void 0
1699
3630
  );
1700
- const submit = reportIssue;
1701
- return {
1702
- reporter,
1703
- open,
1704
- reportIssue,
1705
- submit,
1706
- config: reporter.getConfig(),
1707
- isModalOpen,
1708
- closeModal
1709
- };
3631
+ const slot = /* @__PURE__ */ jsx9(
3632
+ ExperienceSlot,
3633
+ {
3634
+ ...experienceSlotProps,
3635
+ conversationId,
3636
+ metadata,
3637
+ className: slotClassName,
3638
+ onShow: mergedOnShow,
3639
+ onQueueEmpty: mergedOnQueueEmpty
3640
+ }
3641
+ );
3642
+ if (experiencePlacement === "overlay") {
3643
+ return /* @__PURE__ */ jsxs7("section", { className: classNames(styles8.root, className, classNameMap?.root), children: [
3644
+ /* @__PURE__ */ jsx9("div", { className: classNames(styles8.chat, classNameMap?.chat), children }),
3645
+ /* @__PURE__ */ jsx9(
3646
+ "div",
3647
+ {
3648
+ className: classNames(
3649
+ styles8.overlayLayer,
3650
+ classNameMap?.overlayLayer
3651
+ ),
3652
+ children: slot
3653
+ }
3654
+ )
3655
+ ] });
3656
+ }
3657
+ return /* @__PURE__ */ jsxs7("section", { className: classNames(styles8.root, className, classNameMap?.root), children: [
3658
+ /* @__PURE__ */ jsx9("div", { className: classNames(styles8.chat, classNameMap?.chat), children }),
3659
+ /* @__PURE__ */ jsx9(
3660
+ "div",
3661
+ {
3662
+ className: classNames(
3663
+ styles8.slotStackWrap,
3664
+ stackSurveyLift && styles8.slotStackWrapSurvey
3665
+ ),
3666
+ "aria-live": "polite",
3667
+ children: slot
3668
+ }
3669
+ )
3670
+ ] });
1710
3671
  }
1711
3672
 
1712
- // src/issue.ts
1713
- var issue_exports = {};
3673
+ // src/index.ts
3674
+ function createFirstflow2(options) {
3675
+ return createFirstflow(options);
3676
+ }
3677
+ function useFirstflow2() {
3678
+ return useFirstflow();
3679
+ }
1714
3680
  export {
1715
3681
  CHAT_MESSAGE_SENT,
3682
+ EXPERIENCE_ANALYTICS_TYPE,
1716
3683
  EXPERIENCE_ANNOUNCEMENT_ANALYTICS_ACTION,
1717
3684
  EXPERIENCE_ANNOUNCEMENT_BLOCKS_VERSION,
1718
3685
  EXPERIENCE_CLICKED,
1719
3686
  EXPERIENCE_MESSAGE_ANALYTICS_ACTION,
1720
3687
  EXPERIENCE_MESSAGE_BLOCKS_VERSION,
1721
3688
  EXPERIENCE_SHOWN,
1722
- FEEDBACK_SUBMITTED,
3689
+ ExperienceAnnouncementCard,
3690
+ ExperienceMessageCard,
3691
+ ExperienceMessageCarousel,
3692
+ ExperienceMessageInline,
3693
+ ExperienceMessageRichCard,
3694
+ ExperienceSlot,
3695
+ FIRSTFLOW_PUBLIC_API_BASE_URL,
1723
3696
  FirstflowProvider,
1724
- FormEngine,
1725
- ISSUE_SUBMITTED,
1726
- InlineIssueForm,
1727
- issue_exports as Issue,
1728
- IssueModal,
1729
- IssueReporterProvider,
1730
- MessageFeedback,
3697
+ FirstflowWidget,
1731
3698
  SURVEY_COMPLETED,
3699
+ Survey,
1732
3700
  buildExperienceAnnouncementContent,
1733
3701
  buildExperienceAnnouncementUi,
1734
3702
  buildExperienceCarouselCtaAction,
1735
3703
  buildExperienceMessageBlocks,
1736
3704
  buildExperienceMessageUi,
1737
- createFirstflow,
1738
- createIssueReporter,
3705
+ clearFrequencyRecords,
3706
+ createFirstflow2 as createFirstflow,
3707
+ createStorageAdapter,
1739
3708
  fetchSdkAgentConfig,
3709
+ markExperienceShown,
1740
3710
  mount,
1741
3711
  normalizeFlowAnnouncementNodeConfig,
1742
3712
  normalizeFlowMessageStyle,
1743
3713
  normalizeFlowQuickReplyItem,
1744
3714
  normalizeFlowQuickReplyList,
1745
- useCreateFirstflow,
3715
+ parseFlowMessageNodeConfig,
3716
+ tryParseHttpImageUrl,
1746
3717
  useExperienceAnnouncementNode,
3718
+ useExperienceEligibility,
1747
3719
  useExperienceMessageNode,
1748
- useFeedback,
1749
- useFirstflow,
1750
- useFirstflowConfig,
1751
- useIssueForm,
1752
- useIssueReporter,
1753
- useIssueReporterContext
3720
+ useFirstflow2 as useFirstflow,
3721
+ useFirstflowResolveExperience,
3722
+ useFirstflowSelector,
3723
+ useFirstflowSurvey
1754
3724
  };