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.
- package/dist/api/client.d.ts +6 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/i18n/de.d.ts +2 -0
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/es.d.ts +2 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/fr.d.ts +2 -0
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/ja.d.ts +2 -0
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ko.d.ts +2 -0
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/zh-CN.d.ts +2 -0
- package/dist/i18n/zh-CN.d.ts.map +1 -1
- package/dist/makethisbetter.cjs +36 -15
- package/dist/makethisbetter.cjs.map +1 -1
- package/dist/makethisbetter.esm.js +480 -356
- package/dist/makethisbetter.esm.js.map +1 -1
- package/dist/makethisbetter.js +36 -15
- package/dist/makethisbetter.js.map +1 -1
- package/dist/popup/clarify.d.ts +10 -0
- package/dist/popup/clarify.d.ts.map +1 -1
- package/dist/screenshot/capture.d.ts.map +1 -1
- package/dist/widget/controller.d.ts +1 -0
- package/dist/widget/controller.d.ts.map +1 -1
- package/dist/widget/shadow.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/client.test.ts +31 -0
- package/src/api/client.ts +75 -2
- package/src/i18n/de.ts +3 -1
- package/src/i18n/en.ts +3 -1
- package/src/i18n/es.ts +3 -1
- package/src/i18n/fr.ts +3 -1
- package/src/i18n/ja.ts +3 -1
- package/src/i18n/ko.ts +3 -1
- package/src/i18n/zh-CN.ts +3 -1
- package/src/popup/clarify.test.ts +136 -27
- package/src/popup/clarify.ts +128 -42
- package/src/screenshot/capture.test.ts +14 -12
- package/src/screenshot/capture.ts +8 -2
- package/src/styles/widget.css +137 -57
- package/src/widget/controller.test.ts +45 -0
- package/src/widget/controller.ts +11 -5
- package/src/widget/shadow.test.ts +19 -0
- package/src/widget/shadow.ts +4 -1
package/dist/popup/clarify.d.ts
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
|
@@ -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;
|
|
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;
|
|
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
package/src/api/client.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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: '
|
|
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
|
@@ -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('
|
|
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
|
|
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
|
|
155
|
-
const
|
|
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(
|
|
195
|
+
expect(startClarification).toHaveBeenCalledOnce()
|
|
158
196
|
})
|
|
197
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
159
198
|
|
|
160
|
-
const input = shadow.root.querySelector<
|
|
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
|
-
|
|
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',
|