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.
Files changed (77) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +329 -62
  3. package/dist/annotate/session.d.ts +5 -0
  4. package/dist/annotate/session.d.ts.map +1 -1
  5. package/dist/api/client.d.ts +9 -3
  6. package/dist/api/client.d.ts.map +1 -1
  7. package/dist/context/error-source.d.ts +1 -0
  8. package/dist/context/error-source.d.ts.map +1 -1
  9. package/dist/context/frustration.d.ts +25 -1
  10. package/dist/context/frustration.d.ts.map +1 -1
  11. package/dist/i18n/de.d.ts +54 -0
  12. package/dist/i18n/de.d.ts.map +1 -0
  13. package/dist/i18n/en.d.ts +7 -0
  14. package/dist/i18n/en.d.ts.map +1 -1
  15. package/dist/i18n/es.d.ts +54 -0
  16. package/dist/i18n/es.d.ts.map +1 -0
  17. package/dist/i18n/fr.d.ts +54 -0
  18. package/dist/i18n/fr.d.ts.map +1 -0
  19. package/dist/i18n/index.d.ts.map +1 -1
  20. package/dist/i18n/ja.d.ts +54 -0
  21. package/dist/i18n/ja.d.ts.map +1 -0
  22. package/dist/i18n/ko.d.ts +54 -0
  23. package/dist/i18n/ko.d.ts.map +1 -0
  24. package/dist/i18n/zh-CN.d.ts +7 -0
  25. package/dist/i18n/zh-CN.d.ts.map +1 -1
  26. package/dist/makethisbetter.cjs.js +42 -27
  27. package/dist/makethisbetter.cjs.js.map +1 -1
  28. package/dist/makethisbetter.esm.js +1208 -636
  29. package/dist/makethisbetter.esm.js.map +1 -1
  30. package/dist/makethisbetter.js +42 -27
  31. package/dist/makethisbetter.js.map +1 -1
  32. package/dist/popup/clarify.d.ts +37 -0
  33. package/dist/popup/clarify.d.ts.map +1 -0
  34. package/dist/popup/clarify.test.d.ts +2 -0
  35. package/dist/popup/clarify.test.d.ts.map +1 -0
  36. package/dist/popup/success.d.ts +1 -1
  37. package/dist/popup/success.d.ts.map +1 -1
  38. package/dist/record/session.d.ts.map +1 -1
  39. package/dist/types.d.ts +17 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/widget/controller.d.ts +3 -6
  42. package/dist/widget/controller.d.ts.map +1 -1
  43. package/dist/widget/recording-manager.d.ts +15 -0
  44. package/dist/widget/recording-manager.d.ts.map +1 -0
  45. package/dist/widget/recording-manager.test.d.ts +2 -0
  46. package/dist/widget/recording-manager.test.d.ts.map +1 -0
  47. package/dist/widget/tab.d.ts +1 -1
  48. package/dist/widget/tab.d.ts.map +1 -1
  49. package/package.json +13 -3
  50. package/src/annotate/session.test.ts +117 -5
  51. package/src/annotate/session.ts +27 -8
  52. package/src/api/client.test.ts +144 -4
  53. package/src/api/client.ts +56 -5
  54. package/src/context/error-source.test.ts +13 -0
  55. package/src/context/error-source.ts +9 -2
  56. package/src/context/frustration.test.ts +336 -1
  57. package/src/context/frustration.ts +237 -1
  58. package/src/i18n/de.ts +54 -0
  59. package/src/i18n/en.ts +7 -0
  60. package/src/i18n/es.ts +54 -0
  61. package/src/i18n/fr.ts +54 -0
  62. package/src/i18n/index.test.ts +1 -1
  63. package/src/i18n/index.ts +10 -0
  64. package/src/i18n/ja.ts +54 -0
  65. package/src/i18n/ko.ts +54 -0
  66. package/src/i18n/zh-CN.ts +7 -0
  67. package/src/popup/clarify.test.ts +419 -0
  68. package/src/popup/clarify.ts +215 -0
  69. package/src/popup/success.ts +11 -1
  70. package/src/record/session.ts +8 -1
  71. package/src/styles/widget.css +218 -0
  72. package/src/types.ts +20 -0
  73. package/src/widget/controller.test.ts +6 -4
  74. package/src/widget/controller.ts +59 -52
  75. package/src/widget/recording-manager.test.ts +136 -0
  76. package/src/widget/recording-manager.ts +51 -0
  77. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=clarify.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clarify.test.d.ts","sourceRoot":"","sources":["../../src/popup/clarify.test.ts"],"names":[],"mappings":""}
@@ -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;IAoBhF,OAAO,IAAI,IAAI;CAGhB"}
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;AAkCD,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"}
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
@@ -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 recordSession;
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":"AAmBA,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,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,aAAa,CAA6B;IAClD,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,gBAAgB,CAA+B;gBAE3C,MAAM,EAAE,oBAAoB;IAcxC,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,wBAAwB;IAKhC,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,uBAAuB;YAejB,kBAAkB;IAgBhC,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,SAAS;YAcH,YAAY;IAkC1B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,OAAO;IAef,OAAO,IAAI,IAAI;CAWhB"}
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=recording-manager.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recording-manager.test.d.ts","sourceRoot":"","sources":["../../src/widget/recording-manager.test.ts"],"names":[],"mappings":""}
@@ -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
  }
@@ -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;IAWrB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAIhC,OAAO,IAAI,IAAI;CAIhB"}
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.0.0",
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
- "user-feedback",
59
+ "ai",
60
+ "mcp",
61
+ "claude-code",
62
+ "cursor",
63
+ "vibe-coding",
64
+ "agent",
60
65
  "bug-reporting",
61
66
  "annotation"
62
67
  ],
63
- "license": "BSL-1.1"
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 elementFromPoint — stub it
16
- document.elementFromPoint = vi.fn().mockReturnValue(null)
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 elementFromPoint when an element is found', () => {
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.elementFromPoint as ReturnType<typeof vi.fn>).mockReturnValue(btn)
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.elementFromPoint as ReturnType<typeof vi.fn>).mockReturnValue(null)
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()
@@ -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
- const underEl = getElementUnder(e.clientX, e.clientY)
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
- const host = document.getElementById('mtb-widget-host')
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
  }
@@ -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(null, { status: 201 }),
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(null, { status: 201 })
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(null, { status: 201 })
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
  })