@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.
- package/README.md +63 -2
- package/dist/FeedbackErrorBoundary.d.ts +17 -0
- package/dist/FeedbackErrorBoundary.js +26 -0
- package/dist/FeedbackExperience.d.ts +27 -0
- package/dist/FeedbackExperience.js +54 -0
- package/dist/FeedbackModal.d.ts +27 -0
- package/dist/FeedbackModal.js +659 -0
- package/dist/FeedbackPrompt.d.ts +20 -0
- package/dist/FeedbackPrompt.js +43 -0
- package/dist/FeedbackScreenshotThumbnail.d.ts +9 -0
- package/dist/FeedbackScreenshotThumbnail.js +33 -0
- package/dist/StarRating.d.ts +17 -0
- package/dist/StarRating.js +16 -0
- package/dist/featureAvailability.d.ts +21 -0
- package/dist/featureAvailability.js +104 -0
- package/dist/feedback.css +1078 -0
- package/dist/feedbackApi.d.ts +70 -0
- package/dist/feedbackApi.js +164 -0
- package/dist/feedbackTheme.d.ts +4 -0
- package/dist/feedbackTheme.js +22 -0
- package/dist/feedbackUpload.d.ts +50 -0
- package/dist/feedbackUpload.js +190 -0
- package/dist/httpFeedbackApi.d.ts +15 -0
- package/dist/httpFeedbackApi.js +390 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +31 -0
- package/dist/tracking.d.ts +3 -0
- package/dist/tracking.js +22 -0
- package/dist/types.d.ts +276 -0
- package/dist/types.js +152 -0
- package/dist/useFeedbackFlow.d.ts +32 -0
- package/dist/useFeedbackFlow.js +137 -0
- package/dist/useFeedbackPersistence.d.ts +12 -0
- package/dist/useFeedbackPersistence.js +130 -0
- package/package.json +34 -3
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHttpFeedbackApi = createHttpFeedbackApi;
|
|
4
|
+
const PATH = "/segment_click_analytics/v1/feedback";
|
|
5
|
+
const UUID4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
const SERVER_IDS_KEY = "__feedback_api_server_nested_ids";
|
|
7
|
+
const memoryRecords = new Map();
|
|
8
|
+
const readCache = new Map();
|
|
9
|
+
const READ_CACHE_MS = 30000;
|
|
10
|
+
function storageKey(baseUrl, lookup) {
|
|
11
|
+
return `@e-llm-studio/feedback-flow/http/v1:${baseUrl}:${lookup.query_id}:${lookup.feature_id}:${lookup.user_email ?? ""}`;
|
|
12
|
+
}
|
|
13
|
+
function readSaved(baseUrl, lookup) {
|
|
14
|
+
const key = storageKey(baseUrl, lookup);
|
|
15
|
+
try {
|
|
16
|
+
const stored = globalThis.localStorage?.getItem(key);
|
|
17
|
+
if (stored)
|
|
18
|
+
return JSON.parse(stored);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* private browsing uses memory */
|
|
22
|
+
}
|
|
23
|
+
return memoryRecords.get(key) ?? null;
|
|
24
|
+
}
|
|
25
|
+
function writeSaved(baseUrl, record) {
|
|
26
|
+
const form = record.feedback_form;
|
|
27
|
+
const key = storageKey(baseUrl, { query_id: form.query_id, feature_id: form.feature_id, user_email: form.user_email });
|
|
28
|
+
memoryRecords.set(key, record);
|
|
29
|
+
try {
|
|
30
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(record));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* private browsing uses memory */
|
|
34
|
+
}
|
|
35
|
+
return record;
|
|
36
|
+
}
|
|
37
|
+
function asObject(value) {
|
|
38
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
39
|
+
}
|
|
40
|
+
function remoteRecord(value, lookup) {
|
|
41
|
+
const data = asObject(value);
|
|
42
|
+
const metadata = asObject(data.form_metadata ?? data.metadata);
|
|
43
|
+
const feedbackId = String(data.feedback_id);
|
|
44
|
+
const createdAt = typeof data.created_at === "string" ? data.created_at : new Date().toISOString();
|
|
45
|
+
const positives = Array.isArray(data.positive_feedback) ? data.positive_feedback : [];
|
|
46
|
+
const negatives = Array.isArray(data.negative_feedback) ? data.negative_feedback : [];
|
|
47
|
+
const positiveIds = positives.map((item) => String(asObject(item).positive_id ?? ""));
|
|
48
|
+
const negativeIds = negatives.map((item) => String(asObject(item).negative_id ?? ""));
|
|
49
|
+
const attachments = [];
|
|
50
|
+
const collectAttachments = (item, parentId, kind) => {
|
|
51
|
+
const entry = asObject(item);
|
|
52
|
+
for (const raw of Array.isArray(entry.attachments) ? entry.attachments : []) {
|
|
53
|
+
const attachment = asObject(raw);
|
|
54
|
+
if (typeof attachment.gcs_url !== "string")
|
|
55
|
+
continue;
|
|
56
|
+
const attachmentMetadata = asObject(attachment.metadata);
|
|
57
|
+
attachments.push({
|
|
58
|
+
attachment_id: String(attachment.attachment_id ?? attachment.gcs_url),
|
|
59
|
+
[kind]: parentId,
|
|
60
|
+
gcs_url: attachment.gcs_url,
|
|
61
|
+
metadata: {
|
|
62
|
+
...attachmentMetadata,
|
|
63
|
+
file_name: String(attachmentMetadata.file_name ?? attachment.gcs_url.split("/").pop() ?? "Screenshot"),
|
|
64
|
+
},
|
|
65
|
+
created_at: typeof attachment.created_at === "string" ? attachment.created_at : createdAt,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
positives.forEach((item, index) => collectAttachments(item, positiveIds[index], "positive_id"));
|
|
70
|
+
negatives.forEach((item, index) => collectAttachments(item, negativeIds[index], "negative_id"));
|
|
71
|
+
return {
|
|
72
|
+
feedback_form: {
|
|
73
|
+
feedback_id: feedbackId,
|
|
74
|
+
query_id: lookup.query_id,
|
|
75
|
+
feature_id: lookup.feature_id,
|
|
76
|
+
user_email: lookup.user_email ?? "",
|
|
77
|
+
rating: Number(data.rating),
|
|
78
|
+
trace_id: typeof data.trace_id === "string" ? data.trace_id : undefined,
|
|
79
|
+
session_id: typeof data.session_id === "string" ? data.session_id : lookup.session_id,
|
|
80
|
+
request_id: typeof data.request_id === "string" ? data.request_id : lookup.request_id,
|
|
81
|
+
metadata: {
|
|
82
|
+
...metadata,
|
|
83
|
+
userEmail: lookup.user_email ?? data.user_email ?? metadata.userEmail,
|
|
84
|
+
...(lookup.is_ai_query ? { queryId: lookup.query_id } : {}),
|
|
85
|
+
sessionId: typeof data.session_id === "string" ? data.session_id : lookup.session_id ?? metadata.sessionId,
|
|
86
|
+
requestId: typeof data.request_id === "string" ? data.request_id : lookup.request_id ?? metadata.requestId,
|
|
87
|
+
source_request_id: lookup.source_request_id ?? metadata.source_request_id,
|
|
88
|
+
[SERVER_IDS_KEY]: { positive: positiveIds, negative: negativeIds },
|
|
89
|
+
},
|
|
90
|
+
created_at: createdAt,
|
|
91
|
+
},
|
|
92
|
+
positive_feedback: positives.map((raw, index) => {
|
|
93
|
+
const item = asObject(raw);
|
|
94
|
+
return {
|
|
95
|
+
feedback_id: feedbackId,
|
|
96
|
+
positive_id: positiveIds[index],
|
|
97
|
+
description: String(item.description ?? ""),
|
|
98
|
+
time_saved_mins: typeof item.time_saved_mins === "number" ? item.time_saved_mins : undefined,
|
|
99
|
+
metadata: asObject(item.metadata),
|
|
100
|
+
created_at: typeof item.created_at === "string" ? item.created_at : createdAt,
|
|
101
|
+
};
|
|
102
|
+
}),
|
|
103
|
+
gaps: negatives.map((raw, index) => {
|
|
104
|
+
const item = asObject(raw);
|
|
105
|
+
return {
|
|
106
|
+
feedback_id: feedbackId,
|
|
107
|
+
negative_id: negativeIds[index],
|
|
108
|
+
description: String(item.description ?? ""),
|
|
109
|
+
severity: typeof item.severity === "string" ? item.severity.toLowerCase() : undefined,
|
|
110
|
+
metadata: asObject(item.metadata),
|
|
111
|
+
created_at: typeof item.created_at === "string" ? item.created_at : createdAt,
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
feedback_attachment: attachments,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Preserve an identity across reloads when a host's response ID is not a UUID.
|
|
118
|
+
function stableFeatureUuid(value) {
|
|
119
|
+
const hashes = [2166136261, 2166136261 ^ 0x9e3779b9, 2166136261 ^ 0x85ebca6b, 2166136261 ^ 0xc2b2ae35];
|
|
120
|
+
for (const char of value)
|
|
121
|
+
for (let i = 0; i < hashes.length; i += 1) {
|
|
122
|
+
hashes[i] = Math.imul(hashes[i] ^ char.charCodeAt(0), 16777619);
|
|
123
|
+
}
|
|
124
|
+
const bytes = hashes.flatMap((hash) => [hash >>> 24, hash >>> 16, hash >>> 8, hash].map((part) => part & 255));
|
|
125
|
+
bytes[6] = (bytes[6] & 15) | 64;
|
|
126
|
+
bytes[8] = (bytes[8] & 63) | 128;
|
|
127
|
+
const hex = bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
128
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
129
|
+
}
|
|
130
|
+
function feedbackIdentity(queryId, featureId, isAiQuery) {
|
|
131
|
+
if (isAiQuery)
|
|
132
|
+
return { query_id: queryId };
|
|
133
|
+
if (UUID4.test(featureId))
|
|
134
|
+
return { feature_id: featureId };
|
|
135
|
+
return { feature_id: stableFeatureUuid(`${featureId}:${queryId}`) };
|
|
136
|
+
}
|
|
137
|
+
function requestPayload(record, update) {
|
|
138
|
+
const form = record.feedback_form;
|
|
139
|
+
const { [SERVER_IDS_KEY]: serverIdsValue, requestId: _requestId, queryId: _queryId, sessionId: _sessionId, source_request_id: _sourceRequestId, ...publicMetadata } = form.metadata;
|
|
140
|
+
const serverIds = (serverIdsValue && typeof serverIdsValue === "object" ? serverIdsValue : {});
|
|
141
|
+
if (form.session_id && !form.metadata.queryId?.trim()) {
|
|
142
|
+
throw new Error("Chat feedback requires a query_id before it can be saved");
|
|
143
|
+
}
|
|
144
|
+
const identity = feedbackIdentity(form.query_id, form.feature_id, Boolean(form.session_id));
|
|
145
|
+
return {
|
|
146
|
+
...identity,
|
|
147
|
+
...(!update ? { user_email: form.user_email } : {}),
|
|
148
|
+
...(form.session_id ? { session_id: form.session_id } : {}),
|
|
149
|
+
...(form.request_id ? { request_id: form.request_id } : {}),
|
|
150
|
+
rating: form.rating,
|
|
151
|
+
trace_id: form.trace_id,
|
|
152
|
+
metadata: {
|
|
153
|
+
...publicMetadata,
|
|
154
|
+
...(form.session_id ? { source_query_id: form.query_id } : {}),
|
|
155
|
+
source_feature_id: form.feature_id,
|
|
156
|
+
},
|
|
157
|
+
positive_feedback: record.positive_feedback.map((item) => ({
|
|
158
|
+
...(update && serverIds.positive?.includes(item.positive_id) ? { positive_id: item.positive_id } : {}),
|
|
159
|
+
description: item.description,
|
|
160
|
+
time_saved_mins: item.time_saved_mins,
|
|
161
|
+
metadata: item.metadata,
|
|
162
|
+
...(!update ? { attachments: [] } : {}),
|
|
163
|
+
})),
|
|
164
|
+
negative_feedback: record.gaps.map((gap) => ({
|
|
165
|
+
...(update && serverIds.negative?.includes(gap.negative_id) ? { negative_id: gap.negative_id } : {}),
|
|
166
|
+
description: gap.description,
|
|
167
|
+
severity: gap.severity ? gap.severity[0].toUpperCase() + gap.severity.slice(1) : undefined,
|
|
168
|
+
metadata: gap.metadata,
|
|
169
|
+
attachments: record.feedback_attachment
|
|
170
|
+
.filter((attachment) => attachment.negative_id === gap.negative_id)
|
|
171
|
+
.map((attachment) => ({ gcs_url: attachment.gcs_url, metadata: attachment.metadata })),
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function feedbackId(value) {
|
|
176
|
+
if (!value || typeof value !== "object")
|
|
177
|
+
return undefined;
|
|
178
|
+
const data = value;
|
|
179
|
+
if (typeof data.feedback_id === "string")
|
|
180
|
+
return data.feedback_id;
|
|
181
|
+
return feedbackId(data.data ?? data.responseData);
|
|
182
|
+
}
|
|
183
|
+
function nestedServerIds(value) {
|
|
184
|
+
if (!value || typeof value !== "object")
|
|
185
|
+
return {};
|
|
186
|
+
const data = value;
|
|
187
|
+
if (data.data || data.responseData)
|
|
188
|
+
return nestedServerIds(data.data ?? data.responseData);
|
|
189
|
+
const ids = (key, id) => Array.isArray(data[key])
|
|
190
|
+
? data[key]
|
|
191
|
+
.map((item) => (item && typeof item === "object" ? item[id] : undefined))
|
|
192
|
+
.map((item) => (typeof item === "string" && UUID4.test(item) ? item : ""))
|
|
193
|
+
: undefined;
|
|
194
|
+
return { positive: ids("positive_feedback", "positive_id"), negative: ids("negative_feedback", "negative_id") };
|
|
195
|
+
}
|
|
196
|
+
/** GET restores saved feedback; POST creates and PUT updates it. */
|
|
197
|
+
function createHttpFeedbackApi(options) {
|
|
198
|
+
const fetcher = options.fetcher ?? fetch;
|
|
199
|
+
const endpoint = `${options.baseUrl.replace(/\/$/, "")}${PATH}`;
|
|
200
|
+
const readEndpoint = `${(options.readBaseUrl ?? options.baseUrl).replace(/\/$/, "")}${PATH}`;
|
|
201
|
+
const authHeaders = () => {
|
|
202
|
+
const token = options.getAuthToken?.();
|
|
203
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
204
|
+
};
|
|
205
|
+
const fetchSavedById = async (form) => {
|
|
206
|
+
const url = new URL(readEndpoint);
|
|
207
|
+
url.searchParams.set("user_email", form.user_email);
|
|
208
|
+
if (form.session_id)
|
|
209
|
+
url.searchParams.set("session_id", form.session_id);
|
|
210
|
+
else {
|
|
211
|
+
const identity = feedbackIdentity(form.query_id, form.feature_id, false);
|
|
212
|
+
url.searchParams.set("feature_id", "feature_id" in identity ? identity.feature_id : form.feature_id);
|
|
213
|
+
}
|
|
214
|
+
const response = await fetcher(url.toString(), { headers: { accept: "application/json", ...authHeaders() } });
|
|
215
|
+
if (!response.ok)
|
|
216
|
+
throw new Error(`Feedback identity lookup failed (${response.status})`);
|
|
217
|
+
const body = asObject(await response.json());
|
|
218
|
+
const row = (Array.isArray(body.data) ? body.data : []).find((item) => asObject(item).feedback_id === form.feedback_id);
|
|
219
|
+
return row ? remoteRecord(row, {
|
|
220
|
+
query_id: form.query_id,
|
|
221
|
+
feature_id: form.feature_id,
|
|
222
|
+
user_email: form.user_email,
|
|
223
|
+
session_id: form.session_id,
|
|
224
|
+
request_id: form.request_id,
|
|
225
|
+
source_request_id: form.metadata.source_request_id,
|
|
226
|
+
is_ai_query: Boolean(form.session_id),
|
|
227
|
+
}) : null;
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
uploadAttachment: options.uploadAttachment,
|
|
231
|
+
getAttachmentPreviewUrl: options.getAttachmentPreviewUrl,
|
|
232
|
+
async getFeedback(lookup) {
|
|
233
|
+
if (!lookup.user_email || (!lookup.feature_id && !lookup.session_id))
|
|
234
|
+
return readSaved(options.baseUrl, lookup);
|
|
235
|
+
const url = new URL(readEndpoint);
|
|
236
|
+
url.searchParams.set("user_email", lookup.user_email);
|
|
237
|
+
const isAiQuery = Boolean(lookup.session_id);
|
|
238
|
+
const identity = feedbackIdentity(lookup.query_id, lookup.feature_id, isAiQuery);
|
|
239
|
+
if (lookup.session_id)
|
|
240
|
+
url.searchParams.set("session_id", lookup.session_id);
|
|
241
|
+
else
|
|
242
|
+
url.searchParams.set("feature_id", "feature_id" in identity ? identity.feature_id
|
|
243
|
+
: UUID4.test(lookup.feature_id) ? lookup.feature_id : stableFeatureUuid(`${lookup.feature_id}:${lookup.query_id}`));
|
|
244
|
+
const cacheKey = `${url.toString()}::${options.getAuthToken?.() ?? ""}`;
|
|
245
|
+
try {
|
|
246
|
+
let cached = readCache.get(cacheKey);
|
|
247
|
+
if (!cached || cached.expiresAt <= Date.now()) {
|
|
248
|
+
const promise = fetcher(url.toString(), { headers: { accept: "application/json", ...authHeaders() } })
|
|
249
|
+
.then(async (response) => {
|
|
250
|
+
if (!response.ok)
|
|
251
|
+
throw new Error(`Feedback lookup failed (${response.status})`);
|
|
252
|
+
const body = asObject(await response.json());
|
|
253
|
+
return Array.isArray(body.data) ? body.data : [];
|
|
254
|
+
});
|
|
255
|
+
cached = { expiresAt: Date.now() + READ_CACHE_MS, promise };
|
|
256
|
+
readCache.set(cacheKey, cached);
|
|
257
|
+
void promise.catch(() => { if (readCache.get(cacheKey)?.promise === promise)
|
|
258
|
+
readCache.delete(cacheKey); });
|
|
259
|
+
}
|
|
260
|
+
const rows = await cached.promise;
|
|
261
|
+
const match = rows.find((raw) => {
|
|
262
|
+
const item = asObject(raw);
|
|
263
|
+
const metadata = asObject(item.form_metadata ?? item.metadata);
|
|
264
|
+
const legacyMatch = !item.query_id && metadata.source_query_id === lookup.source_request_id;
|
|
265
|
+
if (item.user_email !== lookup.user_email)
|
|
266
|
+
return false;
|
|
267
|
+
if (isAiQuery && lookup.session_id) {
|
|
268
|
+
const sessionMatches = item.session_id === lookup.session_id || metadata.sessionId === lookup.session_id;
|
|
269
|
+
const queryMatches = Boolean(lookup.query_id) && (item.query_id === lookup.query_id || metadata.source_query_id === lookup.query_id || legacyMatch);
|
|
270
|
+
return sessionMatches && queryMatches;
|
|
271
|
+
}
|
|
272
|
+
const featureId = "feature_id" in identity ? identity.feature_id : lookup.feature_id;
|
|
273
|
+
const featureMatches = item.feature_id === featureId || metadata.source_feature_id === lookup.feature_id;
|
|
274
|
+
const requestMatches = !lookup.request_id || !item.request_id || item.request_id === lookup.request_id;
|
|
275
|
+
return featureMatches && requestMatches;
|
|
276
|
+
});
|
|
277
|
+
return match ? writeSaved(options.baseUrl, remoteRecord(match, { ...lookup, is_ai_query: isAiQuery })) : null;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return readSaved(options.baseUrl, lookup);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
async upsertFeedback(record) {
|
|
284
|
+
const form = record.feedback_form;
|
|
285
|
+
let previous = readSaved(options.baseUrl, { query_id: form.query_id, feature_id: form.feature_id, user_email: form.user_email });
|
|
286
|
+
const savedId = previous?.feedback_form.feedback_id;
|
|
287
|
+
const update = Boolean(savedId && UUID4.test(savedId));
|
|
288
|
+
const knownIds = asObject(previous?.feedback_form.metadata[SERVER_IDS_KEY]);
|
|
289
|
+
const knownPositive = Array.isArray(knownIds.positive) ? knownIds.positive : [];
|
|
290
|
+
const knownNegative = Array.isArray(knownIds.negative) ? knownIds.negative : [];
|
|
291
|
+
if (update && previous &&
|
|
292
|
+
(previous.positive_feedback.some((item) => !knownPositive.includes(item.positive_id)) ||
|
|
293
|
+
previous.gaps.some((item) => !knownNegative.includes(item.negative_id)))) {
|
|
294
|
+
const hydrated = await fetchSavedById(previous.feedback_form);
|
|
295
|
+
if (!hydrated ||
|
|
296
|
+
hydrated.positive_feedback.length < previous.positive_feedback.length ||
|
|
297
|
+
hydrated.gaps.length < previous.gaps.length)
|
|
298
|
+
throw new Error("Cannot safely update feedback without its saved answer IDs");
|
|
299
|
+
previous = writeSaved(options.baseUrl, hydrated);
|
|
300
|
+
}
|
|
301
|
+
const recordWithServerIds = previous?.feedback_form.metadata[SERVER_IDS_KEY]
|
|
302
|
+
? {
|
|
303
|
+
...record,
|
|
304
|
+
positive_feedback: record.positive_feedback.map((item, index) => ({
|
|
305
|
+
...item,
|
|
306
|
+
positive_id: previous?.positive_feedback[index]?.positive_id ?? item.positive_id,
|
|
307
|
+
})),
|
|
308
|
+
gaps: record.gaps.map((gap, index) => ({
|
|
309
|
+
...gap,
|
|
310
|
+
negative_id: previous?.gaps.find((item) => item.negative_id === gap.negative_id)?.negative_id ??
|
|
311
|
+
(record.gaps.some((other, otherIndex) => otherIndex !== index &&
|
|
312
|
+
other.negative_id === previous?.gaps[index]?.negative_id)
|
|
313
|
+
? undefined : previous?.gaps[index]?.negative_id) ?? gap.negative_id,
|
|
314
|
+
})),
|
|
315
|
+
feedback_form: {
|
|
316
|
+
...form,
|
|
317
|
+
metadata: { ...form.metadata, [SERVER_IDS_KEY]: previous.feedback_form.metadata[SERVER_IDS_KEY] },
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
: record;
|
|
321
|
+
const response = await fetcher(update ? `${endpoint}/${savedId}` : endpoint, {
|
|
322
|
+
method: update ? "PUT" : "POST",
|
|
323
|
+
headers: { accept: "application/json", "Content-Type": "application/json", ...authHeaders() },
|
|
324
|
+
body: JSON.stringify(requestPayload(recordWithServerIds, update)),
|
|
325
|
+
});
|
|
326
|
+
if (!response.ok)
|
|
327
|
+
throw new Error(`Feedback ${update ? "update" : "submission"} failed (${response.status})`);
|
|
328
|
+
const body = await response.json();
|
|
329
|
+
readCache.clear();
|
|
330
|
+
const serverId = feedbackId(body);
|
|
331
|
+
if (!update && !serverId)
|
|
332
|
+
throw new Error("Feedback saved, but the API did not return feedback_id for future updates");
|
|
333
|
+
// Keep the parent ID even if the nested IDs require a follow-up read.
|
|
334
|
+
// A retry must PUT this form, never create another feedback form.
|
|
335
|
+
writeSaved(options.baseUrl, {
|
|
336
|
+
...recordWithServerIds,
|
|
337
|
+
feedback_form: {
|
|
338
|
+
...recordWithServerIds.feedback_form,
|
|
339
|
+
feedback_id: serverId ?? savedId ?? form.feedback_id,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
const returnedNestedIds = nestedServerIds(body);
|
|
343
|
+
const serverForm = { ...form, feedback_id: serverId ?? savedId ?? form.feedback_id };
|
|
344
|
+
const previousIds = asObject(previous?.feedback_form.metadata[SERVER_IDS_KEY]);
|
|
345
|
+
const knownPositiveIds = Array.isArray(previousIds.positive) ? previousIds.positive : [];
|
|
346
|
+
const knownNegativeIds = Array.isArray(previousIds.negative) ? previousIds.negative : [];
|
|
347
|
+
const needPositiveIds = recordWithServerIds.positive_feedback.some((item) => !knownPositiveIds.includes(item.positive_id)) &&
|
|
348
|
+
!returnedNestedIds.positive?.some(Boolean);
|
|
349
|
+
const needNegativeIds = recordWithServerIds.gaps.some((gap) => !knownNegativeIds.includes(gap.negative_id)) &&
|
|
350
|
+
!returnedNestedIds.negative?.some(Boolean);
|
|
351
|
+
const fetched = (needPositiveIds || needNegativeIds) ? await fetchSavedById(serverForm).catch(() => null) : null;
|
|
352
|
+
if ((needPositiveIds && !fetched?.positive_feedback.length && !asObject(previous?.feedback_form.metadata[SERVER_IDS_KEY]).positive) ||
|
|
353
|
+
(needNegativeIds && !fetched?.gaps.length && !asObject(previous?.feedback_form.metadata[SERVER_IDS_KEY]).negative)) {
|
|
354
|
+
throw new Error("Feedback saved, but its answer IDs could not be loaded safely; please retry after refreshing");
|
|
355
|
+
}
|
|
356
|
+
const nestedIds = {
|
|
357
|
+
positive: [...new Set([...knownPositiveIds, ...(fetched?.positive_feedback.map((item) => item.positive_id) ?? []), ...(returnedNestedIds.positive ?? [])].filter(Boolean))],
|
|
358
|
+
negative: [...new Set([...knownNegativeIds, ...(fetched?.gaps.map((item) => item.negative_id) ?? []), ...(returnedNestedIds.negative ?? [])].filter(Boolean))],
|
|
359
|
+
};
|
|
360
|
+
const resolvedPositive = recordWithServerIds.positive_feedback.map((item, index) => ({
|
|
361
|
+
...item,
|
|
362
|
+
positive_id: knownPositiveIds.includes(item.positive_id) ? item.positive_id
|
|
363
|
+
: returnedNestedIds.positive?.length === record.positive_feedback.length && returnedNestedIds.positive[index]
|
|
364
|
+
? returnedNestedIds.positive[index]
|
|
365
|
+
: fetched?.positive_feedback.find((saved) => saved.description === item.description)?.positive_id ?? item.positive_id,
|
|
366
|
+
}));
|
|
367
|
+
const resolvedGaps = recordWithServerIds.gaps.map((gap, index) => ({
|
|
368
|
+
...gap,
|
|
369
|
+
negative_id: knownNegativeIds.includes(gap.negative_id) ? gap.negative_id
|
|
370
|
+
: returnedNestedIds.negative?.length === record.gaps.length && returnedNestedIds.negative[index]
|
|
371
|
+
? returnedNestedIds.negative[index]
|
|
372
|
+
: fetched?.gaps.find((saved) => saved.description === gap.description && !knownNegativeIds.includes(saved.negative_id))?.negative_id ?? gap.negative_id,
|
|
373
|
+
}));
|
|
374
|
+
const saved = writeSaved(options.baseUrl, {
|
|
375
|
+
...record,
|
|
376
|
+
positive_feedback: resolvedPositive,
|
|
377
|
+
gaps: resolvedGaps,
|
|
378
|
+
feedback_form: {
|
|
379
|
+
...form,
|
|
380
|
+
feedback_id: serverId ?? savedId ?? form.feedback_id,
|
|
381
|
+
metadata: { ...form.metadata, [SERVER_IDS_KEY]: nestedIds },
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
if (resolvedPositive.some((item) => !nestedIds.positive.includes(item.positive_id)) ||
|
|
385
|
+
resolvedGaps.some((item) => !nestedIds.negative.includes(item.negative_id)))
|
|
386
|
+
throw new Error("Feedback saved, but its answer IDs are not available yet; please retry shortly");
|
|
387
|
+
return saved;
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "./feedback.css";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./tracking";
|
|
4
|
+
export * from "./feedbackApi";
|
|
5
|
+
export * from "./httpFeedbackApi";
|
|
6
|
+
export * from "./feedbackUpload";
|
|
7
|
+
export * from "./useFeedbackPersistence";
|
|
8
|
+
export * from "./feedbackTheme";
|
|
9
|
+
export * from "./featureAvailability";
|
|
10
|
+
export * from "./FeedbackErrorBoundary";
|
|
11
|
+
export * from "./useFeedbackFlow";
|
|
12
|
+
export * from "./FeedbackPrompt";
|
|
13
|
+
export * from "./FeedbackModal";
|
|
14
|
+
export * from "./FeedbackExperience";
|
|
15
|
+
export * from "./StarRating";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
require("./feedback.css");
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
__exportStar(require("./tracking"), exports);
|
|
20
|
+
__exportStar(require("./feedbackApi"), exports);
|
|
21
|
+
__exportStar(require("./httpFeedbackApi"), exports);
|
|
22
|
+
__exportStar(require("./feedbackUpload"), exports);
|
|
23
|
+
__exportStar(require("./useFeedbackPersistence"), exports);
|
|
24
|
+
__exportStar(require("./feedbackTheme"), exports);
|
|
25
|
+
__exportStar(require("./featureAvailability"), exports);
|
|
26
|
+
__exportStar(require("./FeedbackErrorBoundary"), exports);
|
|
27
|
+
__exportStar(require("./useFeedbackFlow"), exports);
|
|
28
|
+
__exportStar(require("./FeedbackPrompt"), exports);
|
|
29
|
+
__exportStar(require("./FeedbackModal"), exports);
|
|
30
|
+
__exportStar(require("./FeedbackExperience"), exports);
|
|
31
|
+
__exportStar(require("./StarRating"), exports);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FeedbackMetadata, FeedbackTrackingContext } from "./types";
|
|
2
|
+
/** Merge mode tracking with legacy metadata. Explicit tracking fields take precedence. */
|
|
3
|
+
export declare function createFeedbackMetadata(tracking?: FeedbackTrackingContext, metadata?: FeedbackMetadata): FeedbackMetadata | undefined;
|
package/dist/tracking.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFeedbackMetadata = createFeedbackMetadata;
|
|
4
|
+
/** Merge mode tracking with legacy metadata. Explicit tracking fields take precedence. */
|
|
5
|
+
function createFeedbackMetadata(tracking, metadata) {
|
|
6
|
+
if (!tracking && !metadata)
|
|
7
|
+
return undefined;
|
|
8
|
+
const result = Object.fromEntries(Object.entries({ ...metadata, ...tracking?.custom }).filter(([, value]) => value !== undefined));
|
|
9
|
+
if (tracking?.mode !== undefined)
|
|
10
|
+
result.mode = tracking.mode;
|
|
11
|
+
if (tracking?.modeId !== undefined)
|
|
12
|
+
result.modeId = tracking.modeId;
|
|
13
|
+
if (tracking?.owner !== undefined)
|
|
14
|
+
result.owner = tracking.owner;
|
|
15
|
+
if (tracking?.featureContext !== undefined)
|
|
16
|
+
result.featureContext = tracking.featureContext;
|
|
17
|
+
if (tracking?.userEmail !== undefined)
|
|
18
|
+
result.userEmail = tracking.userEmail;
|
|
19
|
+
if (tracking?.traceId !== undefined)
|
|
20
|
+
result.traceId = tracking.traceId;
|
|
21
|
+
return Object.keys(result).length ? result : undefined;
|
|
22
|
+
}
|