makethisbetter 1.5.0 → 1.5.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 (47) hide show
  1. package/dist/api/client.d.ts +6 -1
  2. package/dist/api/client.d.ts.map +1 -1
  3. package/dist/i18n/de.d.ts +2 -0
  4. package/dist/i18n/de.d.ts.map +1 -1
  5. package/dist/i18n/en.d.ts +2 -0
  6. package/dist/i18n/en.d.ts.map +1 -1
  7. package/dist/i18n/es.d.ts +2 -0
  8. package/dist/i18n/es.d.ts.map +1 -1
  9. package/dist/i18n/fr.d.ts +2 -0
  10. package/dist/i18n/fr.d.ts.map +1 -1
  11. package/dist/i18n/ja.d.ts +2 -0
  12. package/dist/i18n/ja.d.ts.map +1 -1
  13. package/dist/i18n/ko.d.ts +2 -0
  14. package/dist/i18n/ko.d.ts.map +1 -1
  15. package/dist/i18n/zh-CN.d.ts +2 -0
  16. package/dist/i18n/zh-CN.d.ts.map +1 -1
  17. package/dist/makethisbetter.cjs +36 -15
  18. package/dist/makethisbetter.cjs.map +1 -1
  19. package/dist/makethisbetter.esm.js +480 -356
  20. package/dist/makethisbetter.esm.js.map +1 -1
  21. package/dist/makethisbetter.js +36 -15
  22. package/dist/makethisbetter.js.map +1 -1
  23. package/dist/popup/clarify.d.ts +10 -0
  24. package/dist/popup/clarify.d.ts.map +1 -1
  25. package/dist/screenshot/capture.d.ts.map +1 -1
  26. package/dist/widget/controller.d.ts +1 -0
  27. package/dist/widget/controller.d.ts.map +1 -1
  28. package/dist/widget/shadow.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/api/client.test.ts +31 -0
  31. package/src/api/client.ts +75 -2
  32. package/src/i18n/de.ts +3 -1
  33. package/src/i18n/en.ts +3 -1
  34. package/src/i18n/es.ts +3 -1
  35. package/src/i18n/fr.ts +3 -1
  36. package/src/i18n/ja.ts +3 -1
  37. package/src/i18n/ko.ts +3 -1
  38. package/src/i18n/zh-CN.ts +3 -1
  39. package/src/popup/clarify.test.ts +136 -27
  40. package/src/popup/clarify.ts +128 -42
  41. package/src/screenshot/capture.test.ts +14 -12
  42. package/src/screenshot/capture.ts +8 -2
  43. package/src/styles/widget.css +137 -57
  44. package/src/widget/controller.test.ts +45 -0
  45. package/src/widget/controller.ts +11 -5
  46. package/src/widget/shadow.test.ts +19 -0
  47. package/src/widget/shadow.ts +4 -1
@@ -6,6 +6,8 @@ interface ClarifyCardOptions {
6
6
  apiClient: ApiClient;
7
7
  messages: I18nMessages;
8
8
  onDone: () => void;
9
+ x?: number;
10
+ y?: number;
9
11
  }
10
12
  export declare class ClarifyCard {
11
13
  private el;
@@ -13,17 +15,24 @@ export declare class ClarifyCard {
13
15
  private inputEl;
14
16
  private sendBtn;
15
17
  private pollTimer;
18
+ private pollDeadline;
16
19
  private doneTimer;
20
+ private streamingBubble;
17
21
  private feedbackId;
18
22
  private apiClient;
19
23
  private onDone;
20
24
  private sending;
21
25
  private destroyed;
22
26
  constructor(shadow: ShadowContainer, opts: ClarifyCardOptions);
27
+ private positionCard;
23
28
  private buildHTML;
24
29
  private bindEvents;
25
30
  private startConversation;
26
31
  private sendMessage;
32
+ private runTurn;
33
+ private runTurnViaPolling;
34
+ private appendStreamDelta;
35
+ private endStreamingBubble;
27
36
  private startPolling;
28
37
  private stopPolling;
29
38
  private poll;
@@ -32,6 +41,7 @@ export declare class ClarifyCard {
32
41
  private renderMessages;
33
42
  private appendMessage;
34
43
  private showThinking;
44
+ private removeThinking;
35
45
  destroy(): void;
36
46
  }
37
47
  export {};
@@ -1 +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;YAuBJ,iBAAiB;YAiBjB,WAAW;IAiCzB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,WAAW;YAOL,IAAI;IAelB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,YAAY;IAQpB,OAAO,IAAI,IAAI;CAShB"}
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;AAS9C,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,SAAS,CAAA;IACpB,QAAQ,EAAE,YAAY,CAAA;IACtB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,CAAC,EAAE,MAAM,CAAA;CACX;AAKD,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,eAAe,CAA8B;IACrD,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;IAoB7D,OAAO,CAAC,YAAY;IAsBpB,OAAO,CAAC,SAAS;IAmCjB,OAAO,CAAC,UAAU;YAqBJ,iBAAiB;YAKjB,WAAW;YA0BX,OAAO;YAmBP,iBAAiB;IAa/B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,WAAW;YAOL,IAAI;IAoBlB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,cAAc;IAItB,OAAO,IAAI,IAAI;CAShB"}
@@ -1 +1 @@
1
- {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/screenshot/capture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,wBAAsB,iBAAiB,CAAC,WAAW,GAAE,UAAU,EAAO,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAkB5F"}
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/screenshot/capture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAK1C,wBAAsB,iBAAiB,CAAC,WAAW,GAAE,UAAU,EAAO,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAqB5F"}
@@ -15,6 +15,7 @@ export declare class WidgetController {
15
15
  private messages;
16
16
  private mode;
17
17
  private currentAnnotation;
18
+ private lastPopupPos;
18
19
  private recordingManager;
19
20
  private lastHandoff;
20
21
  constructor(config: MakeThisBetterConfig);
@@ -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,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;IACxD,OAAO,CAAC,WAAW,CAAyE;gBAEhF,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;IAejB,OAAO,CAAC,sBAAsB;YAYhB,YAAY;IA8C1B,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,wBAAwB;IAoBhC,OAAO,CAAC,OAAO;IAgBf,OAAO,IAAI,IAAI;CAWhB"}
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,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,YAAY,CAAwC;IAC5D,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,WAAW,CAAyE;gBAEhF,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;IAgBjB,OAAO,CAAC,sBAAsB;YAYhB,YAAY;IA8C1B,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,wBAAwB;IAoBhC,OAAO,CAAC,OAAO;IAgBf,OAAO,IAAI,IAAI;CAWhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/widget/shadow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpD,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAa;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;gBAEb,KAAK,GAAE,oBAAoB,CAAC,OAAO,CAAU;IAczD,EAAE,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC;IAM3E,MAAM,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAI9B,MAAM,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAM9B,OAAO,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/widget/shadow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpD,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAa;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;gBAEb,KAAK,GAAE,oBAAoB,CAAC,OAAO,CAAU;IAczD,EAAE,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC;IAM3E,MAAM,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAI9B,MAAM,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAM9B,OAAO,IAAI,IAAI;CAMhB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makethisbetter",
3
- "version": "1.5.0",
3
+ "version": "1.5.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",
@@ -198,6 +198,37 @@ describe('ApiClient', () => {
198
198
  expect(key2).toBe(key1)
199
199
  })
200
200
 
201
+ it('streams clarify deltas and resolves the done result', async () => {
202
+ const sse = [
203
+ 'event: delta\ndata: {"text":"Hi "}\n\n',
204
+ 'event: delta\ndata: {"text":"there"}\n\n',
205
+ 'event: done\ndata: {"done":true,"messages":[{"role":"assistant","content":"Hi there"}]}\n\n',
206
+ ].join('')
207
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
208
+ new Response(sse, { status: 200, headers: { 'Content-Type': 'text/event-stream' } }),
209
+ )
210
+
211
+ const client = new ApiClient('key', 'https://api.example.com')
212
+ const deltas: string[] = []
213
+ const result = await client.streamClarification('fb_1', 'hello', (t) => deltas.push(t))
214
+
215
+ expect(deltas).toEqual(['Hi ', 'there'])
216
+ expect(result.done).toBe(true)
217
+ expect(result.messages).toEqual([{ role: 'assistant', content: 'Hi there' }])
218
+ })
219
+
220
+ it('throws from streamClarification when the server does not stream', async () => {
221
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
222
+ new Response(JSON.stringify({ done: false }), {
223
+ status: 200,
224
+ headers: { 'Content-Type': 'application/json' },
225
+ }),
226
+ )
227
+
228
+ const client = new ApiClient('key', 'https://api.example.com')
229
+ await expect(client.streamClarification('fb_1', undefined, () => {})).rejects.toThrow()
230
+ })
231
+
201
232
  it('uses a fresh idempotency key for each separate submission', async () => {
202
233
  const keys: string[] = []
203
234
  vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url, init) => {
package/src/api/client.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { FeedbackPayload, FeedbackResponse, ClarifyResponse } from '../types'
1
+ import type { FeedbackPayload, FeedbackResponse, ClarifyResponse, ClarifyMessage } from '../types'
2
+
3
+ export interface ClarifyStreamResult {
4
+ messages: ClarifyMessage[]
5
+ done: boolean
6
+ }
2
7
 
3
8
  const DEFAULT_API_URL = 'https://makethisbetter.dev/api/v1'
4
9
 
@@ -59,7 +64,8 @@ export class ApiClient {
59
64
  }
60
65
 
61
66
  if (screenshot) {
62
- form.append('feedback[screenshot]', screenshot, 'screenshot.png')
67
+ const ext = screenshot.type === 'image/jpeg' ? 'jpg' : 'png'
68
+ form.append('feedback[screenshot]', screenshot, `screenshot.${ext}`)
63
69
  }
64
70
 
65
71
  const headers = await this.authHeaders()
@@ -125,6 +131,33 @@ export class ApiClient {
125
131
  return res.json() as Promise<ClarifyResponse>
126
132
  }
127
133
 
134
+ // Streams a clarification turn over SSE. Calls onDelta for each token and
135
+ // resolves with the committed result. Throws if the server doesn't stream
136
+ // (e.g. old backend returns JSON) so the caller can fall back to polling.
137
+ async streamClarification(
138
+ feedbackId: string,
139
+ message: string | undefined,
140
+ onDelta: (text: string) => void,
141
+ ): Promise<ClarifyStreamResult> {
142
+ const body: Record<string, string> = {}
143
+ if (message) body.message = message
144
+ const headers = await this.authHeaders('application/json')
145
+ headers['Accept'] = 'text/event-stream'
146
+
147
+ const res = await fetch(`${this.apiUrl}/widget/feedbacks/${feedbackId}/clarify`, {
148
+ method: 'POST',
149
+ headers,
150
+ body: JSON.stringify(body),
151
+ })
152
+
153
+ const contentType = res.headers.get('Content-Type') ?? ''
154
+ if (!res.ok || !res.body || !contentType.includes('text/event-stream')) {
155
+ throw new Error('clarify stream unavailable')
156
+ }
157
+
158
+ return consumeSse(res.body, onDelta)
159
+ }
160
+
128
161
  private async fetchWithRetry(url: string, init: RequestInit, retries = 1): Promise<Response> {
129
162
  let res: Response
130
163
  try {
@@ -151,6 +184,46 @@ function delay(ms: number): Promise<void> {
151
184
  return new Promise(resolve => setTimeout(resolve, ms))
152
185
  }
153
186
 
187
+ async function consumeSse(
188
+ body: ReadableStream<Uint8Array>,
189
+ onDelta: (text: string) => void,
190
+ ): Promise<ClarifyStreamResult> {
191
+ const reader = body.getReader()
192
+ const decoder = new TextDecoder()
193
+ let buffer = ''
194
+ let result: ClarifyStreamResult = { messages: [], done: true }
195
+
196
+ const flush = (raw: string): void => {
197
+ // Each SSE record is "event: <name>\n data: <json>". Parse event + data.
198
+ const eventLine = raw.split('\n').find(l => l.startsWith('event:'))
199
+ const dataLine = raw.split('\n').find(l => l.startsWith('data:'))
200
+ if (!dataLine) return
201
+ const event = eventLine?.slice(6).trim()
202
+ const payload = JSON.parse(dataLine.slice(5).trim())
203
+ if (event === 'delta' && typeof payload.text === 'string') {
204
+ onDelta(payload.text)
205
+ } else if (event === 'done') {
206
+ result = {
207
+ messages: Array.isArray(payload.messages) ? payload.messages : [],
208
+ done: payload.done !== false,
209
+ }
210
+ }
211
+ }
212
+
213
+ for (;;) {
214
+ const { done, value } = await reader.read()
215
+ if (done) break
216
+ buffer += decoder.decode(value, { stream: true })
217
+ let sep: number
218
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
219
+ const record = buffer.slice(0, sep)
220
+ buffer = buffer.slice(sep + 2)
221
+ try { flush(record) } catch { /* skip malformed record */ }
222
+ }
223
+ }
224
+ return result
225
+ }
226
+
154
227
  function generateIdempotencyKey(): string {
155
228
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
156
229
  return crypto.randomUUID()
package/src/i18n/de.ts CHANGED
@@ -54,8 +54,10 @@ export default {
54
54
  max_reached: 'Maximale Aufnahmezeit erreicht',
55
55
  },
56
56
  clarify: {
57
+ title: 'Make This Better AI',
58
+ subtitle: 'Wir schärfen dein Feedback',
57
59
  thinking: 'Einen Moment...',
58
- placeholder: 'Antwort eingeben...',
60
+ placeholder: 'Gib deine Antwort ein — oder Überspringen',
59
61
  send: 'Senden',
60
62
  skip: 'Überspringen',
61
63
  },
package/src/i18n/en.ts CHANGED
@@ -54,8 +54,10 @@ export default {
54
54
  max_reached: 'Maximum recording time reached',
55
55
  },
56
56
  clarify: {
57
+ title: 'Make This Better AI',
58
+ subtitle: 'Sharpening your feedback',
57
59
  thinking: 'Thinking...',
58
- placeholder: 'Type your response...',
60
+ placeholder: 'Type your answer — or Skip',
59
61
  send: 'Send',
60
62
  skip: 'Skip',
61
63
  },
package/src/i18n/es.ts CHANGED
@@ -54,8 +54,10 @@ export default {
54
54
  max_reached: 'Se alcanzó el tiempo máximo de grabación',
55
55
  },
56
56
  clarify: {
57
+ title: 'Make This Better AI',
58
+ subtitle: 'Afinando tus comentarios',
57
59
  thinking: 'Pensando...',
58
- placeholder: 'Escribe tu respuesta...',
60
+ placeholder: 'Escribe tu respuesta — o Omitir',
59
61
  send: 'Enviar',
60
62
  skip: 'Omitir',
61
63
  },
package/src/i18n/fr.ts CHANGED
@@ -54,8 +54,10 @@ export default {
54
54
  max_reached: "Durée maximale d'enregistrement atteinte",
55
55
  },
56
56
  clarify: {
57
+ title: 'Make This Better AI',
58
+ subtitle: 'Affinement de votre retour',
57
59
  thinking: 'Réflexion...',
58
- placeholder: 'Tapez votre réponse...',
60
+ placeholder: 'Saisissez votre réponse — ou Ignorer',
59
61
  send: 'Envoyer',
60
62
  skip: 'Passer',
61
63
  },
package/src/i18n/ja.ts CHANGED
@@ -54,8 +54,10 @@ export default {
54
54
  max_reached: '最大録画時間に達しました',
55
55
  },
56
56
  clarify: {
57
+ title: 'Make This Better AI',
58
+ subtitle: 'フィードバックを整えています',
57
59
  thinking: '考え中...',
58
- placeholder: '回答を入力...',
60
+ placeholder: '回答を入力 — またはスキップ',
59
61
  send: '送信',
60
62
  skip: 'スキップ',
61
63
  },
package/src/i18n/ko.ts CHANGED
@@ -54,8 +54,10 @@ export default {
54
54
  max_reached: '최대 녹화 시간에 도달했습니다',
55
55
  },
56
56
  clarify: {
57
+ title: 'Make This Better AI',
58
+ subtitle: '피드백을 다듬는 중',
57
59
  thinking: '생각 중...',
58
- placeholder: '답변을 입력하세요...',
60
+ placeholder: '답변 입력 — 또는 건너뛰기',
59
61
  send: '보내기',
60
62
  skip: '건너뛰기',
61
63
  },
package/src/i18n/zh-CN.ts CHANGED
@@ -54,8 +54,10 @@ export default {
54
54
  max_reached: '已达到最大录制时长',
55
55
  },
56
56
  clarify: {
57
+ title: '让它更好 AI',
58
+ subtitle: '正在完善你的反馈',
57
59
  thinking: '思考中...',
58
- placeholder: '输入你的回复...',
60
+ placeholder: '输入你的回答 —— 或跳过',
59
61
  send: '发送',
60
62
  skip: '跳过',
61
63
  },
@@ -22,7 +22,7 @@ function mockApiClient(overrides: Partial<ApiClient> = {}): ApiClient {
22
22
  } as unknown as ApiClient
23
23
  }
24
24
 
25
- function setupCard(apiOverrides: Partial<ApiClient> = {}) {
25
+ function setupCard(apiOverrides: Partial<ApiClient> = {}, pos?: { x: number; y: number }) {
26
26
  const shadow = new ShadowContainer()
27
27
  const onDone = vi.fn()
28
28
  const apiClient = mockApiClient(apiOverrides)
@@ -33,6 +33,8 @@ function setupCard(apiOverrides: Partial<ApiClient> = {}) {
33
33
  apiClient,
34
34
  messages,
35
35
  onDone,
36
+ x: pos?.x,
37
+ y: pos?.y,
36
38
  })
37
39
 
38
40
  return { shadow, card, onDone, apiClient, messages }
@@ -76,10 +78,56 @@ describe('ClarifyCard', () => {
76
78
  shadow.destroy()
77
79
  })
78
80
 
79
- it('calls startClarification on construction', async () => {
81
+ it('anchors near the given position instead of a fixed corner', async () => {
82
+ const { shadow, card } = setupCard({}, { x: 400, y: 300 })
83
+ const el = shadow.root.querySelector<HTMLDivElement>('.mtb-clarify')!
84
+ // Positioned via left/top near the point, not pinned to a corner.
85
+ expect(el.style.left).toBe('412px')
86
+ expect(el.style.top).toBe('312px')
87
+ expect(el.style.right).toBe('')
88
+ expect(el.style.bottom).toBe('')
89
+
90
+ card.destroy()
91
+ shadow.destroy()
92
+ })
93
+
94
+ it('clamps to the viewport when the point is near a screen edge', async () => {
95
+ // jsdom default viewport is 1024x768; a point at the far-right/bottom must
96
+ // pull the card back inside so it never overflows off-screen.
97
+ const { shadow, card } = setupCard({}, { x: 1020, y: 760 })
98
+ const el = shadow.root.querySelector<HTMLDivElement>('.mtb-clarify')!
99
+ const left = parseInt(el.style.left, 10)
100
+ const top = parseInt(el.style.top, 10)
101
+ expect(left + 346).toBeLessThanOrEqual(1024)
102
+ expect(top).toBeGreaterThanOrEqual(12)
103
+ expect(top).toBeLessThanOrEqual(768 - 12)
104
+
105
+ card.destroy()
106
+ shadow.destroy()
107
+ })
108
+
109
+ it('renders the redesigned header title and subtitle', async () => {
110
+ const { shadow, card, messages } = setupCard()
111
+ expect(shadow.root.querySelector('.mtb-clarify-title')?.textContent).toBe(messages.clarify.title)
112
+ expect(shadow.root.querySelector('.mtb-clarify-subtitle')?.textContent).toBe(messages.clarify.subtitle)
113
+
114
+ card.destroy()
115
+ shadow.destroy()
116
+ })
117
+
118
+ it('header close button ends the conversation', async () => {
119
+ const { shadow, card, onDone } = setupCard()
120
+ shadow.root.querySelector<HTMLButtonElement>('.mtb-clarify-close')!.click()
121
+ expect(onDone).toHaveBeenCalledOnce()
122
+
123
+ card.destroy()
124
+ shadow.destroy()
125
+ })
126
+
127
+ it('falls back to startClarification when streaming is unavailable', async () => {
80
128
  const { shadow, card, apiClient } = setupCard()
81
129
  await vi.waitFor(() => {
82
- expect(apiClient.startClarification).toHaveBeenCalledWith('fb_123')
130
+ expect(apiClient.startClarification).toHaveBeenCalledWith('fb_123', undefined)
83
131
  })
84
132
 
85
133
  card.destroy()
@@ -120,50 +168,44 @@ describe('ClarifyCard', () => {
120
168
  shadow.destroy()
121
169
  })
122
170
 
123
- it('send button is disabled when input is empty', async () => {
171
+ it('send button stays enabled (green) even when input is empty', async () => {
124
172
  const { shadow, card, apiClient } = setupCard()
125
173
  await vi.waitFor(() => {
126
174
  expect(apiClient.startClarification).toHaveBeenCalled()
127
175
  })
128
176
 
177
+ // The mockup keeps Send green at all times; the empty-input guard lives in
178
+ // the send handler, not a disabled attribute.
129
179
  const sendBtn = shadow.root.querySelector<HTMLButtonElement>('.mtb-clarify-send')!
130
- expect(sendBtn.disabled).toBe(true)
131
-
132
- card.destroy()
133
- shadow.destroy()
134
- })
135
-
136
- it('send button enables when user types text', async () => {
137
- const { shadow, card, apiClient } = setupCard()
138
- await vi.waitFor(() => {
139
- expect(apiClient.startClarification).toHaveBeenCalled()
140
- })
141
-
142
- const input = shadow.root.querySelector<HTMLInputElement>('.mtb-clarify-input')!
143
- const sendBtn = shadow.root.querySelector<HTMLButtonElement>('.mtb-clarify-send')!
144
-
145
- input.value = 'It crashes on click'
146
- input.dispatchEvent(new Event('input'))
147
-
148
180
  expect(sendBtn.disabled).toBe(false)
149
181
 
150
182
  card.destroy()
151
183
  shadow.destroy()
152
184
  })
153
185
 
154
- it('send button stays disabled for whitespace-only input', async () => {
155
- const { shadow, card, apiClient } = setupCard()
186
+ it('does not send when input is empty or whitespace-only', async () => {
187
+ const startClarification = vi.fn().mockResolvedValue({
188
+ status: 'pending',
189
+ messages: [{ role: 'assistant', content: 'Tell me more' }],
190
+ done: false,
191
+ } satisfies ClarifyResponse)
192
+
193
+ const { shadow, card } = setupCard({ startClarification })
156
194
  await vi.waitFor(() => {
157
- expect(apiClient.startClarification).toHaveBeenCalled()
195
+ expect(startClarification).toHaveBeenCalledOnce()
158
196
  })
197
+ await vi.advanceTimersByTimeAsync(0)
159
198
 
160
- const input = shadow.root.querySelector<HTMLInputElement>('.mtb-clarify-input')!
199
+ const input = shadow.root.querySelector<HTMLTextAreaElement>('.mtb-clarify-input')!
161
200
  const sendBtn = shadow.root.querySelector<HTMLButtonElement>('.mtb-clarify-send')!
162
201
 
163
202
  input.value = ' '
164
203
  input.dispatchEvent(new Event('input'))
204
+ sendBtn.click()
165
205
 
166
- expect(sendBtn.disabled).toBe(true)
206
+ // No second turn — whitespace is guarded in the send handler.
207
+ await vi.advanceTimersByTimeAsync(0)
208
+ expect(startClarification).toHaveBeenCalledOnce()
167
209
 
168
210
  card.destroy()
169
211
  shadow.destroy()
@@ -498,6 +540,73 @@ describe('ClarifyCard', () => {
498
540
  shadow.destroy()
499
541
  })
500
542
 
543
+ it('renders streamed deltas live and completes on the stream result', async () => {
544
+ const streamClarification = vi.fn().mockImplementation(
545
+ async (_id: string, _msg: string | undefined, onDelta: (t: string) => void) => {
546
+ onDelta('What ')
547
+ onDelta('broke?')
548
+ return {
549
+ messages: [{ role: 'assistant', content: 'What broke?' }],
550
+ done: false,
551
+ }
552
+ },
553
+ )
554
+
555
+ const { shadow, card } = setupCard({ streamClarification } as never)
556
+ await vi.waitFor(() => {
557
+ expect(streamClarification).toHaveBeenCalledWith('fb_123', undefined, expect.any(Function))
558
+ })
559
+ await vi.advanceTimersByTimeAsync(0)
560
+
561
+ expect(shadow.root.querySelector('.mtb-clarify-thinking')).toBeNull()
562
+ expect(shadow.root.querySelector('.mtb-clarify-ai')?.textContent).toBe('What broke?')
563
+
564
+ card.destroy()
565
+ shadow.destroy()
566
+ })
567
+
568
+ it('completes via streaming done and stops without polling', async () => {
569
+ const streamClarification = vi.fn().mockResolvedValue({
570
+ messages: [{ role: 'assistant', content: 'All clear, thanks!' }],
571
+ done: true,
572
+ })
573
+ const getClarification = vi.fn()
574
+
575
+ const { shadow, card, onDone } = setupCard({ streamClarification, getClarification } as never)
576
+ await vi.waitFor(() => {
577
+ expect(streamClarification).toHaveBeenCalled()
578
+ })
579
+ await vi.advanceTimersByTimeAsync(0)
580
+
581
+ await vi.advanceTimersByTimeAsync(2000)
582
+ expect(onDone).toHaveBeenCalledOnce()
583
+ expect(getClarification).not.toHaveBeenCalled()
584
+
585
+ card.destroy()
586
+ shadow.destroy()
587
+ })
588
+
589
+ it('ends the conversation when polling exceeds the timeout with no reply', async () => {
590
+ const getClarification = vi.fn().mockResolvedValue({
591
+ status: 'pending',
592
+ messages: [],
593
+ done: false,
594
+ } satisfies ClarifyResponse)
595
+
596
+ const { shadow, card, onDone } = setupCard({ getClarification })
597
+ await vi.waitFor(() => {
598
+ expect(card).toBeTruthy()
599
+ })
600
+ await vi.advanceTimersByTimeAsync(0)
601
+
602
+ // Poll past the 45s deadline; the card should give up and call onDone.
603
+ await vi.advanceTimersByTimeAsync(48000)
604
+ expect(onDone).toHaveBeenCalledOnce()
605
+
606
+ card.destroy()
607
+ shadow.destroy()
608
+ })
609
+
501
610
  it('stops polling on destroy', async () => {
502
611
  const getClarification = vi.fn().mockResolvedValue({
503
612
  status: 'pending',