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
@@ -2,25 +2,34 @@ import type { ShadowContainer } from '../widget/shadow'
2
2
  import type { I18nMessages } from '../i18n'
3
3
  import type { ApiClient } from '../api/client'
4
4
  import type { ClarifyMessage } from '../types'
5
- import { BRAND_COLOR } from '../styles'
6
5
 
7
6
  const POLL_INTERVAL_MS = 2000
8
7
  const DONE_DELAY_MS = 2000
8
+ // Give up polling after this long with no assistant reply so the card never
9
+ // spins forever (e.g. a backend job that silently died).
10
+ const POLL_TIMEOUT_MS = 45000
9
11
 
10
12
  interface ClarifyCardOptions {
11
13
  feedbackId: string
12
14
  apiClient: ApiClient
13
15
  messages: I18nMessages
14
16
  onDone: () => void
17
+ x?: number
18
+ y?: number
15
19
  }
16
20
 
21
+ const CARD_WIDTH = 346
22
+ const CARD_HEIGHT = 320
23
+
17
24
  export class ClarifyCard {
18
25
  private el: HTMLDivElement
19
26
  private messagesEl!: HTMLDivElement
20
- private inputEl!: HTMLInputElement
27
+ private inputEl!: HTMLTextAreaElement
21
28
  private sendBtn!: HTMLButtonElement
22
29
  private pollTimer: ReturnType<typeof setInterval> | null = null
30
+ private pollDeadline = 0
23
31
  private doneTimer: ReturnType<typeof setTimeout> | null = null
32
+ private streamingBubble: HTMLDivElement | null = null
24
33
  private feedbackId: string
25
34
  private apiClient: ApiClient
26
35
  private onDone: () => void
@@ -33,9 +42,8 @@ export class ClarifyCard {
33
42
  this.onDone = opts.onDone
34
43
 
35
44
  this.el = shadow.el<HTMLDivElement>('div', 'mtb-clarify')
36
- this.el.style.right = '20px'
37
- this.el.style.bottom = '20px'
38
45
  this.el.innerHTML = this.buildHTML(opts.messages)
46
+ this.positionCard(opts.x, opts.y)
39
47
  shadow.append(this.el)
40
48
 
41
49
  this.messagesEl = this.el.querySelector('.mtb-clarify-messages')!
@@ -46,39 +54,78 @@ export class ClarifyCard {
46
54
  this.startConversation()
47
55
  }
48
56
 
57
+ // Anchors the card near the annotation point, clamped to the viewport so it
58
+ // never overflows a screen edge (mirrors CommentPopup.positionPopup).
59
+ private positionCard(x?: number, y?: number): void {
60
+ const vw = window.innerWidth
61
+ const vh = window.innerHeight
62
+ const margin = 12
63
+
64
+ if (x === undefined || y === undefined) {
65
+ this.el.style.left = `${Math.max(margin, (vw - CARD_WIDTH) / 2)}px`
66
+ this.el.style.top = `${Math.max(margin, (vh - CARD_HEIGHT) / 2)}px`
67
+ return
68
+ }
69
+
70
+ let left = x + 12
71
+ let top = y + 12
72
+ if (left + CARD_WIDTH > vw - margin) left = x - CARD_WIDTH - 12
73
+ if (left < margin) left = margin
74
+ if (top + CARD_HEIGHT > vh - margin) top = vh - CARD_HEIGHT - margin
75
+ if (top < margin) top = margin
76
+
77
+ this.el.style.left = `${left}px`
78
+ this.el.style.top = `${top}px`
79
+ }
80
+
49
81
  private buildHTML(m: I18nMessages): string {
50
82
  return `
51
83
  <div class="mtb-clarify-header">
52
- <div class="mtb-clarify-icon">
53
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${BRAND_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
54
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
84
+ <span class="mtb-clarify-icon">
85
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
86
+ <path d="M12 3l1.6 5.4L19 10l-5.4 1.6L12 17l-1.6-5.4L5 10l5.4-1.6L12 3z"/>
55
87
  </svg>
88
+ </span>
89
+ <div class="mtb-clarify-titles">
90
+ <div class="mtb-clarify-title">${escapeHtml(m.clarify.title)}</div>
91
+ <div class="mtb-clarify-subtitle">${escapeHtml(m.clarify.subtitle)}</div>
56
92
  </div>
57
- <button class="mtb-clarify-skip">${escapeHtml(m.clarify.skip)}</button>
93
+ <button class="mtb-clarify-close" type="button" title="${escapeHtml(m.clarify.skip)}" aria-label="${escapeHtml(m.clarify.skip)}">
94
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
95
+ <path d="M18 6L6 18M6 6l12 12"/>
96
+ </svg>
97
+ </button>
58
98
  </div>
59
99
  <div class="mtb-clarify-messages"></div>
60
100
  <div class="mtb-clarify-footer">
61
- <input class="mtb-clarify-input" type="text" placeholder="${escapeHtml(m.clarify.placeholder)}" />
62
- <button class="mtb-clarify-send" disabled>${escapeHtml(m.clarify.send)}</button>
101
+ <textarea class="mtb-clarify-input" rows="2" placeholder="${escapeHtml(m.clarify.placeholder)}"></textarea>
102
+ <div class="mtb-clarify-actions">
103
+ <button class="mtb-clarify-skip" type="button">${escapeHtml(m.clarify.skip)}</button>
104
+ <button class="mtb-clarify-send" type="button">
105
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
106
+ <line x1="22" y1="2" x2="11" y2="13"/>
107
+ <polygon points="22 2 15 22 11 13 2 9 22 2"/>
108
+ </svg>
109
+ <span>${escapeHtml(m.clarify.send)}</span>
110
+ </button>
111
+ </div>
63
112
  </div>
64
113
  `
65
114
  }
66
115
 
67
116
  private bindEvents(m: I18nMessages): void {
68
- this.el.querySelector('.mtb-clarify-skip')!.addEventListener('click', () => {
117
+ const skip = () => {
69
118
  this.stopPolling()
70
119
  this.onDone()
71
- })
72
-
73
- this.inputEl.addEventListener('input', () => {
74
- const hasText = this.inputEl.value.trim().length > 0
75
- this.sendBtn.disabled = !hasText || this.sending
76
- })
120
+ }
121
+ this.el.querySelector('.mtb-clarify-skip')!.addEventListener('click', skip)
122
+ this.el.querySelector('.mtb-clarify-close')!.addEventListener('click', skip)
77
123
 
78
124
  this.inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
79
125
  // keyCode 229: IME composition fallback for browsers without isComposing
80
126
  if (e.isComposing || e.keyCode === 229) return
81
- if (e.key === 'Enter') {
127
+ // Enter sends; Shift+Enter inserts a newline in the textarea.
128
+ if (e.key === 'Enter' && !e.shiftKey) {
82
129
  e.preventDefault()
83
130
  this.sendMessage(m)
84
131
  }
@@ -89,22 +136,11 @@ export class ClarifyCard {
89
136
 
90
137
  private async startConversation(): Promise<void> {
91
138
  this.showThinking()
92
- try {
93
- const res = await this.apiClient.startClarification(this.feedbackId)
94
- if (this.destroyed) return
95
- this.renderConversation(res.messages, res.done)
96
- if (res.done) {
97
- this.handleDone()
98
- } else {
99
- this.startPolling()
100
- }
101
- } catch {
102
- if (this.destroyed) return
103
- this.onDone()
104
- }
139
+ await this.runTurn(undefined)
105
140
  }
106
141
 
107
142
  private async sendMessage(m: I18nMessages): Promise<void> {
143
+ void m
108
144
  const text = this.inputEl.value.trim()
109
145
  if (!text || this.sending) return
110
146
 
@@ -118,27 +154,67 @@ export class ClarifyCard {
118
154
  this.showThinking()
119
155
 
120
156
  try {
121
- const res = await this.apiClient.startClarification(this.feedbackId, text)
157
+ await this.runTurn(text)
158
+ } finally {
159
+ this.sending = false
160
+ this.inputEl.disabled = false
161
+ this.sendBtn.disabled = false
162
+ this.inputEl.focus()
163
+ }
164
+ }
165
+
166
+ // Streams the assistant reply; falls back to POST + polling if the backend
167
+ // doesn't support SSE (older server) or the stream errors mid-flight.
168
+ private async runTurn(message: string | undefined): Promise<void> {
169
+ try {
170
+ const result = await this.apiClient.streamClarification(
171
+ this.feedbackId,
172
+ message,
173
+ (text) => this.appendStreamDelta(text),
174
+ )
175
+ if (this.destroyed) return
176
+ this.endStreamingBubble()
177
+ this.renderConversation(result.messages, result.done)
178
+ if (result.done) this.handleDone()
179
+ else this.startPolling()
180
+ } catch {
181
+ if (this.destroyed) return
182
+ this.endStreamingBubble()
183
+ await this.runTurnViaPolling(message)
184
+ }
185
+ }
186
+
187
+ private async runTurnViaPolling(message: string | undefined): Promise<void> {
188
+ try {
189
+ const res = await this.apiClient.startClarification(this.feedbackId, message)
122
190
  if (this.destroyed) return
123
191
  this.renderConversation(res.messages, res.done)
124
- if (res.done) {
125
- this.handleDone()
126
- } else {
127
- this.startPolling()
128
- }
192
+ if (res.done) this.handleDone()
193
+ else this.startPolling()
129
194
  } catch {
130
195
  if (this.destroyed) return
131
196
  this.onDone()
132
- } finally {
133
- this.sending = false
134
- this.inputEl.disabled = false
135
- this.sendBtn.disabled = this.inputEl.value.trim().length === 0
136
- this.inputEl.focus()
137
197
  }
138
198
  }
139
199
 
200
+ private appendStreamDelta(text: string): void {
201
+ if (!this.streamingBubble) {
202
+ this.removeThinking()
203
+ this.streamingBubble = document.createElement('div')
204
+ this.streamingBubble.className = 'mtb-clarify-bubble mtb-clarify-ai'
205
+ this.messagesEl.appendChild(this.streamingBubble)
206
+ }
207
+ this.streamingBubble.textContent = (this.streamingBubble.textContent ?? '') + text
208
+ this.messagesEl.scrollTop = this.messagesEl.scrollHeight
209
+ }
210
+
211
+ private endStreamingBubble(): void {
212
+ this.streamingBubble = null
213
+ }
214
+
140
215
  private startPolling(): void {
141
216
  this.stopPolling()
217
+ this.pollDeadline = Date.now() + POLL_TIMEOUT_MS
142
218
  this.pollTimer = setInterval(() => this.poll(), POLL_INTERVAL_MS)
143
219
  }
144
220
 
@@ -150,6 +226,11 @@ export class ClarifyCard {
150
226
  }
151
227
 
152
228
  private async poll(): Promise<void> {
229
+ if (Date.now() > this.pollDeadline) {
230
+ this.stopPolling()
231
+ this.handleDone()
232
+ return
233
+ }
153
234
  try {
154
235
  const res = await this.apiClient.getClarification(this.feedbackId)
155
236
  if (this.destroyed) return
@@ -195,6 +276,7 @@ export class ClarifyCard {
195
276
  }
196
277
 
197
278
  private showThinking(): void {
279
+ if (this.messagesEl.querySelector('.mtb-clarify-thinking')) return
198
280
  const dots = document.createElement('div')
199
281
  dots.className = 'mtb-clarify-bubble mtb-clarify-ai mtb-clarify-thinking'
200
282
  dots.innerHTML = '<span class="mtb-dot"></span><span class="mtb-dot"></span><span class="mtb-dot"></span>'
@@ -202,6 +284,10 @@ export class ClarifyCard {
202
284
  this.messagesEl.scrollTop = this.messagesEl.scrollHeight
203
285
  }
204
286
 
287
+ private removeThinking(): void {
288
+ this.messagesEl.querySelector('.mtb-clarify-thinking')?.remove()
289
+ }
290
+
205
291
  destroy(): void {
206
292
  this.destroyed = true
207
293
  this.stopPolling()
@@ -1,28 +1,30 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest'
2
- import { toPng } from 'html-to-image'
2
+ import { toJpeg } from 'html-to-image'
3
3
  import { captureScreenshot } from './capture'
4
4
  import type { Annotation } from '../types'
5
5
 
6
6
  vi.mock('html-to-image', () => ({
7
- toPng: vi.fn(async () => 'data:image/png;base64,AA=='),
7
+ toJpeg: vi.fn(async () => 'data:image/jpeg;base64,/9j/AA=='),
8
8
  }))
9
9
 
10
- const toPngMock = vi.mocked(toPng)
10
+ const toJpegMock = vi.mocked(toJpeg)
11
11
 
12
12
  describe('captureScreenshot', () => {
13
13
  afterEach(() => {
14
14
  document.body.innerHTML = ''
15
- toPngMock.mockClear()
15
+ toJpegMock.mockClear()
16
16
  })
17
17
 
18
- it('captures a png blob', async () => {
18
+ it('captures a compressed jpeg blob', async () => {
19
19
  const blob = await captureScreenshot()
20
20
 
21
21
  expect(blob).toBeInstanceOf(Blob)
22
- expect(blob?.type).toBe('image/png')
23
- expect(toPngMock).toHaveBeenCalledWith(
22
+ expect(blob?.type).toBe('image/jpeg')
23
+ expect(toJpegMock).toHaveBeenCalledWith(
24
24
  document.body,
25
25
  expect.objectContaining({
26
+ quality: 0.85,
27
+ pixelRatio: 1,
26
28
  cacheBust: true,
27
29
  skipAutoScale: false,
28
30
  }),
@@ -31,9 +33,9 @@ describe('captureScreenshot', () => {
31
33
 
32
34
  it('mounts pin annotations during capture and removes them after capture', async () => {
33
35
  let captureText = ''
34
- toPngMock.mockImplementationOnce(async () => {
36
+ toJpegMock.mockImplementationOnce(async () => {
35
37
  captureText = document.querySelector<HTMLElement>('[data-mtb-capture-annotations]')?.textContent ?? ''
36
- return 'data:image/png;base64,AA=='
38
+ return 'data:image/jpeg;base64,/9j/AA=='
37
39
  })
38
40
 
39
41
  const annotation: Annotation = { type: 'pin', x: 40, y: 60 }
@@ -45,9 +47,9 @@ describe('captureScreenshot', () => {
45
47
 
46
48
  it('mounts draw annotations during capture', async () => {
47
49
  let drawPath: string | null = null
48
- toPngMock.mockImplementationOnce(async () => {
50
+ toJpegMock.mockImplementationOnce(async () => {
49
51
  drawPath = document.querySelector('[data-mtb-capture-annotations] path')?.getAttribute('d') ?? null
50
- return 'data:image/png;base64,AA=='
52
+ return 'data:image/jpeg;base64,/9j/AA=='
51
53
  })
52
54
 
53
55
  const annotation: Annotation = { type: 'draw', x: 50, y: 50, drawPath: 'M 10 10 L 90 90' }
@@ -57,7 +59,7 @@ describe('captureScreenshot', () => {
57
59
  })
58
60
 
59
61
  it('returns null and cleans up if capture fails', async () => {
60
- toPngMock.mockRejectedValueOnce(new Error('capture failed'))
62
+ toJpegMock.mockRejectedValueOnce(new Error('capture failed'))
61
63
 
62
64
  const annotation: Annotation = { type: 'pin', x: 40, y: 60 }
63
65
  const blob = await captureScreenshot([annotation])
@@ -1,11 +1,17 @@
1
- import { toPng } from 'html-to-image'
1
+ import { toJpeg } from 'html-to-image'
2
2
  import type { Annotation } from '../types'
3
3
 
4
+ // Capture at CSS pixels (pixelRatio 1) as JPEG to keep the upload small: on
5
+ // retina screens the default devicePixelRatio would quadruple the pixel count,
6
+ // and the model downscales past ~1568px anyway.
4
7
  export async function captureScreenshot(annotations: Annotation[] = []): Promise<Blob | null> {
5
8
  const annotationLayer = mountCaptureAnnotations(annotations)
6
9
 
7
10
  try {
8
- const dataUrl = await toPng(document.body, {
11
+ const dataUrl = await toJpeg(document.body, {
12
+ quality: 0.85,
13
+ pixelRatio: 1,
14
+ backgroundColor: '#ffffff',
9
15
  cacheBust: true,
10
16
  skipAutoScale: false,
11
17
  filter: (node) => {
@@ -934,142 +934,191 @@
934
934
  .mtb-clarify {
935
935
  position: fixed;
936
936
  z-index: 2147483645;
937
- width: 320px;
937
+ width: 346px;
938
938
  background: #fff;
939
939
  border: 1px solid rgba(0, 0, 0, 0.1);
940
- border-radius: 16px;
941
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
940
+ border-radius: 14px;
941
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.16);
942
942
  display: flex;
943
943
  flex-direction: column;
944
- animation: mtbSlideUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
944
+ animation: mtbSlideUp 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
945
945
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
946
946
  overflow: hidden;
947
947
  }
948
948
 
949
949
  .mtb-clarify-header {
950
- padding: 10px 13px;
950
+ padding: 11px 13px 11px 14px;
951
951
  display: flex;
952
952
  align-items: center;
953
- justify-content: space-between;
953
+ gap: 9px;
954
954
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
955
955
  }
956
956
 
957
957
  .mtb-clarify-icon {
958
- width: 26px;
959
- height: 26px;
960
- border-radius: 50%;
961
- background: rgba(5, 150, 105, 0.08);
958
+ width: 24px;
959
+ height: 24px;
960
+ border-radius: 7px;
961
+ background: linear-gradient(140deg, #10b981, #059669);
962
+ box-shadow: 0 1px 3px rgba(5, 150, 105, 0.4);
962
963
  display: flex;
963
964
  align-items: center;
964
965
  justify-content: center;
965
966
  flex-shrink: 0;
966
967
  }
967
968
 
968
- .mtb-clarify-skip {
969
+ .mtb-clarify-titles {
970
+ flex: 1;
971
+ min-width: 0;
972
+ }
973
+
974
+ .mtb-clarify-title {
975
+ font-size: 12.5px;
976
+ font-weight: 650;
977
+ line-height: 1.2;
978
+ color: #191a1c;
979
+ }
980
+
981
+ .mtb-clarify-subtitle {
982
+ font-size: 10px;
983
+ color: rgba(0, 0, 0, 0.42);
984
+ }
985
+
986
+ .mtb-clarify-close {
969
987
  background: none;
970
988
  border: none;
971
989
  cursor: pointer;
972
- font-size: 12px;
973
- color: rgba(0, 0, 0, 0.45);
974
- font-family: inherit;
990
+ padding: 3px;
991
+ color: rgba(0, 0, 0, 0.38);
992
+ display: flex;
975
993
  transition: color 0.12s;
976
994
  }
977
995
 
978
- .mtb-clarify-skip:hover {
996
+ .mtb-clarify-close:hover {
979
997
  color: rgba(0, 0, 0, 0.7);
980
998
  }
981
999
 
982
1000
  .mtb-clarify-messages {
983
1001
  flex: 1;
984
- max-height: 240px;
1002
+ max-height: 250px;
985
1003
  overflow-y: auto;
986
- padding: 12px 13px;
1004
+ padding: 13px 13px 5px;
987
1005
  display: flex;
988
1006
  flex-direction: column;
989
- gap: 8px;
1007
+ gap: 9px;
990
1008
  }
991
1009
 
992
1010
  .mtb-clarify-bubble {
993
- max-width: 85%;
994
- padding: 8px 12px;
995
- border-radius: 12px;
996
- font-size: 13px;
1011
+ max-width: 82%;
1012
+ padding: 9px 12px;
1013
+ font-size: 12.5px;
997
1014
  line-height: 1.5;
998
- word-wrap: break-word;
1015
+ white-space: pre-wrap;
1016
+ word-break: break-word;
999
1017
  }
1000
1018
 
1001
1019
  .mtb-clarify-ai {
1002
1020
  align-self: flex-start;
1003
- background: #f3f4f6;
1021
+ background: #ffffff;
1004
1022
  color: #191a1c;
1005
- border-bottom-left-radius: 4px;
1023
+ border: 1px solid rgba(0, 0, 0, 0.09);
1024
+ border-radius: 13px 13px 13px 4px;
1006
1025
  }
1007
1026
 
1008
1027
  .mtb-clarify-user {
1009
1028
  align-self: flex-end;
1010
1029
  background: #059669;
1011
1030
  color: #fff;
1012
- border-bottom-right-radius: 4px;
1031
+ border: 1px solid #059669;
1032
+ border-radius: 13px 13px 4px 13px;
1013
1033
  }
1014
1034
 
1015
1035
  .mtb-clarify-thinking {
1016
1036
  display: flex;
1017
1037
  align-items: center;
1018
- gap: 3px;
1019
- padding: 10px 16px;
1038
+ gap: 4px;
1039
+ padding: 11px 14px;
1020
1040
  }
1021
1041
 
1022
1042
  .mtb-clarify-thinking .mtb-dot {
1023
- background: #9ca3af;
1043
+ width: 6px;
1044
+ height: 6px;
1045
+ background: rgba(5, 150, 105, 0.7);
1024
1046
  }
1025
1047
 
1026
1048
  .mtb-clarify-footer {
1027
- padding: 10px 13px;
1028
- border-top: 1px solid rgba(0, 0, 0, 0.06);
1049
+ padding: 3px 13px 13px;
1029
1050
  display: flex;
1030
- gap: 8px;
1051
+ flex-direction: column;
1031
1052
  }
1032
1053
 
1033
1054
  .mtb-clarify-input {
1034
- flex: 1;
1035
- height: 32px;
1036
- padding: 0 10px;
1055
+ width: 100%;
1056
+ height: 52px;
1057
+ padding: 9px 11px;
1037
1058
  border: 1px solid rgba(0, 0, 0, 0.12);
1038
- border-radius: 8px;
1059
+ border-radius: 9px;
1039
1060
  font-family: inherit;
1040
- font-size: 12.5px;
1061
+ font-size: 13px;
1062
+ line-height: 1.5;
1041
1063
  color: #191a1c;
1042
1064
  outline: none;
1043
- background: #fff;
1044
- transition: border-color 0.12s, box-shadow 0.12s;
1065
+ background: #fafafa;
1066
+ resize: none;
1067
+ box-sizing: border-box;
1068
+ transition: border-color 0.12s, box-shadow 0.12s, background 0.12s;
1045
1069
  }
1046
1070
 
1047
1071
  .mtb-clarify-input:focus {
1048
1072
  border-color: #10b981;
1049
1073
  box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12);
1074
+ background: #fff;
1050
1075
  }
1051
1076
 
1052
1077
  .mtb-clarify-input::placeholder {
1053
1078
  color: rgba(0, 0, 0, 0.34);
1054
1079
  }
1055
1080
 
1056
- .mtb-clarify-send {
1057
- height: 32px;
1081
+ .mtb-clarify-actions {
1082
+ display: flex;
1083
+ gap: 8px;
1084
+ margin-top: 9px;
1085
+ }
1086
+
1087
+ .mtb-clarify-skip {
1088
+ flex: 0 0 auto;
1089
+ height: 34px;
1058
1090
  padding: 0 14px;
1059
- border: none;
1060
- border-radius: 8px;
1061
- background: #d1d5db;
1062
- color: #fff;
1091
+ border: 1px solid rgba(0, 0, 0, 0.12);
1092
+ border-radius: 9px;
1093
+ background: #fff;
1063
1094
  cursor: pointer;
1064
1095
  font-family: inherit;
1065
- font-size: 12px;
1066
- font-weight: 600;
1096
+ font-size: 12.5px;
1097
+ font-weight: 550;
1098
+ color: rgba(0, 0, 0, 0.55);
1067
1099
  transition: background 0.12s;
1068
- flex-shrink: 0;
1069
1100
  }
1070
1101
 
1071
- .mtb-clarify-send:not(:disabled) {
1102
+ .mtb-clarify-skip:hover {
1103
+ background: rgba(0, 0, 0, 0.04);
1104
+ }
1105
+
1106
+ .mtb-clarify-send {
1107
+ flex: 1;
1108
+ height: 34px;
1109
+ display: inline-flex;
1110
+ align-items: center;
1111
+ justify-content: center;
1112
+ gap: 6px;
1113
+ border: none;
1114
+ border-radius: 9px;
1072
1115
  background: #059669;
1116
+ color: #fff;
1117
+ cursor: pointer;
1118
+ font-family: inherit;
1119
+ font-size: 12.5px;
1120
+ font-weight: 600;
1121
+ transition: background 0.12s;
1073
1122
  }
1074
1123
 
1075
1124
  .mtb-clarify-send:not(:disabled):hover {
@@ -1077,6 +1126,7 @@
1077
1126
  }
1078
1127
 
1079
1128
  .mtb-clarify-send:disabled {
1129
+ background: rgba(0, 0, 0, 0.15);
1080
1130
  cursor: not-allowed;
1081
1131
  }
1082
1132
 
@@ -1090,20 +1140,35 @@
1090
1140
  border-bottom-color: rgba(255, 255, 255, 0.1);
1091
1141
  }
1092
1142
 
1093
- :host([data-mtb-theme="dark"]) .mtb-clarify-footer {
1094
- border-top-color: rgba(255, 255, 255, 0.1);
1143
+ :host([data-mtb-theme="dark"]) .mtb-clarify-title {
1144
+ color: #f9fafb;
1095
1145
  }
1096
1146
 
1097
- :host([data-mtb-theme="dark"]) .mtb-clarify-skip {
1147
+ :host([data-mtb-theme="dark"]) .mtb-clarify-subtitle {
1098
1148
  color: rgba(255, 255, 255, 0.45);
1099
1149
  }
1100
1150
 
1101
- :host([data-mtb-theme="dark"]) .mtb-clarify-skip:hover {
1151
+ :host([data-mtb-theme="dark"]) .mtb-clarify-close {
1152
+ color: rgba(255, 255, 255, 0.45);
1153
+ }
1154
+
1155
+ :host([data-mtb-theme="dark"]) .mtb-clarify-close:hover {
1156
+ color: rgba(255, 255, 255, 0.75);
1157
+ }
1158
+
1159
+ :host([data-mtb-theme="dark"]) .mtb-clarify-skip {
1160
+ background: #1f2327;
1161
+ border-color: rgba(255, 255, 255, 0.14);
1102
1162
  color: rgba(255, 255, 255, 0.7);
1103
1163
  }
1104
1164
 
1165
+ :host([data-mtb-theme="dark"]) .mtb-clarify-skip:hover {
1166
+ background: rgba(255, 255, 255, 0.06);
1167
+ }
1168
+
1105
1169
  :host([data-mtb-theme="dark"]) .mtb-clarify-ai {
1106
1170
  background: #1f2327;
1171
+ border-color: rgba(255, 255, 255, 0.12);
1107
1172
  color: #f9fafb;
1108
1173
  }
1109
1174
 
@@ -1124,20 +1189,35 @@
1124
1189
  border-bottom-color: rgba(255, 255, 255, 0.1);
1125
1190
  }
1126
1191
 
1127
- :host([data-mtb-theme="auto"]) .mtb-clarify-footer {
1128
- border-top-color: rgba(255, 255, 255, 0.1);
1192
+ :host([data-mtb-theme="auto"]) .mtb-clarify-title {
1193
+ color: #f9fafb;
1129
1194
  }
1130
1195
 
1131
- :host([data-mtb-theme="auto"]) .mtb-clarify-skip {
1196
+ :host([data-mtb-theme="auto"]) .mtb-clarify-subtitle {
1132
1197
  color: rgba(255, 255, 255, 0.45);
1133
1198
  }
1134
1199
 
1135
- :host([data-mtb-theme="auto"]) .mtb-clarify-skip:hover {
1200
+ :host([data-mtb-theme="auto"]) .mtb-clarify-close {
1201
+ color: rgba(255, 255, 255, 0.45);
1202
+ }
1203
+
1204
+ :host([data-mtb-theme="auto"]) .mtb-clarify-close:hover {
1205
+ color: rgba(255, 255, 255, 0.75);
1206
+ }
1207
+
1208
+ :host([data-mtb-theme="auto"]) .mtb-clarify-skip {
1209
+ background: #1f2327;
1210
+ border-color: rgba(255, 255, 255, 0.14);
1136
1211
  color: rgba(255, 255, 255, 0.7);
1137
1212
  }
1138
1213
 
1214
+ :host([data-mtb-theme="auto"]) .mtb-clarify-skip:hover {
1215
+ background: rgba(255, 255, 255, 0.06);
1216
+ }
1217
+
1139
1218
  :host([data-mtb-theme="auto"]) .mtb-clarify-ai {
1140
1219
  background: #1f2327;
1220
+ border-color: rgba(255, 255, 255, 0.12);
1141
1221
  color: #f9fafb;
1142
1222
  }
1143
1223