@firstflow/react 0.0.1 → 0.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,915 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/internal.ts
21
+ var internal_exports = {};
22
+ __export(internal_exports, {
23
+ FEEDBACK_SUBMITTED: () => FEEDBACK_SUBMITTED,
24
+ ISSUE_SUBMITTED: () => ISSUE_SUBMITTED,
25
+ Issue: () => issue_exports,
26
+ IssueReporter: () => IssueReporter,
27
+ IssueReporterProvider: () => IssueReporterProvider,
28
+ createIssueReporter: () => createIssueReporter,
29
+ useFirstflowFeedback: () => useFirstflowFeedback,
30
+ useFirstflowIssueForm: () => useFirstflowIssueForm,
31
+ useFirstflowIssueReporter: () => useFirstflowIssueReporter,
32
+ useIssueReporterContext: () => useIssueReporterContext
33
+ });
34
+ module.exports = __toCommonJS(internal_exports);
35
+
36
+ // src/analytics/events.ts
37
+ var ISSUE_SUBMITTED = "issue_submitted";
38
+ var FEEDBACK_SUBMITTED = "feedback_submitted";
39
+
40
+ // src/feedback/hooks/useFirstflowFeedback.ts
41
+ var import_react5 = require("react");
42
+
43
+ // src/platform/FirstflowContext.tsx
44
+ var import_react4 = require("react");
45
+
46
+ // src/analytics/identity.ts
47
+ var ANON_KEY = "__ff_anon_id";
48
+ var SESSION_KEY = "ff_sess";
49
+ function uuid() {
50
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
51
+ const r = Math.random() * 16 | 0;
52
+ return (c === "x" ? r : r & 3 | 8).toString(16);
53
+ });
54
+ }
55
+ function getStorage(key, storage) {
56
+ if (typeof window === "undefined" || !storage) return uuid();
57
+ try {
58
+ let value = storage.getItem(key);
59
+ if (!value) {
60
+ value = uuid();
61
+ storage.setItem(key, value);
62
+ }
63
+ return value;
64
+ } catch {
65
+ return uuid();
66
+ }
67
+ }
68
+ function getAnonymousId() {
69
+ return getStorage(ANON_KEY, typeof window !== "undefined" ? window.localStorage : null);
70
+ }
71
+ function getSessionId() {
72
+ return getStorage(SESSION_KEY, typeof window !== "undefined" ? window.sessionStorage : null);
73
+ }
74
+
75
+ // src/analytics/device.ts
76
+ function getDeviceType() {
77
+ if (typeof window === "undefined") return "desktop";
78
+ const w = window.innerWidth;
79
+ if (w <= 768) return "mobile";
80
+ if (w <= 1024) return "tablet";
81
+ return "desktop";
82
+ }
83
+
84
+ // src/analytics/jitsu.ts
85
+ var DEFAULT_JITSU_HOST = "https://cmmiyu46c00003b6unusxjf0j.analytics.firstflow.dev";
86
+ var DEFAULT_JITSU_KEY = "P5qcd1d4Bd98EdxS4EfnucLohE2Ch37K:U4OECTXofJVzR5jhjaCpaYj478871PFU";
87
+ function uuid2() {
88
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
89
+ const r = Math.random() * 16 | 0;
90
+ return (c === "x" ? r : r & 3 | 8).toString(16);
91
+ });
92
+ }
93
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
94
+ "anonymousId",
95
+ "agent_id",
96
+ "session_id",
97
+ "device_type",
98
+ "user_id",
99
+ "userId"
100
+ ]);
101
+ function sanitizeTraits(traits) {
102
+ if (!traits) return {};
103
+ const out = {};
104
+ for (const [key, value] of Object.entries(traits)) {
105
+ if (RESERVED_KEYS.has(key)) continue;
106
+ out[key] = value;
107
+ }
108
+ return out;
109
+ }
110
+ function coerceTraitValueToString(value) {
111
+ if (value === null || value === void 0) return null;
112
+ if (typeof value === "string") return value;
113
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
114
+ if (typeof value === "boolean") return String(value);
115
+ try {
116
+ return JSON.stringify(value);
117
+ } catch {
118
+ return String(value);
119
+ }
120
+ }
121
+ function traitsRecordToStringMap(traits) {
122
+ const out = {};
123
+ for (const [k, v] of Object.entries(traits)) {
124
+ const s = coerceTraitValueToString(v);
125
+ if (s !== null) out[k] = s;
126
+ }
127
+ return Object.keys(out).length > 0 ? out : void 0;
128
+ }
129
+ function buildTraitsStringMap(config) {
130
+ const merged = { ...sanitizeTraits(config.traits) };
131
+ const uid = config.userId?.trim();
132
+ if (uid) merged.user_id = uid;
133
+ return traitsRecordToStringMap(merged);
134
+ }
135
+ function buildJitsuTrackPayload(config, event, properties = {}) {
136
+ const traits = buildTraitsStringMap(config);
137
+ const payload = {
138
+ messageId: uuid2(),
139
+ type: "track",
140
+ event,
141
+ anonymousId: getAnonymousId(),
142
+ ...config.userId ? { userId: config.userId } : {},
143
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
144
+ properties: {
145
+ agent_id: config.agentId,
146
+ session_id: getSessionId(),
147
+ device_type: getDeviceType(),
148
+ ...properties
149
+ }
150
+ };
151
+ if (traits) payload.traits = traits;
152
+ return payload;
153
+ }
154
+ function sendTrack(config, event, properties = {}) {
155
+ const host = config.jitsuHost || DEFAULT_JITSU_HOST;
156
+ const key = config.jitsuWriteKey || DEFAULT_JITSU_KEY;
157
+ const payload = buildJitsuTrackPayload(config, event, properties);
158
+ const auth = "Basic " + btoa(key);
159
+ fetch(`${host.replace(/\/$/, "")}/api/s/track`, {
160
+ method: "POST",
161
+ headers: {
162
+ "Content-Type": "application/json",
163
+ Authorization: auth
164
+ },
165
+ body: JSON.stringify(payload),
166
+ keepalive: true
167
+ }).catch(() => {
168
+ });
169
+ }
170
+
171
+ // src/analytics/analytics.ts
172
+ var DEFAULT_JITSU_HOST2 = "https://cmmiyu46c00003b6unusxjf0j.analytics.firstflow.dev";
173
+ var DEFAULT_JITSU_KEY2 = "P5qcd1d4Bd98EdxS4EfnucLohE2Ch37K:U4OECTXofJVzR5jhjaCpaYj478871PFU";
174
+ function createAnalytics(config) {
175
+ const agentId = config.agentId;
176
+ const jitsuHost = config.jitsuHost ?? DEFAULT_JITSU_HOST2;
177
+ const jitsuWriteKey = config.jitsuWriteKey ?? DEFAULT_JITSU_KEY2;
178
+ const identity = {
179
+ userId: config.userId,
180
+ traits: config.traits ?? {}
181
+ };
182
+ function transportConfig() {
183
+ return {
184
+ jitsuHost,
185
+ jitsuWriteKey,
186
+ agentId,
187
+ userId: identity.userId,
188
+ traits: identity.traits
189
+ };
190
+ }
191
+ return {
192
+ track(eventName, properties = {}) {
193
+ sendTrack(transportConfig(), eventName, properties);
194
+ },
195
+ identify(userId, traits = {}) {
196
+ this.setUser(userId, traits);
197
+ if (!userId) return;
198
+ sendTrack(transportConfig(), "user_identified", {});
199
+ },
200
+ page(name, properties = {}) {
201
+ sendTrack(transportConfig(), "page_view", { page_name: name ?? void 0, ...properties });
202
+ },
203
+ setUser(userId, traits = {}) {
204
+ identity.userId = userId || void 0;
205
+ identity.traits = { ...traits ?? {} };
206
+ },
207
+ getUser() {
208
+ return {
209
+ userId: identity.userId,
210
+ traits: { ...identity.traits ?? {} }
211
+ };
212
+ }
213
+ };
214
+ }
215
+
216
+ // src/issue/defaults.ts
217
+ var DEFAULT_PROMPT_TEXT = "Something wrong? Report an issue and we'll look into it.";
218
+ var DEFAULT_ISSUE_TITLE = "Report an issue";
219
+ var DEFAULT_ISSUE_TRIGGER_LABEL = "Report issue";
220
+ var DEFAULT_ISSUE_SUBMIT_LABEL = "Submit";
221
+ var DEFAULT_ISSUE_CANCEL_LABEL = "Cancel";
222
+ var DEFAULT_ISSUE_CANCEL_ENABLED = true;
223
+ var DEFAULT_FIELDS = [
224
+ { id: "name", label: "Name", type: "text", required: true },
225
+ { id: "email", label: "Email", type: "text", required: true },
226
+ { id: "subject", label: "Subject", type: "text", required: true },
227
+ { id: "description", label: "Description", type: "textarea", required: true },
228
+ { id: "session_id", label: "Session ID", type: "text", required: false }
229
+ ];
230
+
231
+ // src/issue/normalizer.ts
232
+ function normalizeField(raw) {
233
+ const id = raw.id?.trim();
234
+ if (!id) return null;
235
+ if (raw.allowed === false) return null;
236
+ const type = raw.type === "textarea" || raw.type === "select" ? raw.type : "text";
237
+ return {
238
+ id,
239
+ type,
240
+ label: raw.label?.trim() ?? id,
241
+ required: raw.required === true,
242
+ options: type === "select" && Array.isArray(raw.options) ? raw.options : void 0
243
+ };
244
+ }
245
+ function normalizeConfig(agentId, apiUrl, raw) {
246
+ const enabled = raw?.enabled === true;
247
+ const title = typeof raw?.title === "string" && raw.title.trim() ? raw.title.trim() : DEFAULT_ISSUE_TITLE;
248
+ const triggerLabel = typeof raw?.triggerLabel === "string" && raw.triggerLabel.trim() ? raw.triggerLabel.trim() : DEFAULT_ISSUE_TRIGGER_LABEL;
249
+ const submitLabel = typeof raw?.submitLabel === "string" && raw.submitLabel.trim() ? raw.submitLabel.trim() : DEFAULT_ISSUE_SUBMIT_LABEL;
250
+ const cancelLabel = typeof raw?.cancelLabel === "string" && raw.cancelLabel.trim() ? raw.cancelLabel.trim() : DEFAULT_ISSUE_CANCEL_LABEL;
251
+ const cancelEnabled = typeof raw?.cancelEnabled === "boolean" ? raw.cancelEnabled : DEFAULT_ISSUE_CANCEL_ENABLED;
252
+ const promptText = typeof raw?.promptText === "string" && raw.promptText.trim() ? raw.promptText.trim() : DEFAULT_PROMPT_TEXT;
253
+ let fields = DEFAULT_FIELDS;
254
+ if (Array.isArray(raw?.fields) && raw.fields.length > 0) {
255
+ const normalized = raw.fields.map(normalizeField).filter((f) => f !== null);
256
+ if (normalized.length > 0) fields = normalized;
257
+ }
258
+ return {
259
+ agentId,
260
+ apiUrl: apiUrl.replace(/\/$/, ""),
261
+ enabled,
262
+ title,
263
+ triggerLabel,
264
+ submitLabel,
265
+ cancelLabel,
266
+ cancelEnabled,
267
+ promptText,
268
+ fields
269
+ };
270
+ }
271
+
272
+ // src/issue/validate.ts
273
+ var CONTEXT_KEYS = /* @__PURE__ */ new Set([
274
+ "conversationId",
275
+ "messageId",
276
+ "sessionId",
277
+ "agentId",
278
+ "model"
279
+ ]);
280
+ function isContextKey(key) {
281
+ return CONTEXT_KEYS.has(key);
282
+ }
283
+ function validatePayload(data, fields) {
284
+ const errors = {};
285
+ const fieldIds = new Set(fields.map((f) => f.id));
286
+ for (const field of fields) {
287
+ const value = data[field.id];
288
+ const str = typeof value === "string" ? value.trim() : "";
289
+ if (field.required && !str) {
290
+ errors[field.id] = `${field.label ?? field.id} is required`;
291
+ continue;
292
+ }
293
+ if (!field.required && !str) continue;
294
+ if (field.type === "select" && field.options?.length) {
295
+ if (!field.options.includes(str)) {
296
+ errors[field.id] = `Please choose a valid option`;
297
+ }
298
+ }
299
+ }
300
+ return {
301
+ valid: Object.keys(errors).length === 0,
302
+ errors: Object.keys(errors).length > 0 ? errors : void 0
303
+ };
304
+ }
305
+ function getFormFieldsOnly(data, fields) {
306
+ const fieldIds = new Set(fields.map((f) => f.id));
307
+ const out = {};
308
+ for (const key of Object.keys(data)) {
309
+ if (isContextKey(key)) continue;
310
+ if (fieldIds.has(key)) out[key] = data[key];
311
+ }
312
+ return out;
313
+ }
314
+
315
+ // src/issue/IssueReporter.ts
316
+ function extractContextMetadata(data, formFieldIds) {
317
+ const ctx = {};
318
+ for (const key of Object.keys(data)) {
319
+ if (formFieldIds.has(key)) continue;
320
+ const v = data[key];
321
+ if (v !== void 0) ctx[key] = v;
322
+ }
323
+ return ctx;
324
+ }
325
+ function buildPayload(formValues, contextMetadata) {
326
+ return { ...formValues, ...contextMetadata };
327
+ }
328
+ function createIssueReporter(options) {
329
+ const { agentId, apiUrl, config: rawConfig, analytics: analyticsOpt } = options;
330
+ const analytics = analyticsOpt ?? createAnalytics({ agentId });
331
+ let config = normalizeConfig(agentId, apiUrl, rawConfig);
332
+ let openHandler;
333
+ const instance = {
334
+ getConfig() {
335
+ return config;
336
+ },
337
+ setConfig(next) {
338
+ config = next;
339
+ },
340
+ async reportIssue(data) {
341
+ const formValues = getFormFieldsOnly(
342
+ data,
343
+ config.fields
344
+ );
345
+ const result = validatePayload(formValues, config.fields);
346
+ if (!result.valid) {
347
+ throw new Error(
348
+ result.errors ? Object.entries(result.errors).map(([k, v]) => `${k}: ${v}`).join(", ") : "Validation failed"
349
+ );
350
+ }
351
+ const formFieldIds = new Set(config.fields.map((f) => f.id));
352
+ const contextMetadata = extractContextMetadata(data, formFieldIds);
353
+ const payload = buildPayload(formValues, contextMetadata);
354
+ analytics.track(ISSUE_SUBMITTED, {
355
+ payload: JSON.stringify(payload),
356
+ metadata: JSON.stringify({})
357
+ });
358
+ },
359
+ open(options2) {
360
+ openHandler?.(options2);
361
+ },
362
+ setOpenHandler(handler) {
363
+ openHandler = handler;
364
+ },
365
+ destroy() {
366
+ openHandler = void 0;
367
+ }
368
+ };
369
+ return instance;
370
+ }
371
+
372
+ // src/issue/IssueReporterContext.tsx
373
+ var import_react3 = require("react");
374
+
375
+ // src/components/IssueForm.tsx
376
+ var import_react2 = require("react");
377
+
378
+ // src/issue/hooks/useFirstflowIssueForm.ts
379
+ var import_react = require("react");
380
+ function emptyValues(fields) {
381
+ const init = {};
382
+ fields.forEach((f) => {
383
+ init[f.id] = "";
384
+ });
385
+ return init;
386
+ }
387
+ function useFirstflowIssueForm() {
388
+ const { reporter, openOptionsRef } = useIssueReporterContext();
389
+ const config = reporter.getConfig();
390
+ const fields = config.fields;
391
+ const [values, setValuesState] = (0, import_react.useState)(() => emptyValues(fields));
392
+ const [errors, setErrors] = (0, import_react.useState)({});
393
+ const [submitting, setSubmitting] = (0, import_react.useState)(false);
394
+ const [submitted, setSubmitted] = (0, import_react.useState)(false);
395
+ const [submitError, setSubmitError] = (0, import_react.useState)(null);
396
+ const fieldsKey = fields.map((f) => f.id).join("\0");
397
+ (0, import_react.useEffect)(() => {
398
+ setValuesState(emptyValues(fields));
399
+ setErrors({});
400
+ setSubmitted(false);
401
+ setSubmitError(null);
402
+ }, [fieldsKey, reporter]);
403
+ const setValue = (0, import_react.useCallback)((fieldId, value) => {
404
+ setValuesState((prev) => ({ ...prev, [fieldId]: value }));
405
+ setErrors((prev) => prev[fieldId] ? { ...prev, [fieldId]: "" } : prev);
406
+ }, []);
407
+ const setValues = (0, import_react.useCallback)((next) => {
408
+ setValuesState(next);
409
+ }, []);
410
+ const validate = (0, import_react.useCallback)(() => {
411
+ const result = validatePayload(
412
+ values,
413
+ fields
414
+ );
415
+ if (!result.valid) {
416
+ setErrors(result.errors ?? {});
417
+ return false;
418
+ }
419
+ setErrors({});
420
+ return true;
421
+ }, [values, fields]);
422
+ const submit = (0, import_react.useCallback)(async () => {
423
+ if (!validate()) return;
424
+ setSubmitting(true);
425
+ setSubmitError(null);
426
+ const context = openOptionsRef.current ?? {};
427
+ try {
428
+ await reporter.reportIssue({ ...values, ...context });
429
+ setSubmitted(true);
430
+ } catch (e) {
431
+ const msg = e instanceof Error ? e.message : String(e);
432
+ setSubmitError(msg);
433
+ throw e;
434
+ } finally {
435
+ setSubmitting(false);
436
+ }
437
+ }, [validate, values, reporter, openOptionsRef]);
438
+ const reset = (0, import_react.useCallback)(() => {
439
+ setValuesState(emptyValues(fields));
440
+ setErrors({});
441
+ setSubmitted(false);
442
+ setSubmitError(null);
443
+ }, [fields]);
444
+ const clearSubmitError = (0, import_react.useCallback)(() => setSubmitError(null), []);
445
+ return {
446
+ values,
447
+ setValue,
448
+ setValues,
449
+ errors,
450
+ submitting,
451
+ submitted,
452
+ submitError,
453
+ validate,
454
+ submit,
455
+ reset,
456
+ clearSubmitError,
457
+ isEnabled: config.enabled,
458
+ config,
459
+ title: config.title,
460
+ triggerLabel: config.triggerLabel,
461
+ submitLabel: config.submitLabel,
462
+ cancelLabel: config.cancelLabel,
463
+ cancelEnabled: config.cancelEnabled,
464
+ uiText: {
465
+ title: config.title,
466
+ triggerLabel: config.triggerLabel,
467
+ submitLabel: config.submitLabel,
468
+ cancelLabel: config.cancelLabel,
469
+ cancelEnabled: config.cancelEnabled,
470
+ promptText: config.promptText
471
+ },
472
+ fields,
473
+ promptText: config.promptText
474
+ };
475
+ }
476
+
477
+ // #style-inject:#style-inject
478
+ function styleInject(css, { insertAt } = {}) {
479
+ if (!css || typeof document === "undefined") return;
480
+ const head = document.head || document.getElementsByTagName("head")[0];
481
+ const style = document.createElement("style");
482
+ style.type = "text/css";
483
+ if (insertAt === "top") {
484
+ if (head.firstChild) {
485
+ head.insertBefore(style, head.firstChild);
486
+ } else {
487
+ head.appendChild(style);
488
+ }
489
+ } else {
490
+ head.appendChild(style);
491
+ }
492
+ if (style.styleSheet) {
493
+ style.styleSheet.cssText = css;
494
+ } else {
495
+ style.appendChild(document.createTextNode(css));
496
+ }
497
+ }
498
+
499
+ // src/components/IssueForm.css
500
+ styleInject(".ffIssueBackdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: grid;\n place-items: center;\n z-index: 1000;\n}\n.ffIssuePanel {\n width: min(560px, calc(100vw - 24px));\n border-radius: 14px;\n background: #fff;\n border: 1px solid #e6e6e6;\n padding: 16px;\n color: #121212;\n}\n.ffIssueTitle {\n margin: 0 0 8px;\n font-size: 18px;\n}\n.ffIssuePrompt {\n margin: 0 0 12px;\n font-size: 14px;\n color: #525252;\n}\n.ffIssueField {\n margin-bottom: 10px;\n}\n.ffIssueLabel {\n display: block;\n font-size: 12px;\n margin-bottom: 4px;\n color: #3f3f3f;\n}\n.ffIssueInput,\n.ffIssueTextarea,\n.ffIssueSelect {\n width: 100%;\n border: 1px solid #d4d4d4;\n border-radius: 8px;\n padding: 8px 10px;\n font-size: 14px;\n}\n.ffIssueTextarea {\n min-height: 84px;\n resize: vertical;\n}\n.ffIssueErrorText {\n margin-top: 4px;\n font-size: 12px;\n color: #b91c1c;\n}\n.ffIssueActions {\n margin-top: 12px;\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.ffIssueButton {\n border-radius: 8px;\n border: 1px solid #d4d4d4;\n padding: 8px 12px;\n background: #fff;\n color: #111;\n cursor: pointer;\n}\n.ffIssuePrimary {\n background: #111;\n color: #fff;\n border-color: #111;\n}\n.ffIssueStatusError {\n margin-top: 8px;\n font-size: 12px;\n color: #b91c1c;\n}\n.ffIssueStatusSuccess {\n margin-top: 8px;\n font-size: 12px;\n color: #166534;\n}\n");
501
+
502
+ // src/components/IssueForm.tsx
503
+ var import_jsx_runtime = require("react/jsx-runtime");
504
+ var styles = {
505
+ backdrop: "ffIssueBackdrop",
506
+ panel: "ffIssuePanel",
507
+ title: "ffIssueTitle",
508
+ prompt: "ffIssuePrompt",
509
+ field: "ffIssueField",
510
+ label: "ffIssueLabel",
511
+ input: "ffIssueInput",
512
+ textarea: "ffIssueTextarea",
513
+ select: "ffIssueSelect",
514
+ errorText: "ffIssueErrorText",
515
+ actions: "ffIssueActions",
516
+ button: "ffIssueButton",
517
+ primary: "ffIssuePrimary",
518
+ statusError: "ffIssueStatusError",
519
+ statusSuccess: "ffIssueStatusSuccess"
520
+ };
521
+ function renderField(field, value, error, onChange) {
522
+ if (field.type === "textarea") {
523
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
524
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
525
+ "textarea",
526
+ {
527
+ className: styles.textarea,
528
+ value,
529
+ onChange: (event) => onChange(field.id, event.target.value)
530
+ }
531
+ ),
532
+ error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: styles.errorText, children: error }) : null
533
+ ] });
534
+ }
535
+ if (field.type === "select") {
536
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
537
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
538
+ "select",
539
+ {
540
+ className: styles.select,
541
+ value,
542
+ onChange: (event) => onChange(field.id, event.target.value),
543
+ children: [
544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "Select an option" }),
545
+ (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option, children: option }, `${field.id}:${option}`))
546
+ ]
547
+ }
548
+ ),
549
+ error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: styles.errorText, children: error }) : null
550
+ ] });
551
+ }
552
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
554
+ "input",
555
+ {
556
+ className: styles.input,
557
+ value,
558
+ onChange: (event) => onChange(field.id, event.target.value)
559
+ }
560
+ ),
561
+ error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: styles.errorText, children: error }) : null
562
+ ] });
563
+ }
564
+ function IssueReporter({ showTrigger = true }) {
565
+ const { isModalOpen, openModal, closeModal } = useIssueReporterContext();
566
+ const {
567
+ fields,
568
+ values,
569
+ errors,
570
+ setValue,
571
+ submit,
572
+ submitting,
573
+ submitted,
574
+ submitError,
575
+ clearSubmitError,
576
+ reset,
577
+ title,
578
+ promptText,
579
+ triggerLabel,
580
+ submitLabel,
581
+ cancelLabel,
582
+ cancelEnabled,
583
+ isEnabled
584
+ } = useFirstflowIssueForm();
585
+ (0, import_react2.useEffect)(() => {
586
+ if (!isModalOpen) {
587
+ reset();
588
+ clearSubmitError();
589
+ }
590
+ }, [isModalOpen, reset, clearSubmitError]);
591
+ if (!isEnabled) return null;
592
+ const handleSubmit = async (event) => {
593
+ event.preventDefault();
594
+ await submit();
595
+ };
596
+ const body = isModalOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: styles.backdrop, role: "presentation", onClick: closeModal, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
597
+ "section",
598
+ {
599
+ className: styles.panel,
600
+ role: "dialog",
601
+ "aria-modal": "true",
602
+ "aria-label": title,
603
+ onClick: (event) => event.stopPropagation(),
604
+ children: [
605
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: styles.title, children: title }),
606
+ promptText ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: styles.prompt, children: promptText }) : null,
607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: handleSubmit, children: [
608
+ fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: styles.field, children: [
609
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: styles.label, htmlFor: `ff-issue-${field.id}`, children: field.label }),
610
+ renderField(
611
+ field,
612
+ values[field.id] ?? "",
613
+ errors[field.id],
614
+ (fieldId, next) => setValue(fieldId, next)
615
+ )
616
+ ] }, field.id)),
617
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: styles.actions, children: [
618
+ cancelEnabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: styles.button, onClick: closeModal, children: cancelLabel }) : null,
619
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
620
+ "button",
621
+ {
622
+ type: "submit",
623
+ className: `${styles.button} ${styles.primary}`,
624
+ disabled: submitting,
625
+ children: submitting ? "Submitting..." : submitLabel
626
+ }
627
+ )
628
+ ] }),
629
+ submitError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: styles.statusError, children: submitError }) : null,
630
+ submitted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: styles.statusSuccess, children: "Thanks for your report." }) : null
631
+ ] })
632
+ ]
633
+ }
634
+ ) }) : null;
635
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
636
+ showTrigger ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: styles.button, onClick: () => openModal(), children: triggerLabel }) : null,
637
+ body
638
+ ] });
639
+ }
640
+
641
+ // src/issue/IssueReporterContext.tsx
642
+ var import_jsx_runtime2 = require("react/jsx-runtime");
643
+ var IssueReporterContext = (0, import_react3.createContext)(
644
+ null
645
+ );
646
+ function IssueReporterProvider({
647
+ reporter,
648
+ children
649
+ }) {
650
+ const [isModalOpen, setIsModalOpen] = (0, import_react3.useState)(false);
651
+ const openOptionsRef = (0, import_react3.useRef)(void 0);
652
+ const openModal = (0, import_react3.useCallback)((options) => {
653
+ openOptionsRef.current = options;
654
+ setIsModalOpen(true);
655
+ }, []);
656
+ const closeModal = (0, import_react3.useCallback)(() => {
657
+ setIsModalOpen(false);
658
+ openOptionsRef.current = void 0;
659
+ }, []);
660
+ (0, import_react3.useEffect)(() => {
661
+ reporter.setOpenHandler(openModal);
662
+ return () => {
663
+ reporter.setOpenHandler(void 0);
664
+ };
665
+ }, [reporter, openModal]);
666
+ const value = {
667
+ reporter,
668
+ isModalOpen,
669
+ openModal,
670
+ closeModal,
671
+ openOptionsRef
672
+ };
673
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(IssueReporterContext.Provider, { value, children: [
674
+ children,
675
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReporter, { showTrigger: false })
676
+ ] });
677
+ }
678
+ function useIssueReporterContext() {
679
+ const ctx = (0, import_react3.useContext)(IssueReporterContext);
680
+ if (ctx == null) {
681
+ throw new Error(
682
+ "useIssueReporterContext must be used within IssueReporterProvider"
683
+ );
684
+ }
685
+ return ctx;
686
+ }
687
+
688
+ // src/platform/FirstflowContext.tsx
689
+ var import_jsx_runtime3 = require("react/jsx-runtime");
690
+ var FirstflowContext = (0, import_react4.createContext)(null);
691
+ function useFirstflow() {
692
+ const ctx = (0, import_react4.useContext)(FirstflowContext);
693
+ if (ctx == null) {
694
+ throw new Error("useFirstflow must be used within FirstflowProvider");
695
+ }
696
+ return ctx;
697
+ }
698
+
699
+ // src/feedback/hooks/useFirstflowFeedback.ts
700
+ function storageKey(agentId, messageId) {
701
+ return `ff_feedback_${agentId}_${messageId}`;
702
+ }
703
+ function isMessageScope(o) {
704
+ return o != null && typeof o.conversationId === "string" && typeof o.messageId === "string" && typeof o.messagePreview === "string";
705
+ }
706
+ function useFirstflowFeedback(options) {
707
+ const firstflow = useFirstflow();
708
+ const scoped = isMessageScope(options);
709
+ const [rating, setRatingState] = (0, import_react5.useState)(null);
710
+ const [selectedTags, setSelectedTags] = (0, import_react5.useState)([]);
711
+ const [comment, setCommentState] = (0, import_react5.useState)("");
712
+ const [submitting, setSubmitting] = (0, import_react5.useState)(false);
713
+ const [error, setError] = (0, import_react5.useState)(null);
714
+ const [submitted, setSubmitted] = (0, import_react5.useState)(false);
715
+ const isEnabled = firstflow.feedback.isEnabled();
716
+ const config = isEnabled ? firstflow.feedback.getConfig() : null;
717
+ const sideConfig = (0, import_react5.useMemo)(() => {
718
+ if (!scoped || !config || !rating) return null;
719
+ return rating === "like" ? config.like : config.dislike;
720
+ }, [scoped, config, rating]);
721
+ const ratingRef = (0, import_react5.useRef)(rating);
722
+ ratingRef.current = rating;
723
+ const conversationId = scoped ? options.conversationId : "";
724
+ const messageId = scoped ? options.messageId : "";
725
+ const messagePreview = scoped ? options.messagePreview : "";
726
+ const metadata = scoped ? options.metadata : void 0;
727
+ (0, import_react5.useEffect)(() => {
728
+ if (!scoped) return;
729
+ setRatingState(null);
730
+ setSelectedTags([]);
731
+ setCommentState("");
732
+ setError(null);
733
+ setSubmitted(false);
734
+ if (!isEnabled) return;
735
+ try {
736
+ const raw = localStorage.getItem(storageKey(firstflow.agentId, messageId));
737
+ if (!raw) return;
738
+ const parsed = JSON.parse(raw);
739
+ if (parsed.rating === "like" || parsed.rating === "dislike") {
740
+ setRatingState(parsed.rating);
741
+ setSelectedTags(Array.isArray(parsed.tags) ? parsed.tags : []);
742
+ setCommentState(typeof parsed.comment === "string" ? parsed.comment : "");
743
+ }
744
+ } catch {
745
+ }
746
+ }, [scoped, firstflow.agentId, isEnabled, messageId]);
747
+ const setRating = (0, import_react5.useCallback)(
748
+ (r) => {
749
+ if (!scoped) return;
750
+ if (r !== ratingRef.current) {
751
+ setSelectedTags([]);
752
+ setCommentState("");
753
+ }
754
+ setRatingState(r);
755
+ setError(null);
756
+ setSubmitted(false);
757
+ if (!isEnabled) return;
758
+ const cfg = firstflow.feedback.getConfig();
759
+ const sideCfg = r === "like" ? cfg.like : cfg.dislike;
760
+ if (sideCfg.showTags || sideCfg.showComment) return;
761
+ setSubmitting(true);
762
+ const autoPayload = {
763
+ conversationId,
764
+ messageId,
765
+ rating: r,
766
+ showTags: false,
767
+ showComment: false,
768
+ messagePreview: messagePreview.slice(0, 2e3),
769
+ metadata
770
+ };
771
+ void firstflow.feedback.submit(autoPayload).then(() => {
772
+ try {
773
+ localStorage.setItem(
774
+ storageKey(firstflow.agentId, messageId),
775
+ JSON.stringify({ rating: r, tags: [], comment: "" })
776
+ );
777
+ } catch {
778
+ }
779
+ setSubmitted(true);
780
+ }).catch((e) => {
781
+ setError(e instanceof Error ? e.message : "Failed to send feedback");
782
+ }).finally(() => {
783
+ setSubmitting(false);
784
+ });
785
+ },
786
+ // eslint-disable-next-line react-hooks/exhaustive-deps
787
+ [scoped, isEnabled, firstflow, conversationId, messageId, messagePreview, metadata]
788
+ );
789
+ const toggleTag = (0, import_react5.useCallback)(
790
+ (tag) => {
791
+ if (!scoped) return;
792
+ setSelectedTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]);
793
+ setError(null);
794
+ },
795
+ [scoped]
796
+ );
797
+ const setComment = (0, import_react5.useCallback)(
798
+ (c) => {
799
+ if (!scoped) return;
800
+ setCommentState(c);
801
+ },
802
+ [scoped]
803
+ );
804
+ const clearError = (0, import_react5.useCallback)(() => setError(null), []);
805
+ const submit = (0, import_react5.useCallback)(async () => {
806
+ if (!scoped || !isEnabled || !rating) return;
807
+ setSubmitting(true);
808
+ setError(null);
809
+ const cfg = firstflow.feedback.getConfig();
810
+ const sideCfg = rating === "like" ? cfg.like : cfg.dislike;
811
+ const showTags = sideCfg.showTags === true;
812
+ const showComment = sideCfg.showComment === true;
813
+ const payload = {
814
+ conversationId,
815
+ messageId,
816
+ rating,
817
+ showTags,
818
+ showComment,
819
+ tags: showTags && selectedTags.length ? [...new Set(selectedTags)] : void 0,
820
+ comment: showComment && comment.trim() ? comment.trim() : void 0,
821
+ messagePreview: messagePreview.slice(0, 2e3),
822
+ metadata
823
+ };
824
+ try {
825
+ await firstflow.feedback.submit(payload);
826
+ try {
827
+ localStorage.setItem(
828
+ storageKey(firstflow.agentId, messageId),
829
+ JSON.stringify({ rating, tags: selectedTags, comment })
830
+ );
831
+ } catch {
832
+ }
833
+ setSubmitted(true);
834
+ } catch (e) {
835
+ setError(e instanceof Error ? e.message : "Failed to send feedback");
836
+ } finally {
837
+ setSubmitting(false);
838
+ }
839
+ }, [
840
+ scoped,
841
+ isEnabled,
842
+ rating,
843
+ conversationId,
844
+ messageId,
845
+ messagePreview,
846
+ metadata,
847
+ selectedTags,
848
+ comment,
849
+ firstflow,
850
+ isEnabled
851
+ ]);
852
+ if (!scoped) {
853
+ return firstflow.feedback;
854
+ }
855
+ return {
856
+ rating,
857
+ selectedTags,
858
+ comment,
859
+ submitting,
860
+ submitted,
861
+ error,
862
+ setRating,
863
+ toggleTag,
864
+ setComment,
865
+ submit,
866
+ clearError,
867
+ isEnabled,
868
+ config,
869
+ sideConfig
870
+ };
871
+ }
872
+
873
+ // src/issue/hooks/useFirstflowIssueReporter.ts
874
+ var import_react6 = require("react");
875
+ function useFirstflowIssueReporter() {
876
+ const { reporter, isModalOpen, openModal, closeModal } = useIssueReporterContext();
877
+ const open = (0, import_react6.useCallback)(
878
+ (options) => {
879
+ openModal(options);
880
+ },
881
+ [openModal]
882
+ );
883
+ const reportIssue = (0, import_react6.useCallback)(
884
+ async (data) => {
885
+ await reporter.reportIssue(data);
886
+ },
887
+ [reporter]
888
+ );
889
+ const submit = reportIssue;
890
+ return {
891
+ reporter,
892
+ open,
893
+ reportIssue,
894
+ submit,
895
+ config: reporter.getConfig(),
896
+ isModalOpen,
897
+ closeModal
898
+ };
899
+ }
900
+
901
+ // src/issue/issue.ts
902
+ var issue_exports = {};
903
+ // Annotate the CommonJS export names for ESM import in node:
904
+ 0 && (module.exports = {
905
+ FEEDBACK_SUBMITTED,
906
+ ISSUE_SUBMITTED,
907
+ Issue,
908
+ IssueReporter,
909
+ IssueReporterProvider,
910
+ createIssueReporter,
911
+ useFirstflowFeedback,
912
+ useFirstflowIssueForm,
913
+ useFirstflowIssueReporter,
914
+ useIssueReporterContext
915
+ });