@e-llm-studio/feedback-flow 0.1.31 → 0.1.33
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/FeedbackExperience.d.ts +2 -0
- package/dist/FeedbackExperience.js +4 -4
- package/dist/FeedbackModal.js +69 -33
- package/dist/FeedbackPrompt.d.ts +6 -1
- package/dist/FeedbackPrompt.js +78 -10
- package/dist/StarRating.d.ts +1 -1
- package/dist/StarRating.js +1 -1
- package/dist/featureAvailability.d.ts +6 -0
- package/dist/featureAvailability.js +8 -2
- package/dist/feedback.css +145 -13
- package/dist/types.js +5 -5
- package/dist/useFeedbackFlow.js +4 -7
- package/package.json +1 -1
|
@@ -6,6 +6,8 @@ export interface FeedbackExperienceProps {
|
|
|
6
6
|
/** Optional for feature-level feedback; chat responses should provide their request ID. */
|
|
7
7
|
requestId?: string;
|
|
8
8
|
featureId: string;
|
|
9
|
+
featureName?: string;
|
|
10
|
+
featureDescription?: string;
|
|
9
11
|
/** Consulted only for feature-level feedback, never for chat responses. */
|
|
10
12
|
featureAvailabilityApi?: FeatureAvailabilityApi;
|
|
11
13
|
/** Backend mode UUID used to list its features. */
|
|
@@ -14,18 +14,18 @@ const useFeedbackPersistence_1 = require("./useFeedbackPersistence");
|
|
|
14
14
|
const featureAvailability_1 = require("./featureAvailability");
|
|
15
15
|
/** Standalone experience for hosts that do not need chat-specific anchoring. */
|
|
16
16
|
function FeedbackExperience(props) {
|
|
17
|
-
const available = (0, featureAvailability_1.
|
|
17
|
+
const { available, feature } = (0, featureAvailability_1.useFeatureAvailabilityDetails)(props.featureAvailabilityModeId, props.featureId, props.featureAvailabilityApi, !props.requestId);
|
|
18
18
|
if (!available)
|
|
19
19
|
return null;
|
|
20
|
-
return (0, jsx_runtime_1.jsx)(FeedbackExperienceContent, { ...props, requestId: props.requestId || props.featureId });
|
|
20
|
+
return (0, jsx_runtime_1.jsx)(FeedbackExperienceContent, { ...props, featureName: props.featureName ?? feature?.name ?? undefined, featureDescription: props.featureDescription ?? feature?.description ?? undefined, requestId: props.requestId || props.featureId });
|
|
21
21
|
}
|
|
22
|
-
function FeedbackExperienceContent({ requestId, featureId, metadata, tracking, copy, theme, onSubmit, api, className, style, styles, classNames, align = "start", slots, }) {
|
|
22
|
+
function FeedbackExperienceContent({ requestId, featureId, featureName, featureDescription, metadata, tracking, copy, theme, onSubmit, api, className, style, styles, classNames, align = "start", slots, }) {
|
|
23
23
|
const feedbackMetadata = (0, tracking_1.createFeedbackMetadata)(tracking, metadata);
|
|
24
24
|
const flow = (0, useFeedbackFlow_1.useFeedbackFlow)(requestId, featureId, feedbackMetadata);
|
|
25
25
|
const { saveFeedback, saveProgress, saveOnDismiss, isLoading, isSaving, saveError } = (0, useFeedbackPersistence_1.useFeedbackPersistence)(flow, feedbackMetadata, onSubmit, api);
|
|
26
26
|
const anchorRef = (0, react_1.useRef)(null);
|
|
27
27
|
const [promptPosition, setPromptPosition] = (0, react_1.useState)(null);
|
|
28
|
-
const prompt = ((0, jsx_runtime_1.jsx)(FeedbackPrompt_1.FeedbackPrompt, { flow: flow, loading: isLoading, disabled: isSaving, promptRef: anchorRef, copy: copy ?? types_1.defaultFeedbackCopy, theme: theme, styles: styles?.prompt, classNames: classNames?.prompt, onRatingSelected: (rating) => {
|
|
28
|
+
const prompt = ((0, jsx_runtime_1.jsx)(FeedbackPrompt_1.FeedbackPrompt, { flow: flow, loading: isLoading, disabled: isSaving, promptRef: anchorRef, copy: copy ?? types_1.defaultFeedbackCopy, theme: theme, featureName: featureName, featureDescription: featureDescription, styles: styles?.prompt, classNames: classNames?.prompt, onRatingSelected: (rating) => {
|
|
29
29
|
const nextStep = rating === 3 || rating === 4 ? "rating" : rating <= 2 ? "gap" : "time";
|
|
30
30
|
void saveProgress({ requestId, featureId, rating, gaps: [], metadata: feedbackMetadata }, nextStep)
|
|
31
31
|
.catch((error) => console.error("Could not save rating", error));
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -17,6 +17,10 @@ function FeedbackModal(props) {
|
|
|
17
17
|
}
|
|
18
18
|
function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onProgress, onDismiss, saving = false, resolveScreenshotUrl, anchorRef, overlayHostRef, anchorKey, slots, className, style, styles, classNames, backdropClassName, backdropStyle, dialogClassName, dialogStyle, }) {
|
|
19
19
|
const copy = (0, types_1.createFeedbackCopy)(copyOverrides);
|
|
20
|
+
const hasEnoughDetail = (value) => wordCount(value) >= copy.usefulMinimumWords;
|
|
21
|
+
const requiresGapWordMinimum = flow.ratingBase !== 5;
|
|
22
|
+
const gapHasRequiredDetail = (gap) => hasEnoughDetail(gap.description) || (!requiresGapWordMinimum && gap.detailRequested);
|
|
23
|
+
const detailRequirement = (_value, prompt) => prompt;
|
|
20
24
|
const mergedTheme = (0, types_1.createFeedbackTheme)(theme);
|
|
21
25
|
const [customTime, setCustomTime] = (0, react_1.useState)(false);
|
|
22
26
|
const [introDismissed, setIntroDismissed] = (0, react_1.useState)(false);
|
|
@@ -30,6 +34,10 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
30
34
|
const [screenshotPreview, setScreenshotPreview] = (0, react_1.useState)(null);
|
|
31
35
|
const [capturingScreenshot, setCapturingScreenshot] = (0, react_1.useState)(false);
|
|
32
36
|
const [captureError, setCaptureError] = (0, react_1.useState)('');
|
|
37
|
+
const [rangeActive, setRangeActive] = (0, react_1.useState)(false);
|
|
38
|
+
const [rangeTooltipPosition, setRangeTooltipPosition] = (0, react_1.useState)(null);
|
|
39
|
+
const rangeRef = (0, react_1.useRef)(null);
|
|
40
|
+
const rangeTooltipRef = (0, react_1.useRef)(null);
|
|
33
41
|
const submittingRef = (0, react_1.useRef)(false);
|
|
34
42
|
const [position, setPosition] = (0, react_1.useState)({ left: 12, top: 12, width: 480, maxHeight: 600 });
|
|
35
43
|
const dialogRef = (0, react_1.useRef)(null);
|
|
@@ -38,6 +46,31 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
38
46
|
const dismissRef = (0, react_1.useRef)(() => undefined);
|
|
39
47
|
const wasOpenRef = (0, react_1.useRef)(false);
|
|
40
48
|
const isOpen = flow.step !== 'closed';
|
|
49
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
50
|
+
if (!rangeActive || flow.step !== 'rating' || flow.rating === null)
|
|
51
|
+
return;
|
|
52
|
+
const rating = flow.rating;
|
|
53
|
+
const updatePosition = () => {
|
|
54
|
+
const range = rangeRef.current;
|
|
55
|
+
const tooltip = rangeTooltipRef.current;
|
|
56
|
+
if (!range || !tooltip)
|
|
57
|
+
return;
|
|
58
|
+
const rangeRect = range.getBoundingClientRect();
|
|
59
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
60
|
+
const fraction = Math.max(0, Math.min(1, rating - flow.ratingBase));
|
|
61
|
+
const thumbX = rangeRect.left + rangeRect.width * fraction;
|
|
62
|
+
const left = Math.max(8, Math.min(window.innerWidth - tooltipRect.width - 8, thumbX - tooltipRect.width / 2));
|
|
63
|
+
const top = Math.max(8, rangeRect.top - tooltipRect.height - 16);
|
|
64
|
+
setRangeTooltipPosition({ top, left });
|
|
65
|
+
};
|
|
66
|
+
updatePosition();
|
|
67
|
+
window.addEventListener('resize', updatePosition);
|
|
68
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener('resize', updatePosition);
|
|
71
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
72
|
+
};
|
|
73
|
+
}, [rangeActive, flow.step, flow.rating, flow.ratingBase]);
|
|
41
74
|
const dismiss = () => {
|
|
42
75
|
if (flow.step !== 'success' && flow.step !== 'closed' && flow.submission) {
|
|
43
76
|
void onDismiss?.(flow.submission, flow.step, flow.completed)
|
|
@@ -75,7 +108,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
75
108
|
if (isOpen && !wasOpenRef.current) {
|
|
76
109
|
setShowUseful((flow.rating ?? 0) >= 4 && flow.timeSavedMinutes !== undefined);
|
|
77
110
|
setShowLowRatingTime((flow.rating ?? 0) < 4 && flow.timeSavedMinutes !== undefined);
|
|
78
|
-
const firstShortGap = flow.gaps.findIndex((gap) => !gap.description.trim() || (
|
|
111
|
+
const firstShortGap = flow.gaps.findIndex((gap) => !gap.description.trim() || (!hasEnoughDetail(gap.description) && !gap.detailRequested));
|
|
79
112
|
setRevealedGapCount(firstShortGap < 0 ? flow.gaps.length : firstShortGap);
|
|
80
113
|
setReviewPageIndex(0);
|
|
81
114
|
}
|
|
@@ -188,7 +221,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
188
221
|
: current > index ? current - 1 : current);
|
|
189
222
|
};
|
|
190
223
|
const canAdvanceGap = revealedGapCount >= flow.gaps.length && flow.gaps.every((gap) => Boolean(gap.severity) && Boolean(gap.description.trim()) &&
|
|
191
|
-
(
|
|
224
|
+
gapHasRequiredDetail(gap));
|
|
192
225
|
const persistAndAdvance = (nextStep, advance, submission = flow.submission) => {
|
|
193
226
|
if (!submission || submittingRef.current)
|
|
194
227
|
return;
|
|
@@ -206,9 +239,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
206
239
|
if (!flow.submission || submittingRef.current)
|
|
207
240
|
return;
|
|
208
241
|
if (!flow.mostUseful.trim() ||
|
|
209
|
-
(
|
|
210
|
-
flow.gaps.some((gap) => !gap.description.trim() ||
|
|
211
|
-
(wordCount(gap.description) < copy.usefulMinimumWords && !gap.detailRequested)) ||
|
|
242
|
+
flow.gaps.some((gap) => !gap.description.trim() || !gapHasRequiredDetail(gap)) ||
|
|
212
243
|
flow.timeSavedMinutes === undefined)
|
|
213
244
|
return;
|
|
214
245
|
submittingRef.current = true;
|
|
@@ -229,21 +260,21 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
229
260
|
}
|
|
230
261
|
};
|
|
231
262
|
const nextReviewPage = () => {
|
|
232
|
-
if (!canContinueReview)
|
|
233
|
-
return;
|
|
234
263
|
if (reviewPage === 'gap') {
|
|
235
|
-
const gaps = flow.gaps.map((gap) =>
|
|
264
|
+
const gaps = flow.gaps.map((gap) => !hasEnoughDetail(gap.description) && !gap.detailRequested
|
|
236
265
|
? { ...gap, detailRequested: true } : gap);
|
|
237
|
-
if (gaps.some((gap
|
|
266
|
+
if (flow.gaps.some((gap) => !gapHasRequiredDetail(gap))) {
|
|
238
267
|
flow.setGaps(gaps);
|
|
239
268
|
return;
|
|
240
269
|
}
|
|
241
270
|
}
|
|
242
271
|
else if ((reviewPage === 'useful' || reviewPage === 'time') &&
|
|
243
|
-
|
|
272
|
+
!hasEnoughDetail(flow.mostUseful) && !flow.usefulDetailShown) {
|
|
244
273
|
flow.setUsefulDetailShown(true);
|
|
245
274
|
return;
|
|
246
275
|
}
|
|
276
|
+
if (!canContinueReview)
|
|
277
|
+
return;
|
|
247
278
|
if (reviewPageIndex < reviewPages.length - 1) {
|
|
248
279
|
const nextPage = reviewPages[reviewPageIndex + 1];
|
|
249
280
|
void persistAndAdvance(nextPage, () => setReviewPageIndex(reviewPageIndex + 1));
|
|
@@ -256,9 +287,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
256
287
|
setReviewPageIndex(reviewPageIndex - 1);
|
|
257
288
|
};
|
|
258
289
|
const continueUseful = () => {
|
|
259
|
-
if (!flow.mostUseful.
|
|
260
|
-
return;
|
|
261
|
-
if (wordCount(flow.mostUseful) < copy.usefulMinimumWords && !flow.usefulDetailShown) {
|
|
290
|
+
if (!hasEnoughDetail(flow.mostUseful) && !flow.usefulDetailShown) {
|
|
262
291
|
flow.setUsefulDetailShown(true);
|
|
263
292
|
return;
|
|
264
293
|
}
|
|
@@ -270,9 +299,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
270
299
|
void submitFeedback();
|
|
271
300
|
};
|
|
272
301
|
const continueTime = () => {
|
|
273
|
-
if (showUseful && !flow.mostUseful.
|
|
274
|
-
return;
|
|
275
|
-
if (showUseful && wordCount(flow.mostUseful) < copy.usefulMinimumWords && !flow.usefulDetailShown) {
|
|
302
|
+
if (showUseful && !hasEnoughDetail(flow.mostUseful) && !flow.usefulDetailShown) {
|
|
276
303
|
flow.setUsefulDetailShown(true);
|
|
277
304
|
return;
|
|
278
305
|
}
|
|
@@ -461,10 +488,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
461
488
|
((flow.step === 'time' && flow.ratingBase === 4) ||
|
|
462
489
|
(flow.step === 'gap' && flow.rating !== null && flow.rating > 2) ||
|
|
463
490
|
(flow.step === 'useful' && isGapFirstFlow) ||
|
|
464
|
-
flow.step === 'success') && ((0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-dialog__topbar', classNames?.topbar].filter(Boolean).join(' '), style: styles?.topbar, children: [flow.step === 'time' || flow.step === 'gap' || flow.step === 'useful' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: back, "aria-label": copy.backLabel, className: classNames?.backButton, style: styles?.backButton, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { size: 19 }) })) : ((0, jsx_runtime_1.jsx)("span", {})), flow.step === 'success' && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: flow.close, "aria-label": copy.closeLabel, className: classNames?.closeButton, style: styles?.closeButton, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 19 }) }))] })), flow.step === 'rating' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h2", { className: classNames?.title, style: styles?.title, children: copy.ratingTitle }), (0, jsx_runtime_1.jsxs)("p", { className: ['cw-feedback-subtitle', classNames?.subtitle].filter(Boolean).join(' '), style: styles?.subtitle, children: ["\u2728 ", copy.ratingSubtitle] }), (0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-range', classNames?.rangeContainer].filter(Boolean).join(' '), style: styles?.rangeContainer, children: [flow.rating !== null && (
|
|
465
|
-
left: `clamp(150px, ${(flow.rating - ratingBase) * 100}%, calc(100% - 150px))`,
|
|
466
|
-
...styles?.rangeTooltip,
|
|
467
|
-
}, children: copy.ratingValueLabel(flow.rating) })), (0, jsx_runtime_1.jsx)("input", { "aria-label": copy.ratingControlLabel, type: "range", min: ratingBase, max: ratingBase + 1, step: "0.1", value: flow.rating ?? ratingBase, className: classNames?.rangeInput, style: styles?.rangeInput, onChange: (e) => (isReview ? flow.setReviewRating : flow.setRating)(Number(e.target.value)), onPointerUp: (event) => selectDecimalRating(Number(event.currentTarget.value)), onKeyUp: (event) => {
|
|
491
|
+
flow.step === 'success') && ((0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-dialog__topbar', classNames?.topbar].filter(Boolean).join(' '), style: styles?.topbar, children: [flow.step === 'time' || flow.step === 'gap' || flow.step === 'useful' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: back, "aria-label": copy.backLabel, className: classNames?.backButton, style: styles?.backButton, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { size: 19 }) })) : ((0, jsx_runtime_1.jsx)("span", {})), flow.step === 'success' && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: flow.close, "aria-label": copy.closeLabel, className: classNames?.closeButton, style: styles?.closeButton, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 19 }) }))] })), flow.step === 'rating' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h2", { className: classNames?.title, style: styles?.title, children: copy.ratingTitle }), (0, jsx_runtime_1.jsxs)("p", { className: ['cw-feedback-subtitle', classNames?.subtitle].filter(Boolean).join(' '), style: styles?.subtitle, children: ["\u2728 ", copy.ratingSubtitle] }), (0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-range', classNames?.rangeContainer].filter(Boolean).join(' '), style: styles?.rangeContainer, children: [rangeActive && flow.rating !== null && (0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-range__pointer", style: { left: `clamp(8px, ${(flow.rating - ratingBase) * 100}%, calc(100% - 8px))` }, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { ref: rangeRef, "aria-label": copy.ratingControlLabel, type: "range", min: ratingBase, max: ratingBase + 1, step: "0.1", value: flow.rating ?? ratingBase, onPointerEnter: () => setRangeActive(true), onPointerLeave: () => setRangeActive(false), onFocus: () => setRangeActive(true), onBlur: () => setRangeActive(false), className: classNames?.rangeInput, style: styles?.rangeInput, onChange: (e) => (isReview ? flow.setReviewRating : flow.setRating)(Number(e.target.value)), onPointerUp: (event) => selectDecimalRating(Number(event.currentTarget.value)), onKeyUp: (event) => {
|
|
468
492
|
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
|
|
469
493
|
selectDecimalRating(Number(event.currentTarget.value));
|
|
470
494
|
}
|
|
@@ -474,7 +498,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
474
498
|
event.stopPropagation();
|
|
475
499
|
selectDecimalRating(Number(event.currentTarget.value));
|
|
476
500
|
}
|
|
477
|
-
} })] }), (0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-rating-options', classNames?.ratingOptionsContainer].filter(Boolean).join(' '), style: styles?.ratingOptionsContainer, children: decimalRatings.map((value) => {
|
|
501
|
+
} }), (0, jsx_runtime_1.jsx)("div", { className: "cw-feedback-range__ticks", "aria-hidden": "true", children: Array.from({ length: 11 }, (_, index) => (0, jsx_runtime_1.jsx)("span", { className: index % 2 === 0 ? 'is-major' : '' }, index)) })] }), (0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-rating-options', classNames?.ratingOptionsContainer].filter(Boolean).join(' '), style: styles?.ratingOptionsContainer, children: decimalRatings.map((value) => {
|
|
478
502
|
const isSelected = flow.rating === value;
|
|
479
503
|
return ((0, jsx_runtime_1.jsx)("button", { type: "button", className: [
|
|
480
504
|
isSelected ? 'is-selected' : '',
|
|
@@ -517,7 +541,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
517
541
|
event.preventDefault();
|
|
518
542
|
event.stopPropagation();
|
|
519
543
|
selectTimeSaved(flow.timeSavedMinutes);
|
|
520
|
-
} }), ' ', copy.minuteUnit] }))] }), !showUseful && flow.timeSavedMinutes === undefined && ((0, jsx_runtime_1.jsx)("p", { className: "cw-feedback-time-hint", children: copy.enterHint })), showUseful && ((0, jsx_runtime_1.jsxs)("label", { className: ['cw-feedback-field', classNames?.usefulField].filter(Boolean).join(' '), style: styles?.usefulField, children: [(0, jsx_runtime_1.jsx)("span", { children: copy.usefulTitle }), flow.usefulDetailShown &&
|
|
544
|
+
} }), ' ', copy.minuteUnit] }))] }), !showUseful && flow.timeSavedMinutes === undefined && ((0, jsx_runtime_1.jsx)("p", { className: "cw-feedback-time-hint", children: copy.enterHint })), showUseful && ((0, jsx_runtime_1.jsxs)("label", { className: ['cw-feedback-field', 'cw-feedback-useful-field', classNames?.usefulField].filter(Boolean).join(' '), style: styles?.usefulField, children: [(0, jsx_runtime_1.jsx)("span", { children: copy.usefulTitle }), flow.usefulDetailShown && !hasEnoughDetail(flow.mostUseful) && ((0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-inline-detail-prompt", children: detailRequirement(flow.mostUseful, copy.usefulMoreDetailPrompt) })), (0, jsx_runtime_1.jsx)("textarea", { autoFocus: true, value: flow.mostUseful, className: classNames?.usefulTextarea, style: styles?.usefulTextarea, onChange: (e) => flow.setMostUseful(e.target.value), placeholder: copy.usefulPlaceholder, onKeyDown: (e) => {
|
|
521
545
|
if (e.key === 'Enter' && !e.shiftKey && flow.mostUseful.trim()) {
|
|
522
546
|
e.preventDefault();
|
|
523
547
|
if (isReview)
|
|
@@ -556,22 +580,24 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
556
580
|
event.preventDefault();
|
|
557
581
|
event.stopPropagation();
|
|
558
582
|
selectTimeSaved(flow.timeSavedMinutes);
|
|
559
|
-
} }), ' ', copy.minuteUnit] })), showLowRatingTime && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h2", { className: ['cw-feedback-useful-title', classNames?.title, classNames?.usefulTitle].filter(Boolean).join(' '), style: { ...styles?.title, ...styles?.usefulTitle }, children: copy.lowRatingUsefulTitle }), flow.usefulDetailShown &&
|
|
583
|
+
} }), ' ', copy.minuteUnit] })), showLowRatingTime && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h2", { className: ['cw-feedback-useful-title', classNames?.title, classNames?.usefulTitle].filter(Boolean).join(' '), style: { ...styles?.title, ...styles?.usefulTitle }, children: copy.lowRatingUsefulTitle }), flow.usefulDetailShown && !hasEnoughDetail(flow.mostUseful) && ((0, jsx_runtime_1.jsx)("p", { className: ['cw-feedback-useful-prompt', 'cw-feedback-inline-detail-prompt', classNames?.usefulPrompt].filter(Boolean).join(' '), style: styles?.usefulPrompt, children: detailRequirement(flow.mostUseful, copy.usefulMoreDetailPrompt) })), (0, jsx_runtime_1.jsx)("label", { className: ['cw-feedback-field', 'cw-feedback-useful-field', classNames?.usefulField].filter(Boolean).join(' '), style: styles?.usefulField, children: (0, jsx_runtime_1.jsx)("textarea", { autoFocus: true, value: flow.mostUseful, className: classNames?.usefulTextarea, style: styles?.usefulTextarea, onChange: (event) => flow.setMostUseful(event.target.value), placeholder: copy.usefulPlaceholder, onKeyDown: (event) => {
|
|
560
584
|
if (event.key !== 'Enter' || event.shiftKey)
|
|
561
585
|
return;
|
|
562
586
|
event.preventDefault();
|
|
563
587
|
continueUseful();
|
|
564
|
-
} }) })] }))] })), flow.step === 'gap' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [showLowRatingIntro && ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-low-rating-intro", children: [(0, jsx_runtime_1.jsx)("div", { className: "cw-feedback-low-rating-intro__emoji", "aria-hidden": "true", children: "\uD83D\uDE14\uD83E\uDD7A" }), (0, jsx_runtime_1.jsx)("strong", { children: copy.lowRatingIntroTitle }), (0, jsx_runtime_1.jsx)("p", { children: copy.lowRatingIntroDescription })] })), (0, jsx_runtime_1.jsx)("h2", { className: classNames?.title, style: styles?.title, children: flow.rating === 5 ? copy.fiveStarGapTitle : copy.gapTitle }), copy.gapSubtitle && (
|
|
565
|
-
flow.gaps.length === 1 && flow.gaps[0].detailRequested &&
|
|
588
|
+
} }) })] }))] })), flow.step === 'gap' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [showLowRatingIntro && ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-low-rating-intro", children: [(0, jsx_runtime_1.jsx)("div", { className: "cw-feedback-low-rating-intro__emoji", "aria-hidden": "true", children: "\uD83D\uDE14\uD83E\uDD7A" }), (0, jsx_runtime_1.jsx)("strong", { children: copy.lowRatingIntroTitle }), (0, jsx_runtime_1.jsx)("p", { children: copy.lowRatingIntroDescription })] })), (0, jsx_runtime_1.jsx)("h2", { className: classNames?.title, style: styles?.title, children: flow.rating === 5 ? copy.fiveStarGapTitle : copy.gapTitle }), copy.gapSubtitle && ((0, jsx_runtime_1.jsx)("p", { className: [
|
|
589
|
+
flow.gaps.length === 1 && flow.gaps[0].detailRequested && !hasEnoughDetail(flow.gaps[0].description)
|
|
566
590
|
? 'cw-feedback-inline-detail-prompt' : 'cw-feedback-subtitle',
|
|
567
591
|
classNames?.subtitle,
|
|
568
|
-
].filter(Boolean).join(' '), style:
|
|
569
|
-
?
|
|
570
|
-
:
|
|
592
|
+
].filter(Boolean).join(' '), style: flow.gaps.length === 1 && flow.gaps[0].detailRequested && !hasEnoughDetail(flow.gaps[0].description)
|
|
593
|
+
? { ...styles?.subtitle, marginTop: 10, marginBottom: 4 }
|
|
594
|
+
: styles?.subtitle, children: flow.gaps.length === 1 && flow.gaps[0].detailRequested && !hasEnoughDetail(flow.gaps[0].description)
|
|
595
|
+
? detailRequirement(flow.gaps[0].description, copy.gapMoreDetailPrompt)
|
|
596
|
+
: `✨ ${copy.gapSubtitle}` })), (0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-gap-list', classNames?.gapList].filter(Boolean).join(' '), style: styles?.gapList, children: flow.gaps.map((gap, index) => ((0, jsx_runtime_1.jsxs)("section", { className: ['cw-feedback-gap', flow.gaps.length >= 2 && index !== activeGapIndex ? 'cw-feedback-gap--collapsed' : '', classNames?.gapItem].filter(Boolean).join(' '), style: styles?.gapItem, children: [flow.gaps.length >= 2 && ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-gap__header", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "cw-feedback-gap__toggle", onClick: () => setActiveGapIndex(index === activeGapIndex ? -1 : index), "aria-expanded": index === activeGapIndex, children: ["Gap ", index + 1, index !== activeGapIndex && gap.severity && (0, jsx_runtime_1.jsx)("span", { className: `cw-feedback-gap__severity ${gap.severity}`, children: copy.severityLabels[gap.severity] }), ((0, jsx_runtime_1.jsx)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: index === activeGapIndex ? 'm6 15 6-6 6 6' : 'm6 9 6 6 6-6' }) }))] }), index === activeGapIndex && !gap.description.trim() && gap.screenshots.length === 0 && (0, jsx_runtime_1.jsx)("button", { type: "button", className: "cw-feedback-gap__remove", onClick: () => removeGap(index), "aria-label": `Delete gap ${index + 1}`, title: `Delete gap ${index + 1}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, "aria-hidden": "true" }) })] })), (flow.gaps.length < 2 || index === activeGapIndex) && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [flow.gaps.length >= 2 && gap.detailRequested && !hasEnoughDetail(gap.description) && ((0, jsx_runtime_1.jsx)("p", { className: "cw-feedback-inline-detail-prompt cw-feedback-gap__detail-prompt", children: detailRequirement(gap.description, copy.gapMoreDetailPrompt) })), (0, jsx_runtime_1.jsx)("textarea", { autoFocus: index === 0 && !isLowRatingGap, value: gap.description, className: classNames?.gapTextarea, style: styles?.gapTextarea, onFocus: () => { if (isLowRatingGap)
|
|
571
597
|
setIntroDismissed(true); }, onChange: (e) => updateGap(index, { description: e.target.value }), onKeyDown: (event) => {
|
|
572
598
|
if (event.key === 'Enter' && !event.shiftKey && gap.description.trim()) {
|
|
573
599
|
event.preventDefault();
|
|
574
|
-
if (
|
|
600
|
+
if (!gapHasRequiredDetail(gap)) {
|
|
575
601
|
updateGap(index, { detailRequested: true });
|
|
576
602
|
return;
|
|
577
603
|
}
|
|
@@ -620,7 +646,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
620
646
|
const gap = flow.gaps[index];
|
|
621
647
|
if (!gap?.description.trim())
|
|
622
648
|
return;
|
|
623
|
-
if (
|
|
649
|
+
if (!gapHasRequiredDetail(gap)) {
|
|
624
650
|
updateGap(index, { detailRequested: true });
|
|
625
651
|
return;
|
|
626
652
|
}
|
|
@@ -638,7 +664,7 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
638
664
|
...styles?.enterHintButton,
|
|
639
665
|
}, type: "button", onClick: continueUseful, children: copy.enterHint })), showLowRatingTime && flow.timeSavedMinutes !== undefined &&
|
|
640
666
|
Boolean(flow.mostUseful.trim()) &&
|
|
641
|
-
(
|
|
667
|
+
(hasEnoughDetail(flow.mostUseful) || flow.usefulDetailShown) && ((0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-primary', classNames?.primaryButton].filter(Boolean).join(' '), style: styles?.primaryButton, type: "button", onClick: submitFeedback, children: copy.doneLabel }))] })), isReview && ((0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-review-nav', classNames?.reviewNav].filter(Boolean).join(' '), style: styles?.reviewNav, children: [(0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-review-nav__button', classNames?.reviewNavButton].filter(Boolean).join(' '), style: styles?.reviewNavButton, type: "button", onClick: previousReviewPage, disabled: reviewPageIndex === 0, "aria-label": copy.backLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { size: 18 }) }), (0, jsx_runtime_1.jsxs)("button", { className: [
|
|
642
668
|
'cw-feedback-review-nav__button',
|
|
643
669
|
'cw-feedback-review-nav__button--next',
|
|
644
670
|
classNames?.reviewNavButton,
|
|
@@ -653,7 +679,17 @@ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onPr
|
|
|
653
679
|
if (event.target === event.currentTarget)
|
|
654
680
|
setScreenshotPreview(null);
|
|
655
681
|
}, children: (0, jsx_runtime_1.jsxs)("div", { ref: screenshotPreviewRef, className: "cw-feedback-image-preview-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Screenshot preview: ${screenshotPreview.name}`, tabIndex: -1, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "cw-feedback-image-preview-close", onClick: () => setScreenshotPreview(null), "aria-label": "Close screenshot preview", children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 22 }) }), (0, jsx_runtime_1.jsx)("img", { src: screenshotPreview.url, alt: screenshotPreview.name })] }) }));
|
|
682
|
+
const rangeTooltip = rangeActive && flow.step === 'rating' && flow.rating !== null && ((0, jsx_runtime_1.jsx)("div", { ref: rangeTooltipRef, role: "tooltip", className: ['cw-feedback-range-tooltip', classNames?.rangeTooltip].filter(Boolean).join(' '), style: {
|
|
683
|
+
...(0, feedbackTheme_1.feedbackThemeStyle)(theme),
|
|
684
|
+
...styles?.rangeTooltip,
|
|
685
|
+
position: 'fixed',
|
|
686
|
+
zIndex: 10002,
|
|
687
|
+
top: rangeTooltipPosition?.top ?? -1000,
|
|
688
|
+
left: rangeTooltipPosition?.left ?? -1000,
|
|
689
|
+
bottom: 'auto',
|
|
690
|
+
transform: 'none',
|
|
691
|
+
}, children: copy.ratingValueLabel(flow.rating) }));
|
|
656
692
|
return typeof document !== 'undefined'
|
|
657
|
-
? (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, react_dom_1.createPortal)(backdrop, document.body), (0, react_dom_1.createPortal)(dialog, document.body), previewDialog && (0, react_dom_1.createPortal)(previewDialog, document.body)] })
|
|
658
|
-
: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [backdrop, dialog, previewDialog] });
|
|
693
|
+
? (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, react_dom_1.createPortal)(backdrop, document.body), (0, react_dom_1.createPortal)(dialog, document.body), rangeTooltip && (0, react_dom_1.createPortal)(rangeTooltip, document.body), previewDialog && (0, react_dom_1.createPortal)(previewDialog, document.body)] })
|
|
694
|
+
: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [backdrop, dialog, rangeTooltip, previewDialog] });
|
|
659
695
|
}
|
package/dist/FeedbackPrompt.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface FeedbackPromptProps {
|
|
|
6
6
|
copy?: Partial<FeedbackCopy>;
|
|
7
7
|
theme?: FeedbackTheme;
|
|
8
8
|
compact?: boolean;
|
|
9
|
+
compactRatingLabel?: boolean;
|
|
10
|
+
ratingCardLabel?: string;
|
|
11
|
+
animateUnrated?: boolean;
|
|
12
|
+
featureName?: string;
|
|
13
|
+
featureDescription?: string;
|
|
9
14
|
promptRef?: Ref<HTMLDivElement>;
|
|
10
15
|
extraContent?: ReactNode;
|
|
11
16
|
loading?: boolean;
|
|
@@ -17,4 +22,4 @@ export interface FeedbackPromptProps {
|
|
|
17
22
|
styles?: FeedbackPromptStyles;
|
|
18
23
|
classNames?: FeedbackPromptClassNames;
|
|
19
24
|
}
|
|
20
|
-
export declare function FeedbackPrompt({ flow, copy: copyOverrides, theme, compact, promptRef, extraContent, loading, disabled, onBeforeOpen, onRatingSelected, className, style, styles, classNames, }: FeedbackPromptProps): JSX.Element | null;
|
|
25
|
+
export declare function FeedbackPrompt({ flow, copy: copyOverrides, theme, compact, compactRatingLabel, ratingCardLabel, animateUnrated, featureName, featureDescription, promptRef, extraContent, loading, disabled, onBeforeOpen, onRatingSelected, className, style, styles, classNames, }: FeedbackPromptProps): JSX.Element | null;
|
package/dist/FeedbackPrompt.js
CHANGED
|
@@ -3,19 +3,60 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FeedbackPrompt = FeedbackPrompt;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const react_1 = require("react");
|
|
6
|
+
const react_dom_1 = require("react-dom");
|
|
6
7
|
const types_1 = require("./types");
|
|
7
8
|
const feedbackTheme_1 = require("./feedbackTheme");
|
|
8
9
|
const StarRating_1 = require("./StarRating");
|
|
9
|
-
function FeedbackPrompt({ flow, copy: copyOverrides, theme, compact = false, promptRef, extraContent, loading = false, disabled = false, onBeforeOpen, onRatingSelected, className, style, styles, classNames, }) {
|
|
10
|
+
function FeedbackPrompt({ flow, copy: copyOverrides, theme, compact = false, compactRatingLabel = false, ratingCardLabel, animateUnrated = false, featureName, featureDescription, promptRef, extraContent, loading = false, disabled = false, onBeforeOpen, onRatingSelected, className, style, styles, classNames, }) {
|
|
10
11
|
const copy = (0, types_1.createFeedbackCopy)(copyOverrides);
|
|
11
12
|
const [hovered, setHovered] = (0, react_1.useState)(null);
|
|
12
|
-
const [
|
|
13
|
+
const [pendingStar, setPendingStar] = (0, react_1.useState)(null);
|
|
14
|
+
const [tooltipPosition, setTooltipPosition] = (0, react_1.useState)(null);
|
|
15
|
+
const tooltipRef = (0, react_1.useRef)(null);
|
|
16
|
+
const hoveredButtonRef = (0, react_1.useRef)(null);
|
|
17
|
+
const reselectTimerRef = (0, react_1.useRef)(null);
|
|
18
|
+
const tooltipVisible = !disabled && flow.step === 'closed' && flow.rating !== null && hovered === Math.ceil(flow.rating);
|
|
19
|
+
const fullRatingLabel = flow.rating === null ? null : flow.rating % 1
|
|
20
|
+
? copy.ratingValueLabel(flow.rating)
|
|
21
|
+
: copy.starTooltipLabel?.(flow.rating) ?? copy.starLabel(flow.rating);
|
|
22
|
+
const cardLabel = flow.rating !== null && ratingCardLabel ? ratingCardLabel : fullRatingLabel
|
|
23
|
+
? compactRatingLabel ? fullRatingLabel.split(/[—–]/)[0].trim() : fullRatingLabel
|
|
24
|
+
: copy.prompt;
|
|
25
|
+
(0, react_1.useEffect)(() => setPendingStar(null), [flow.requestId, flow.featureId, flow.step]);
|
|
26
|
+
(0, react_1.useEffect)(() => () => { if (reselectTimerRef.current)
|
|
27
|
+
clearTimeout(reselectTimerRef.current); }, []);
|
|
28
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
29
|
+
if (!tooltipVisible)
|
|
30
|
+
return;
|
|
31
|
+
const updatePlacement = () => {
|
|
32
|
+
const tooltip = tooltipRef.current;
|
|
33
|
+
const button = hoveredButtonRef.current;
|
|
34
|
+
if (!tooltip || !button)
|
|
35
|
+
return;
|
|
36
|
+
const buttonRect = button.getBoundingClientRect();
|
|
37
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
38
|
+
const spaceBelow = window.innerHeight - buttonRect.bottom;
|
|
39
|
+
const top = spaceBelow >= tooltipRect.height + 8
|
|
40
|
+
? buttonRect.bottom + 8
|
|
41
|
+
: Math.max(8, buttonRect.top - tooltipRect.height - 8);
|
|
42
|
+
const left = Math.max(8, Math.min(window.innerWidth - tooltipRect.width - 8, buttonRect.left + buttonRect.width / 2 - tooltipRect.width / 2));
|
|
43
|
+
setTooltipPosition((current) => current?.top === top && current.left === left ? current : { top, left });
|
|
44
|
+
};
|
|
45
|
+
updatePlacement();
|
|
46
|
+
window.addEventListener('resize', updatePlacement);
|
|
47
|
+
window.addEventListener('scroll', updatePlacement, true);
|
|
48
|
+
return () => {
|
|
49
|
+
window.removeEventListener('resize', updatePlacement);
|
|
50
|
+
window.removeEventListener('scroll', updatePlacement, true);
|
|
51
|
+
};
|
|
52
|
+
}, [tooltipVisible, hovered]);
|
|
13
53
|
if (!flow)
|
|
14
54
|
return null;
|
|
15
55
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: promptRef, className: [
|
|
16
56
|
'cw-feedback-prompt',
|
|
17
57
|
loading ? 'cw-feedback-prompt--loading' : '',
|
|
18
58
|
compact ? 'cw-feedback-prompt--compact' : '',
|
|
59
|
+
animateUnrated && flow.rating === null && !loading ? 'cw-feedback-prompt--new' : '',
|
|
19
60
|
className,
|
|
20
61
|
classNames?.root,
|
|
21
62
|
]
|
|
@@ -24,20 +65,47 @@ function FeedbackPrompt({ flow, copy: copyOverrides, theme, compact = false, pro
|
|
|
24
65
|
...(0, feedbackTheme_1.feedbackThemeStyle)(theme),
|
|
25
66
|
...style,
|
|
26
67
|
...styles?.root,
|
|
27
|
-
}, "aria-busy": loading, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", disabled: disabled, className: ['cw-feedback-prompt__label', classNames?.labelButton].filter(Boolean).join(' '), style: styles?.labelButton, onClick: () => {
|
|
68
|
+
}, "aria-busy": loading, children: [featureName && ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-prompt__feature-context", children: [(0, jsx_runtime_1.jsx)("strong", { children: featureName }), featureDescription && (0, jsx_runtime_1.jsx)("span", { title: featureDescription, children: featureDescription })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", disabled: disabled, className: ['cw-feedback-prompt__label', classNames?.labelButton].filter(Boolean).join(' '), style: styles?.labelButton, onClick: () => {
|
|
28
69
|
if (flow.rating === null || flow.step !== 'closed')
|
|
29
70
|
return;
|
|
71
|
+
if (reselectTimerRef.current)
|
|
72
|
+
clearTimeout(reselectTimerRef.current);
|
|
73
|
+
reselectTimerRef.current = null;
|
|
74
|
+
setPendingStar(null);
|
|
30
75
|
onBeforeOpen?.();
|
|
31
76
|
flow.setStep(flow.completed ? (flow.ratingBase >= 3 && flow.ratingBase < 5 ? 'rating' : flow.ratingBase === 5 ? 'time' : 'gap') : flow.resumeStep);
|
|
32
|
-
}, children:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
77
|
+
}, children: cardLabel }), (0, jsx_runtime_1.jsx)(StarRating_1.StarRating, { value: flow.rating, disabled: disabled, groupLabel: copy.starGroupLabel, starLabel: copy.starLabel, theme: theme, className: classNames?.stars, style: styles?.stars, styles: styles?.starRating, classNames: classNames?.starRating, onHoverChange: (star, target) => { hoveredButtonRef.current = target ?? null; setHovered(star); }, onChange: (star) => {
|
|
78
|
+
if (reselectTimerRef.current) {
|
|
79
|
+
clearTimeout(reselectTimerRef.current);
|
|
80
|
+
reselectTimerRef.current = null;
|
|
81
|
+
}
|
|
82
|
+
if (flow.rating !== null && pendingStar !== star) {
|
|
83
|
+
setPendingStar(star);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
setPendingStar(null);
|
|
87
|
+
if (flow.rating !== null) {
|
|
88
|
+
flow.setRating(star);
|
|
89
|
+
onRatingSelected?.(star);
|
|
90
|
+
reselectTimerRef.current = setTimeout(() => {
|
|
91
|
+
onBeforeOpen?.();
|
|
92
|
+
flow.selectRating(star);
|
|
93
|
+
reselectTimerRef.current = null;
|
|
94
|
+
}, 350);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
onBeforeOpen?.();
|
|
98
|
+
flow.selectRating(star);
|
|
99
|
+
onRatingSelected?.(star);
|
|
100
|
+
}
|
|
37
101
|
} }), flow.rating !== null && flow.step === 'closed' && ((0, jsx_runtime_1.jsx)("button", { type: "button", disabled: disabled, className: ['cw-feedback-review', classNames?.reviewButton].filter(Boolean).join(' '), style: styles?.reviewButton, onClick: () => {
|
|
102
|
+
if (reselectTimerRef.current)
|
|
103
|
+
clearTimeout(reselectTimerRef.current);
|
|
104
|
+
reselectTimerRef.current = null;
|
|
105
|
+
setPendingStar(null);
|
|
38
106
|
onBeforeOpen?.();
|
|
39
107
|
flow.setStep(flow.completed ? (flow.ratingBase >= 3 && flow.ratingBase < 5 ? 'rating' : flow.ratingBase === 5 ? 'time' : 'gap') : flow.resumeStep);
|
|
40
|
-
}, children: flow.completed ? copy.reviewLabel : copy.continueLabel })),
|
|
108
|
+
}, children: flow.completed ? copy.reviewLabel : copy.continueLabel })), flow.rating !== null && pendingStar !== null && (0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-prompt__change-hint", role: "alert", children: "Click the star again to change your rating" }), tooltipVisible && typeof document !== 'undefined' && (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { ref: tooltipRef, role: "tooltip", className: ['cw-feedback-tooltip', classNames?.tooltip].filter(Boolean).join(' '), style: { ...(0, feedbackTheme_1.feedbackThemeStyle)(theme), ...styles?.tooltip, display: 'block', position: 'fixed', zIndex: 2147483647, top: tooltipPosition?.top ?? -1000, left: tooltipPosition?.left ?? -1000, right: 'auto' }, children: hovered === 5 && !copy.starTooltipLabel
|
|
41
109
|
? copy.fiveStarTooltip
|
|
42
|
-
: copy.starTooltipLabel?.(hovered
|
|
110
|
+
: copy.starTooltipLabel?.(hovered) ?? copy.starLabel(hovered) }), document.body), extraContent && ((0, jsx_runtime_1.jsx)("div", { className: classNames?.extraContent, style: styles?.extraContent, children: extraContent })), loading && (0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-prompt__loading", role: "status", children: [(0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-prompt__spinner" }), " Loading feedback\u2026"] })] }));
|
|
43
111
|
}
|
package/dist/StarRating.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface StarRatingProps {
|
|
|
7
7
|
groupLabel?: string;
|
|
8
8
|
starLabel?: (star: number) => string;
|
|
9
9
|
theme?: FeedbackTheme;
|
|
10
|
-
onHoverChange?: (star: number | null) => void;
|
|
10
|
+
onHoverChange?: (star: number | null, target?: HTMLButtonElement) => void;
|
|
11
11
|
className?: string;
|
|
12
12
|
style?: CSSProperties;
|
|
13
13
|
styles?: StarRatingStyles;
|
package/dist/StarRating.js
CHANGED
|
@@ -11,6 +11,6 @@ function StarRating({ value, onChange, disabled = false, groupLabel = 'Rate this
|
|
|
11
11
|
const displayedRating = hovered ?? (value === null ? 0 : Math.floor(value) + (value % 1 ? 0.5 : 0));
|
|
12
12
|
return ((0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-stars', className, classNames?.root].filter(Boolean).join(' '), style: { ...style, ...styles?.root }, role: "radiogroup", "aria-label": groupLabel, children: [1, 2, 3, 4, 5].map((star) => {
|
|
13
13
|
const fill = Math.max(0, Math.min(1, displayedRating - star + 1)) * 100;
|
|
14
|
-
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": Math.ceil(value ?? 0) === star, "aria-label": starLabel(star), disabled: disabled, className: classNames?.starButton, style: styles?.starButton, onMouseEnter: () => { setHovered(star); onHoverChange?.(star); }, onMouseLeave: () => { setHovered(null); onHoverChange?.(null); }, onFocus: () => { setHovered(star); onHoverChange?.(star); }, onBlur: () => { setHovered(null); onHoverChange?.(null); }, onClick: () => onChange(star), children: [(0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M10.0003 2.5L12.2503 7.16667L17.3337 7.91667L13.667 11.5L14.5003 16.5833L10.0003 14.1667L5.50033 16.5833L6.33366 11.5L2.66699 7.91667L7.75033 7.16667L10.0003 2.5Z", stroke: mergedTheme.starEmpty ?? '#cbd5e1', strokeWidth: "1.33333", strokeLinejoin: "round" }) }), fill > 0 && ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", className: "cw-feedback-stars__filled", style: { clipPath: `inset(0 ${100 - fill}% 0 0)` }, children: (0, jsx_runtime_1.jsx)("path", { d: "M10.0003 2.5L12.2503 7.16667L17.3337 7.91667L13.667 11.5L14.5003 16.5833L10.0003 14.1667L5.50033 16.5833L6.33366 11.5L2.66699 7.91667L7.75033 7.16667L10.0003 2.5Z", fill: mergedTheme.accent ?? '#5b21e6', stroke: mergedTheme.accent ?? '#5b21e6', strokeWidth: "1.33333", strokeLinejoin: "round" }) }))] }, star));
|
|
14
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": Math.ceil(value ?? 0) === star, "aria-label": starLabel(star), disabled: disabled, className: classNames?.starButton, style: { width: 20, height: 20, flex: '0 0 20px', ...styles?.starButton }, onMouseEnter: (event) => { setHovered(star); onHoverChange?.(star, event.currentTarget); }, onMouseLeave: () => { setHovered(null); onHoverChange?.(null); }, onFocus: (event) => { setHovered(star); onHoverChange?.(star, event.currentTarget); }, onBlur: () => { setHovered(null); onHoverChange?.(null); }, onClick: () => onChange(star), children: [(0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M10.0003 2.5L12.2503 7.16667L17.3337 7.91667L13.667 11.5L14.5003 16.5833L10.0003 14.1667L5.50033 16.5833L6.33366 11.5L2.66699 7.91667L7.75033 7.16667L10.0003 2.5Z", stroke: mergedTheme.starEmpty ?? '#cbd5e1', strokeWidth: "1.33333", strokeLinejoin: "round" }) }), fill > 0 && ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", className: "cw-feedback-stars__filled", style: { clipPath: `inset(0 ${100 - fill}% 0 0)` }, children: (0, jsx_runtime_1.jsx)("path", { d: "M10.0003 2.5L12.2503 7.16667L17.3337 7.91667L13.667 11.5L14.5003 16.5833L10.0003 14.1667L5.50033 16.5833L6.33366 11.5L2.66699 7.91667L7.75033 7.16667L10.0003 2.5Z", fill: mergedTheme.accent ?? '#5b21e6', stroke: mergedTheme.accent ?? '#5b21e6', strokeWidth: "1.33333", strokeLinejoin: "round" }) }))] }, star));
|
|
15
15
|
}) }));
|
|
16
16
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface FeatureAvailabilityWindow {
|
|
2
2
|
enabled?: boolean;
|
|
3
|
+
name?: string | null;
|
|
4
|
+
description?: string | null;
|
|
3
5
|
startsAt?: string | null;
|
|
4
6
|
endsAt?: string | null;
|
|
5
7
|
uiNewTtl?: string | null;
|
|
@@ -18,4 +20,8 @@ export interface HttpFeatureAvailabilityOptions {
|
|
|
18
20
|
export declare function createHttpFeatureAvailabilityApi(options: HttpFeatureAvailabilityOptions): FeatureAvailabilityApi;
|
|
19
21
|
export declare function isFeatureAvailable(window: FeatureAvailabilityWindow | null, now?: number): boolean;
|
|
20
22
|
/** Feature-only gate; chat responses with request IDs never call this API. */
|
|
23
|
+
export declare function useFeatureAvailabilityDetails(modeId: string | undefined, featureId: string, api: FeatureAvailabilityApi | undefined, enabled: boolean): {
|
|
24
|
+
available: boolean;
|
|
25
|
+
feature: FeatureAvailabilityWindow | null;
|
|
26
|
+
};
|
|
21
27
|
export declare function useFeatureAvailability(modeId: string | undefined, featureId: string, api: FeatureAvailabilityApi | undefined, enabled: boolean): boolean;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createMockFeatureAvailabilityApi = createMockFeatureAvailabilityApi;
|
|
4
4
|
exports.createHttpFeatureAvailabilityApi = createHttpFeatureAvailabilityApi;
|
|
5
5
|
exports.isFeatureAvailable = isFeatureAvailable;
|
|
6
|
+
exports.useFeatureAvailabilityDetails = useFeatureAvailabilityDetails;
|
|
6
7
|
exports.useFeatureAvailability = useFeatureAvailability;
|
|
7
8
|
const react_1 = require("react");
|
|
8
9
|
/** Replace this adapter with the feature-availability endpoint when its contract is available. */
|
|
@@ -47,6 +48,8 @@ function createHttpFeatureAvailabilityApi(options) {
|
|
|
47
48
|
return null;
|
|
48
49
|
return {
|
|
49
50
|
enabled: true,
|
|
51
|
+
name: feature.feature_name,
|
|
52
|
+
description: feature.feature_description,
|
|
50
53
|
startsAt: feature.start_date,
|
|
51
54
|
endsAt: feature.end_date,
|
|
52
55
|
uiNewTtl: feature.ui_new_ttl,
|
|
@@ -65,7 +68,7 @@ function isFeatureAvailable(window, now = Date.now()) {
|
|
|
65
68
|
(ttl === null || (Number.isFinite(ttl) && now < ttl));
|
|
66
69
|
}
|
|
67
70
|
/** Feature-only gate; chat responses with request IDs never call this API. */
|
|
68
|
-
function
|
|
71
|
+
function useFeatureAvailabilityDetails(modeId, featureId, api, enabled) {
|
|
69
72
|
const [result, setResult] = (0, react_1.useState)(null);
|
|
70
73
|
const [now, setNow] = (0, react_1.useState)(() => Date.now());
|
|
71
74
|
const window = result && result.modeId === modeId && result.featureId === featureId ? result.window : null;
|
|
@@ -100,5 +103,8 @@ function useFeatureAvailability(modeId, featureId, api, enabled) {
|
|
|
100
103
|
document.removeEventListener("visibilitychange", update);
|
|
101
104
|
};
|
|
102
105
|
}, [enabled, now, window]);
|
|
103
|
-
return !enabled || (Boolean(modeId) && isFeatureAvailable(window, now));
|
|
106
|
+
return { available: !enabled || (Boolean(modeId) && isFeatureAvailable(window, now)), feature: window };
|
|
107
|
+
}
|
|
108
|
+
function useFeatureAvailability(modeId, featureId, api, enabled) {
|
|
109
|
+
return useFeatureAvailabilityDetails(modeId, featureId, api, enabled).available;
|
|
104
110
|
}
|
package/dist/feedback.css
CHANGED
|
@@ -54,9 +54,46 @@
|
|
|
54
54
|
font-size: 13px;
|
|
55
55
|
font-weight: 600;
|
|
56
56
|
}
|
|
57
|
+
.cw-feedback-prompt__feature-context {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: 2px;
|
|
60
|
+
max-width: 240px;
|
|
61
|
+
margin-bottom: 7px;
|
|
62
|
+
color: #344054;
|
|
63
|
+
font-size: 11px;
|
|
64
|
+
line-height: 1.3;
|
|
65
|
+
}
|
|
66
|
+
.cw-feedback-prompt__feature-context strong {
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
}
|
|
69
|
+
.cw-feedback-prompt__feature-context span {
|
|
70
|
+
display: -webkit-box;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
-webkit-box-orient: vertical;
|
|
73
|
+
-webkit-line-clamp: 2;
|
|
74
|
+
color: #667085;
|
|
75
|
+
}
|
|
57
76
|
.cw-feedback-stars {
|
|
58
77
|
display: flex;
|
|
59
|
-
gap:
|
|
78
|
+
gap: 5px;
|
|
79
|
+
}
|
|
80
|
+
.cw-feedback-prompt__change-hint {
|
|
81
|
+
display: block;
|
|
82
|
+
margin-top: 4px;
|
|
83
|
+
color: #dc2626;
|
|
84
|
+
font-size: 10px;
|
|
85
|
+
line-height: 1.2;
|
|
86
|
+
}
|
|
87
|
+
.cw-feedback-prompt--new .cw-feedback-stars button {
|
|
88
|
+
animation: cw-feedback-star-wave 2.5s ease-in-out infinite;
|
|
89
|
+
}
|
|
90
|
+
.cw-feedback-prompt--new .cw-feedback-stars button:nth-child(2) { animation-delay: .16s; }
|
|
91
|
+
.cw-feedback-prompt--new .cw-feedback-stars button:nth-child(3) { animation-delay: .32s; }
|
|
92
|
+
.cw-feedback-prompt--new .cw-feedback-stars button:nth-child(4) { animation-delay: .48s; }
|
|
93
|
+
.cw-feedback-prompt--new .cw-feedback-stars button:nth-child(5) { animation-delay: .64s; }
|
|
94
|
+
@keyframes cw-feedback-star-wave {
|
|
95
|
+
0%, 32%, 100% { filter: drop-shadow(0 0 0 transparent); transform: translateY(0); }
|
|
96
|
+
14% { filter: drop-shadow(0 0 5px #8b5cf6); transform: translateY(-2px); }
|
|
60
97
|
}
|
|
61
98
|
.cw-feedback-stars button {
|
|
62
99
|
position: relative;
|
|
@@ -83,13 +120,18 @@
|
|
|
83
120
|
position: absolute;
|
|
84
121
|
right: 0;
|
|
85
122
|
top: calc(100% + 7px);
|
|
86
|
-
|
|
123
|
+
max-width: min(320px, calc(100vw - 16px));
|
|
124
|
+
white-space: normal;
|
|
87
125
|
background: #273349;
|
|
88
126
|
color: #fff;
|
|
89
127
|
border-radius: 5px;
|
|
90
128
|
padding: 6px 9px;
|
|
91
129
|
font-size: 11px;
|
|
92
130
|
}
|
|
131
|
+
.cw-feedback-tooltip--above {
|
|
132
|
+
top: auto;
|
|
133
|
+
bottom: calc(100% + 7px);
|
|
134
|
+
}
|
|
93
135
|
.cw-feedback-backdrop {
|
|
94
136
|
position: fixed;
|
|
95
137
|
inset: 0;
|
|
@@ -462,10 +504,39 @@ body.cw-feedback-capturing .cw-feedback-prompt {
|
|
|
462
504
|
cursor: pointer;
|
|
463
505
|
}
|
|
464
506
|
.cw-feedback-prompt:has(.cw-feedback-review) {
|
|
465
|
-
|
|
507
|
+
display: flex;
|
|
508
|
+
flex-direction: column;
|
|
509
|
+
align-items: center;
|
|
510
|
+
row-gap: 4px;
|
|
511
|
+
min-width: 0;
|
|
466
512
|
}
|
|
467
513
|
.cw-feedback-prompt:has(.cw-feedback-review) .cw-feedback-stars {
|
|
468
|
-
|
|
514
|
+
flex: 0 0 auto;
|
|
515
|
+
padding-right: 0;
|
|
516
|
+
}
|
|
517
|
+
.cw-feedback-prompt:has(.cw-feedback-review) .cw-feedback-prompt__change-hint {
|
|
518
|
+
grid-column: 1 / -1;
|
|
519
|
+
width: 100%;
|
|
520
|
+
max-width: 240px;
|
|
521
|
+
justify-self: center;
|
|
522
|
+
margin-top: 2px;
|
|
523
|
+
text-align: center;
|
|
524
|
+
white-space: normal;
|
|
525
|
+
overflow-wrap: anywhere;
|
|
526
|
+
}
|
|
527
|
+
.cw-feedback-prompt:has(.cw-feedback-review) .cw-feedback-prompt__label {
|
|
528
|
+
width: 100%;
|
|
529
|
+
min-width: 0;
|
|
530
|
+
overflow-wrap: anywhere;
|
|
531
|
+
}
|
|
532
|
+
.cw-feedback-prompt:has(.cw-feedback-review) .cw-feedback-prompt__feature-context {
|
|
533
|
+
width: 100%;
|
|
534
|
+
}
|
|
535
|
+
.cw-feedback-prompt:has(.cw-feedback-review) .cw-feedback-review {
|
|
536
|
+
position: static;
|
|
537
|
+
margin: 2px 0 0;
|
|
538
|
+
padding: 0;
|
|
539
|
+
white-space: nowrap;
|
|
469
540
|
}
|
|
470
541
|
.cw-feedback-stars button:disabled {
|
|
471
542
|
cursor: default;
|
|
@@ -497,7 +568,11 @@ body.cw-feedback-capturing .cw-feedback-prompt {
|
|
|
497
568
|
border-radius: 50%;
|
|
498
569
|
background: #bce8ce;
|
|
499
570
|
color: #fff;
|
|
500
|
-
animation: cw-feedback-success-pop 600ms cubic-bezier(.2, 1.5, .35, 1) both;
|
|
571
|
+
animation: cw-feedback-success-pop 600ms cubic-bezier(.2, 1.5, .35, 1) both, cw-feedback-success-glow 2.4s 600ms ease-in-out infinite;
|
|
572
|
+
}
|
|
573
|
+
@keyframes cw-feedback-success-glow {
|
|
574
|
+
0%, 100% { box-shadow: 0 0 0 0 #16a34a20; }
|
|
575
|
+
50% { box-shadow: 0 0 0 12px #16a34a16, 0 0 28px 7px #16a34a50; }
|
|
501
576
|
}
|
|
502
577
|
.cw-feedback-success__check::after {
|
|
503
578
|
content: "";
|
|
@@ -527,6 +602,8 @@ body.cw-feedback-capturing .cw-feedback-prompt {
|
|
|
527
602
|
to { stroke-dashoffset: 0; }
|
|
528
603
|
}
|
|
529
604
|
@media (prefers-reduced-motion: reduce) {
|
|
605
|
+
.cw-feedback-prompt--new .cw-feedback-stars button,
|
|
606
|
+
.cw-feedback-inline-detail-prompt,
|
|
530
607
|
.cw-feedback-success__check,
|
|
531
608
|
.cw-feedback-success__check::after,
|
|
532
609
|
.cw-feedback-success__check svg :is(path, polyline) {
|
|
@@ -596,7 +673,7 @@ body.cw-feedback-capturing .cw-feedback-prompt {
|
|
|
596
673
|
color: #16a34a;
|
|
597
674
|
}
|
|
598
675
|
.cw-feedback-prompt:has(.cw-feedback-review) .cw-feedback-stars {
|
|
599
|
-
padding-right:
|
|
676
|
+
padding-right: 0;
|
|
600
677
|
}
|
|
601
678
|
.cw-feedback-hint {
|
|
602
679
|
font-size: 12px;
|
|
@@ -703,8 +780,8 @@ body.cw-feedback-capturing .cw-feedback-prompt {
|
|
|
703
780
|
}
|
|
704
781
|
.cw-feedback-tooltip,
|
|
705
782
|
.cw-feedback-range-tooltip {
|
|
706
|
-
background: var(--cw-feedback-tooltip-bg);
|
|
707
|
-
color: var(--cw-feedback-tooltip-text);
|
|
783
|
+
background: var(--cw-feedback-tooltip-bg, #273349);
|
|
784
|
+
color: var(--cw-feedback-tooltip-text, #fff);
|
|
708
785
|
}
|
|
709
786
|
.cw-feedback-range-tooltip:after {
|
|
710
787
|
border-top-color: var(--cw-feedback-tooltip-bg);
|
|
@@ -759,7 +836,48 @@ body.cw-feedback-capturing .cw-feedback-prompt {
|
|
|
759
836
|
font-weight: 500;
|
|
760
837
|
}
|
|
761
838
|
.cw-feedback-range {
|
|
762
|
-
margin-top:
|
|
839
|
+
margin-top: 18px;
|
|
840
|
+
}
|
|
841
|
+
.cw-feedback-range-tooltip {
|
|
842
|
+
width: max-content;
|
|
843
|
+
max-width: calc(100vw - 16px);
|
|
844
|
+
white-space: nowrap;
|
|
845
|
+
bottom: calc(100% + 20px);
|
|
846
|
+
transform: none;
|
|
847
|
+
}
|
|
848
|
+
@media (max-width: 480px) {
|
|
849
|
+
.cw-feedback-range-tooltip {
|
|
850
|
+
white-space: normal;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
.cw-feedback-range-tooltip::after { display: none; }
|
|
854
|
+
.cw-feedback-range__pointer {
|
|
855
|
+
position: absolute;
|
|
856
|
+
bottom: calc(100% + 8px);
|
|
857
|
+
width: 0;
|
|
858
|
+
height: 0;
|
|
859
|
+
border: 6px solid transparent;
|
|
860
|
+
border-top-color: var(--cw-feedback-tooltip-bg, #344054);
|
|
861
|
+
transform: translateX(-50%);
|
|
862
|
+
pointer-events: none;
|
|
863
|
+
}
|
|
864
|
+
.cw-feedback-range__ticks {
|
|
865
|
+
display: flex;
|
|
866
|
+
justify-content: space-between;
|
|
867
|
+
height: 8px;
|
|
868
|
+
margin: -1px 2px 0;
|
|
869
|
+
pointer-events: none;
|
|
870
|
+
}
|
|
871
|
+
.cw-feedback-range__ticks span {
|
|
872
|
+
width: 1px;
|
|
873
|
+
height: 5px;
|
|
874
|
+
background: #cbd5e1;
|
|
875
|
+
}
|
|
876
|
+
.cw-feedback-range__ticks span.is-major {
|
|
877
|
+
height: 8px;
|
|
878
|
+
}
|
|
879
|
+
.cw-feedback-rating-options {
|
|
880
|
+
margin-top: 8px;
|
|
763
881
|
}
|
|
764
882
|
.cw-feedback-low-rating-intro {
|
|
765
883
|
margin: 0 0 10px;
|
|
@@ -788,17 +906,31 @@ body.cw-feedback-capturing .cw-feedback-prompt {
|
|
|
788
906
|
font-size: 13px;
|
|
789
907
|
}
|
|
790
908
|
.cw-feedback-inline-detail-prompt {
|
|
791
|
-
margin: 0;
|
|
909
|
+
margin: 10px 0 2px;
|
|
792
910
|
color: var(--cw-feedback-accent);
|
|
793
911
|
font-size: 13px;
|
|
794
912
|
font-weight: 400;
|
|
795
913
|
line-height: 1.35;
|
|
914
|
+
animation: cw-feedback-detail-arrive 1.15s ease-out both;
|
|
796
915
|
}
|
|
797
916
|
.cw-feedback-dialog__content > .cw-feedback-inline-detail-prompt {
|
|
798
|
-
margin-
|
|
917
|
+
margin-top: 14px;
|
|
918
|
+
margin-bottom: 3px;
|
|
919
|
+
}
|
|
920
|
+
@keyframes cw-feedback-detail-arrive {
|
|
921
|
+
0% { opacity: 0; filter: drop-shadow(0 0 8px var(--cw-feedback-accent)); transform: translateY(3px); }
|
|
922
|
+
55% { opacity: 1; filter: drop-shadow(0 0 5px var(--cw-feedback-accent)); }
|
|
923
|
+
100% { opacity: 1; filter: none; transform: translateY(0); }
|
|
799
924
|
}
|
|
800
925
|
.cw-feedback-useful-field {
|
|
801
|
-
margin-top:
|
|
926
|
+
margin-top: 6px;
|
|
927
|
+
gap: 3px;
|
|
928
|
+
}
|
|
929
|
+
.cw-feedback-useful-field .cw-feedback-inline-detail-prompt {
|
|
930
|
+
margin: 2px 0 0;
|
|
931
|
+
}
|
|
932
|
+
.cw-feedback-useful-prompt.cw-feedback-inline-detail-prompt {
|
|
933
|
+
margin: 4px 0 2px;
|
|
802
934
|
}
|
|
803
935
|
.cw-feedback-time-title {
|
|
804
936
|
margin-top: 5px !important;
|
|
@@ -1047,7 +1179,7 @@ body .cw-feedback-dialog {
|
|
|
1047
1179
|
.cw-feedback-gap__severity.medium { color: #ca8a04; }
|
|
1048
1180
|
.cw-feedback-gap__severity.low { color: #64748b; }
|
|
1049
1181
|
.cw-feedback-gap__detail-prompt {
|
|
1050
|
-
margin:
|
|
1182
|
+
margin: 10px 0 2px;
|
|
1051
1183
|
}
|
|
1052
1184
|
.cw-feedback-gap__remove {
|
|
1053
1185
|
display: inline-flex;
|
package/dist/types.js
CHANGED
|
@@ -61,15 +61,15 @@ exports.defaultFeedbackCopy = {
|
|
|
61
61
|
usefulTitle: "What part did you find most useful?",
|
|
62
62
|
lowRatingUsefulTitle: "What did you find most useful?",
|
|
63
63
|
usefulMoreDetailTitle: "What did you find most useful?",
|
|
64
|
-
usefulMoreDetailPrompt: "Could you share a little more detail
|
|
64
|
+
usefulMoreDetailPrompt: "Could you share a little more detail before you move forward?",
|
|
65
65
|
usefulPlaceholder: "The part that helped me most was...",
|
|
66
|
-
usefulMinimumWords:
|
|
67
|
-
gapTitle: "What
|
|
66
|
+
usefulMinimumWords: 5,
|
|
67
|
+
gapTitle: "What is the biggest gap we could address to improve this response?",
|
|
68
68
|
fiveStarGapTitle: "Is there anything we could improve to make this even better?",
|
|
69
69
|
lowRatingIntroTitle: "We’re truly sorry. You deserve a better experience!",
|
|
70
70
|
lowRatingIntroDescription: "Tell us what went wrong in a few quick steps, so we can make things better for you.",
|
|
71
|
-
gapSubtitle: "
|
|
72
|
-
gapMoreDetailPrompt: "Could you share a little more detail
|
|
71
|
+
gapSubtitle: "Tell us what was missing, incorrect, or difficult to use.",
|
|
72
|
+
gapMoreDetailPrompt: "Could you share a little more detail before you move forward?",
|
|
73
73
|
gapPlaceholder: "Tell us about the gap...",
|
|
74
74
|
severityQuestion: "How severely does this issue impact your work?",
|
|
75
75
|
severityLabels: { critical: "Critical", high: "High", medium: "Medium", low: "Low" },
|
package/dist/useFeedbackFlow.js
CHANGED
|
@@ -44,20 +44,17 @@ function useFeedbackFlow(requestId, featureId, metadata) {
|
|
|
44
44
|
}, []);
|
|
45
45
|
const changeRating = (0, react_1.useCallback)((value) => {
|
|
46
46
|
hasInteracted.current = true;
|
|
47
|
-
if (rating !== value)
|
|
47
|
+
if (rating !== value || completed)
|
|
48
48
|
clearDraft();
|
|
49
49
|
setRating(value);
|
|
50
|
-
}, [clearDraft, rating]);
|
|
50
|
+
}, [clearDraft, completed, rating]);
|
|
51
51
|
const close = (0, react_1.useCallback)(() => {
|
|
52
52
|
setRatingLocked(true);
|
|
53
53
|
setStep("closed");
|
|
54
54
|
}, []);
|
|
55
55
|
const selectRating = (0, react_1.useCallback)((value) => {
|
|
56
56
|
hasInteracted.current = true;
|
|
57
|
-
//
|
|
58
|
-
// the closed card is read-only and the user resumes with Continue.
|
|
59
|
-
if (ratingLocked && step === "closed")
|
|
60
|
-
return;
|
|
57
|
+
// A confirmed re-rating can reopen a completed response.
|
|
61
58
|
changeRating(value);
|
|
62
59
|
setRatingBase(value);
|
|
63
60
|
if (value === 5) {
|
|
@@ -72,7 +69,7 @@ function useFeedbackFlow(requestId, featureId, metadata) {
|
|
|
72
69
|
}
|
|
73
70
|
else
|
|
74
71
|
setStep("closed");
|
|
75
|
-
}, [changeRating
|
|
72
|
+
}, [changeRating]);
|
|
76
73
|
const submission = (0, react_1.useMemo)(() => rating === null
|
|
77
74
|
? null
|
|
78
75
|
: {
|