@e-llm-studio/feedback-flow 0.0.0-stage → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ import type { CSSProperties } from "react";
2
+ export type FeedbackSeverity = "critical" | "high" | "medium" | "low";
3
+ export interface FeedbackAttachmentRef {
4
+ attachmentId: string;
5
+ gcsUrl: string;
6
+ publicUrl?: string;
7
+ name: string;
8
+ type?: string;
9
+ size?: number;
10
+ lastModified?: number;
11
+ }
12
+ export interface FeedbackGap {
13
+ /** Existing server identity; absent only for a newly added gap. */
14
+ negativeId?: string;
15
+ description: string;
16
+ severity?: FeedbackSeverity;
17
+ /** The short-answer follow-up has been shown for this gap. */
18
+ detailRequested?: boolean;
19
+ screenshots: (File | FeedbackAttachmentRef)[];
20
+ }
21
+ export interface FeedbackSubmission {
22
+ requestId: string;
23
+ featureId: string;
24
+ rating: number;
25
+ timeSavedMinutes?: number;
26
+ mostUseful?: string;
27
+ usefulMoreDetail?: string;
28
+ usefulDetailShown?: boolean;
29
+ gaps: FeedbackGap[];
30
+ metadata?: FeedbackMetadata;
31
+ }
32
+ export type FeedbackProgressStep = "rating" | "time" | "useful" | "gap" | "success";
33
+ /** Host-defined context included with every submission; the library does not interpret it. */
34
+ export type FeedbackMetadata = Record<string, unknown> & {
35
+ mode?: string;
36
+ modeId?: string;
37
+ owner?: string;
38
+ featureContext?: string;
39
+ userEmail?: string;
40
+ traceId?: string;
41
+ /** Optional backend query ID for the response being rated. */
42
+ queryId?: string;
43
+ /** Optional chat session ID; typically supplied by LSC hosts. */
44
+ sessionId?: string;
45
+ /** Optional request ID of the AI response being rated. */
46
+ requestId?: string;
47
+ /** Saved after each completed step so an interrupted form can resume. */
48
+ feedbackProgress?: {
49
+ nextStep: FeedbackProgressStep;
50
+ completed: boolean;
51
+ };
52
+ };
53
+ /** Context supplied by each integration; custom fields are flattened into submission.metadata. */
54
+ export interface FeedbackTrackingContext {
55
+ mode?: string;
56
+ modeId?: string;
57
+ owner?: string;
58
+ featureContext?: string;
59
+ userEmail?: string;
60
+ traceId?: string;
61
+ custom?: Record<string, unknown>;
62
+ }
63
+ export interface FeedbackCopy {
64
+ prompt: string;
65
+ continueLabel: string;
66
+ savedLabel: string;
67
+ reviewLabel: string;
68
+ fiveStarTooltip: string;
69
+ starTooltipLabel?: (star: number) => string;
70
+ starGroupLabel: string;
71
+ starLabel: (star: number) => string;
72
+ backLabel: string;
73
+ closeLabel: string;
74
+ ratingControlLabel: string;
75
+ ratingContinueLabel: string;
76
+ ratingTitle: string;
77
+ ratingSubtitle: string;
78
+ ratingHint: string;
79
+ enterHint: string;
80
+ ratingValueLabel: (rating: number) => string;
81
+ timeSavedTitle: string;
82
+ positiveRatingIntroTitle: string;
83
+ positiveRatingIntroDescription: string;
84
+ timeOptions: {
85
+ value: number;
86
+ label: string;
87
+ }[];
88
+ otherLabel: string;
89
+ minuteUnit: string;
90
+ usefulTitle: string;
91
+ lowRatingUsefulTitle: string;
92
+ usefulMoreDetailTitle: string;
93
+ usefulMoreDetailPrompt: string;
94
+ usefulPlaceholder: string;
95
+ usefulMinimumWords: number;
96
+ gapTitle: string;
97
+ fiveStarGapTitle: string;
98
+ lowRatingIntroTitle: string;
99
+ lowRatingIntroDescription: string;
100
+ gapSubtitle: string;
101
+ gapMoreDetailPrompt: string;
102
+ gapPlaceholder: string;
103
+ severityQuestion: string;
104
+ severityLabels: Record<FeedbackSeverity, string>;
105
+ attachScreenshot: string;
106
+ addAnotherGap: string;
107
+ nextLabel: string;
108
+ doneLabel: string;
109
+ saveChangesLabel: string;
110
+ savingLabel: string;
111
+ submitErrorLabel: string;
112
+ successTitle: string;
113
+ successDescription: string;
114
+ successComingSoon: string;
115
+ }
116
+ export interface FeedbackTheme {
117
+ accent?: string;
118
+ text?: string;
119
+ muted?: string;
120
+ border?: string;
121
+ surface?: string;
122
+ gapSurface?: string;
123
+ overlay?: string;
124
+ success?: string;
125
+ starEmpty?: string;
126
+ tooltipBackground?: string;
127
+ tooltipText?: string;
128
+ severityColors?: Partial<Record<FeedbackSeverity, string>>;
129
+ severityBackgrounds?: Partial<Record<FeedbackSeverity, string>>;
130
+ }
131
+ export declare const defaultFeedbackCopy: FeedbackCopy;
132
+ export declare const defaultFeedbackTheme: FeedbackTheme;
133
+ export declare function createFeedbackTheme(overrides?: Partial<FeedbackTheme>): FeedbackTheme;
134
+ export declare function createFeedbackCopy(overrides?: Partial<Omit<FeedbackCopy, "severityLabels">> & {
135
+ severityLabels?: Partial<FeedbackCopy["severityLabels"]>;
136
+ }): FeedbackCopy;
137
+ export interface StarRatingStyles {
138
+ root?: CSSProperties;
139
+ starButton?: CSSProperties;
140
+ starIcon?: CSSProperties;
141
+ }
142
+ export interface StarRatingClassNames {
143
+ root?: string;
144
+ starButton?: string;
145
+ starIcon?: string;
146
+ }
147
+ export interface FeedbackPromptStyles {
148
+ root?: CSSProperties;
149
+ labelButton?: CSSProperties;
150
+ stars?: CSSProperties;
151
+ starRating?: StarRatingStyles;
152
+ reviewButton?: CSSProperties;
153
+ tooltip?: CSSProperties;
154
+ extraContent?: CSSProperties;
155
+ }
156
+ export interface FeedbackPromptClassNames {
157
+ root?: string;
158
+ labelButton?: string;
159
+ stars?: string;
160
+ starRating?: StarRatingClassNames;
161
+ reviewButton?: string;
162
+ tooltip?: string;
163
+ extraContent?: string;
164
+ }
165
+ export interface FeedbackModalStyles {
166
+ backdrop?: CSSProperties;
167
+ dialog?: CSSProperties;
168
+ content?: CSSProperties;
169
+ topbar?: CSSProperties;
170
+ backButton?: CSSProperties;
171
+ closeButton?: CSSProperties;
172
+ title?: CSSProperties;
173
+ subtitle?: CSSProperties;
174
+ hint?: CSSProperties;
175
+ enterHintButton?: CSSProperties;
176
+ rangeContainer?: CSSProperties;
177
+ rangeTooltip?: CSSProperties;
178
+ rangeInput?: CSSProperties;
179
+ ratingOptionsContainer?: CSSProperties;
180
+ ratingOptionButton?: CSSProperties;
181
+ ratingOptionButtonSelected?: CSSProperties;
182
+ timePillsContainer?: CSSProperties;
183
+ timePillButton?: CSSProperties;
184
+ timePillButtonSelected?: CSSProperties;
185
+ customTimeWrapper?: CSSProperties;
186
+ customTimeInput?: CSSProperties;
187
+ footer?: CSSProperties;
188
+ usefulTitle?: CSSProperties;
189
+ usefulPrompt?: CSSProperties;
190
+ usefulField?: CSSProperties;
191
+ usefulTextarea?: CSSProperties;
192
+ gapList?: CSSProperties;
193
+ gapItem?: CSSProperties;
194
+ gapTextarea?: CSSProperties;
195
+ severityTitle?: CSSProperties;
196
+ severityContainer?: CSSProperties;
197
+ severityButton?: CSSProperties;
198
+ uploadButton?: CSSProperties;
199
+ uploadInput?: CSSProperties;
200
+ filesList?: CSSProperties;
201
+ fileTag?: CSSProperties;
202
+ addGapButton?: CSSProperties;
203
+ submitError?: CSSProperties;
204
+ successContainer?: CSSProperties;
205
+ successCheck?: CSSProperties;
206
+ successTitle?: CSSProperties;
207
+ successDescription?: CSSProperties;
208
+ successComingSoon?: CSSProperties;
209
+ actionBar?: CSSProperties;
210
+ primaryButton?: CSSProperties;
211
+ reviewNav?: CSSProperties;
212
+ reviewNavButton?: CSSProperties;
213
+ reviewNavNextButton?: CSSProperties;
214
+ }
215
+ export interface FeedbackModalClassNames {
216
+ backdrop?: string;
217
+ dialog?: string;
218
+ content?: string;
219
+ topbar?: string;
220
+ backButton?: string;
221
+ closeButton?: string;
222
+ title?: string;
223
+ subtitle?: string;
224
+ hint?: string;
225
+ enterHintButton?: string;
226
+ rangeContainer?: string;
227
+ rangeTooltip?: string;
228
+ rangeInput?: string;
229
+ ratingOptionsContainer?: string;
230
+ ratingOptionButton?: string;
231
+ ratingOptionButtonSelected?: string;
232
+ timePillsContainer?: string;
233
+ timePillButton?: string;
234
+ timePillButtonSelected?: string;
235
+ customTimeWrapper?: string;
236
+ customTimeInput?: string;
237
+ footer?: string;
238
+ usefulTitle?: string;
239
+ usefulPrompt?: string;
240
+ usefulField?: string;
241
+ usefulTextarea?: string;
242
+ gapList?: string;
243
+ gapItem?: string;
244
+ gapTextarea?: string;
245
+ severityTitle?: string;
246
+ severityContainer?: string;
247
+ severityButton?: string;
248
+ uploadButton?: string;
249
+ uploadInput?: string;
250
+ filesList?: string;
251
+ fileTag?: string;
252
+ addGapButton?: string;
253
+ submitError?: string;
254
+ successContainer?: string;
255
+ successCheck?: string;
256
+ successTitle?: string;
257
+ successDescription?: string;
258
+ successComingSoon?: string;
259
+ actionBar?: string;
260
+ primaryButton?: string;
261
+ reviewNav?: string;
262
+ reviewNavButton?: string;
263
+ reviewNavNextButton?: string;
264
+ }
265
+ export interface FeedbackExperienceStyles {
266
+ container?: CSSProperties;
267
+ floatingWrapper?: CSSProperties;
268
+ prompt?: FeedbackPromptStyles;
269
+ modal?: FeedbackModalStyles;
270
+ }
271
+ export interface FeedbackExperienceClassNames {
272
+ container?: string;
273
+ floatingWrapper?: string;
274
+ prompt?: FeedbackPromptClassNames;
275
+ modal?: FeedbackModalClassNames;
276
+ }
package/dist/types.js ADDED
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultFeedbackTheme = exports.defaultFeedbackCopy = void 0;
4
+ exports.createFeedbackTheme = createFeedbackTheme;
5
+ exports.createFeedbackCopy = createFeedbackCopy;
6
+ exports.defaultFeedbackCopy = {
7
+ prompt: "Help us help you!",
8
+ continueLabel: "Continue",
9
+ savedLabel: "Feedback Saved",
10
+ reviewLabel: "Review",
11
+ fiveStarTooltip: "5 · Next Level Useful – Enhances my Life",
12
+ starTooltipLabel: (star) => ({
13
+ 1: "1 · Not useful — missed what I needed",
14
+ 2: "2 · Slightly useful — needs improvement",
15
+ 3: "3 · Somewhat useful — helped in part",
16
+ 4: "4 · Very useful — saved me effort",
17
+ 5: "5 · Next Level Useful — Enhances my Life",
18
+ }[star] ?? `${star} stars`),
19
+ starGroupLabel: "Rate this response",
20
+ starLabel: (star) => `${star} star${star === 1 ? "" : "s"}`,
21
+ backLabel: "Back",
22
+ closeLabel: "Close",
23
+ ratingControlLabel: "Fine tune rating",
24
+ ratingContinueLabel: "Continue to time saved question",
25
+ ratingTitle: "Somewhere in between? Fine-tune your rating to what feels right!",
26
+ ratingSubtitle: "Your input shapes what we improve next.",
27
+ ratingHint: "Select the rating that conveys the best",
28
+ enterHint: "Press Enter ↵ to continue",
29
+ ratingValueLabel: (rating) => {
30
+ const descriptions = {
31
+ "3.0": "A starting point, but I need more",
32
+ "3.1": "A little more helpful than average",
33
+ "3.2": "Useful for some of what I needed",
34
+ "3.3": "Helpful, with room to improve",
35
+ "3.4": "Solid help on this task",
36
+ "3.5": "Good help, though not quite complete",
37
+ "3.6": "Helpful for most of my task",
38
+ "3.7": "Quite useful, with minor gaps",
39
+ "3.8": "Very helpful overall",
40
+ "3.9": "Nearly everything I needed",
41
+ "4.0": "Helpful enough to move me forward",
42
+ "4.1": "Strong help for this task",
43
+ "4.2": "Made my work noticeably easier",
44
+ "4.3": "Really useful and reliable",
45
+ "4.4": "Saved me considerable effort",
46
+ "4.5": "Excellent help — made a difference",
47
+ "4.6": "Extremely useful for my work",
48
+ "4.7": "Almost indispensable today",
49
+ "4.8": "Outstanding — exceeded expectations",
50
+ "4.9": "Nearly perfect for what I needed",
51
+ "5.0": "Exactly what I needed today",
52
+ };
53
+ return `${rating.toFixed(1)} · ${descriptions[rating.toFixed(1)] ?? "Useful"}`;
54
+ },
55
+ timeSavedTitle: "How much time (in mins) did this save you?",
56
+ positiveRatingIntroTitle: "We’re so glad you’re enjoying it!",
57
+ positiveRatingIntroDescription: "What stood out to you today? Tell us in a few quick steps so we can keep making your experience great!",
58
+ timeOptions: [2, 5, 10, 20].map((value) => ({ value, label: `${value} min` })),
59
+ otherLabel: "✎ Other",
60
+ minuteUnit: "min",
61
+ usefulTitle: "What part did you find most useful?",
62
+ lowRatingUsefulTitle: "What did you find most useful?",
63
+ usefulMoreDetailTitle: "What did you find most useful?",
64
+ usefulMoreDetailPrompt: "Could you share a little more detail to understand you better?",
65
+ usefulPlaceholder: "The part that helped me most was...",
66
+ usefulMinimumWords: 10,
67
+ gapTitle: "What’s the biggest gap we could improve to make it better?",
68
+ fiveStarGapTitle: "Is there anything we could improve to make this even better?",
69
+ lowRatingIntroTitle: "We’re truly sorry. You deserve a better experience!",
70
+ lowRatingIntroDescription: "Tell us what went wrong in a few quick steps, so we can make things better for you.",
71
+ gapSubtitle: "Help us help you better",
72
+ gapMoreDetailPrompt: "Could you share a little more detail to help you better?",
73
+ gapPlaceholder: "Tell us about the gap...",
74
+ severityQuestion: "How severely does this issue impact your work?",
75
+ severityLabels: { critical: "Critical", high: "High", medium: "Medium", low: "Low" },
76
+ attachScreenshot: "Attach a screenshot",
77
+ addAnotherGap: "Add another gap",
78
+ nextLabel: "Next →",
79
+ doneLabel: "Done!",
80
+ saveChangesLabel: "Save changes",
81
+ savingLabel: "Saving feedback…",
82
+ submitErrorLabel: "Feedback could not be saved. Please try again.",
83
+ successTitle: "Thank you for your feedback!",
84
+ successDescription: "Your feedback is valuable and our team is already reviewing it.",
85
+ successComingSoon: "Coming soon",
86
+ };
87
+ exports.defaultFeedbackTheme = {
88
+ accent: "#6025e5",
89
+ text: "#172033",
90
+ muted: "#718096",
91
+ border: "#c8b6ff",
92
+ surface: "#fff",
93
+ gapSurface: "#faf8ff",
94
+ overlay: "#11182755",
95
+ success: "#16a34a",
96
+ starEmpty: "#cbd5e1",
97
+ tooltipBackground: "#344054",
98
+ tooltipText: "#fff",
99
+ severityColors: {
100
+ critical: "#dc2626",
101
+ high: "#ea580c",
102
+ medium: "#c89b25",
103
+ low: "#64748b",
104
+ },
105
+ severityBackgrounds: {
106
+ critical: "#fff1f2",
107
+ high: "#fff7ed",
108
+ medium: "#fefce8",
109
+ low: "#f8fafc",
110
+ },
111
+ };
112
+ function createFeedbackTheme(overrides) {
113
+ if (!overrides)
114
+ return exports.defaultFeedbackTheme;
115
+ return {
116
+ ...exports.defaultFeedbackTheme,
117
+ ...overrides,
118
+ severityColors: {
119
+ ...exports.defaultFeedbackTheme.severityColors,
120
+ ...overrides.severityColors,
121
+ },
122
+ severityBackgrounds: {
123
+ ...exports.defaultFeedbackTheme.severityBackgrounds,
124
+ ...overrides.severityBackgrounds,
125
+ },
126
+ };
127
+ }
128
+ function createFeedbackCopy(overrides = {}) {
129
+ const source = overrides && typeof overrides === "object" ? overrides : {};
130
+ const labels = source.severityLabels && typeof source.severityLabels === "object" ? source.severityLabels : {};
131
+ const copy = { ...exports.defaultFeedbackCopy };
132
+ for (const key of Object.keys(exports.defaultFeedbackCopy)) {
133
+ if (key === "severityLabels" || key === "timeOptions")
134
+ continue;
135
+ const value = source[key];
136
+ if (typeof value === typeof exports.defaultFeedbackCopy[key]) {
137
+ // This keeps missing and malformed integrations from replacing safe defaults.
138
+ Object.assign(copy, { [key]: value });
139
+ }
140
+ }
141
+ const validTimeOptions = Array.isArray(source.timeOptions)
142
+ ? source.timeOptions.filter((option) => typeof option?.value === "number" && Number.isFinite(option.value) && option.value >= 0 && typeof option.label === "string")
143
+ : [];
144
+ return {
145
+ ...copy,
146
+ timeOptions: validTimeOptions.length ? validTimeOptions : exports.defaultFeedbackCopy.timeOptions,
147
+ severityLabels: {
148
+ ...exports.defaultFeedbackCopy.severityLabels,
149
+ ...Object.fromEntries(Object.keys(exports.defaultFeedbackCopy.severityLabels).filter((severity) => typeof labels[severity] === "string").map((severity) => [severity, labels[severity]])),
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,32 @@
1
+ import { type Dispatch, type SetStateAction } from "react";
2
+ import type { FeedbackGap, FeedbackMetadata, FeedbackProgressStep, FeedbackSubmission } from "./types";
3
+ export type FeedbackStep = "closed" | FeedbackProgressStep;
4
+ export declare function useFeedbackFlow(requestId: string, featureId: string, metadata?: FeedbackMetadata): FeedbackFlowController;
5
+ export interface FeedbackFlowController {
6
+ requestId: string;
7
+ featureId: string;
8
+ step: FeedbackStep;
9
+ setStep: (step: FeedbackStep) => void;
10
+ resumeStep: FeedbackStep;
11
+ rating: number | null;
12
+ setRating: (rating: number) => void;
13
+ setReviewRating: (rating: number) => void;
14
+ ratingBase: number;
15
+ selectRating: (rating: number) => void;
16
+ timeSavedMinutes: number | undefined;
17
+ setTimeSavedMinutes: (minutes: number | undefined) => void;
18
+ mostUseful: string;
19
+ setMostUseful: (text: string) => void;
20
+ usefulMoreDetail: string;
21
+ setUsefulMoreDetail: (text: string) => void;
22
+ usefulDetailShown: boolean;
23
+ setUsefulDetailShown: (shown: boolean) => void;
24
+ gaps: FeedbackGap[];
25
+ setGaps: Dispatch<SetStateAction<FeedbackGap[]>>;
26
+ completed: boolean;
27
+ setCompleted: (completed: boolean) => void;
28
+ ratingLocked: boolean;
29
+ close: () => void;
30
+ submission: FeedbackSubmission | null;
31
+ loadSavedSubmission: (submission: FeedbackSubmission) => void;
32
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFeedbackFlow = useFeedbackFlow;
4
+ const react_1 = require("react");
5
+ function useFeedbackFlow(requestId, featureId, metadata) {
6
+ const [step, setStep] = (0, react_1.useState)("closed");
7
+ const [resumeStep, setResumeStep] = (0, react_1.useState)("rating");
8
+ const [rating, setRating] = (0, react_1.useState)(null);
9
+ const [ratingBase, setRatingBase] = (0, react_1.useState)(4);
10
+ const [timeSavedMinutes, setTimeSavedMinutes] = (0, react_1.useState)();
11
+ const [mostUseful, setMostUseful] = (0, react_1.useState)("");
12
+ const [usefulMoreDetail, setUsefulMoreDetail] = (0, react_1.useState)("");
13
+ const [usefulDetailShown, setUsefulDetailShown] = (0, react_1.useState)(false);
14
+ const [gaps, setGaps] = (0, react_1.useState)([{ description: "", screenshots: [] }]);
15
+ const [completed, setCompleted] = (0, react_1.useState)(false);
16
+ const [ratingLocked, setRatingLocked] = (0, react_1.useState)(false);
17
+ const hasInteracted = (0, react_1.useRef)(false);
18
+ (0, react_1.useEffect)(() => {
19
+ hasInteracted.current = false;
20
+ setStep("closed");
21
+ setResumeStep("rating");
22
+ setRating(null);
23
+ setRatingBase(4);
24
+ setTimeSavedMinutes(undefined);
25
+ setMostUseful("");
26
+ setUsefulMoreDetail("");
27
+ setUsefulDetailShown(false);
28
+ setGaps([{ description: "", screenshots: [] }]);
29
+ setCompleted(false);
30
+ setRatingLocked(false);
31
+ }, [requestId, featureId]);
32
+ (0, react_1.useEffect)(() => {
33
+ if (step !== "closed" && step !== "success")
34
+ setResumeStep(step);
35
+ }, [step]);
36
+ const clearDraft = (0, react_1.useCallback)(() => {
37
+ setTimeSavedMinutes(undefined);
38
+ setMostUseful("");
39
+ setUsefulMoreDetail("");
40
+ setUsefulDetailShown(false);
41
+ setGaps([{ description: "", screenshots: [] }]);
42
+ setCompleted(false);
43
+ setRatingLocked(false);
44
+ }, []);
45
+ const changeRating = (0, react_1.useCallback)((value) => {
46
+ hasInteracted.current = true;
47
+ if (rating !== value)
48
+ clearDraft();
49
+ setRating(value);
50
+ }, [clearDraft, rating]);
51
+ const close = (0, react_1.useCallback)(() => {
52
+ setRatingLocked(true);
53
+ setStep("closed");
54
+ }, []);
55
+ const selectRating = (0, react_1.useCallback)((value) => {
56
+ hasInteracted.current = true;
57
+ // Keep the stars usable while their detail popup is open. Once dismissed,
58
+ // the closed card is read-only and the user resumes with Continue.
59
+ if (ratingLocked && step === "closed")
60
+ return;
61
+ changeRating(value);
62
+ setRatingBase(value);
63
+ if (value === 5) {
64
+ setResumeStep("time");
65
+ setStep("time");
66
+ }
67
+ else if (value === 3 || value === 4)
68
+ setStep("rating");
69
+ else if (value <= 2) {
70
+ setResumeStep("gap");
71
+ setStep("gap");
72
+ }
73
+ else
74
+ setStep("closed");
75
+ }, [changeRating, ratingLocked, step]);
76
+ const submission = (0, react_1.useMemo)(() => rating === null
77
+ ? null
78
+ : {
79
+ requestId,
80
+ featureId,
81
+ rating,
82
+ timeSavedMinutes,
83
+ mostUseful: mostUseful.trim() || undefined,
84
+ usefulDetailShown,
85
+ gaps: gaps.filter((gap) => gap.description.trim() || gap.screenshots.length),
86
+ metadata,
87
+ }, [featureId, gaps, metadata, mostUseful, rating, requestId, timeSavedMinutes, usefulMoreDetail, usefulDetailShown]);
88
+ const loadSavedSubmission = (0, react_1.useCallback)((saved) => {
89
+ if (hasInteracted.current || saved.requestId !== requestId || saved.featureId !== featureId)
90
+ return;
91
+ setRating(saved.rating);
92
+ setRatingBase(Math.floor(saved.rating));
93
+ setTimeSavedMinutes(saved.timeSavedMinutes);
94
+ const originalUseful = saved.mostUseful?.trim() ?? "";
95
+ const extraUseful = saved.usefulMoreDetail?.trim() ?? "";
96
+ setMostUseful(extraUseful
97
+ ? extraUseful.startsWith(originalUseful) ? extraUseful : `${originalUseful} ${extraUseful}`.trim()
98
+ : originalUseful);
99
+ setUsefulMoreDetail("");
100
+ setUsefulDetailShown(saved.usefulDetailShown ?? Boolean(saved.usefulMoreDetail));
101
+ setGaps(saved.gaps.length ? saved.gaps : [{ description: "", screenshots: [] }]);
102
+ const progress = saved.metadata?.feedbackProgress;
103
+ const isComplete = progress?.completed !== false;
104
+ setCompleted(isComplete);
105
+ setRatingLocked(isComplete);
106
+ setResumeStep(!isComplete && progress ? progress.nextStep : "rating");
107
+ setStep("closed");
108
+ }, [requestId, featureId]);
109
+ return {
110
+ requestId,
111
+ featureId,
112
+ step,
113
+ setStep,
114
+ resumeStep,
115
+ rating,
116
+ setRating: changeRating,
117
+ setReviewRating: changeRating,
118
+ ratingBase,
119
+ selectRating,
120
+ timeSavedMinutes,
121
+ setTimeSavedMinutes,
122
+ mostUseful,
123
+ setMostUseful,
124
+ usefulMoreDetail,
125
+ setUsefulMoreDetail,
126
+ usefulDetailShown,
127
+ setUsefulDetailShown,
128
+ gaps,
129
+ setGaps,
130
+ completed,
131
+ setCompleted,
132
+ ratingLocked,
133
+ close,
134
+ submission,
135
+ loadSavedSubmission,
136
+ };
137
+ }
@@ -0,0 +1,12 @@
1
+ import { type FeedbackApi } from "./feedbackApi";
2
+ import type { FeedbackFlowController } from "./useFeedbackFlow";
3
+ import type { FeedbackProgressStep, FeedbackSubmission } from "./types";
4
+ /** Loads saved feedback and saves new/reviewed answers through an interchangeable API. */
5
+ export declare function useFeedbackPersistence(flow: FeedbackFlowController, metadata: FeedbackSubmission["metadata"], onSubmit?: (submission: FeedbackSubmission) => void | Promise<void>, api?: FeedbackApi): {
6
+ saveFeedback: (submission: FeedbackSubmission) => Promise<void>;
7
+ saveProgress: (submission: FeedbackSubmission, nextStep: FeedbackProgressStep) => Promise<void>;
8
+ saveOnDismiss: (submission: FeedbackSubmission, nextStep: FeedbackProgressStep, completed: boolean) => Promise<void>;
9
+ isLoading: boolean;
10
+ isSaving: boolean;
11
+ saveError: boolean;
12
+ };