makethisbetter 1.1.3 → 1.3.0

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 (51) hide show
  1. package/LICENSE +21 -62
  2. package/README.md +4 -4
  3. package/dist/context/frustration.d.ts +1 -1
  4. package/dist/context/frustration.d.ts.map +1 -1
  5. package/dist/i18n/de.d.ts +2 -0
  6. package/dist/i18n/de.d.ts.map +1 -1
  7. package/dist/i18n/en.d.ts +2 -0
  8. package/dist/i18n/en.d.ts.map +1 -1
  9. package/dist/i18n/es.d.ts +2 -0
  10. package/dist/i18n/es.d.ts.map +1 -1
  11. package/dist/i18n/fr.d.ts +2 -0
  12. package/dist/i18n/fr.d.ts.map +1 -1
  13. package/dist/i18n/ja.d.ts +2 -0
  14. package/dist/i18n/ja.d.ts.map +1 -1
  15. package/dist/i18n/ko.d.ts +2 -0
  16. package/dist/i18n/ko.d.ts.map +1 -1
  17. package/dist/i18n/zh-CN.d.ts +2 -0
  18. package/dist/i18n/zh-CN.d.ts.map +1 -1
  19. package/dist/makethisbetter.cjs.js +21 -35
  20. package/dist/makethisbetter.cjs.js.map +1 -1
  21. package/dist/makethisbetter.esm.js +178 -190
  22. package/dist/makethisbetter.esm.js.map +1 -1
  23. package/dist/makethisbetter.js +18 -32
  24. package/dist/makethisbetter.js.map +1 -1
  25. package/dist/popup/success.d.ts +4 -1
  26. package/dist/popup/success.d.ts.map +1 -1
  27. package/dist/record/session.cdn.test.d.ts +2 -0
  28. package/dist/record/session.cdn.test.d.ts.map +1 -0
  29. package/dist/record/session.d.ts.map +1 -1
  30. package/dist/types.d.ts +2 -1
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/widget/controller.d.ts +0 -1
  33. package/dist/widget/controller.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/context/frustration.test.ts +2 -3
  36. package/src/context/frustration.ts +0 -1
  37. package/src/i18n/de.ts +2 -0
  38. package/src/i18n/en.ts +2 -0
  39. package/src/i18n/es.ts +2 -0
  40. package/src/i18n/fr.ts +2 -0
  41. package/src/i18n/ja.ts +2 -0
  42. package/src/i18n/ko.ts +2 -0
  43. package/src/i18n/zh-CN.ts +2 -0
  44. package/src/popup/success.test.ts +13 -0
  45. package/src/popup/success.ts +10 -3
  46. package/src/record/session.cdn.test.ts +89 -0
  47. package/src/record/session.ts +21 -7
  48. package/src/types.ts +2 -1
  49. package/src/widget/controller.test.ts +4 -1
  50. package/src/widget/controller.ts +15 -23
  51. package/src/widget/pet.test.ts +34 -32
@@ -1,8 +1,11 @@
1
1
  import type { ShadowContainer } from '../widget/shadow';
2
2
  import type { I18nMessages } from '../i18n';
3
+ export interface SuccessCardOptions {
4
+ skipFollowup?: boolean;
5
+ }
3
6
  export declare class SuccessCard {
4
7
  private el;
5
- constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void);
8
+ constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void, options?: SuccessCardOptions);
6
9
  destroy(): void;
7
10
  }
8
11
  //# 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,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI;IA8B7G,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,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,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,EAAE,OAAO,CAAC,EAAE,kBAAkB;IAiC3I,OAAO,IAAI,IAAI;CAGhB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session.cdn.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.cdn.test.d.ts","sourceRoot":"","sources":["../../src/record/session.cdn.test.ts"],"names":[],"mappings":""}
@@ -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;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"}
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;AAuDD,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
@@ -1,6 +1,6 @@
1
1
  export interface MakeThisBetterConfig {
2
2
  projectKey: string;
3
- entryMode?: 'button' | 'pet';
3
+ entryMode?: 'button';
4
4
  locale?: string;
5
5
  position?: 'left' | 'right';
6
6
  theme?: 'light' | 'dark' | 'auto';
@@ -50,6 +50,7 @@ export interface FeedbackResponse {
50
50
  status: string;
51
51
  project_id: string;
52
52
  ai_clarify_available?: boolean;
53
+ skip_followup?: boolean;
53
54
  }
54
55
  export interface ClarifyMessage {
55
56
  role: string;
@@ -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,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;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;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"}
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,CAAA;IACpB,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;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;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"}
@@ -3,7 +3,6 @@ export declare class WidgetController {
3
3
  private config;
4
4
  private shadow;
5
5
  private tab;
6
- private pet;
7
6
  private toolbar;
8
7
  private session;
9
8
  private popup;
@@ -1 +1 @@
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;IAuC1B,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,WAAW;IAkBnB,OAAO,CAAC,OAAO;IAiBf,OAAO,IAAI,IAAI;CAYhB"}
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,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;IAYf,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,wBAAwB;IAKhC,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,uBAAuB;IA8B/B,OAAO,CAAC,SAAS;YAcH,YAAY;IAwC1B,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,OAAO;IAgBf,OAAO,IAAI,IAAI;CAWhB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makethisbetter",
3
- "version": "1.1.3",
3
+ "version": "1.3.0",
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",
@@ -70,5 +70,5 @@
70
70
  "type": "git",
71
71
  "url": "https://github.com/makethisbetter/makethisbetter-js.git"
72
72
  },
73
- "license": "BUSL-1.1"
73
+ "license": "MIT"
74
74
  }
@@ -112,15 +112,14 @@ describe('FrustrationDetector', () => {
112
112
  document.body.removeChild(host)
113
113
  })
114
114
 
115
- it('does not directly emit console errors (only counts them as signal)', () => {
115
+ it('does not directly emit console errors (only counts them as dead-click input)', () => {
116
116
  makeDetector()
117
117
 
118
118
  if (window.onerror) {
119
119
  ;(window.onerror as Function)('Test error', 'test.js', 1, 1, new Error('Test error'))
120
120
  }
121
121
 
122
- const errorEvents = events.filter(e => e.signal === 'console_error')
123
- expect(errorEvents.length).toBe(0)
122
+ expect(events.length).toBe(0)
124
123
  })
125
124
 
126
125
  it('stops listening after stop()', () => {
@@ -3,7 +3,6 @@ import { errorSource } from './error-source'
3
3
  export type FrustrationSignal =
4
4
  | 'rage_click'
5
5
  | 'dead_click'
6
- | 'console_error'
7
6
  | 'rapid_navigation'
8
7
  | 'form_failure'
9
8
  | 'error_page'
package/src/i18n/de.ts CHANGED
@@ -22,6 +22,8 @@ export default {
22
22
  success: {
23
23
  title: 'Vielen Dank!',
24
24
  message: 'Ihr Feedback wurde empfangen.',
25
+ title_no_ai: 'Gesendet!',
26
+ message_no_ai: 'Wir kümmern uns darum. Sie werden benachrichtigt, sobald es gelöst ist.',
25
27
  view_feedback: 'Mein Feedback ansehen',
26
28
  close: 'Schließen',
27
29
  },
package/src/i18n/en.ts CHANGED
@@ -22,6 +22,8 @@ export default {
22
22
  success: {
23
23
  title: 'Thanks!',
24
24
  message: 'Your feedback has been received.',
25
+ title_no_ai: 'Sent — thanks!',
26
+ message_no_ai: "We'll take it from here. You'll hear back when it's resolved.",
25
27
  view_feedback: 'View my feedback',
26
28
  close: 'Close',
27
29
  },
package/src/i18n/es.ts CHANGED
@@ -22,6 +22,8 @@ export default {
22
22
  success: {
23
23
  title: '¡Gracias!',
24
24
  message: 'Tu comentario ha sido recibido.',
25
+ title_no_ai: '¡Enviado!',
26
+ message_no_ai: 'Nos encargamos. Te avisaremos cuando se resuelva.',
25
27
  view_feedback: 'Ver mis comentarios',
26
28
  close: 'Cerrar',
27
29
  },
package/src/i18n/fr.ts CHANGED
@@ -22,6 +22,8 @@ export default {
22
22
  success: {
23
23
  title: 'Merci !',
24
24
  message: 'Votre avis a bien été reçu.',
25
+ title_no_ai: 'Envoyé !',
26
+ message_no_ai: "On s'en occupe. Vous serez informé(e) dès que ce sera résolu.",
25
27
  view_feedback: 'Voir mes avis',
26
28
  close: 'Fermer',
27
29
  },
package/src/i18n/ja.ts CHANGED
@@ -22,6 +22,8 @@ export default {
22
22
  success: {
23
23
  title: 'ありがとうございます!',
24
24
  message: 'フィードバックを受け付けました。',
25
+ title_no_ai: '送信しました!',
26
+ message_no_ai: 'チームが対応します。解決次第お知らせします。',
25
27
  view_feedback: 'フィードバックを確認',
26
28
  close: '閉じる',
27
29
  },
package/src/i18n/ko.ts CHANGED
@@ -22,6 +22,8 @@ export default {
22
22
  success: {
23
23
  title: '감사합니다!',
24
24
  message: '피드백이 접수되었습니다.',
25
+ title_no_ai: '전송 완료!',
26
+ message_no_ai: '팀에서 처리 후 알려드리겠습니다.',
25
27
  view_feedback: '내 피드백 보기',
26
28
  close: '닫기',
27
29
  },
package/src/i18n/zh-CN.ts CHANGED
@@ -22,6 +22,8 @@ export default {
22
22
  success: {
23
23
  title: '感谢!',
24
24
  message: '您的反馈已收到。',
25
+ title_no_ai: '已发送,感谢!',
26
+ message_no_ai: '我们会跟进处理,解决后会通知您。',
25
27
  view_feedback: '查看我的反馈',
26
28
  close: '关闭',
27
29
  },
@@ -72,4 +72,17 @@ describe('SuccessCard', () => {
72
72
 
73
73
  shadow.destroy()
74
74
  })
75
+
76
+ it('shows depleted copy when skipFollowup is true', () => {
77
+ const shadow = new ShadowContainer()
78
+ const onClose = vi.fn()
79
+ const messages = getMessages('en')
80
+ const card = new SuccessCard(shadow, messages, onClose, undefined, { skipFollowup: true })
81
+
82
+ expect(shadow.root.querySelector('.mtb-success-title')?.textContent).toBe(messages.success.title_no_ai)
83
+ expect(shadow.root.querySelector('.mtb-success-msg')?.textContent).toBe(messages.success.message_no_ai)
84
+
85
+ card.destroy()
86
+ shadow.destroy()
87
+ })
75
88
  })
@@ -4,14 +4,21 @@ import { BRAND_COLOR } from '../styles'
4
4
 
5
5
  const SUCCESS_AUTO_CLOSE_MS = 5000
6
6
 
7
+ export interface SuccessCardOptions {
8
+ skipFollowup?: boolean
9
+ }
10
+
7
11
  export class SuccessCard {
8
12
  private el: HTMLDivElement
9
13
 
10
- constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void) {
14
+ constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void, options?: SuccessCardOptions) {
11
15
  this.el = shadow.el<HTMLDivElement>('div', 'mtb-success')
12
16
  this.el.style.right = '20px'
13
17
  this.el.style.bottom = '20px'
14
18
 
19
+ const title = options?.skipFollowup ? messages.success.title_no_ai : messages.success.title
20
+ const msg = options?.skipFollowup ? messages.success.message_no_ai : messages.success.message
21
+
15
22
  const viewLink = onViewFeedback
16
23
  ? `<button class="mtb-view-feedback-link">${messages.success.view_feedback}</button>`
17
24
  : ''
@@ -22,8 +29,8 @@ export class SuccessCard {
22
29
  <polyline points="20 6 9 17 4 12"/>
23
30
  </svg>
24
31
  </div>
25
- <div class="mtb-success-title">${messages.success.title}</div>
26
- <div class="mtb-success-msg">${messages.success.message}</div>
32
+ <div class="mtb-success-title">${title}</div>
33
+ <div class="mtb-success-msg">${msg}</div>
27
34
  ${viewLink}
28
35
  <button class="mtb-close-link">${messages.success.close}</button>
29
36
  `
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import type { RecordSession } from './session'
3
+ import type { ShadowContainer } from '../widget/shadow'
4
+
5
+ const mockStopFn = vi.fn()
6
+ const mockRecord = Object.assign(
7
+ vi.fn(() => mockStopFn),
8
+ { addCustomEvent: vi.fn() },
9
+ )
10
+
11
+ // Force the bundler import path to fail so start() exercises the CDN fallback,
12
+ // mirroring the IIFE/CDN build where the bare specifier is unresolvable.
13
+ vi.mock('rrweb', () => {
14
+ throw new Error('rrweb not installed')
15
+ })
16
+
17
+ function createMockShadow(): ShadowContainer {
18
+ return {
19
+ el: <T extends HTMLElement>(tag: string, className?: string): T => {
20
+ const el = document.createElement(tag) as T
21
+ if (className) el.className = className
22
+ return el
23
+ },
24
+ append: vi.fn(),
25
+ remove: vi.fn(),
26
+ root: document.createElement('div') as unknown as ShadowRoot,
27
+ destroy: vi.fn(),
28
+ } as unknown as ShadowContainer
29
+ }
30
+
31
+ // session.ts caches the rrweb loader promise at module level, so each test
32
+ // re-imports a fresh module instance.
33
+ async function freshSession(): Promise<RecordSession> {
34
+ vi.resetModules()
35
+ const { RecordSession: Cls } = await import('./session')
36
+ return new Cls(createMockShadow())
37
+ }
38
+
39
+ describe('RecordSession CDN fallback', () => {
40
+ let session: RecordSession | null = null
41
+
42
+ beforeEach(() => {
43
+ vi.clearAllMocks()
44
+ delete (globalThis as Record<string, unknown>).rrwebRecord
45
+ })
46
+
47
+ afterEach(() => {
48
+ session?.destroy()
49
+ session = null
50
+ vi.restoreAllMocks()
51
+ delete (globalThis as Record<string, unknown>).rrwebRecord
52
+ })
53
+
54
+ it('loads @rrweb/record UMD build via classic script tag', async () => {
55
+ const appended: HTMLScriptElement[] = []
56
+ vi.spyOn(document.head, 'appendChild').mockImplementation((node) => {
57
+ const script = node as HTMLScriptElement
58
+ appended.push(script)
59
+ // Simulate the UMD script executing: it sets the global, then load fires
60
+ ;(globalThis as Record<string, unknown>).rrwebRecord = { record: mockRecord }
61
+ script.onload?.(new Event('load'))
62
+ return node
63
+ })
64
+
65
+ session = await freshSession()
66
+ await session.start()
67
+
68
+ expect(appended).toHaveLength(1)
69
+ const script = appended[0]
70
+ // rrweb 2.x publishes no UMD build in the `rrweb` package — its dist files
71
+ // are ESM and throw "Unexpected token 'export'" in a classic script tag.
72
+ // The record-only UMD lives in @rrweb/record's static /umd/ directory.
73
+ expect(script.src).toContain('/npm/@rrweb/record@')
74
+ expect(script.src).toContain('/umd/record.min.js')
75
+ expect(script.integrity).toMatch(/^sha384-/)
76
+ expect(script.crossOrigin).toBe('anonymous')
77
+ expect(mockRecord).toHaveBeenCalledOnce()
78
+ })
79
+
80
+ it('rejects when the CDN script exposes no rrwebRecord global', async () => {
81
+ vi.spyOn(document.head, 'appendChild').mockImplementation((node) => {
82
+ ;(node as HTMLScriptElement).onload?.(new Event('load'))
83
+ return node
84
+ })
85
+
86
+ session = await freshSession()
87
+ await expect(session.start()).rejects.toThrow(/not found after script load/)
88
+ })
89
+ })
@@ -18,8 +18,12 @@ const MAX_DURATION_S = 60
18
18
  // floating tag or unverified content would be a supply-chain hole. Bumping
19
19
  // the version requires recomputing the hash from the exact CDN file:
20
20
  // curl -sL <url> | openssl dgst -sha384 -binary | openssl base64 -A
21
- const RRWEB_CDN = 'https://cdn.jsdelivr.net/npm/rrweb@2.1.0/dist/rrweb.min.js'
22
- const RRWEB_SRI = 'sha384-6qZKf9GE/E2uhA8IdLwXsQiLN/jVx8mhA0i04rQ1aHLrs2URo3ivHlSbaccG59zt'
21
+ // Must stay on @rrweb/record's static /umd/ build: the `rrweb` package ships
22
+ // no UMD in 2.x (its dist files are ESM and throw "Unexpected token 'export'"
23
+ // in a classic script tag), and jsDelivr's auto-minified files are unsafe for
24
+ // SRI because their hash changes when jsDelivr upgrades its minifier.
25
+ const RRWEB_CDN = 'https://cdn.jsdelivr.net/npm/@rrweb/record@2.1.0/umd/record.min.js'
26
+ const RRWEB_SRI = 'sha384-MrD66HBNSykaP2N95+6hQCFlF5oH2tvL3TD/zyvHNkP/sAFWZx98DX9MEDy8MdVT'
23
27
 
24
28
  let rrwebLoaded: Promise<RRWebRecordFn> | null = null
25
29
 
@@ -34,18 +38,28 @@ async function loadViaImport(): Promise<RRWebRecordFn> {
34
38
  return mod.record as unknown as RRWebRecordFn
35
39
  }
36
40
 
41
+ interface RRWebGlobals {
42
+ rrwebRecord?: { record: RRWebRecordFn }
43
+ rrweb?: { record: RRWebRecordFn }
44
+ }
45
+
46
+ function findGlobalRecord(): RRWebRecordFn | undefined {
47
+ const win = globalThis as unknown as Window & RRWebGlobals
48
+ return win.rrwebRecord?.record ?? win.rrweb?.record
49
+ }
50
+
37
51
  function loadViaCdn(): Promise<RRWebRecordFn> {
38
52
  return new Promise((resolve, reject) => {
39
- const win = globalThis as unknown as Window & { rrweb?: { record: RRWebRecordFn } }
40
- if (win.rrweb?.record) { resolve(win.rrweb.record); return }
53
+ const existing = findGlobalRecord()
54
+ if (existing) { resolve(existing); return }
41
55
  const script = document.createElement('script')
42
56
  script.src = RRWEB_CDN
43
57
  script.integrity = RRWEB_SRI
44
58
  script.crossOrigin = 'anonymous'
45
59
  script.onload = () => {
46
- const w = globalThis as unknown as Window & { rrweb?: { record: RRWebRecordFn } }
47
- if (w.rrweb?.record) resolve(w.rrweb.record)
48
- else reject(new Error('rrweb global not found after script load'))
60
+ const record = findGlobalRecord()
61
+ if (record) resolve(record)
62
+ else reject(new Error('rrweb record global not found after script load'))
49
63
  }
50
64
  script.onerror = () => reject(new Error('Failed to load rrweb from CDN'))
51
65
  document.head.appendChild(script)
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export interface MakeThisBetterConfig {
2
2
  projectKey: string
3
- entryMode?: 'button' | 'pet'
3
+ entryMode?: 'button'
4
4
  locale?: string
5
5
  position?: 'left' | 'right'
6
6
  theme?: 'light' | 'dark' | 'auto'
@@ -53,6 +53,7 @@ export interface FeedbackResponse {
53
53
  status: string
54
54
  project_id: string
55
55
  ai_clarify_available?: boolean
56
+ skip_followup?: boolean
56
57
  }
57
58
 
58
59
  export interface ClarifyMessage {
@@ -92,9 +92,12 @@ describe('MakeThisBetter widget flow', () => {
92
92
  shadow.querySelector<HTMLButtonElement>('.mtb-submit-btn')!.click()
93
93
 
94
94
  await vi.waitFor(() => {
95
- expect(shadow.querySelector('.mtb-success-title')?.textContent).toBe('Thanks!')
95
+ expect(shadow.querySelector('.mtb-success-title')?.textContent).toBe('Sent — thanks!')
96
96
  }, { timeout: 5000 })
97
97
 
98
+ expect(shadow.querySelector('.mtb-success-msg')?.textContent).toBe(
99
+ "We'll take it from here. You'll hear back when it's resolved."
100
+ )
98
101
  expect(shadow.querySelector('.mtb-clarify')).toBeNull()
99
102
  expect(fetchSpy).toHaveBeenCalledTimes(1)
100
103
  })
@@ -1,6 +1,5 @@
1
1
  import { ShadowContainer } from './shadow'
2
2
  import { FeedbackTab } from './tab'
3
- import { PetEntry } from './pet'
4
3
  import { FrustrationPromptCard } from './frustration-prompt'
5
4
  import { AnnotationToolbar } from '../annotate/toolbar'
6
5
  import type { ToolbarMode } from '../annotate/toolbar'
@@ -8,6 +7,7 @@ import { AnnotationSession } from '../annotate/session'
8
7
  import { CommentPopup } from '../popup/comment'
9
8
  import { ClarifyCard } from '../popup/clarify'
10
9
  import { SuccessCard } from '../popup/success'
10
+ import type { SuccessCardOptions } from '../popup/success'
11
11
  import { captureScreenshot } from '../screenshot/capture'
12
12
  import { ConsoleErrorCollector } from '../context/console'
13
13
  import { FrustrationDetector } from '../context/frustration'
@@ -24,7 +24,6 @@ export class WidgetController {
24
24
  private config: MakeThisBetterConfig
25
25
  private shadow!: ShadowContainer
26
26
  private tab: FeedbackTab | null = null
27
- private pet: PetEntry | null = null
28
27
  private toolbar: AnnotationToolbar | null = null
29
28
  private session: AnnotationSession | null = null
30
29
  private popup: CommentPopup | null = null
@@ -56,17 +55,13 @@ export class WidgetController {
56
55
  private buildUI(): void {
57
56
  this.shadow = new ShadowContainer(this.config.theme ?? 'auto')
58
57
 
59
- if ((this.config.entryMode ?? 'button') === 'pet') {
60
- this.pet = new PetEntry(this.shadow, this.messages, () => this.handleTabClick())
61
- } else {
62
- this.tab = new FeedbackTab(
63
- this.shadow,
64
- this.messages,
65
- this.config.position ?? 'right',
66
- () => this.handleTabClick(),
67
- this.config.tabText,
68
- )
69
- }
58
+ this.tab = new FeedbackTab(
59
+ this.shadow,
60
+ this.messages,
61
+ this.config.position ?? 'right',
62
+ () => this.handleTabClick(),
63
+ this.config.tabText,
64
+ )
70
65
  }
71
66
 
72
67
  private handleTabClick(): void {
@@ -98,7 +93,6 @@ export class WidgetController {
98
93
  private enterAnnotationMode(): void {
99
94
  this.mode = 'annotating'
100
95
  this.tab?.setActive(true)
101
- this.pet?.setActive(true)
102
96
 
103
97
  this.toolbar = new AnnotationToolbar(
104
98
  this.shadow,
@@ -122,8 +116,9 @@ export class WidgetController {
122
116
  if (!this.recordingManager) return
123
117
  this.recordingManager.stop()
124
118
  this.showPopup(window.innerWidth / 2, window.innerHeight / 2, this.messages.record.timer_label)
125
- }).catch(() => {
119
+ }).catch((error) => {
126
120
  // rrweb failed to load (network / SRI mismatch) — fall back to markup
121
+ console.warn('[MakeThisBetter] Screen recording unavailable, falling back to markup:', error)
127
122
  if (this.mode !== 'recording') return
128
123
  this.toolbar?.setMode('markup')
129
124
  this.handleToolbarModeChange('markup')
@@ -183,8 +178,9 @@ export class WidgetController {
183
178
  this.session = null
184
179
  this.recordingManager?.destroy()
185
180
  this.recordingManager = null
186
- if (feedback.ai_clarify_available === false) {
187
- this.showSuccess(feedback.project_id)
181
+ const skipFollowup = feedback.skip_followup === true || feedback.ai_clarify_available === false
182
+ if (skipFollowup) {
183
+ this.showSuccess(feedback.project_id, { skipFollowup: true })
188
184
  } else {
189
185
  this.showClarify(feedback.id, feedback.project_id)
190
186
  }
@@ -197,7 +193,6 @@ export class WidgetController {
197
193
  private showClarify(feedbackId: string, projectId: string): void {
198
194
  this.mode = 'idle'
199
195
  this.tab?.setActive(false)
200
- this.pet?.setActive(false)
201
196
  this.clarify = new ClarifyCard(this.shadow, {
202
197
  feedbackId,
203
198
  apiClient: this.apiClient,
@@ -210,10 +205,9 @@ export class WidgetController {
210
205
  })
211
206
  }
212
207
 
213
- private showSuccess(projectId: string): void {
208
+ private showSuccess(projectId: string, options?: SuccessCardOptions): void {
214
209
  this.mode = 'idle'
215
210
  this.tab?.setActive(false)
216
- this.pet?.setActive(false)
217
211
  const baseUrl = this.config.apiUrl
218
212
  ? this.config.apiUrl.replace(/\/api\/v1\/?$/, '')
219
213
  : 'https://makethisbetter.dev'
@@ -225,13 +219,12 @@ export class WidgetController {
225
219
  }, hasIdentity ? async () => {
226
220
  const token = await this.apiClient.resolveUserToken()
227
221
  if (token) window.open(`${boardUrl}?identity=${encodeURIComponent(token)}`, '_blank')
228
- } : undefined)
222
+ } : undefined, options)
229
223
  }
230
224
 
231
225
  private exitAll(): void {
232
226
  this.mode = 'idle'
233
227
  this.tab?.setActive(false)
234
- this.pet?.setActive(false)
235
228
  this.toolbar?.destroy()
236
229
  this.toolbar = null
237
230
  this.session?.destroy()
@@ -253,7 +246,6 @@ export class WidgetController {
253
246
  this.clarify?.destroy()
254
247
  this.success?.destroy()
255
248
  this.tab?.destroy()
256
- this.pet?.destroy()
257
249
  this.recordingManager?.destroy()
258
250
  this.shadow.destroy()
259
251
  }
@@ -1,56 +1,58 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { PetEntry } from './pet'
3
+ import { ShadowContainer } from './shadow'
4
+ import { getMessages } from '../i18n'
2
5
  import { MakeThisBetter } from '../index'
3
6
 
4
7
  vi.mock('html-to-image', () => ({
5
8
  toPng: vi.fn(async () => 'data:image/png;base64,AA=='),
6
9
  }))
7
10
 
8
- describe('Pet mode', () => {
11
+ // PetEntry is kept in the repo for the phase-2 pet mode but is not wired into
12
+ // the widget build, so it is unit-tested directly instead of via init().
13
+ describe('PetEntry', () => {
14
+ let shadow: ShadowContainer
15
+
9
16
  afterEach(() => {
10
- MakeThisBetter.destroy()
11
- vi.restoreAllMocks()
17
+ shadow?.destroy()
12
18
  document.body.innerHTML = ''
13
19
  })
14
20
 
15
- it('renders pet entry instead of tab when entryMode is pet', () => {
16
- MakeThisBetter.init({
17
- projectKey: 'proj_test',
18
- entryMode: 'pet',
19
- apiUrl: 'https://api.example.com/api/v1',
20
- })
21
+ function makePet(onClick: () => void = () => {}) {
22
+ shadow = new ShadowContainer('light')
23
+ return new PetEntry(shadow, getMessages('en'), onClick)
24
+ }
21
25
 
22
- const shadow = document.getElementById('mtb-widget-host')!.shadowRoot!
23
- expect(shadow.querySelector('.mtb-pet')).toBeTruthy()
24
- expect(shadow.querySelector('.mtb-tab')).toBeNull()
25
- })
26
+ function shadowRoot() {
27
+ return document.getElementById('mtb-widget-host')!.shadowRoot!
28
+ }
26
29
 
27
- it('shows speech bubble on the pet', () => {
28
- MakeThisBetter.init({
29
- projectKey: 'proj_test',
30
- entryMode: 'pet',
31
- apiUrl: 'https://api.example.com/api/v1',
32
- })
30
+ it('renders the pet element with a speech bubble', () => {
31
+ makePet()
33
32
 
34
- const shadow = document.getElementById('mtb-widget-host')!.shadowRoot!
35
- const bubble = shadow.querySelector('.mtb-pet-bubble')
33
+ expect(shadowRoot().querySelector('.mtb-pet')).toBeTruthy()
34
+ const bubble = shadowRoot().querySelector('.mtb-pet-bubble')
36
35
  expect(bubble).toBeTruthy()
37
36
  expect(bubble!.textContent).toBe('Need help?')
38
37
  })
39
38
 
40
- it('opens annotation mode on pet click', () => {
41
- MakeThisBetter.init({
42
- projectKey: 'proj_test',
43
- entryMode: 'pet',
44
- apiUrl: 'https://api.example.com/api/v1',
45
- })
39
+ it('invokes the click callback on click', () => {
40
+ const onClick = vi.fn()
41
+ makePet(onClick)
46
42
 
47
- const shadow = document.getElementById('mtb-widget-host')!.shadowRoot!
48
- shadow.querySelector<HTMLDivElement>('.mtb-pet')!.click()
49
- expect(shadow.querySelector('.mtb-overlay')).toBeTruthy()
50
- expect(shadow.querySelector('.mtb-toolbar')).toBeTruthy()
43
+ shadowRoot().querySelector<HTMLDivElement>('.mtb-pet')!.click()
44
+ expect(onClick).toHaveBeenCalledOnce()
45
+ })
46
+ })
47
+
48
+ describe('Widget entry', () => {
49
+ afterEach(() => {
50
+ MakeThisBetter.destroy()
51
+ vi.restoreAllMocks()
52
+ document.body.innerHTML = ''
51
53
  })
52
54
 
53
- it('renders tab by default (no entryMode)', () => {
55
+ it('renders the tab entry and never the pet', () => {
54
56
  MakeThisBetter.init({
55
57
  projectKey: 'proj_test',
56
58
  apiUrl: 'https://api.example.com/api/v1',