@cohiva/support-widget 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1008 @@
1
+ // src/components/SupportWidget.tsx
2
+ import { useCallback, useEffect as useEffect2, useMemo, useState } from "react";
3
+
4
+ // src/adapters/centralApi.ts
5
+ function createCentralFeedbackClient(options) {
6
+ const endpointPath = options.endpointPath || "/api/feedback";
7
+ const fetchImpl = options.fetchImpl || fetch;
8
+ return async function submitFeedback(payload) {
9
+ const token = options.getAuthToken?.();
10
+ const response = await fetchImpl(`${options.apiBaseUrl}${endpointPath}`, {
11
+ method: "POST",
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ ...token ? { Authorization: `Bearer ${token}` } : {}
15
+ },
16
+ body: JSON.stringify(payload)
17
+ });
18
+ const contentType = response.headers.get("content-type") || "";
19
+ const responseBody = contentType.includes("application/json") ? await response.json() : void 0;
20
+ if (!response.ok) {
21
+ const detail = responseBody?.detail || `Request failed with status ${response.status}`;
22
+ throw new Error(String(detail));
23
+ }
24
+ return responseBody || {};
25
+ };
26
+ }
27
+
28
+ // src/adapters/githubIssueProxy.ts
29
+ function createGitHubIssueProxyClient(options) {
30
+ const endpointPath = options.endpointPath || "/api/feedback/github-issue";
31
+ const fetchImpl = options.fetchImpl || fetch;
32
+ return async function submitFeedbackAsIssue(payload) {
33
+ const token = options.getAuthToken?.();
34
+ const response = await fetchImpl(`${options.apiBaseUrl}${endpointPath}`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ ...token ? { Authorization: `Bearer ${token}` } : {}
39
+ },
40
+ body: JSON.stringify(payload)
41
+ });
42
+ const contentType = response.headers.get("content-type") || "";
43
+ const responseBody = contentType.includes("application/json") ? await response.json() : void 0;
44
+ if (!response.ok) {
45
+ const detail = responseBody?.detail || `Request failed with status ${response.status}`;
46
+ throw new Error(String(detail));
47
+ }
48
+ return responseBody || {};
49
+ };
50
+ }
51
+
52
+ // src/diagnostics/index.ts
53
+ var STORAGE_KEY = "cohiva_feedback_diagnostics_v1";
54
+ var MAX_RECENT_ACTIONS = 20;
55
+ var MAX_CONSOLE_ERRORS = 15;
56
+ var MAX_MESSAGE_LENGTH = 500;
57
+ var DEFAULT_CONFIG = {
58
+ enabled: true,
59
+ captureClicks: true,
60
+ captureFormSubmits: true,
61
+ captureConsoleWarnings: true,
62
+ captureConsoleErrors: true,
63
+ captureUnhandledRejections: true,
64
+ captureWindowErrors: true,
65
+ clearPolicy: "on_success"
66
+ };
67
+ var cleanupRef = null;
68
+ function truncate(message) {
69
+ return message.slice(0, MAX_MESSAGE_LENGTH);
70
+ }
71
+ function readStoredDiagnostics() {
72
+ if (typeof window === "undefined") {
73
+ return { recent_actions: [], recent_console_errors: [] };
74
+ }
75
+ try {
76
+ const parsed = JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || "{}");
77
+ return {
78
+ recent_actions: Array.isArray(parsed.recent_actions) ? parsed.recent_actions : [],
79
+ recent_console_errors: Array.isArray(parsed.recent_console_errors) ? parsed.recent_console_errors : []
80
+ };
81
+ } catch {
82
+ return { recent_actions: [], recent_console_errors: [] };
83
+ }
84
+ }
85
+ function writeStoredDiagnostics(payload) {
86
+ if (typeof window === "undefined") {
87
+ return;
88
+ }
89
+ window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
90
+ }
91
+ function pushItem(key, entry, config) {
92
+ const redacted = config.redact ? config.redact(entry) : entry;
93
+ if (!redacted) {
94
+ return;
95
+ }
96
+ const stored = readStoredDiagnostics();
97
+ const nextItem = {
98
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
99
+ type: redacted.type,
100
+ message: truncate(redacted.message)
101
+ };
102
+ if (key === "recent_actions") {
103
+ stored.recent_actions = [nextItem, ...stored.recent_actions].slice(0, MAX_RECENT_ACTIONS);
104
+ } else {
105
+ stored.recent_console_errors = [nextItem, ...stored.recent_console_errors].slice(0, MAX_CONSOLE_ERRORS);
106
+ }
107
+ writeStoredDiagnostics(stored);
108
+ }
109
+ function elementLabel(element) {
110
+ return (element.getAttribute("aria-label") || element.getAttribute("data-testid") || element.getAttribute("name") || element.getAttribute("title") || element.textContent || element.tagName).trim().slice(0, 120);
111
+ }
112
+ function normalizeConfig(config) {
113
+ return {
114
+ ...DEFAULT_CONFIG,
115
+ ...config || {}
116
+ };
117
+ }
118
+ function installFeedbackDiagnostics(config) {
119
+ if (typeof window === "undefined") {
120
+ return () => void 0;
121
+ }
122
+ const normalized = normalizeConfig(config);
123
+ if (!normalized.enabled) {
124
+ return () => void 0;
125
+ }
126
+ if (cleanupRef) {
127
+ return cleanupRef;
128
+ }
129
+ const clickHandler = (event) => {
130
+ if (!normalized.captureClicks) {
131
+ return;
132
+ }
133
+ const target = event.target;
134
+ if (!target) {
135
+ return;
136
+ }
137
+ const label = elementLabel(target);
138
+ const type = target.tagName.toLowerCase();
139
+ pushItem("recent_actions", { type: "click", message: `Interacted with ${type}: ${label}` }, normalized);
140
+ };
141
+ const submitHandler = (event) => {
142
+ if (!normalized.captureFormSubmits) {
143
+ return;
144
+ }
145
+ const target = event.target;
146
+ if (!target) {
147
+ return;
148
+ }
149
+ const label = elementLabel(target);
150
+ pushItem("recent_actions", { type: "submit", message: `Submitted form: ${label}` }, normalized);
151
+ };
152
+ const errorHandler = (event) => {
153
+ if (!normalized.captureWindowErrors) {
154
+ return;
155
+ }
156
+ pushItem("recent_console_errors", {
157
+ type: "window_error",
158
+ message: event.message || "Unhandled window error"
159
+ }, normalized);
160
+ };
161
+ const rejectionHandler = (event) => {
162
+ if (!normalized.captureUnhandledRejections) {
163
+ return;
164
+ }
165
+ pushItem("recent_console_errors", {
166
+ type: "unhandled_rejection",
167
+ message: String(event.reason || "Unhandled promise rejection")
168
+ }, normalized);
169
+ };
170
+ const originalConsoleError = console.error;
171
+ const originalConsoleWarn = console.warn;
172
+ if (normalized.captureConsoleErrors) {
173
+ console.error = (...args) => {
174
+ pushItem("recent_console_errors", { type: "console_error", message: args.map(String).join(" ") }, normalized);
175
+ originalConsoleError(...args);
176
+ };
177
+ }
178
+ if (normalized.captureConsoleWarnings) {
179
+ console.warn = (...args) => {
180
+ pushItem("recent_console_errors", { type: "console_warn", message: args.map(String).join(" ") }, normalized);
181
+ originalConsoleWarn(...args);
182
+ };
183
+ }
184
+ document.addEventListener("click", clickHandler, true);
185
+ document.addEventListener("submit", submitHandler, true);
186
+ window.addEventListener("error", errorHandler);
187
+ window.addEventListener("unhandledrejection", rejectionHandler);
188
+ cleanupRef = () => {
189
+ document.removeEventListener("click", clickHandler, true);
190
+ document.removeEventListener("submit", submitHandler, true);
191
+ window.removeEventListener("error", errorHandler);
192
+ window.removeEventListener("unhandledrejection", rejectionHandler);
193
+ console.error = originalConsoleError;
194
+ console.warn = originalConsoleWarn;
195
+ cleanupRef = null;
196
+ };
197
+ return cleanupRef;
198
+ }
199
+ function clearFeedbackDiagnostics() {
200
+ if (typeof window === "undefined") {
201
+ return;
202
+ }
203
+ window.sessionStorage.removeItem(STORAGE_KEY);
204
+ }
205
+ function recordFeedbackRouteVisit(route) {
206
+ const pathname = route.pathname || "unknown";
207
+ const href = route.href || "";
208
+ const title = route.title || "";
209
+ pushItem("recent_actions", {
210
+ type: "route_visit",
211
+ message: `Visited route ${pathname}${href ? ` (${href})` : ""}${title ? ` - ${title}` : ""}`
212
+ }, normalizeConfig());
213
+ }
214
+ function getFeedbackDiagnosticsSnapshot(route) {
215
+ if (typeof window === "undefined") {
216
+ return {};
217
+ }
218
+ const stored = readStoredDiagnostics();
219
+ const navigatorInfo = window.navigator;
220
+ const navEntry = performance.getEntriesByType("navigation")[0];
221
+ const memory = performance.memory;
222
+ return {
223
+ page_title: route?.title || document.title,
224
+ page_url: route?.href || window.location.href,
225
+ current_route: route?.pathname || window.location.pathname,
226
+ referrer: document.referrer || void 0,
227
+ language: navigator.language,
228
+ languages: Array.from(navigator.languages || []),
229
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
230
+ submitted_at_client: (/* @__PURE__ */ new Date()).toISOString(),
231
+ browser_info: navigator.userAgent,
232
+ online: navigator.onLine,
233
+ device_pixel_ratio: window.devicePixelRatio,
234
+ viewport: {
235
+ width: window.innerWidth,
236
+ height: window.innerHeight
237
+ },
238
+ screen: {
239
+ width: window.screen?.width,
240
+ height: window.screen?.height
241
+ },
242
+ network_info: navigatorInfo.connection ? {
243
+ type: navigatorInfo.connection.type,
244
+ effective_type: navigatorInfo.connection.effectiveType,
245
+ downlink_mbps: navigatorInfo.connection.downlink,
246
+ rtt_ms: navigatorInfo.connection.rtt,
247
+ save_data: navigatorInfo.connection.saveData
248
+ } : void 0,
249
+ performance_timing: navEntry ? {
250
+ page_load_ms: Math.round(navEntry.loadEventEnd - navEntry.startTime),
251
+ dom_interactive_ms: Math.round(navEntry.domInteractive - navEntry.startTime),
252
+ dom_content_loaded_ms: Math.round(
253
+ navEntry.domContentLoadedEventEnd - navEntry.startTime
254
+ ),
255
+ ttfb_ms: Math.round(navEntry.responseStart - navEntry.startTime)
256
+ } : void 0,
257
+ memory_info: memory ? {
258
+ used_js_heap_mb: Math.round((memory.usedJSHeapSize || 0) / 1024 / 1024),
259
+ total_js_heap_mb: Math.round((memory.totalJSHeapSize || 0) / 1024 / 1024),
260
+ limit_mb: Math.round((memory.jsHeapSizeLimit || 0) / 1024 / 1024)
261
+ } : void 0,
262
+ recent_actions: stored.recent_actions,
263
+ recent_console_errors: stored.recent_console_errors
264
+ };
265
+ }
266
+
267
+ // src/hooks/useRouteTracking.ts
268
+ import { useEffect, useRef } from "react";
269
+ function useRouteTracking(getCurrentRoute, enabled) {
270
+ const previousPathRef = useRef(void 0);
271
+ useEffect(() => {
272
+ if (!enabled) {
273
+ return;
274
+ }
275
+ const checkRoute = () => {
276
+ const route = getCurrentRoute();
277
+ const currentPath = route.pathname || route.href;
278
+ if (!currentPath || previousPathRef.current === currentPath) {
279
+ return;
280
+ }
281
+ previousPathRef.current = currentPath;
282
+ recordFeedbackRouteVisit(route);
283
+ };
284
+ checkRoute();
285
+ const interval = window.setInterval(checkRoute, 1200);
286
+ return () => window.clearInterval(interval);
287
+ }, [enabled, getCurrentRoute]);
288
+ }
289
+
290
+ // src/schema/formSchema.ts
291
+ import { z } from "zod";
292
+ var feedbackFieldLabels = {
293
+ type: "Feedback Type",
294
+ title: "Title",
295
+ priority: "Priority",
296
+ satisfaction_rating: "Satisfaction Rating",
297
+ experience_summary: "Experience Summary",
298
+ what_were_you_doing: "What were you doing?",
299
+ what_happened: "What happened?",
300
+ expected_outcome: "Expected outcome",
301
+ steps_to_reproduce: "Steps to reproduce",
302
+ desired_solution: "Desired solution",
303
+ impact_details: "Impact details",
304
+ additional_context: "Additional context",
305
+ frequency: "Frequency",
306
+ impact_level: "Impact level"
307
+ };
308
+ var priorityOptions = [
309
+ { value: "low", label: "Low" },
310
+ { value: "medium", label: "Medium" },
311
+ { value: "high", label: "High" },
312
+ { value: "critical", label: "Critical" }
313
+ ];
314
+ var frequencyOptions = [
315
+ { value: "once", label: "Once" },
316
+ { value: "sometimes", label: "Sometimes" },
317
+ { value: "often", label: "Often" },
318
+ { value: "every_time", label: "Every time" }
319
+ ];
320
+ var impactLevelOptions = [
321
+ { value: "minor", label: "Minor" },
322
+ { value: "moderate", label: "Moderate" },
323
+ { value: "major", label: "Major" },
324
+ { value: "critical", label: "Critical" }
325
+ ];
326
+ var feedbackTypes = [
327
+ {
328
+ value: "bug",
329
+ label: "Bug Report",
330
+ description: "Something is broken or not working as expected",
331
+ icon: "bug",
332
+ fields: [
333
+ {
334
+ key: "what_were_you_doing",
335
+ label: "What were you doing?",
336
+ placeholder: "Describe what you were trying to do",
337
+ required: true,
338
+ inputType: "textarea"
339
+ },
340
+ {
341
+ key: "what_happened",
342
+ label: "What happened?",
343
+ placeholder: "Describe what actually happened",
344
+ required: true,
345
+ inputType: "textarea"
346
+ },
347
+ {
348
+ key: "expected_outcome",
349
+ label: "What did you expect?",
350
+ placeholder: "Describe expected behavior",
351
+ required: true,
352
+ inputType: "textarea"
353
+ },
354
+ {
355
+ key: "steps_to_reproduce",
356
+ label: "Steps to reproduce",
357
+ placeholder: "List the steps so we can reproduce the issue",
358
+ inputType: "textarea"
359
+ },
360
+ {
361
+ key: "impact_details",
362
+ label: "Impact details",
363
+ placeholder: "How is this affecting your work?",
364
+ inputType: "textarea"
365
+ },
366
+ {
367
+ key: "additional_context",
368
+ label: "Additional context",
369
+ placeholder: "Anything else that may help us debug",
370
+ inputType: "textarea"
371
+ }
372
+ ]
373
+ },
374
+ {
375
+ value: "feature",
376
+ label: "Feature Request",
377
+ description: "Request a new capability or enhancement",
378
+ icon: "sparkles",
379
+ fields: [
380
+ {
381
+ key: "experience_summary",
382
+ label: "Experience summary",
383
+ placeholder: "What are you trying to achieve?",
384
+ required: true,
385
+ inputType: "textarea"
386
+ },
387
+ {
388
+ key: "desired_solution",
389
+ label: "Desired solution",
390
+ placeholder: "Describe the capability you want",
391
+ required: true,
392
+ inputType: "textarea"
393
+ },
394
+ {
395
+ key: "what_were_you_doing",
396
+ label: "Current workflow",
397
+ placeholder: "How do you do this today?",
398
+ inputType: "textarea"
399
+ },
400
+ {
401
+ key: "impact_details",
402
+ label: "Impact details",
403
+ placeholder: "How much value would this create?",
404
+ inputType: "textarea"
405
+ },
406
+ {
407
+ key: "additional_context",
408
+ label: "Additional context",
409
+ placeholder: "Any extra context for product/engineering",
410
+ inputType: "textarea"
411
+ }
412
+ ]
413
+ },
414
+ {
415
+ value: "feedback",
416
+ label: "General Feedback",
417
+ description: "Share your experience and suggestions",
418
+ icon: "message",
419
+ fields: [
420
+ {
421
+ key: "experience_summary",
422
+ label: "Experience summary",
423
+ placeholder: "What feedback would you like to share?",
424
+ required: true,
425
+ inputType: "textarea"
426
+ },
427
+ {
428
+ key: "satisfaction_rating",
429
+ label: "Satisfaction rating",
430
+ helpText: "Rate your current experience from 1 to 5",
431
+ inputType: "rating"
432
+ },
433
+ {
434
+ key: "what_were_you_doing",
435
+ label: "What were you doing?",
436
+ placeholder: "What context were you in when this came up?",
437
+ inputType: "textarea"
438
+ },
439
+ {
440
+ key: "what_happened",
441
+ label: "What happened?",
442
+ placeholder: "What stood out in your experience?",
443
+ inputType: "textarea"
444
+ },
445
+ {
446
+ key: "desired_solution",
447
+ label: "Suggested improvement",
448
+ placeholder: "How could we improve this?",
449
+ inputType: "textarea"
450
+ },
451
+ {
452
+ key: "additional_context",
453
+ label: "Additional context",
454
+ placeholder: "Anything else to share?",
455
+ inputType: "textarea"
456
+ }
457
+ ]
458
+ },
459
+ {
460
+ value: "improvement",
461
+ label: "Improvement Idea",
462
+ description: "Propose a focused improvement to an existing flow",
463
+ icon: "wrench",
464
+ fields: [
465
+ {
466
+ key: "experience_summary",
467
+ label: "Experience summary",
468
+ placeholder: "What part of the experience should improve?",
469
+ required: true,
470
+ inputType: "textarea"
471
+ },
472
+ {
473
+ key: "desired_solution",
474
+ label: "Desired solution",
475
+ placeholder: "Describe your improvement idea",
476
+ required: true,
477
+ inputType: "textarea"
478
+ },
479
+ {
480
+ key: "what_were_you_doing",
481
+ label: "Current behavior",
482
+ placeholder: "What were you doing when this came up?",
483
+ inputType: "textarea"
484
+ },
485
+ {
486
+ key: "what_happened",
487
+ label: "Current pain point",
488
+ placeholder: "What made this difficult or inefficient?",
489
+ inputType: "textarea"
490
+ },
491
+ {
492
+ key: "impact_details",
493
+ label: "Impact details",
494
+ placeholder: "What is the impact of this issue?",
495
+ inputType: "textarea"
496
+ },
497
+ {
498
+ key: "additional_context",
499
+ label: "Additional context",
500
+ placeholder: "Any additional details",
501
+ inputType: "textarea"
502
+ }
503
+ ]
504
+ }
505
+ ];
506
+ var feedbackTypeValues = z.enum(["bug", "feature", "feedback", "improvement"]);
507
+ var feedbackFormSchema = z.object({
508
+ type: z.union([feedbackTypeValues, z.literal("")]),
509
+ title: z.string().max(200),
510
+ priority: z.enum(["low", "medium", "high", "critical"]),
511
+ satisfaction_rating: z.number().int().min(1).max(5).nullable(),
512
+ experience_summary: z.string(),
513
+ what_were_you_doing: z.string(),
514
+ what_happened: z.string(),
515
+ expected_outcome: z.string(),
516
+ steps_to_reproduce: z.string(),
517
+ desired_solution: z.string(),
518
+ impact_details: z.string(),
519
+ additional_context: z.string(),
520
+ frequency: z.enum(["", "once", "sometimes", "often", "every_time"]),
521
+ impact_level: z.enum(["", "minor", "moderate", "major", "critical"])
522
+ });
523
+ var requiredByType = {
524
+ bug: ["what_were_you_doing", "what_happened", "expected_outcome"],
525
+ feature: ["experience_summary", "desired_solution"],
526
+ feedback: ["experience_summary"],
527
+ improvement: ["experience_summary", "desired_solution"]
528
+ };
529
+ function createEmptyFeedbackForm() {
530
+ return {
531
+ type: "",
532
+ title: "",
533
+ priority: "medium",
534
+ satisfaction_rating: null,
535
+ experience_summary: "",
536
+ what_were_you_doing: "",
537
+ what_happened: "",
538
+ expected_outcome: "",
539
+ steps_to_reproduce: "",
540
+ desired_solution: "",
541
+ impact_details: "",
542
+ additional_context: "",
543
+ frequency: "",
544
+ impact_level: ""
545
+ };
546
+ }
547
+ function getRequiredFeedbackFields(type) {
548
+ return requiredByType[type];
549
+ }
550
+ function validateFeedbackForm(formData) {
551
+ const parse = feedbackFormSchema.safeParse(formData);
552
+ if (!parse.success) {
553
+ return {
554
+ valid: false,
555
+ title: "Invalid Form Data",
556
+ description: "Please check your feedback inputs and try again."
557
+ };
558
+ }
559
+ if (!formData.title.trim()) {
560
+ return {
561
+ valid: false,
562
+ title: "Missing Information",
563
+ description: "Please add a title for this feedback."
564
+ };
565
+ }
566
+ if (!formData.type) {
567
+ return {
568
+ valid: false,
569
+ title: "Missing Type",
570
+ description: "Please select a feedback type."
571
+ };
572
+ }
573
+ const missingField = getRequiredFeedbackFields(formData.type).find((field) => {
574
+ const value = formData[field];
575
+ return typeof value === "string" ? !value.trim() : value == null;
576
+ });
577
+ if (missingField) {
578
+ return {
579
+ valid: false,
580
+ title: "Missing Information",
581
+ description: `Please complete "${feedbackFieldLabels[missingField]}".`
582
+ };
583
+ }
584
+ if (formData.title.trim().length < 5) {
585
+ return {
586
+ valid: false,
587
+ title: "Missing Information",
588
+ description: "Please add a more descriptive title (minimum 5 characters)."
589
+ };
590
+ }
591
+ return { valid: true };
592
+ }
593
+ function buildExperienceDetailsPayload(formData) {
594
+ const details = {};
595
+ const detailKeys = [
596
+ "experience_summary",
597
+ "what_were_you_doing",
598
+ "what_happened",
599
+ "expected_outcome",
600
+ "steps_to_reproduce",
601
+ "desired_solution",
602
+ "impact_details",
603
+ "additional_context",
604
+ "frequency",
605
+ "impact_level"
606
+ ];
607
+ detailKeys.forEach((key) => {
608
+ const value = formData[key];
609
+ if (typeof value === "string" && value.trim()) {
610
+ details[key] = value.trim();
611
+ }
612
+ });
613
+ if (Number.isInteger(formData.satisfaction_rating)) {
614
+ details.satisfaction_rating = formData.satisfaction_rating;
615
+ }
616
+ return details;
617
+ }
618
+
619
+ // src/schema/descriptionBuilder.ts
620
+ function section(title, lines) {
621
+ const cleaned = lines.filter((line) => line.trim());
622
+ if (cleaned.length === 0) {
623
+ return "";
624
+ }
625
+ return [`## ${title}`, ...cleaned].join("\n");
626
+ }
627
+ function formatValue(value) {
628
+ if (value == null) {
629
+ return "";
630
+ }
631
+ if (Array.isArray(value)) {
632
+ return value.join(", ");
633
+ }
634
+ return String(value).trim();
635
+ }
636
+ function buildFeedbackDescription(formData, diagnostics) {
637
+ const details = buildExperienceDetailsPayload(formData);
638
+ const detailLines = Object.entries(details).filter(([key]) => key !== "satisfaction_rating").map(([key, value]) => {
639
+ const label = feedbackFieldLabels[key] || key;
640
+ return `- ${label}: ${formatValue(value)}`;
641
+ });
642
+ const satisfactionLine = Number.isInteger(details.satisfaction_rating) ? [`- Satisfaction Rating: ${details.satisfaction_rating}/5`] : [];
643
+ const pageContext = section("Page Context", [
644
+ diagnostics?.page_title ? `- Page title: ${diagnostics.page_title}` : "",
645
+ diagnostics?.page_url ? `- Page URL: ${diagnostics.page_url}` : "",
646
+ diagnostics?.current_route ? `- Current route: ${diagnostics.current_route}` : "",
647
+ diagnostics?.referrer ? `- Referrer: ${diagnostics.referrer}` : "",
648
+ diagnostics?.browser_info ? `- Browser info: ${diagnostics.browser_info}` : ""
649
+ ]);
650
+ const network = section("Network", [
651
+ diagnostics?.network_info?.effective_type ? `- Effective type: ${diagnostics.network_info.effective_type}` : "",
652
+ diagnostics?.network_info?.downlink_mbps ? `- Downlink (Mbps): ${diagnostics.network_info.downlink_mbps}` : "",
653
+ diagnostics?.network_info?.rtt_ms ? `- RTT (ms): ${diagnostics.network_info.rtt_ms}` : "",
654
+ diagnostics?.network_info?.save_data != null ? `- Save data: ${diagnostics.network_info.save_data ? "Yes" : "No"}` : ""
655
+ ]);
656
+ const performance2 = section("Performance", [
657
+ diagnostics?.performance_timing?.page_load_ms ? `- Page load (ms): ${diagnostics.performance_timing.page_load_ms}` : "",
658
+ diagnostics?.performance_timing?.dom_interactive_ms ? `- DOM interactive (ms): ${diagnostics.performance_timing.dom_interactive_ms}` : "",
659
+ diagnostics?.performance_timing?.dom_content_loaded_ms ? `- DOM content loaded (ms): ${diagnostics.performance_timing.dom_content_loaded_ms}` : "",
660
+ diagnostics?.performance_timing?.ttfb_ms ? `- TTFB (ms): ${diagnostics.performance_timing.ttfb_ms}` : ""
661
+ ]);
662
+ const recentActions = section(
663
+ "Recent Actions",
664
+ (diagnostics?.recent_actions || []).map(
665
+ (item) => `- [${item.timestamp || "unknown"}] ${item.type || "event"}: ${item.message || ""}`
666
+ )
667
+ );
668
+ const recentErrors = section(
669
+ "Recent Console Errors",
670
+ (diagnostics?.recent_console_errors || []).map(
671
+ (item) => `- [${item.timestamp || "unknown"}] ${item.type || "event"}: ${item.message || ""}`
672
+ )
673
+ );
674
+ const blocks = [
675
+ `# ${formData.title.trim()}`,
676
+ `Type: ${formData.type}`,
677
+ section("Experience Details", [...satisfactionLine, ...detailLines]),
678
+ pageContext,
679
+ network,
680
+ performance2,
681
+ recentActions,
682
+ recentErrors
683
+ ].filter(Boolean);
684
+ return blocks.join("\n\n").slice(0, 1e4);
685
+ }
686
+
687
+ // src/components/SupportWidget.tsx
688
+ import "./widget-PDWZ5OMY.css";
689
+
690
+ // src/utils/classNames.ts
691
+ function classNames(...items) {
692
+ return items.filter(Boolean).join(" ");
693
+ }
694
+
695
+ // src/components/SupportWidget.tsx
696
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
697
+ function labelForType(type) {
698
+ return feedbackTypes.find((item) => item.value === type)?.label || type;
699
+ }
700
+ function getErrorMessage(error) {
701
+ if (error instanceof Error) {
702
+ return error.message;
703
+ }
704
+ return "Unknown submission error";
705
+ }
706
+ function SupportWidget({ config, isVisible = true, onSuccess, onError, onSubmit }) {
707
+ const [isOpen, setIsOpen] = useState(false);
708
+ const [step, setStep] = useState(1);
709
+ const [isSubmitting, setIsSubmitting] = useState(false);
710
+ const [isSubmitted, setIsSubmitted] = useState(false);
711
+ const [announce, setAnnounce] = useState("");
712
+ const [formData, setFormData] = useState(createEmptyFeedbackForm());
713
+ const [submitResult, setSubmitResult] = useState(null);
714
+ const diagnosticsConfig = useMemo(() => config.diagnostics || {}, [config.diagnostics]);
715
+ const diagnosticsEnabled = diagnosticsConfig.enabled !== false;
716
+ const submissionConfig = useMemo(() => config.submission || {}, [config.submission]);
717
+ const defaultSubmit = useMemo(
718
+ () => {
719
+ if (submissionConfig.mode === "github_issue_proxy") {
720
+ return createGitHubIssueProxyClient({
721
+ apiBaseUrl: config.apiBaseUrl,
722
+ getAuthToken: config.getAuthToken,
723
+ endpointPath: submissionConfig.endpointPath
724
+ });
725
+ }
726
+ return createCentralFeedbackClient({
727
+ apiBaseUrl: config.apiBaseUrl,
728
+ getAuthToken: config.getAuthToken,
729
+ endpointPath: submissionConfig.endpointPath
730
+ });
731
+ },
732
+ [config.apiBaseUrl, config.getAuthToken, submissionConfig.endpointPath, submissionConfig.mode]
733
+ );
734
+ useEffect2(() => {
735
+ const cleanup = installFeedbackDiagnostics(diagnosticsConfig);
736
+ return cleanup;
737
+ }, [diagnosticsConfig]);
738
+ useRouteTracking(config.getCurrentRoute, diagnosticsEnabled);
739
+ function resetState() {
740
+ setStep(1);
741
+ setIsSubmitting(false);
742
+ setIsSubmitted(false);
743
+ setFormData(createEmptyFeedbackForm());
744
+ setAnnounce("");
745
+ setSubmitResult(null);
746
+ }
747
+ const handleClose = useCallback(() => {
748
+ setIsOpen(false);
749
+ if (diagnosticsConfig.clearPolicy === "on_close") {
750
+ clearFeedbackDiagnostics();
751
+ }
752
+ resetState();
753
+ }, [diagnosticsConfig.clearPolicy]);
754
+ useEffect2(() => {
755
+ if (!isOpen) {
756
+ return;
757
+ }
758
+ const onKeyDown = (event) => {
759
+ if (event.key === "Escape") {
760
+ handleClose();
761
+ }
762
+ };
763
+ window.addEventListener("keydown", onKeyDown);
764
+ return () => window.removeEventListener("keydown", onKeyDown);
765
+ }, [isOpen, handleClose]);
766
+ function handleTypeSelect(type) {
767
+ setFormData((previous) => ({ ...previous, type }));
768
+ setStep(2);
769
+ setAnnounce(`Selected ${labelForType(type)}.`);
770
+ }
771
+ function updateField(key, value) {
772
+ setFormData((previous) => ({ ...previous, [key]: value }));
773
+ }
774
+ async function handleSubmit(event) {
775
+ event.preventDefault();
776
+ const validation2 = validateFeedbackForm(formData);
777
+ if (!validation2.valid) {
778
+ setAnnounce(`${validation2.title}: ${validation2.description}`);
779
+ return;
780
+ }
781
+ if (!formData.type) {
782
+ return;
783
+ }
784
+ setIsSubmitting(true);
785
+ const route = config.getCurrentRoute();
786
+ const diagnostics = diagnosticsEnabled ? getFeedbackDiagnosticsSnapshot(route) : void 0;
787
+ const payload = {
788
+ type: formData.type,
789
+ title: formData.title.trim(),
790
+ description: buildFeedbackDescription(formData, diagnostics),
791
+ priority: formData.priority,
792
+ satisfaction_rating: formData.satisfaction_rating,
793
+ page_url: diagnostics?.page_url,
794
+ browser_info: diagnostics?.browser_info,
795
+ experience_details: buildExperienceDetailsPayload(formData),
796
+ technical_context: diagnostics,
797
+ app_name: config.appName,
798
+ environment: config.environment,
799
+ repo_slug: config.repoSlug,
800
+ route_path: route.pathname,
801
+ context: config.getCurrentUserContext?.(),
802
+ custom_fields: config.customFields
803
+ };
804
+ try {
805
+ const result = await (onSubmit || defaultSubmit)(payload);
806
+ setSubmitResult(result);
807
+ setIsSubmitted(true);
808
+ setAnnounce("Thank you. Your support ticket was submitted successfully.");
809
+ if (diagnosticsConfig.clearPolicy !== "manual") {
810
+ clearFeedbackDiagnostics();
811
+ }
812
+ onSuccess?.(result);
813
+ window.setTimeout(() => {
814
+ handleClose();
815
+ }, 2e3);
816
+ } catch (error) {
817
+ const message = getErrorMessage(error);
818
+ setAnnounce(`Submission failed: ${message}`);
819
+ onError?.(error);
820
+ } finally {
821
+ setIsSubmitting(false);
822
+ }
823
+ }
824
+ if (!isVisible) {
825
+ return null;
826
+ }
827
+ const selectedTypeConfig = feedbackTypes.find((item) => item.value === formData.type);
828
+ const validation = validateFeedbackForm(formData);
829
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
830
+ /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "sw-sr-only", children: announce }),
831
+ /* @__PURE__ */ jsxs(
832
+ "button",
833
+ {
834
+ "aria-label": config.theme?.launcherLabel || "Support Ticket",
835
+ className: classNames("sw-launcher", config.theme?.classNames?.launcher),
836
+ "data-testid": "feedback-widget-button",
837
+ onClick: () => setIsOpen(true),
838
+ type: "button",
839
+ children: [
840
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "sw-launcher-icon", children: "?" }),
841
+ /* @__PURE__ */ jsx("span", { children: config.theme?.launcherLabel || "Support Ticket" })
842
+ ]
843
+ }
844
+ ),
845
+ isOpen ? /* @__PURE__ */ jsx("div", { className: "sw-overlay", onClick: handleClose, children: /* @__PURE__ */ jsxs(
846
+ "div",
847
+ {
848
+ "aria-describedby": "support-widget-description",
849
+ "aria-modal": "true",
850
+ className: classNames("sw-modal", config.theme?.classNames?.dialog),
851
+ onClick: (event) => event.stopPropagation(),
852
+ role: "dialog",
853
+ children: [
854
+ /* @__PURE__ */ jsx("button", { "aria-label": "Close", className: "sw-close", onClick: handleClose, type: "button", children: "x" }),
855
+ /* @__PURE__ */ jsx("p", { className: "sw-sr-only", id: "support-widget-description", children: "Support ticket dialog" }),
856
+ isSubmitted ? /* @__PURE__ */ jsxs("div", { className: "sw-success", "data-testid": "feedback-success-screen", children: [
857
+ /* @__PURE__ */ jsx("h2", { children: "Thank You!" }),
858
+ /* @__PURE__ */ jsx("p", { children: "Your feedback helps us improve the product." }),
859
+ submitResult?.issue_number || submitResult?.issue_url ? /* @__PURE__ */ jsxs("p", { "data-testid": "feedback-success-issue-link", children: [
860
+ "GitHub issue created:",
861
+ submitResult.issue_url ? /* @__PURE__ */ jsxs(Fragment, { children: [
862
+ " ",
863
+ /* @__PURE__ */ jsx("a", { href: submitResult.issue_url, rel: "noreferrer", target: "_blank", children: submitResult.issue_number ? `#${submitResult.issue_number}` : "Open issue" })
864
+ ] }) : ` #${submitResult.issue_number}`
865
+ ] }) : null
866
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
867
+ /* @__PURE__ */ jsxs("header", { className: "sw-header", children: [
868
+ /* @__PURE__ */ jsx("h2", { children: step === 1 ? "Send Feedback" : selectedTypeConfig?.label || "Support Ticket" }),
869
+ /* @__PURE__ */ jsx("p", { children: step === 1 ? "What kind of support ticket would you like to submit?" : selectedTypeConfig?.description })
870
+ ] }),
871
+ step === 1 ? /* @__PURE__ */ jsx("div", { className: "sw-type-grid", children: feedbackTypes.map((typeConfig) => /* @__PURE__ */ jsxs(
872
+ "button",
873
+ {
874
+ className: "sw-type-card",
875
+ "data-testid": `feedback-type-${typeConfig.value}`,
876
+ onClick: () => handleTypeSelect(typeConfig.value),
877
+ type: "button",
878
+ children: [
879
+ /* @__PURE__ */ jsx("strong", { children: typeConfig.label }),
880
+ /* @__PURE__ */ jsx("span", { children: typeConfig.description })
881
+ ]
882
+ },
883
+ typeConfig.value
884
+ )) }) : /* @__PURE__ */ jsxs("form", { className: "sw-form", onSubmit: handleSubmit, children: [
885
+ /* @__PURE__ */ jsx("label", { htmlFor: "support-title", children: "Title *" }),
886
+ /* @__PURE__ */ jsx(
887
+ "input",
888
+ {
889
+ id: "support-title",
890
+ maxLength: 200,
891
+ onChange: (event) => updateField("title", event.target.value),
892
+ placeholder: "Short summary of your support ticket",
893
+ value: formData.title
894
+ }
895
+ ),
896
+ /* @__PURE__ */ jsxs("section", { className: "sw-section", children: [
897
+ /* @__PURE__ */ jsx("h3", { children: "Experience details" }),
898
+ selectedTypeConfig?.fields.map((field) => {
899
+ if (field.inputType === "rating") {
900
+ return /* @__PURE__ */ jsxs("div", { className: "sw-rating", children: [
901
+ /* @__PURE__ */ jsx("label", { children: field.label }),
902
+ /* @__PURE__ */ jsx("div", { role: "group", "aria-label": "Satisfaction rating", children: [1, 2, 3, 4, 5].map((value) => /* @__PURE__ */ jsx(
903
+ "button",
904
+ {
905
+ "aria-label": `${value} out of 5`,
906
+ className: classNames(
907
+ "sw-rating-button",
908
+ formData.satisfaction_rating && value <= formData.satisfaction_rating ? "is-active" : void 0
909
+ ),
910
+ onClick: () => updateField("satisfaction_rating", value),
911
+ type: "button",
912
+ children: "*"
913
+ },
914
+ value
915
+ )) })
916
+ ] }, field.key);
917
+ }
918
+ return /* @__PURE__ */ jsxs("div", { className: "sw-field", children: [
919
+ /* @__PURE__ */ jsxs("label", { htmlFor: field.key, children: [
920
+ field.label,
921
+ field.required ? " *" : ""
922
+ ] }),
923
+ /* @__PURE__ */ jsx(
924
+ "textarea",
925
+ {
926
+ id: field.key,
927
+ onChange: (event) => updateField(field.key, event.target.value),
928
+ placeholder: field.placeholder,
929
+ rows: 3,
930
+ value: String(formData[field.key] || "")
931
+ }
932
+ )
933
+ ] }, field.key);
934
+ })
935
+ ] }),
936
+ /* @__PURE__ */ jsxs("div", { className: "sw-row", children: [
937
+ /* @__PURE__ */ jsxs("div", { className: "sw-field", children: [
938
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-frequency", children: "Frequency" }),
939
+ /* @__PURE__ */ jsxs(
940
+ "select",
941
+ {
942
+ id: "feedback-frequency",
943
+ onChange: (event) => updateField("frequency", event.target.value),
944
+ value: formData.frequency,
945
+ children: [
946
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select frequency" }),
947
+ frequencyOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
948
+ ]
949
+ }
950
+ )
951
+ ] }),
952
+ /* @__PURE__ */ jsxs("div", { className: "sw-field", children: [
953
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-impact", children: "Impact" }),
954
+ /* @__PURE__ */ jsxs(
955
+ "select",
956
+ {
957
+ id: "feedback-impact",
958
+ onChange: (event) => updateField("impact_level", event.target.value),
959
+ value: formData.impact_level,
960
+ children: [
961
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select impact" }),
962
+ impactLevelOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
963
+ ]
964
+ }
965
+ )
966
+ ] })
967
+ ] }),
968
+ /* @__PURE__ */ jsxs("div", { className: "sw-field", children: [
969
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-priority-select", children: "Priority" }),
970
+ /* @__PURE__ */ jsx(
971
+ "select",
972
+ {
973
+ id: "feedback-priority-select",
974
+ onChange: (event) => updateField("priority", event.target.value),
975
+ value: formData.priority,
976
+ children: priorityOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
977
+ }
978
+ )
979
+ ] }),
980
+ /* @__PURE__ */ jsx("div", { className: "sw-diagnostics-note", children: "Diagnostics attached: page context, route history, recent errors, and environment metrics." }),
981
+ !validation.valid ? /* @__PURE__ */ jsx("p", { className: "sw-error", children: validation.description }) : null,
982
+ /* @__PURE__ */ jsxs("div", { className: "sw-actions", children: [
983
+ /* @__PURE__ */ jsx("button", { onClick: () => setStep(1), type: "button", children: "Back" }),
984
+ /* @__PURE__ */ jsx("button", { disabled: isSubmitting || !validation.valid, type: "submit", children: isSubmitting ? "Submitting..." : "Submit" })
985
+ ] })
986
+ ] })
987
+ ] })
988
+ ]
989
+ }
990
+ ) }) : null
991
+ ] });
992
+ }
993
+ export {
994
+ SupportWidget,
995
+ buildExperienceDetailsPayload,
996
+ buildFeedbackDescription,
997
+ clearFeedbackDiagnostics,
998
+ createCentralFeedbackClient,
999
+ createEmptyFeedbackForm,
1000
+ createGitHubIssueProxyClient,
1001
+ feedbackFieldLabels,
1002
+ feedbackTypes,
1003
+ getFeedbackDiagnosticsSnapshot,
1004
+ getRequiredFeedbackFields,
1005
+ installFeedbackDiagnostics,
1006
+ recordFeedbackRouteVisit,
1007
+ validateFeedbackForm
1008
+ };