@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,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFeedbackPersistence = useFeedbackPersistence;
4
+ const react_1 = require("react");
5
+ const feedbackApi_1 = require("./feedbackApi");
6
+ function draftKey(submission) {
7
+ return JSON.stringify({
8
+ rating: submission.rating,
9
+ timeSavedMinutes: submission.timeSavedMinutes ?? null,
10
+ mostUseful: submission.mostUseful?.trim() ?? "",
11
+ usefulDetailShown: submission.usefulDetailShown ?? false,
12
+ gaps: submission.gaps.map((gap) => ({
13
+ description: gap.description.trim(),
14
+ severity: gap.severity ?? null,
15
+ detailRequested: gap.detailRequested ?? false,
16
+ screenshots: gap.screenshots.map((file) => ({
17
+ name: file.name,
18
+ type: file.type,
19
+ size: file.size,
20
+ lastModified: file.lastModified,
21
+ })),
22
+ })),
23
+ });
24
+ }
25
+ /** Loads saved feedback and saves new/reviewed answers through an interchangeable API. */
26
+ function useFeedbackPersistence(flow, metadata, onSubmit, api = feedbackApi_1.mockFeedbackApi) {
27
+ const userEmail = String(metadata?.userEmail ?? metadata?.owner ?? "");
28
+ const [isLoading, setIsLoading] = (0, react_1.useState)(true);
29
+ const [isSaving, setIsSaving] = (0, react_1.useState)(false);
30
+ const [saveError, setSaveError] = (0, react_1.useState)(false);
31
+ const savedRecordRef = (0, react_1.useRef)(null);
32
+ const loadPromiseRef = (0, react_1.useRef)(null);
33
+ const saveQueueRef = (0, react_1.useRef)(Promise.resolve());
34
+ const uploadedFilesRef = (0, react_1.useRef)(new WeakMap());
35
+ const lastQueuedDraftRef = (0, react_1.useRef)(null);
36
+ const lastSavedDraftRef = (0, react_1.useRef)(null);
37
+ (0, react_1.useEffect)(() => {
38
+ let cancelled = false;
39
+ setIsLoading(true);
40
+ savedRecordRef.current = null;
41
+ lastQueuedDraftRef.current = null;
42
+ lastSavedDraftRef.current = null;
43
+ const loading = api.getFeedback((0, feedbackApi_1.feedbackLookup)({ requestId: flow.requestId, featureId: flow.featureId, metadata }))
44
+ .then((saved) => {
45
+ if (!cancelled)
46
+ savedRecordRef.current = saved;
47
+ if (!cancelled && saved) {
48
+ const submission = (0, feedbackApi_1.fromFeedbackRecord)(saved);
49
+ lastSavedDraftRef.current = draftKey(submission);
50
+ if (lastQueuedDraftRef.current === null)
51
+ lastQueuedDraftRef.current = lastSavedDraftRef.current;
52
+ flow.loadSavedSubmission(submission);
53
+ }
54
+ })
55
+ .catch((error) => console.error("Could not load saved feedback", error))
56
+ .finally(() => { if (!cancelled)
57
+ setIsLoading(false); });
58
+ loadPromiseRef.current = loading;
59
+ return () => {
60
+ cancelled = true;
61
+ };
62
+ }, [api, flow.requestId, flow.featureId, flow.loadSavedSubmission, userEmail, metadata?.queryId, metadata?.sessionId]);
63
+ const persist = (0, react_1.useCallback)((submission, nextStep, completed, final = false) => {
64
+ const key = draftKey(submission);
65
+ lastQueuedDraftRef.current = key;
66
+ const operation = saveQueueRef.current.catch(() => undefined).then(async () => {
67
+ if (final)
68
+ setIsSaving(true);
69
+ setSaveError(false);
70
+ try {
71
+ await loadPromiseRef.current;
72
+ const gaps = await Promise.all(submission.gaps.map(async (gap) => ({
73
+ ...gap,
74
+ screenshots: await Promise.all(gap.screenshots.map(async (screenshot) => {
75
+ if ("attachmentId" in screenshot || !api.uploadAttachment)
76
+ return screenshot;
77
+ const cached = uploadedFilesRef.current.get(screenshot);
78
+ if (cached)
79
+ return cached;
80
+ const uploaded = await api.uploadAttachment(screenshot);
81
+ const reference = {
82
+ attachmentId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`,
83
+ gcsUrl: typeof uploaded === "string" ? uploaded : uploaded.gcsUrl,
84
+ ...(typeof uploaded === "string" || !uploaded.publicUrl ? {} : { publicUrl: uploaded.publicUrl }),
85
+ name: screenshot.name,
86
+ type: screenshot.type,
87
+ size: screenshot.size,
88
+ lastModified: screenshot.lastModified,
89
+ };
90
+ uploadedFilesRef.current.set(screenshot, reference);
91
+ return reference;
92
+ })),
93
+ })));
94
+ const snapshot = {
95
+ ...submission,
96
+ gaps,
97
+ metadata: { ...submission.metadata, feedbackProgress: { nextStep, completed } },
98
+ };
99
+ const saved = await api.upsertFeedback((0, feedbackApi_1.toFeedbackRecord)(snapshot, savedRecordRef.current));
100
+ savedRecordRef.current = saved;
101
+ lastSavedDraftRef.current = key;
102
+ const gapsWithIds = gaps.map((gap, index) => ({
103
+ ...gap,
104
+ negativeId: saved.gaps[index]?.negative_id ?? gap.negativeId,
105
+ }));
106
+ flow.setGaps((current) => current.map((gap, index) => gap === submission.gaps[index] ? gapsWithIds[index] ?? gap : gap));
107
+ if (final)
108
+ await onSubmit?.(snapshot);
109
+ }
110
+ catch (error) {
111
+ if (lastQueuedDraftRef.current === key)
112
+ lastQueuedDraftRef.current = lastSavedDraftRef.current;
113
+ setSaveError(true);
114
+ throw error;
115
+ }
116
+ finally {
117
+ if (final)
118
+ setIsSaving(false);
119
+ }
120
+ });
121
+ saveQueueRef.current = operation;
122
+ return operation;
123
+ }, [api, onSubmit, flow.setGaps]);
124
+ const saveFeedback = (0, react_1.useCallback)((submission) => persist(submission, "success", true, true), [persist]);
125
+ const saveProgress = (0, react_1.useCallback)((submission, nextStep) => persist(submission, nextStep, false), [persist]);
126
+ const saveOnDismiss = (0, react_1.useCallback)((submission, nextStep, completed) => lastQueuedDraftRef.current === draftKey(submission)
127
+ ? Promise.resolve()
128
+ : persist(submission, nextStep, completed), [persist]);
129
+ return { saveFeedback, saveProgress, saveOnDismiss, isLoading, isSaving, saveError };
130
+ }
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@e-llm-studio/feedback-flow",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.31",
4
+ "private": false,
5
+ "description": "Reusable configurable feedback rating and progressive feedback screens",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "sideEffects": [
13
+ "dist/feedback.css"
14
+ ],
15
+ "peerDependencies": {
16
+ "lucide-react": ">=0.474.0",
17
+ "react": ">=18.0.0 <20.0.0",
18
+ "react-dom": ">=18.0.0 <20.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.3.12",
22
+ "@types/react-dom": "^18.3.1",
23
+ "typescript": "~5.6.2"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.js"
30
+ }
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json && cp src/feedback.css dist/feedback.css",
34
+ "dev": "tsc --watch --preserveWatchOutput",
35
+ "check-types": "tsc -p tsconfig.json --noEmit"
36
+ }
6
37
  }