@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,228 @@
1
+ import {
2
+ FEEDBACK_SUBMITTED,
3
+ ISSUE_SUBMITTED,
4
+ IssueReporter,
5
+ IssueReporterProvider,
6
+ createIssueReporter,
7
+ useFirstflow,
8
+ useFirstflowIssueForm,
9
+ useIssueReporterContext
10
+ } from "./chunk-WTTDFCFD.mjs";
11
+
12
+ // src/feedback/hooks/useFirstflowFeedback.ts
13
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
14
+ function storageKey(agentId, messageId) {
15
+ return `ff_feedback_${agentId}_${messageId}`;
16
+ }
17
+ function isMessageScope(o) {
18
+ return o != null && typeof o.conversationId === "string" && typeof o.messageId === "string" && typeof o.messagePreview === "string";
19
+ }
20
+ function useFirstflowFeedback(options) {
21
+ const firstflow = useFirstflow();
22
+ const scoped = isMessageScope(options);
23
+ const [rating, setRatingState] = useState(null);
24
+ const [selectedTags, setSelectedTags] = useState([]);
25
+ const [comment, setCommentState] = useState("");
26
+ const [submitting, setSubmitting] = useState(false);
27
+ const [error, setError] = useState(null);
28
+ const [submitted, setSubmitted] = useState(false);
29
+ const isEnabled = firstflow.feedback.isEnabled();
30
+ const config = isEnabled ? firstflow.feedback.getConfig() : null;
31
+ const sideConfig = useMemo(() => {
32
+ if (!scoped || !config || !rating) return null;
33
+ return rating === "like" ? config.like : config.dislike;
34
+ }, [scoped, config, rating]);
35
+ const ratingRef = useRef(rating);
36
+ ratingRef.current = rating;
37
+ const conversationId = scoped ? options.conversationId : "";
38
+ const messageId = scoped ? options.messageId : "";
39
+ const messagePreview = scoped ? options.messagePreview : "";
40
+ const metadata = scoped ? options.metadata : void 0;
41
+ useEffect(() => {
42
+ if (!scoped) return;
43
+ setRatingState(null);
44
+ setSelectedTags([]);
45
+ setCommentState("");
46
+ setError(null);
47
+ setSubmitted(false);
48
+ if (!isEnabled) return;
49
+ try {
50
+ const raw = localStorage.getItem(storageKey(firstflow.agentId, messageId));
51
+ if (!raw) return;
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed.rating === "like" || parsed.rating === "dislike") {
54
+ setRatingState(parsed.rating);
55
+ setSelectedTags(Array.isArray(parsed.tags) ? parsed.tags : []);
56
+ setCommentState(typeof parsed.comment === "string" ? parsed.comment : "");
57
+ }
58
+ } catch {
59
+ }
60
+ }, [scoped, firstflow.agentId, isEnabled, messageId]);
61
+ const setRating = useCallback(
62
+ (r) => {
63
+ if (!scoped) return;
64
+ if (r !== ratingRef.current) {
65
+ setSelectedTags([]);
66
+ setCommentState("");
67
+ }
68
+ setRatingState(r);
69
+ setError(null);
70
+ setSubmitted(false);
71
+ if (!isEnabled) return;
72
+ const cfg = firstflow.feedback.getConfig();
73
+ const sideCfg = r === "like" ? cfg.like : cfg.dislike;
74
+ if (sideCfg.showTags || sideCfg.showComment) return;
75
+ setSubmitting(true);
76
+ const autoPayload = {
77
+ conversationId,
78
+ messageId,
79
+ rating: r,
80
+ showTags: false,
81
+ showComment: false,
82
+ messagePreview: messagePreview.slice(0, 2e3),
83
+ metadata
84
+ };
85
+ void firstflow.feedback.submit(autoPayload).then(() => {
86
+ try {
87
+ localStorage.setItem(
88
+ storageKey(firstflow.agentId, messageId),
89
+ JSON.stringify({ rating: r, tags: [], comment: "" })
90
+ );
91
+ } catch {
92
+ }
93
+ setSubmitted(true);
94
+ }).catch((e) => {
95
+ setError(e instanceof Error ? e.message : "Failed to send feedback");
96
+ }).finally(() => {
97
+ setSubmitting(false);
98
+ });
99
+ },
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ [scoped, isEnabled, firstflow, conversationId, messageId, messagePreview, metadata]
102
+ );
103
+ const toggleTag = useCallback(
104
+ (tag) => {
105
+ if (!scoped) return;
106
+ setSelectedTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]);
107
+ setError(null);
108
+ },
109
+ [scoped]
110
+ );
111
+ const setComment = useCallback(
112
+ (c) => {
113
+ if (!scoped) return;
114
+ setCommentState(c);
115
+ },
116
+ [scoped]
117
+ );
118
+ const clearError = useCallback(() => setError(null), []);
119
+ const submit = useCallback(async () => {
120
+ if (!scoped || !isEnabled || !rating) return;
121
+ setSubmitting(true);
122
+ setError(null);
123
+ const cfg = firstflow.feedback.getConfig();
124
+ const sideCfg = rating === "like" ? cfg.like : cfg.dislike;
125
+ const showTags = sideCfg.showTags === true;
126
+ const showComment = sideCfg.showComment === true;
127
+ const payload = {
128
+ conversationId,
129
+ messageId,
130
+ rating,
131
+ showTags,
132
+ showComment,
133
+ tags: showTags && selectedTags.length ? [...new Set(selectedTags)] : void 0,
134
+ comment: showComment && comment.trim() ? comment.trim() : void 0,
135
+ messagePreview: messagePreview.slice(0, 2e3),
136
+ metadata
137
+ };
138
+ try {
139
+ await firstflow.feedback.submit(payload);
140
+ try {
141
+ localStorage.setItem(
142
+ storageKey(firstflow.agentId, messageId),
143
+ JSON.stringify({ rating, tags: selectedTags, comment })
144
+ );
145
+ } catch {
146
+ }
147
+ setSubmitted(true);
148
+ } catch (e) {
149
+ setError(e instanceof Error ? e.message : "Failed to send feedback");
150
+ } finally {
151
+ setSubmitting(false);
152
+ }
153
+ }, [
154
+ scoped,
155
+ isEnabled,
156
+ rating,
157
+ conversationId,
158
+ messageId,
159
+ messagePreview,
160
+ metadata,
161
+ selectedTags,
162
+ comment,
163
+ firstflow,
164
+ isEnabled
165
+ ]);
166
+ if (!scoped) {
167
+ return firstflow.feedback;
168
+ }
169
+ return {
170
+ rating,
171
+ selectedTags,
172
+ comment,
173
+ submitting,
174
+ submitted,
175
+ error,
176
+ setRating,
177
+ toggleTag,
178
+ setComment,
179
+ submit,
180
+ clearError,
181
+ isEnabled,
182
+ config,
183
+ sideConfig
184
+ };
185
+ }
186
+
187
+ // src/issue/hooks/useFirstflowIssueReporter.ts
188
+ import { useCallback as useCallback2 } from "react";
189
+ function useFirstflowIssueReporter() {
190
+ const { reporter, isModalOpen, openModal, closeModal } = useIssueReporterContext();
191
+ const open = useCallback2(
192
+ (options) => {
193
+ openModal(options);
194
+ },
195
+ [openModal]
196
+ );
197
+ const reportIssue = useCallback2(
198
+ async (data) => {
199
+ await reporter.reportIssue(data);
200
+ },
201
+ [reporter]
202
+ );
203
+ const submit = reportIssue;
204
+ return {
205
+ reporter,
206
+ open,
207
+ reportIssue,
208
+ submit,
209
+ config: reporter.getConfig(),
210
+ isModalOpen,
211
+ closeModal
212
+ };
213
+ }
214
+
215
+ // src/issue/issue.ts
216
+ var issue_exports = {};
217
+ export {
218
+ FEEDBACK_SUBMITTED,
219
+ ISSUE_SUBMITTED,
220
+ issue_exports as Issue,
221
+ IssueReporter,
222
+ IssueReporterProvider,
223
+ createIssueReporter,
224
+ useFirstflowFeedback,
225
+ useFirstflowIssueForm,
226
+ useFirstflowIssueReporter,
227
+ useIssueReporterContext
228
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firstflow/react",
3
- "version": "0.0.1",
4
- "description": "Firstflow platform React SDK — issues, message feedback, and experience flow helpers",
3
+ "version": "0.0.101",
4
+ "description": "Firstflow platform React SDK — surveys, experiences (messages, announcements, slots), and analytics",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -11,8 +11,11 @@
11
11
  "import": "./dist/index.mjs",
12
12
  "require": "./dist/index.js"
13
13
  },
14
- "./index.css": "./dist/index.css",
15
- "./dist/index.css": "./dist/index.css"
14
+ "./internal": {
15
+ "types": "./dist/internal.d.ts",
16
+ "import": "./dist/internal.mjs",
17
+ "require": "./dist/internal.js"
18
+ }
16
19
  },
17
20
  "files": [
18
21
  "dist"
@@ -20,8 +23,8 @@
20
23
  "keywords": [
21
24
  "firstflow",
22
25
  "react",
23
- "issue-reporter",
24
- "feedback",
26
+ "onboarding",
27
+ "survey",
25
28
  "sdk"
26
29
  ],
27
30
  "license": "UNLICENSED",
@@ -29,9 +32,10 @@
29
32
  "access": "public"
30
33
  },
31
34
  "scripts": {
32
- "build": "tsup src/index.ts --format cjs --format esm --dts --clean",
35
+ "build": "tsup",
33
36
  "prepublishOnly": "npm run build",
34
- "typecheck": "tsc --noEmit"
37
+ "typecheck": "tsc --noEmit",
38
+ "test:analytics-payload": "tsx src/analytics/jitsu.payload.run.ts"
35
39
  },
36
40
  "peerDependencies": {
37
41
  "react": ">=18.0.0",
@@ -43,7 +47,21 @@
43
47
  "react": "^18.2.0",
44
48
  "react-dom": "^18.2.0",
45
49
  "tsup": "^8.0.0",
50
+ "tsx": "^4.19.2",
46
51
  "typescript": "^5.0.0"
47
52
  },
48
- "sideEffects": false
53
+ "sideEffects": [
54
+ "*.css",
55
+ "dist/index.js",
56
+ "dist/index.mjs"
57
+ ],
58
+ "dependencies": {
59
+ "@radix-ui/react-checkbox": "^1.3.3",
60
+ "@radix-ui/react-dialog": "^1.1.15",
61
+ "@radix-ui/react-label": "^2.1.8",
62
+ "@radix-ui/react-radio-group": "^1.3.8",
63
+ "@radix-ui/react-slider": "^1.3.6",
64
+ "@radix-ui/react-toggle-group": "^1.1.11",
65
+ "fast-deep-equal": "^3.1.3"
66
+ }
49
67
  }
package/dist/index.css DELETED
@@ -1,360 +0,0 @@
1
- /* src/components/FormEngine.module.css */
2
- .form {
3
- display: flex;
4
- flex-direction: column;
5
- gap: 1rem;
6
- }
7
- .field {
8
- display: flex;
9
- flex-direction: column;
10
- gap: 0.25rem;
11
- }
12
- .label {
13
- font-size: 0.875rem;
14
- font-weight: 600;
15
- color: #334155;
16
- }
17
- .input,
18
- .textarea,
19
- .select {
20
- width: 100%;
21
- padding: 0.5rem 0.75rem;
22
- font-size: 0.875rem;
23
- border: 1px solid #e2e8f0;
24
- border-radius: 0.5rem;
25
- color: #0f172a;
26
- background: #fff;
27
- box-sizing: border-box;
28
- }
29
- .textarea {
30
- min-height: 5rem;
31
- resize: vertical;
32
- }
33
- .input:focus,
34
- .textarea:focus,
35
- .select:focus {
36
- outline: none;
37
- border-color: #0f766e;
38
- box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.2);
39
- }
40
- .inputError,
41
- .textareaError,
42
- .selectError {
43
- border-color: #dc2626;
44
- }
45
- .errorText {
46
- font-size: 0.75rem;
47
- color: #dc2626;
48
- }
49
- .actions {
50
- display: flex;
51
- gap: 0.5rem;
52
- justify-content: flex-end;
53
- margin-top: 0.5rem;
54
- }
55
- .prompt {
56
- font-size: 0.875rem;
57
- color: #64748b;
58
- margin: 0 0 0.5rem 0;
59
- }
60
- .submitError {
61
- font-size: 0.8125rem;
62
- color: #dc2626;
63
- margin: 0;
64
- }
65
-
66
- /* src/components/IssueModal.module.css */
67
- .overlay {
68
- position: fixed;
69
- inset: 0;
70
- background: rgba(0, 0, 0, 0.4);
71
- z-index: 9998;
72
- display: flex;
73
- align-items: center;
74
- justify-content: center;
75
- }
76
- .panel {
77
- position: relative;
78
- width: 90%;
79
- max-width: 400px;
80
- background: #fff;
81
- border-radius: 1rem;
82
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
83
- z-index: 9999;
84
- padding: 1.5rem;
85
- }
86
- .close {
87
- position: absolute;
88
- top: 0.5rem;
89
- right: 0.5rem;
90
- background: none;
91
- border: none;
92
- font-size: 1.5rem;
93
- line-height: 1;
94
- cursor: pointer;
95
- color: #94a3b8;
96
- padding: 0.25rem;
97
- }
98
- .close:hover {
99
- color: #0f172a;
100
- }
101
- .title {
102
- font-size: 1rem;
103
- font-weight: 700;
104
- color: #0f172a;
105
- margin: 0 0 0.5rem 0;
106
- }
107
-
108
- /* src/components/MessageFeedback.module.css */
109
- .card {
110
- margin-top: 0.75rem;
111
- padding: 1rem 1.125rem;
112
- border-radius: 12px;
113
- border: 1px solid #e2e8f0;
114
- background: #f8fafc;
115
- font-family:
116
- ui-sans-serif,
117
- system-ui,
118
- -apple-system,
119
- Segoe UI,
120
- Roboto,
121
- sans-serif;
122
- max-width: 100%;
123
- box-sizing: border-box;
124
- }
125
- .previewLabel {
126
- font-size: 0.6875rem;
127
- font-weight: 700;
128
- letter-spacing: 0.06em;
129
- color: #64748b;
130
- margin: 0 0 0.5rem 0;
131
- }
132
- .previewBox {
133
- padding: 0.75rem 0.875rem;
134
- border-radius: 8px;
135
- background: #fff;
136
- border: 1px solid #e2e8f0;
137
- font-size: 0.875rem;
138
- line-height: 1.45;
139
- color: #1e293b;
140
- margin-bottom: 0.875rem;
141
- }
142
- .thumbs {
143
- display: flex;
144
- gap: 0.75rem;
145
- align-items: center;
146
- }
147
- .thumbBtn {
148
- display: inline-flex;
149
- align-items: center;
150
- justify-content: center;
151
- width: 44px;
152
- height: 44px;
153
- border-radius: 10px;
154
- border: 2px solid #cbd5e1;
155
- background: #fff;
156
- cursor: pointer;
157
- transition:
158
- border-color 0.15s,
159
- background 0.15s,
160
- color 0.15s;
161
- color: #64748b;
162
- }
163
- .thumbBtn:hover {
164
- border-color: #94a3b8;
165
- background: #f1f5f9;
166
- }
167
- .thumbBtnLike {
168
- border-color: #86efac;
169
- color: #15803d;
170
- background: #ecfdf5;
171
- }
172
- .thumbBtnLike:hover {
173
- border-color: #4ade80;
174
- background: #d1fae5;
175
- color: #166534;
176
- }
177
- .thumbBtnActiveLike {
178
- border-color: #16a34a;
179
- color: #fff;
180
- background:
181
- linear-gradient(
182
- 180deg,
183
- #22c55e 0%,
184
- #16a34a 100%);
185
- box-shadow: 0 1px 3px rgba(22, 163, 74, 0.35);
186
- }
187
- .thumbBtnActiveLike:hover {
188
- border-color: #15803d;
189
- background:
190
- linear-gradient(
191
- 180deg,
192
- #16a34a 0%,
193
- #15803d 100%);
194
- color: #fff;
195
- }
196
- .thumbBtnActiveDislike {
197
- border-color: #f87171;
198
- color: #dc2626;
199
- background: #fef2f2;
200
- }
201
- .sectionHeading {
202
- font-size: 0.6875rem;
203
- font-weight: 700;
204
- letter-spacing: 0.06em;
205
- color: #334155;
206
- margin: 1.125rem 0 0.625rem 0;
207
- }
208
- .tags {
209
- display: flex;
210
- flex-wrap: wrap;
211
- gap: 0.5rem;
212
- margin-bottom: 0.75rem;
213
- }
214
- .tag {
215
- padding: 0.375rem 0.75rem;
216
- border-radius: 999px;
217
- border: 1px solid #e2e8f0;
218
- background: #fff;
219
- font-size: 0.8125rem;
220
- color: #475569;
221
- cursor: pointer;
222
- transition: border-color 0.15s, background 0.15s;
223
- }
224
- .tag:hover {
225
- border-color: #cbd5e1;
226
- }
227
- .tagSelected {
228
- border-color: #3b82f6;
229
- background: #eff6ff;
230
- color: #1d4ed8;
231
- }
232
- .textarea {
233
- width: 100%;
234
- min-height: 72px;
235
- padding: 0.625rem 0.75rem;
236
- border-radius: 8px;
237
- border: 1px solid #e2e8f0;
238
- font-size: 0.8125rem;
239
- font-family: inherit;
240
- resize: vertical;
241
- box-sizing: border-box;
242
- margin-bottom: 0.75rem;
243
- }
244
- .textarea:focus {
245
- outline: none;
246
- border-color: #3b82f6;
247
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
248
- }
249
- .submitBtn {
250
- padding: 0.5rem 1rem;
251
- border-radius: 8px;
252
- border: none;
253
- background: #0f172a;
254
- color: #fff;
255
- font-size: 0.8125rem;
256
- font-weight: 600;
257
- cursor: pointer;
258
- }
259
- .submitBtn:hover:not(:disabled) {
260
- background: #1e293b;
261
- }
262
- .submitBtn:disabled {
263
- opacity: 0.5;
264
- cursor: not-allowed;
265
- }
266
- .hint {
267
- font-size: 0.75rem;
268
- color: #64748b;
269
- margin: 0.5rem 0 0 0;
270
- }
271
- .error {
272
- font-size: 0.75rem;
273
- color: #dc2626;
274
- margin: 0.5rem 0 0 0;
275
- }
276
- .cardInline {
277
- margin-top: 0;
278
- padding: 0.45rem 0.55rem;
279
- align-self: flex-start;
280
- width: auto;
281
- max-width: 100%;
282
- }
283
- .cardInline .thumbs {
284
- gap: 0.35rem;
285
- }
286
- .cardInline .thumbBtn {
287
- width: 34px;
288
- height: 34px;
289
- border-radius: 8px;
290
- }
291
- .cardInline .thumbBtn svg {
292
- width: 18px;
293
- height: 18px;
294
- }
295
- .cardInline .sectionHeading {
296
- margin-top: 0.65rem;
297
- }
298
- .inlineFormBelow {
299
- box-sizing: border-box;
300
- width: 100%;
301
- padding: 0.75rem 1rem;
302
- border-radius: 10px;
303
- border: 1px solid #e2e8f0;
304
- background: #fff;
305
- margin-top: 0.125rem;
306
- }
307
- .inlineFormBelow .sectionHeading {
308
- margin-top: 0;
309
- }
310
- .inlineFormBelowDark {
311
- border-color: rgba(255, 255, 255, 0.12);
312
- background: rgba(255, 255, 255, 0.06);
313
- }
314
- .inlineFormBelowDark .sectionHeading {
315
- color: rgba(255, 255, 255, 0.65);
316
- }
317
- .inlineFormBelowDark .tag {
318
- border-color: rgba(255, 255, 255, 0.15);
319
- background: rgba(0, 0, 0, 0.2);
320
- color: rgba(255, 255, 255, 0.85);
321
- }
322
- .inlineFormBelowDark .tag:hover {
323
- border-color: rgba(255, 255, 255, 0.25);
324
- background: rgba(0, 0, 0, 0.35);
325
- }
326
- .inlineFormBelowDark .tagSelected {
327
- border-color: #60a5fa;
328
- background: rgba(59, 130, 246, 0.2);
329
- color: #93c5fd;
330
- }
331
- .inlineFormBelowDark .textarea {
332
- background: rgba(0, 0, 0, 0.25);
333
- border-color: rgba(255, 255, 255, 0.12);
334
- color: #f1f5f9;
335
- }
336
- .inlineFormBelowDark .textarea:focus {
337
- border-color: #60a5fa;
338
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
339
- }
340
- .inlineFormBelowDark .hint {
341
- color: rgba(255, 255, 255, 0.5);
342
- }
343
- .inlineFormBelowDark .submitBtn {
344
- background: #e2e8f0;
345
- color: #0f172a;
346
- }
347
- .inlineFormBelowDark .submitBtn:hover:not(:disabled) {
348
- background: #f8fafc;
349
- }
350
-
351
- /* src/components/InlineIssueForm.module.css */
352
- .wrapper {
353
- margin-top: 1rem;
354
- }
355
- .title {
356
- font-size: 1rem;
357
- font-weight: 700;
358
- color: #0f172a;
359
- margin: 0 0 0.5rem 0;
360
- }