makethisbetter 1.0.0 → 1.1.2
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/LICENSE +1 -1
- package/README.md +329 -62
- package/dist/annotate/session.d.ts +5 -0
- package/dist/annotate/session.d.ts.map +1 -1
- package/dist/api/client.d.ts +9 -3
- package/dist/api/client.d.ts.map +1 -1
- package/dist/context/error-source.d.ts +1 -0
- package/dist/context/error-source.d.ts.map +1 -1
- package/dist/context/frustration.d.ts +25 -1
- package/dist/context/frustration.d.ts.map +1 -1
- package/dist/i18n/de.d.ts +54 -0
- package/dist/i18n/de.d.ts.map +1 -0
- package/dist/i18n/en.d.ts +7 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/es.d.ts +54 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/fr.d.ts +54 -0
- package/dist/i18n/fr.d.ts.map +1 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/ja.d.ts +54 -0
- package/dist/i18n/ja.d.ts.map +1 -0
- package/dist/i18n/ko.d.ts +54 -0
- package/dist/i18n/ko.d.ts.map +1 -0
- package/dist/i18n/zh-CN.d.ts +7 -0
- package/dist/i18n/zh-CN.d.ts.map +1 -1
- package/dist/makethisbetter.cjs.js +42 -27
- package/dist/makethisbetter.cjs.js.map +1 -1
- package/dist/makethisbetter.esm.js +1208 -636
- package/dist/makethisbetter.esm.js.map +1 -1
- package/dist/makethisbetter.js +42 -27
- package/dist/makethisbetter.js.map +1 -1
- package/dist/popup/clarify.d.ts +37 -0
- package/dist/popup/clarify.d.ts.map +1 -0
- package/dist/popup/clarify.test.d.ts +2 -0
- package/dist/popup/clarify.test.d.ts.map +1 -0
- package/dist/popup/success.d.ts +1 -1
- package/dist/popup/success.d.ts.map +1 -1
- package/dist/record/session.d.ts.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/widget/controller.d.ts +3 -6
- package/dist/widget/controller.d.ts.map +1 -1
- package/dist/widget/recording-manager.d.ts +15 -0
- package/dist/widget/recording-manager.d.ts.map +1 -0
- package/dist/widget/recording-manager.test.d.ts +2 -0
- package/dist/widget/recording-manager.test.d.ts.map +1 -0
- package/dist/widget/tab.d.ts +1 -1
- package/dist/widget/tab.d.ts.map +1 -1
- package/package.json +13 -3
- package/src/annotate/session.test.ts +117 -5
- package/src/annotate/session.ts +27 -8
- package/src/api/client.test.ts +144 -4
- package/src/api/client.ts +56 -5
- package/src/context/error-source.test.ts +13 -0
- package/src/context/error-source.ts +9 -2
- package/src/context/frustration.test.ts +336 -1
- package/src/context/frustration.ts +237 -1
- package/src/i18n/de.ts +54 -0
- package/src/i18n/en.ts +7 -0
- package/src/i18n/es.ts +54 -0
- package/src/i18n/fr.ts +54 -0
- package/src/i18n/index.test.ts +1 -1
- package/src/i18n/index.ts +10 -0
- package/src/i18n/ja.ts +54 -0
- package/src/i18n/ko.ts +54 -0
- package/src/i18n/zh-CN.ts +7 -0
- package/src/popup/clarify.test.ts +419 -0
- package/src/popup/clarify.ts +215 -0
- package/src/popup/success.ts +11 -1
- package/src/record/session.ts +8 -1
- package/src/styles/widget.css +218 -0
- package/src/types.ts +20 -0
- package/src/widget/controller.test.ts +6 -4
- package/src/widget/controller.ts +59 -52
- package/src/widget/recording-manager.test.ts +136 -0
- package/src/widget/recording-manager.ts +51 -0
- package/src/widget/tab.ts +4 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ShadowContainer } from '../widget/shadow';
|
|
2
|
+
import type { I18nMessages } from '../i18n';
|
|
3
|
+
import type { ApiClient } from '../api/client';
|
|
4
|
+
interface ClarifyCardOptions {
|
|
5
|
+
feedbackId: string;
|
|
6
|
+
apiClient: ApiClient;
|
|
7
|
+
messages: I18nMessages;
|
|
8
|
+
onDone: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare class ClarifyCard {
|
|
11
|
+
private el;
|
|
12
|
+
private messagesEl;
|
|
13
|
+
private inputEl;
|
|
14
|
+
private sendBtn;
|
|
15
|
+
private pollTimer;
|
|
16
|
+
private doneTimer;
|
|
17
|
+
private feedbackId;
|
|
18
|
+
private apiClient;
|
|
19
|
+
private onDone;
|
|
20
|
+
private sending;
|
|
21
|
+
private destroyed;
|
|
22
|
+
constructor(shadow: ShadowContainer, opts: ClarifyCardOptions);
|
|
23
|
+
private buildHTML;
|
|
24
|
+
private bindEvents;
|
|
25
|
+
private startConversation;
|
|
26
|
+
private sendMessage;
|
|
27
|
+
private startPolling;
|
|
28
|
+
private stopPolling;
|
|
29
|
+
private poll;
|
|
30
|
+
private handleDone;
|
|
31
|
+
private renderMessages;
|
|
32
|
+
private appendMessage;
|
|
33
|
+
private showThinking;
|
|
34
|
+
destroy(): void;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=clarify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clarify.d.ts","sourceRoot":"","sources":["../../src/popup/clarify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAO9C,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,SAAS,CAAA;IACpB,QAAQ,EAAE,YAAY,CAAA;IACtB,MAAM,EAAE,MAAM,IAAI,CAAA;CACnB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,SAAS,CAAW;IAC5B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,SAAS,CAAQ;gBAEb,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,kBAAkB;IAmB7D,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,UAAU;YAqBJ,iBAAiB;YAiBjB,WAAW;IAiCzB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,WAAW;YAOL,IAAI;IAelB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,YAAY;IAQpB,OAAO,IAAI,IAAI;CAShB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clarify.test.d.ts","sourceRoot":"","sources":["../../src/popup/clarify.test.ts"],"names":[],"mappings":""}
|
package/dist/popup/success.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ShadowContainer } from '../widget/shadow';
|
|
|
2
2
|
import type { I18nMessages } from '../i18n';
|
|
3
3
|
export declare class SuccessCard {
|
|
4
4
|
private el;
|
|
5
|
-
constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void);
|
|
5
|
+
constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void);
|
|
6
6
|
destroy(): void;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=success.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"success.d.ts","sourceRoot":"","sources":["../../src/popup/success.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAK3C,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAgB;gBAEd,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI;
|
|
1
|
+
{"version":3,"file":"success.d.ts","sourceRoot":"","sources":["../../src/popup/success.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAK3C,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAgB;gBAEd,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI;IA8B7G,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/record/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAQzC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/record/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAQzC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAyCD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,YAAY,CAAC,CAAY;IACjC,OAAO,CAAC,OAAO,CAAQ;gBAEX,MAAM,EAAE,eAAe,EAAE,YAAY,CAAC,EAAE,MAAM,IAAI;IAKxD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B5B,IAAI,IAAI,eAAe;IAavB,WAAW,IAAI,MAAM;IAKrB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,eAAe;CAOxB"}
|
package/dist/types.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export interface MakeThisBetterConfig {
|
|
|
6
6
|
theme?: 'light' | 'dark' | 'auto';
|
|
7
7
|
apiUrl?: string;
|
|
8
8
|
frustrationDetection?: boolean;
|
|
9
|
+
tabText?: string;
|
|
10
|
+
userToken?: string;
|
|
11
|
+
userTokenFn?: () => Promise<string>;
|
|
9
12
|
user?: {
|
|
10
13
|
id?: string;
|
|
11
14
|
email?: string;
|
|
@@ -42,4 +45,18 @@ export interface FeedbackPayload {
|
|
|
42
45
|
recording_events?: Record<string, unknown>[];
|
|
43
46
|
recording_duration?: number;
|
|
44
47
|
}
|
|
48
|
+
export interface FeedbackResponse {
|
|
49
|
+
id: string;
|
|
50
|
+
status: string;
|
|
51
|
+
project_id: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ClarifyMessage {
|
|
54
|
+
role: string;
|
|
55
|
+
content: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ClarifyResponse {
|
|
58
|
+
status: string;
|
|
59
|
+
messages: ClarifyMessage[];
|
|
60
|
+
done: boolean;
|
|
61
|
+
}
|
|
45
62
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,IAAI,CAAC,EAAE;QACL,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;IACpB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,cAAc,CAAC,EAAE;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC5C,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IACnC,IAAI,CAAC,EAAE;QACL,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;IACpB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,cAAc,CAAC,EAAE;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC5C,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;CACd"}
|
|
@@ -7,6 +7,7 @@ export declare class WidgetController {
|
|
|
7
7
|
private toolbar;
|
|
8
8
|
private session;
|
|
9
9
|
private popup;
|
|
10
|
+
private clarify;
|
|
10
11
|
private success;
|
|
11
12
|
private frustrationPrompt;
|
|
12
13
|
private apiClient;
|
|
@@ -15,9 +16,7 @@ export declare class WidgetController {
|
|
|
15
16
|
private messages;
|
|
16
17
|
private mode;
|
|
17
18
|
private currentAnnotation;
|
|
18
|
-
private
|
|
19
|
-
private recordBar;
|
|
20
|
-
private currentRecording;
|
|
19
|
+
private recordingManager;
|
|
21
20
|
constructor(config: MakeThisBetterConfig);
|
|
22
21
|
private buildUI;
|
|
23
22
|
private handleTabClick;
|
|
@@ -25,11 +24,9 @@ export declare class WidgetController {
|
|
|
25
24
|
private dismissFrustrationPrompt;
|
|
26
25
|
private enterAnnotationMode;
|
|
27
26
|
private handleToolbarModeChange;
|
|
28
|
-
private enterRecordingMode;
|
|
29
|
-
private stopRecording;
|
|
30
|
-
private exitRecordingMode;
|
|
31
27
|
private showPopup;
|
|
32
28
|
private handleSubmit;
|
|
29
|
+
private showClarify;
|
|
33
30
|
private showSuccess;
|
|
34
31
|
private exitAll;
|
|
35
32
|
destroy(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/widget/controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/widget/controller.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,oBAAoB,EAAc,MAAM,UAAU,CAAA;AAIhE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,GAAG,CAAwB;IACnC,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,iBAAiB,CAAqC;IAC9D,OAAO,CAAC,SAAS,CAAW;IAC5B,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,gBAAgB,CAAgC;gBAE5C,MAAM,EAAE,oBAAoB;IAcxC,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,wBAAwB;IAKhC,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,uBAAuB;IA6B/B,OAAO,CAAC,SAAS;YAcH,YAAY;IAmC1B,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,WAAW;IAkBnB,OAAO,CAAC,OAAO;IAiBf,OAAO,IAAI,IAAI;CAYhB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RecordingResult } from '../record/session';
|
|
2
|
+
import type { ShadowContainer } from './shadow';
|
|
3
|
+
import type { I18nMessages } from '../i18n';
|
|
4
|
+
export type { RecordingResult };
|
|
5
|
+
export declare class RecordingManager {
|
|
6
|
+
private session;
|
|
7
|
+
private bar;
|
|
8
|
+
private result;
|
|
9
|
+
start(shadow: ShadowContainer, messages: I18nMessages, onStop: () => void): Promise<void>;
|
|
10
|
+
stop(): RecordingResult | null;
|
|
11
|
+
getRecording(): RecordingResult | null;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
private teardownUI;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=recording-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-manager.d.ts","sourceRoot":"","sources":["../../src/widget/recording-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,YAAY,EAAE,eAAe,EAAE,CAAA;AAE/B,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,MAAM,CAA+B;IAEvC,KAAK,CACT,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,MAAM,IAAI,GACjB,OAAO,CAAC,IAAI,CAAC;IAWhB,IAAI,IAAI,eAAe,GAAG,IAAI;IAO9B,YAAY,IAAI,eAAe,GAAG,IAAI;IAItC,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,UAAU;CAMnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-manager.test.d.ts","sourceRoot":"","sources":["../../src/widget/recording-manager.test.ts"],"names":[],"mappings":""}
|
package/dist/widget/tab.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { I18nMessages } from '../i18n';
|
|
|
3
3
|
export declare class FeedbackTab {
|
|
4
4
|
private el;
|
|
5
5
|
private onClick;
|
|
6
|
-
constructor(shadow: ShadowContainer, messages: I18nMessages, position: 'left' | 'right', onClick: () => void);
|
|
6
|
+
constructor(shadow: ShadowContainer, messages: I18nMessages, position: 'left' | 'right', onClick: () => void, customText?: string);
|
|
7
7
|
setActive(active: boolean): void;
|
|
8
8
|
destroy(): void;
|
|
9
9
|
}
|
package/dist/widget/tab.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab.d.ts","sourceRoot":"","sources":["../../src/widget/tab.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAmB;IAC7B,OAAO,CAAC,OAAO,CAAY;gBAGzB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,MAAM,GAAG,OAAO,EAC1B,OAAO,EAAE,MAAM,IAAI;
|
|
1
|
+
{"version":3,"file":"tab.d.ts","sourceRoot":"","sources":["../../src/widget/tab.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAmB;IAC7B,OAAO,CAAC,OAAO,CAAY;gBAGzB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,MAAM,GAAG,OAAO,EAC1B,OAAO,EAAE,MAAM,IAAI,EACnB,UAAU,CAAC,EAAE,MAAM;IAYrB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAIhC,OAAO,IAAI,IAAI;CAIhB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makethisbetter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Make This Better JS Widget SDK — embed user feedback collection in any website",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/makethisbetter.cjs.js",
|
|
@@ -56,9 +56,19 @@
|
|
|
56
56
|
"keywords": [
|
|
57
57
|
"feedback",
|
|
58
58
|
"widget",
|
|
59
|
-
"
|
|
59
|
+
"ai",
|
|
60
|
+
"mcp",
|
|
61
|
+
"claude-code",
|
|
62
|
+
"cursor",
|
|
63
|
+
"vibe-coding",
|
|
64
|
+
"agent",
|
|
60
65
|
"bug-reporting",
|
|
61
66
|
"annotation"
|
|
62
67
|
],
|
|
63
|
-
"
|
|
68
|
+
"homepage": "https://makethisbetter.dev",
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/makethisbetter/makethisbetter-js.git"
|
|
72
|
+
},
|
|
73
|
+
"license": "BUSL-1.1"
|
|
64
74
|
}
|
|
@@ -11,9 +11,16 @@ describe('AnnotationSession', () => {
|
|
|
11
11
|
let onReady: ReturnType<typeof vi.fn>
|
|
12
12
|
const messages = getMessages('en')
|
|
13
13
|
|
|
14
|
+
let rafQueue: FrameRequestCallback[]
|
|
15
|
+
|
|
14
16
|
beforeEach(() => {
|
|
15
|
-
// jsdom does not implement
|
|
16
|
-
document.
|
|
17
|
+
// jsdom does not implement elementsFromPoint — stub it
|
|
18
|
+
document.elementsFromPoint = vi.fn().mockReturnValue([])
|
|
19
|
+
rafQueue = []
|
|
20
|
+
vi.stubGlobal('requestAnimationFrame', vi.fn((cb: FrameRequestCallback) => rafQueue.push(cb)))
|
|
21
|
+
vi.stubGlobal('cancelAnimationFrame', vi.fn((id: number) => {
|
|
22
|
+
rafQueue.splice(id - 1, 1)
|
|
23
|
+
}))
|
|
17
24
|
})
|
|
18
25
|
|
|
19
26
|
function setup() {
|
|
@@ -22,9 +29,16 @@ describe('AnnotationSession', () => {
|
|
|
22
29
|
session = new AnnotationSession(shadow, messages, onReady)
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
function flushFrame() {
|
|
33
|
+
const queue = rafQueue
|
|
34
|
+
rafQueue = []
|
|
35
|
+
queue.forEach(cb => cb(0))
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
afterEach(() => {
|
|
26
39
|
document.body.innerHTML = ''
|
|
27
40
|
vi.restoreAllMocks()
|
|
41
|
+
vi.unstubAllGlobals()
|
|
28
42
|
})
|
|
29
43
|
|
|
30
44
|
describe('constructor', () => {
|
|
@@ -96,11 +110,11 @@ describe('AnnotationSession', () => {
|
|
|
96
110
|
session.destroy()
|
|
97
111
|
})
|
|
98
112
|
|
|
99
|
-
it('uses element name from
|
|
113
|
+
it('uses element name from elementsFromPoint when an element is found', () => {
|
|
100
114
|
setup()
|
|
101
115
|
const btn = document.createElement('button')
|
|
102
116
|
btn.setAttribute('aria-label', 'Save')
|
|
103
|
-
;(document.
|
|
117
|
+
;(document.elementsFromPoint as ReturnType<typeof vi.fn>).mockReturnValue([btn])
|
|
104
118
|
|
|
105
119
|
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
106
120
|
overlayEl.dispatchEvent(
|
|
@@ -118,7 +132,7 @@ describe('AnnotationSession', () => {
|
|
|
118
132
|
|
|
119
133
|
it('falls back to messages.annotation.element when no element under cursor', () => {
|
|
120
134
|
setup()
|
|
121
|
-
;(document.
|
|
135
|
+
;(document.elementsFromPoint as ReturnType<typeof vi.fn>).mockReturnValue([])
|
|
122
136
|
|
|
123
137
|
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
124
138
|
overlayEl.dispatchEvent(
|
|
@@ -180,6 +194,104 @@ describe('AnnotationSession', () => {
|
|
|
180
194
|
})
|
|
181
195
|
})
|
|
182
196
|
|
|
197
|
+
describe('hover highlight', () => {
|
|
198
|
+
it('coalesces mousemove hit tests into one per animation frame with the latest coordinates', () => {
|
|
199
|
+
setup()
|
|
200
|
+
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
201
|
+
|
|
202
|
+
for (let i = 1; i <= 5; i++) {
|
|
203
|
+
overlayEl.dispatchEvent(
|
|
204
|
+
new MouseEvent('mousemove', { clientX: i * 10, clientY: i * 10, bubbles: true }),
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
expect(document.elementsFromPoint).not.toHaveBeenCalled()
|
|
209
|
+
flushFrame()
|
|
210
|
+
expect(document.elementsFromPoint).toHaveBeenCalledOnce()
|
|
211
|
+
expect(document.elementsFromPoint).toHaveBeenCalledWith(50, 50)
|
|
212
|
+
session.destroy()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('shows the highlight box over the element under the cursor', () => {
|
|
216
|
+
setup()
|
|
217
|
+
const btn = document.createElement('button')
|
|
218
|
+
document.body.appendChild(btn)
|
|
219
|
+
;(document.elementsFromPoint as ReturnType<typeof vi.fn>).mockReturnValue([btn])
|
|
220
|
+
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
221
|
+
|
|
222
|
+
overlayEl.dispatchEvent(
|
|
223
|
+
new MouseEvent('mousemove', { clientX: 10, clientY: 10, bubbles: true }),
|
|
224
|
+
)
|
|
225
|
+
flushFrame()
|
|
226
|
+
|
|
227
|
+
const highlightEl = shadow.root.querySelector<HTMLDivElement>('.mtb-highlight')!
|
|
228
|
+
expect(highlightEl.style.display).toBe('block')
|
|
229
|
+
session.destroy()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('ignores the widget host when resolving the element under the cursor', () => {
|
|
233
|
+
setup()
|
|
234
|
+
const hostEl = document.getElementById('mtb-widget-host')!
|
|
235
|
+
const btn = document.createElement('button')
|
|
236
|
+
btn.setAttribute('aria-label', 'Save')
|
|
237
|
+
;(document.elementsFromPoint as ReturnType<typeof vi.fn>).mockReturnValue([hostEl, btn])
|
|
238
|
+
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
239
|
+
|
|
240
|
+
overlayEl.dispatchEvent(
|
|
241
|
+
new MouseEvent('mousedown', { clientX: 50, clientY: 50, bubbles: true }),
|
|
242
|
+
)
|
|
243
|
+
overlayEl.dispatchEvent(
|
|
244
|
+
new MouseEvent('mouseup', { clientX: 50, clientY: 50, bubbles: true }),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const [, targetName] = onReady.mock.calls[0]
|
|
248
|
+
expect(targetName).toBe('Save')
|
|
249
|
+
session.destroy()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('does not toggle the widget host display during hit testing', () => {
|
|
253
|
+
setup()
|
|
254
|
+
const hostEl = document.getElementById('mtb-widget-host')!
|
|
255
|
+
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
256
|
+
|
|
257
|
+
overlayEl.dispatchEvent(
|
|
258
|
+
new MouseEvent('mousemove', { clientX: 10, clientY: 10, bubbles: true }),
|
|
259
|
+
)
|
|
260
|
+
flushFrame()
|
|
261
|
+
|
|
262
|
+
expect(hostEl.style.display).toBe('')
|
|
263
|
+
session.destroy()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('cancels a pending highlight when the mouse leaves the overlay', () => {
|
|
267
|
+
setup()
|
|
268
|
+
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
269
|
+
|
|
270
|
+
overlayEl.dispatchEvent(
|
|
271
|
+
new MouseEvent('mousemove', { clientX: 10, clientY: 10, bubbles: true }),
|
|
272
|
+
)
|
|
273
|
+
overlayEl.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }))
|
|
274
|
+
flushFrame()
|
|
275
|
+
|
|
276
|
+
expect(document.elementsFromPoint).not.toHaveBeenCalled()
|
|
277
|
+
session.destroy()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('cancels a pending highlight on dismissInteraction', () => {
|
|
281
|
+
setup()
|
|
282
|
+
const overlayEl = shadow.root.querySelector('.mtb-overlay')!
|
|
283
|
+
|
|
284
|
+
overlayEl.dispatchEvent(
|
|
285
|
+
new MouseEvent('mousemove', { clientX: 10, clientY: 10, bubbles: true }),
|
|
286
|
+
)
|
|
287
|
+
session.dismissInteraction()
|
|
288
|
+
flushFrame()
|
|
289
|
+
|
|
290
|
+
expect(document.elementsFromPoint).not.toHaveBeenCalled()
|
|
291
|
+
session.destroy()
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
183
295
|
describe('dismissInteraction', () => {
|
|
184
296
|
it('removes overlay and highlighter but keeps pins and draw layer', () => {
|
|
185
297
|
setup()
|
package/src/annotate/session.ts
CHANGED
|
@@ -15,6 +15,9 @@ export class AnnotationSession {
|
|
|
15
15
|
private drawLayer: DrawLayer
|
|
16
16
|
private pinCount = 0
|
|
17
17
|
private interactionActive = true
|
|
18
|
+
private pendingHighlightFrame: number | null = null
|
|
19
|
+
private lastMoveX = 0
|
|
20
|
+
private lastMoveY = 0
|
|
18
21
|
|
|
19
22
|
constructor(
|
|
20
23
|
shadow: ShadowContainer,
|
|
@@ -32,6 +35,7 @@ export class AnnotationSession {
|
|
|
32
35
|
onMouseUp: (e) => this.handleMouseUp(e),
|
|
33
36
|
onMouseLeave: () => {
|
|
34
37
|
if (this.drawLayer.isCurrentlyDrawing()) this.drawLayer.endDraw()
|
|
38
|
+
this.cancelPendingHighlight()
|
|
35
39
|
this.highlighter.hide()
|
|
36
40
|
},
|
|
37
41
|
})
|
|
@@ -44,16 +48,34 @@ export class AnnotationSession {
|
|
|
44
48
|
|
|
45
49
|
private handleMouseMove(e: MouseEvent): void {
|
|
46
50
|
if (this.drawLayer.isCurrentlyDrawing()) {
|
|
51
|
+
this.cancelPendingHighlight()
|
|
47
52
|
this.drawLayer.continueDraw(e.clientX, e.clientY)
|
|
48
53
|
this.highlighter.hide()
|
|
49
54
|
return
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
this.lastMoveX = e.clientX
|
|
58
|
+
this.lastMoveY = e.clientY
|
|
59
|
+
if (this.pendingHighlightFrame !== null) return
|
|
60
|
+
this.pendingHighlightFrame = requestAnimationFrame(() => {
|
|
61
|
+
this.pendingHighlightFrame = null
|
|
62
|
+
this.updateHighlight()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private updateHighlight(): void {
|
|
67
|
+
if (!this.interactionActive || this.drawLayer.isCurrentlyDrawing()) return
|
|
68
|
+
const underEl = getElementUnder(this.lastMoveX, this.lastMoveY)
|
|
53
69
|
if (underEl) this.highlighter.highlight(underEl)
|
|
54
70
|
else this.highlighter.hide()
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
private cancelPendingHighlight(): void {
|
|
74
|
+
if (this.pendingHighlightFrame === null) return
|
|
75
|
+
cancelAnimationFrame(this.pendingHighlightFrame)
|
|
76
|
+
this.pendingHighlightFrame = null
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
private handleMouseUp(e: MouseEvent): void {
|
|
58
80
|
if (!this.drawLayer.isCurrentlyDrawing()) return
|
|
59
81
|
|
|
@@ -82,6 +104,7 @@ export class AnnotationSession {
|
|
|
82
104
|
dismissInteraction(): void {
|
|
83
105
|
if (!this.interactionActive) return
|
|
84
106
|
this.interactionActive = false
|
|
107
|
+
this.cancelPendingHighlight()
|
|
85
108
|
this.overlay.destroy()
|
|
86
109
|
this.highlighter.destroy()
|
|
87
110
|
}
|
|
@@ -93,12 +116,8 @@ export class AnnotationSession {
|
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
// Skips the widget host via elementsFromPoint instead of toggling its
|
|
120
|
+
// display — hiding the host on every mousemove flickered the toolbar.
|
|
96
121
|
function getElementUnder(x: number, y: number): Element | null {
|
|
97
|
-
|
|
98
|
-
if (host) host.style.display = 'none'
|
|
99
|
-
|
|
100
|
-
const el = document.elementFromPoint(x, y)
|
|
101
|
-
|
|
102
|
-
if (host) host.style.display = ''
|
|
103
|
-
return el
|
|
122
|
+
return document.elementsFromPoint(x, y).find(el => el.id !== 'mtb-widget-host') ?? null
|
|
104
123
|
}
|
package/src/api/client.test.ts
CHANGED
|
@@ -20,8 +20,9 @@ describe('ApiClient', () => {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
it('sends POST to correct URL with project key header', async () => {
|
|
23
|
+
const jsonBody = JSON.stringify({ id: 'fb_1', status: 'open' })
|
|
23
24
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
24
|
-
new Response(
|
|
25
|
+
new Response(jsonBody, { status: 201 }),
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
const client = new ApiClient('proj_test123', 'https://api.example.com')
|
|
@@ -31,16 +32,17 @@ describe('ApiClient', () => {
|
|
|
31
32
|
'https://api.example.com/widget/feedbacks',
|
|
32
33
|
expect.objectContaining({
|
|
33
34
|
method: 'POST',
|
|
34
|
-
headers: { 'X-Project-Key': 'proj_test123' },
|
|
35
|
+
headers: expect.objectContaining({ 'X-Project-Key': 'proj_test123' }),
|
|
35
36
|
}),
|
|
36
37
|
)
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
it('includes description in FormData', async () => {
|
|
40
41
|
let capturedBody: FormData | null = null
|
|
42
|
+
const jsonBody = JSON.stringify({ id: 'fb_1', status: 'open' })
|
|
41
43
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url: RequestInfo | URL, init?: RequestInit) => {
|
|
42
44
|
capturedBody = init?.body instanceof FormData ? init.body : null
|
|
43
|
-
return new Response(
|
|
45
|
+
return new Response(jsonBody, { status: 201 })
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
const client = new ApiClient('key', 'https://api.example.com')
|
|
@@ -53,9 +55,10 @@ describe('ApiClient', () => {
|
|
|
53
55
|
|
|
54
56
|
it('uploads screenshot when present', async () => {
|
|
55
57
|
let capturedBody: FormData | null = null
|
|
58
|
+
const jsonBody = JSON.stringify({ id: 'fb_1', status: 'open' })
|
|
56
59
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url: RequestInfo | URL, init?: RequestInit) => {
|
|
57
60
|
capturedBody = init?.body instanceof FormData ? init.body : null
|
|
58
|
-
return new Response(
|
|
61
|
+
return new Response(jsonBody, { status: 201 })
|
|
59
62
|
})
|
|
60
63
|
|
|
61
64
|
const screenshot = new Blob(['png'], { type: 'image/png' })
|
|
@@ -66,10 +69,147 @@ describe('ApiClient', () => {
|
|
|
66
69
|
expect(formData.get('feedback[screenshot]')).toBeInstanceOf(File)
|
|
67
70
|
})
|
|
68
71
|
|
|
72
|
+
it('includes X-User-Token header when userToken is set', async () => {
|
|
73
|
+
const jsonBody = JSON.stringify({ id: 'fb_1', status: 'open' })
|
|
74
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
75
|
+
new Response(jsonBody, { status: 201 }),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const client = new ApiClient('key', 'https://api.example.com', 1000, 'jwt_static_token')
|
|
79
|
+
await client.submitFeedback(mockPayload, null)
|
|
80
|
+
|
|
81
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
82
|
+
expect.any(String),
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
headers: expect.objectContaining({
|
|
85
|
+
'X-Project-Key': 'key',
|
|
86
|
+
'X-User-Token': 'jwt_static_token',
|
|
87
|
+
}),
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('calls userTokenFn to get fresh token before each request', async () => {
|
|
93
|
+
const jsonBody = JSON.stringify({ id: 'fb_1', status: 'open' })
|
|
94
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
95
|
+
new Response(jsonBody, { status: 201 }),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const tokenFn = vi.fn(async () => 'jwt_dynamic_token')
|
|
99
|
+
const client = new ApiClient('key', 'https://api.example.com', 1000, undefined, tokenFn)
|
|
100
|
+
await client.submitFeedback(mockPayload, null)
|
|
101
|
+
|
|
102
|
+
expect(tokenFn).toHaveBeenCalledOnce()
|
|
103
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
104
|
+
expect.any(String),
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
headers: expect.objectContaining({
|
|
107
|
+
'X-User-Token': 'jwt_dynamic_token',
|
|
108
|
+
}),
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('prefers userTokenFn over static userToken', async () => {
|
|
114
|
+
const jsonBody = JSON.stringify({ id: 'fb_1', status: 'open' })
|
|
115
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
116
|
+
new Response(jsonBody, { status: 201 }),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const tokenFn = vi.fn(async () => 'from_fn')
|
|
120
|
+
const client = new ApiClient('key', 'https://api.example.com', 1000, 'from_static', tokenFn)
|
|
121
|
+
await client.submitFeedback(mockPayload, null)
|
|
122
|
+
|
|
123
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
124
|
+
expect.any(String),
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
headers: expect.objectContaining({
|
|
127
|
+
'X-User-Token': 'from_fn',
|
|
128
|
+
}),
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('includes X-User-Token in startClarification', async () => {
|
|
134
|
+
const jsonBody = JSON.stringify({ status: 'pending', messages: [], done: false })
|
|
135
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
136
|
+
new Response(jsonBody, { status: 200 }),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const client = new ApiClient('key', 'https://api.example.com', 1000, 'jwt_token')
|
|
140
|
+
await client.startClarification('fb_1', 'hello')
|
|
141
|
+
|
|
142
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
143
|
+
expect.any(String),
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
headers: expect.objectContaining({
|
|
146
|
+
'X-Project-Key': 'key',
|
|
147
|
+
'X-User-Token': 'jwt_token',
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
}),
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('includes X-User-Token in getClarification', async () => {
|
|
155
|
+
const jsonBody = JSON.stringify({ status: 'done', messages: [], done: true })
|
|
156
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
157
|
+
new Response(jsonBody, { status: 200 }),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const tokenFn = vi.fn(async () => 'jwt_dynamic')
|
|
161
|
+
const client = new ApiClient('key', 'https://api.example.com', 1000, undefined, tokenFn)
|
|
162
|
+
await client.getClarification('fb_1')
|
|
163
|
+
|
|
164
|
+
expect(tokenFn).toHaveBeenCalledOnce()
|
|
165
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
166
|
+
expect.any(String),
|
|
167
|
+
expect.objectContaining({
|
|
168
|
+
headers: expect.objectContaining({
|
|
169
|
+
'X-Project-Key': 'key',
|
|
170
|
+
'X-User-Token': 'jwt_dynamic',
|
|
171
|
+
}),
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
69
176
|
it('throws after retries exhausted', async () => {
|
|
70
177
|
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Network error'))
|
|
71
178
|
|
|
72
179
|
const client = new ApiClient('key', 'https://api.example.com', 0)
|
|
73
180
|
await expect(client.submitFeedback(mockPayload, null)).rejects.toThrow()
|
|
74
181
|
})
|
|
182
|
+
|
|
183
|
+
it('sends an idempotency key and reuses it across retries', async () => {
|
|
184
|
+
const calls: RequestInit[] = []
|
|
185
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url, init) => {
|
|
186
|
+
calls.push(init!)
|
|
187
|
+
if (calls.length === 1) return new Response('oops', { status: 500 })
|
|
188
|
+
return new Response(JSON.stringify({ id: 'fb_1' }), { status: 201 })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const client = new ApiClient('proj_test123', 'https://api.example.com', 0)
|
|
192
|
+
await client.submitFeedback(mockPayload, null)
|
|
193
|
+
|
|
194
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2)
|
|
195
|
+
const key1 = (calls[0].headers as Record<string, string>)['X-Idempotency-Key']
|
|
196
|
+
const key2 = (calls[1].headers as Record<string, string>)['X-Idempotency-Key']
|
|
197
|
+
expect(key1).toBeTruthy()
|
|
198
|
+
expect(key2).toBe(key1)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('uses a fresh idempotency key for each separate submission', async () => {
|
|
202
|
+
const keys: string[] = []
|
|
203
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url, init) => {
|
|
204
|
+
keys.push((init!.headers as Record<string, string>)['X-Idempotency-Key'])
|
|
205
|
+
return new Response(JSON.stringify({ id: 'fb_1' }), { status: 201 })
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const client = new ApiClient('proj_test123', 'https://api.example.com')
|
|
209
|
+
await client.submitFeedback(mockPayload, null)
|
|
210
|
+
await client.submitFeedback(mockPayload, null)
|
|
211
|
+
|
|
212
|
+
expect(keys[0]).toBeTruthy()
|
|
213
|
+
expect(keys[0]).not.toBe(keys[1])
|
|
214
|
+
})
|
|
75
215
|
})
|