@customerhero/js 0.0.1 → 1.0.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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/defaults.ts
2
2
  var DEFAULTS = {
3
- apiBase: "https://customerhero.app",
3
+ apiBase: "https://api.customerhero.app",
4
4
  primaryColor: "#6C3CE1",
5
5
  backgroundColor: "#FFFFFF",
6
6
  textColor: "#1A1A2E",
@@ -10,7 +10,7 @@ var DEFAULTS = {
10
10
  title: "CustomerHero"
11
11
  };
12
12
 
13
- // src/i18n.ts
13
+ // src/i18n/locales/en.ts
14
14
  var en = {
15
15
  online: "Online",
16
16
  typing: "Typing...",
@@ -22,8 +22,20 @@ var en = {
22
22
  send_message: "Send message",
23
23
  helpful: "Helpful",
24
24
  not_helpful: "Not helpful",
25
- menu: "Menu"
25
+ menu: "Menu",
26
+ action_approve: "Approve",
27
+ action_cancel: "Cancel",
28
+ action_what_will_happen: "What will happen?",
29
+ action_already_resolved: "This action has already been resolved.",
30
+ action_failed: "Couldn't complete that action. Please try again.",
31
+ status_sending: "Sending",
32
+ status_sent: "Sent",
33
+ status_failed: "Failed to send",
34
+ screenshot_capture: "Capture screenshot",
35
+ attachment_remove: "Remove attachment"
26
36
  };
37
+
38
+ // src/i18n/locales/es.ts
27
39
  var es = {
28
40
  online: "En l\xEDnea",
29
41
  typing: "Escribiendo...",
@@ -35,25 +47,471 @@ var es = {
35
47
  send_message: "Enviar mensaje",
36
48
  helpful: "\xDAtil",
37
49
  not_helpful: "No \xFAtil",
38
- menu: "Men\xFA"
50
+ menu: "Men\xFA",
51
+ action_approve: "Aprobar",
52
+ action_cancel: "Cancelar",
53
+ action_what_will_happen: "\xBFQu\xE9 pasar\xE1?",
54
+ action_already_resolved: "Esta acci\xF3n ya se ha resuelto.",
55
+ action_failed: "No se pudo completar la acci\xF3n. Int\xE9ntalo de nuevo.",
56
+ status_sending: "Enviando",
57
+ status_sent: "Enviado",
58
+ status_failed: "Error al enviar",
59
+ screenshot_capture: "Capturar pantalla",
60
+ attachment_remove: "Quitar adjunto"
61
+ };
62
+
63
+ // src/i18n/locales/pt-BR.ts
64
+ var ptBR = {
65
+ online: "Online",
66
+ typing: "Digitando...",
67
+ unable_to_load: "N\xE3o foi poss\xEDvel carregar o chat",
68
+ powered_by: "Desenvolvido por",
69
+ new_conversation: "Nova conversa",
70
+ open_chat: "Abrir chat",
71
+ close_chat: "Fechar chat",
72
+ send_message: "Enviar mensagem",
73
+ helpful: "\xDAtil",
74
+ not_helpful: "N\xE3o \xFAtil",
75
+ menu: "Menu",
76
+ action_approve: "Aprovar",
77
+ action_cancel: "Cancelar",
78
+ action_what_will_happen: "O que vai acontecer?",
79
+ action_already_resolved: "Esta a\xE7\xE3o j\xE1 foi resolvida.",
80
+ action_failed: "N\xE3o foi poss\xEDvel concluir a a\xE7\xE3o. Tente novamente.",
81
+ status_sending: "Enviando",
82
+ status_sent: "Enviado",
83
+ status_failed: "Falha no envio",
84
+ screenshot_capture: "Capturar tela",
85
+ attachment_remove: "Remover anexo"
86
+ };
87
+
88
+ // src/i18n/locales/pt-PT.ts
89
+ var ptPT = {
90
+ online: "Online",
91
+ typing: "A escrever...",
92
+ unable_to_load: "N\xE3o foi poss\xEDvel carregar o chat",
93
+ powered_by: "Desenvolvido por",
94
+ new_conversation: "Nova conversa",
95
+ open_chat: "Abrir chat",
96
+ close_chat: "Fechar chat",
97
+ send_message: "Enviar mensagem",
98
+ helpful: "\xDAtil",
99
+ not_helpful: "N\xE3o \xFAtil",
100
+ menu: "Menu",
101
+ action_approve: "Aprovar",
102
+ action_cancel: "Cancelar",
103
+ action_what_will_happen: "O que ir\xE1 acontecer?",
104
+ action_already_resolved: "Esta a\xE7\xE3o j\xE1 foi resolvida.",
105
+ action_failed: "N\xE3o foi poss\xEDvel concluir a a\xE7\xE3o. Tente novamente.",
106
+ status_sending: "A enviar",
107
+ status_sent: "Enviada",
108
+ status_failed: "Falha no envio",
109
+ screenshot_capture: "Capturar ecr\xE3",
110
+ attachment_remove: "Remover anexo"
111
+ };
112
+
113
+ // src/i18n/locales/fr.ts
114
+ var fr = {
115
+ online: "En ligne",
116
+ typing: "Saisie...",
117
+ unable_to_load: "Impossible de charger le chat",
118
+ powered_by: "Propuls\xE9 par",
119
+ new_conversation: "Nouvelle conversation",
120
+ open_chat: "Ouvrir le chat",
121
+ close_chat: "Fermer le chat",
122
+ send_message: "Envoyer le message",
123
+ helpful: "Utile",
124
+ not_helpful: "Pas utile",
125
+ menu: "Menu",
126
+ action_approve: "Approuver",
127
+ action_cancel: "Annuler",
128
+ action_what_will_happen: "Que va-t-il se passer ?",
129
+ action_already_resolved: "Cette action a d\xE9j\xE0 \xE9t\xE9 trait\xE9e.",
130
+ action_failed: "Impossible de r\xE9aliser cette action. Veuillez r\xE9essayer.",
131
+ status_sending: "Envoi\u2026",
132
+ status_sent: "Envoy\xE9",
133
+ status_failed: "\xC9chec de l'envoi",
134
+ screenshot_capture: "Capture d'\xE9cran",
135
+ attachment_remove: "Supprimer la pi\xE8ce jointe"
136
+ };
137
+
138
+ // src/i18n/locales/de.ts
139
+ var de = {
140
+ online: "Online",
141
+ typing: "Schreibt...",
142
+ unable_to_load: "Chat konnte nicht geladen werden",
143
+ powered_by: "Bereitgestellt von",
144
+ new_conversation: "Neue Unterhaltung",
145
+ open_chat: "Chat \xF6ffnen",
146
+ close_chat: "Chat schlie\xDFen",
147
+ send_message: "Nachricht senden",
148
+ helpful: "Hilfreich",
149
+ not_helpful: "Nicht hilfreich",
150
+ menu: "Men\xFC",
151
+ action_approve: "Best\xE4tigen",
152
+ action_cancel: "Abbrechen",
153
+ action_what_will_happen: "Was passiert dann?",
154
+ action_already_resolved: "Diese Aktion wurde bereits abgeschlossen.",
155
+ action_failed: "Aktion konnte nicht abgeschlossen werden. Bitte erneut versuchen.",
156
+ status_sending: "Wird gesendet",
157
+ status_sent: "Gesendet",
158
+ status_failed: "Senden fehlgeschlagen",
159
+ screenshot_capture: "Bildschirmfoto erstellen",
160
+ attachment_remove: "Anhang entfernen"
161
+ };
162
+
163
+ // src/i18n/locales/it.ts
164
+ var it = {
165
+ online: "Online",
166
+ typing: "Sta scrivendo...",
167
+ unable_to_load: "Impossibile caricare la chat",
168
+ powered_by: "Realizzato da",
169
+ new_conversation: "Nuova conversazione",
170
+ open_chat: "Apri chat",
171
+ close_chat: "Chiudi chat",
172
+ send_message: "Invia messaggio",
173
+ helpful: "Utile",
174
+ not_helpful: "Non utile",
175
+ menu: "Menu",
176
+ action_approve: "Approva",
177
+ action_cancel: "Annulla",
178
+ action_what_will_happen: "Cosa accadr\xE0?",
179
+ action_already_resolved: "Questa azione \xE8 gi\xE0 stata risolta.",
180
+ action_failed: "Impossibile completare l'azione. Riprova.",
181
+ status_sending: "Invio in corso",
182
+ status_sent: "Inviato",
183
+ status_failed: "Invio non riuscito",
184
+ screenshot_capture: "Cattura schermata",
185
+ attachment_remove: "Rimuovi allegato"
186
+ };
187
+
188
+ // src/i18n/locales/nl.ts
189
+ var nl = {
190
+ online: "Online",
191
+ typing: "Typt...",
192
+ unable_to_load: "Chat kon niet worden geladen",
193
+ powered_by: "Mogelijk gemaakt door",
194
+ new_conversation: "Nieuw gesprek",
195
+ open_chat: "Chat openen",
196
+ close_chat: "Chat sluiten",
197
+ send_message: "Bericht verzenden",
198
+ helpful: "Nuttig",
199
+ not_helpful: "Niet nuttig",
200
+ menu: "Menu",
201
+ action_approve: "Goedkeuren",
202
+ action_cancel: "Annuleren",
203
+ action_what_will_happen: "Wat gaat er gebeuren?",
204
+ action_already_resolved: "Deze actie is al afgehandeld.",
205
+ action_failed: "Actie kon niet worden voltooid. Probeer het opnieuw.",
206
+ status_sending: "Verzenden",
207
+ status_sent: "Verzonden",
208
+ status_failed: "Verzenden mislukt",
209
+ screenshot_capture: "Schermafbeelding maken",
210
+ attachment_remove: "Bijlage verwijderen"
211
+ };
212
+
213
+ // src/i18n/locales/pl.ts
214
+ var pl = {
215
+ online: "Online",
216
+ typing: "Pisze...",
217
+ unable_to_load: "Nie mo\u017Cna za\u0142adowa\u0107 czatu",
218
+ powered_by: "Dzia\u0142a dzi\u0119ki",
219
+ new_conversation: "Nowa rozmowa",
220
+ open_chat: "Otw\xF3rz czat",
221
+ close_chat: "Zamknij czat",
222
+ send_message: "Wy\u015Blij wiadomo\u015B\u0107",
223
+ helpful: "Pomocne",
224
+ not_helpful: "Niepomocne",
225
+ menu: "Menu",
226
+ action_approve: "Zatwierd\u017A",
227
+ action_cancel: "Anuluj",
228
+ action_what_will_happen: "Co si\u0119 stanie?",
229
+ action_already_resolved: "Ta akcja zosta\u0142a ju\u017C rozwi\u0105zana.",
230
+ action_failed: "Nie uda\u0142o si\u0119 wykona\u0107 akcji. Spr\xF3buj ponownie.",
231
+ status_sending: "Wysy\u0142anie",
232
+ status_sent: "Wys\u0142ano",
233
+ status_failed: "Nie uda\u0142o si\u0119 wys\u0142a\u0107",
234
+ screenshot_capture: "Zr\xF3b zrzut ekranu",
235
+ attachment_remove: "Usu\u0144 za\u0142\u0105cznik"
236
+ };
237
+
238
+ // src/i18n/locales/tr.ts
239
+ var tr = {
240
+ online: "\xC7evrimi\xE7i",
241
+ typing: "Yaz\u0131yor...",
242
+ unable_to_load: "Sohbet y\xFCklenemedi",
243
+ powered_by: "Sa\u011Flayan",
244
+ new_conversation: "Yeni sohbet",
245
+ open_chat: "Sohbeti a\xE7",
246
+ close_chat: "Sohbeti kapat",
247
+ send_message: "Mesaj g\xF6nder",
248
+ helpful: "Yararl\u0131",
249
+ not_helpful: "Yararl\u0131 de\u011Fil",
250
+ menu: "Men\xFC",
251
+ action_approve: "Onayla",
252
+ action_cancel: "\u0130ptal",
253
+ action_what_will_happen: "Ne olacak?",
254
+ action_already_resolved: "Bu i\u015Flem zaten \xE7\xF6z\xFCld\xFC.",
255
+ action_failed: "\u0130\u015Flem tamamlanamad\u0131. L\xFCtfen tekrar deneyin.",
256
+ status_sending: "G\xF6nderiliyor",
257
+ status_sent: "G\xF6nderildi",
258
+ status_failed: "G\xF6nderilemedi",
259
+ screenshot_capture: "Ekran g\xF6r\xFCnt\xFCs\xFC al",
260
+ attachment_remove: "Eki kald\u0131r"
261
+ };
262
+
263
+ // src/i18n/locales/ar.ts
264
+ var ar = {
265
+ online: "\u0645\u062A\u0635\u0644",
266
+ typing: "\u064A\u0643\u062A\u0628...",
267
+ unable_to_load: "\u062A\u0639\u0630\u0631 \u062A\u062D\u0645\u064A\u0644 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629",
268
+ powered_by: "\u0645\u062F\u0639\u0648\u0645 \u0645\u0646",
269
+ new_conversation: "\u0645\u062D\u0627\u062F\u062B\u0629 \u062C\u062F\u064A\u062F\u0629",
270
+ open_chat: "\u0641\u062A\u062D \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629",
271
+ close_chat: "\u0625\u063A\u0644\u0627\u0642 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629",
272
+ send_message: "\u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629",
273
+ helpful: "\u0645\u0641\u064A\u062F",
274
+ not_helpful: "\u063A\u064A\u0631 \u0645\u0641\u064A\u062F",
275
+ menu: "\u0627\u0644\u0642\u0627\u0626\u0645\u0629",
276
+ action_approve: "\u0645\u0648\u0627\u0641\u0642\u0629",
277
+ action_cancel: "\u0625\u0644\u063A\u0627\u0621",
278
+ action_what_will_happen: "\u0645\u0627\u0630\u0627 \u0633\u064A\u062D\u062F\u062B\u061F",
279
+ action_already_resolved: "\u062A\u0645 \u062D\u0644 \u0647\u0630\u0627 \u0627\u0644\u0625\u062C\u0631\u0627\u0621 \u0628\u0627\u0644\u0641\u0639\u0644.",
280
+ action_failed: "\u062A\u0639\u0630\u0631 \u0625\u0643\u0645\u0627\u0644 \u0627\u0644\u0625\u062C\u0631\u0627\u0621. \u064A\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649.",
281
+ status_sending: "\u062C\u0627\u0631\u064D \u0627\u0644\u0625\u0631\u0633\u0627\u0644",
282
+ status_sent: "\u062A\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644",
283
+ status_failed: "\u0641\u0634\u0644 \u0627\u0644\u0625\u0631\u0633\u0627\u0644",
284
+ screenshot_capture: "\u0627\u0644\u062A\u0642\u0627\u0637 \u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629",
285
+ attachment_remove: "\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0645\u0631\u0641\u0642"
286
+ };
287
+
288
+ // src/i18n/locales/ja.ts
289
+ var ja = {
290
+ online: "\u30AA\u30F3\u30E9\u30A4\u30F3",
291
+ typing: "\u5165\u529B\u4E2D...",
292
+ unable_to_load: "\u30C1\u30E3\u30C3\u30C8\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093",
293
+ powered_by: "\u63D0\u4F9B\uFF1A",
294
+ new_conversation: "\u65B0\u3057\u3044\u4F1A\u8A71",
295
+ open_chat: "\u30C1\u30E3\u30C3\u30C8\u3092\u958B\u304F",
296
+ close_chat: "\u30C1\u30E3\u30C3\u30C8\u3092\u9589\u3058\u308B",
297
+ send_message: "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u9001\u4FE1",
298
+ helpful: "\u5F79\u306B\u7ACB\u3063\u305F",
299
+ not_helpful: "\u5F79\u306B\u7ACB\u305F\u306A\u304B\u3063\u305F",
300
+ menu: "\u30E1\u30CB\u30E5\u30FC",
301
+ action_approve: "\u627F\u8A8D",
302
+ action_cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
303
+ action_what_will_happen: "\u4F55\u304C\u8D77\u3053\u308A\u307E\u3059\u304B\uFF1F",
304
+ action_already_resolved: "\u3053\u306E\u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u65E2\u306B\u51E6\u7406\u3055\u308C\u3066\u3044\u307E\u3059\u3002",
305
+ action_failed: "\u30A2\u30AF\u30B7\u30E7\u30F3\u3092\u5B8C\u4E86\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002",
306
+ status_sending: "\u9001\u4FE1\u4E2D",
307
+ status_sent: "\u9001\u4FE1\u6E08\u307F",
308
+ status_failed: "\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
309
+ screenshot_capture: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u3092\u64AE\u308B",
310
+ attachment_remove: "\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u3092\u524A\u9664"
311
+ };
312
+
313
+ // src/i18n/locales/ko.ts
314
+ var ko = {
315
+ online: "\uC628\uB77C\uC778",
316
+ typing: "\uC785\uB825 \uC911...",
317
+ unable_to_load: "\uCC44\uD305\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
318
+ powered_by: "\uC81C\uACF5:",
319
+ new_conversation: "\uC0C8 \uB300\uD654",
320
+ open_chat: "\uCC44\uD305 \uC5F4\uAE30",
321
+ close_chat: "\uCC44\uD305 \uB2EB\uAE30",
322
+ send_message: "\uBA54\uC2DC\uC9C0 \uBCF4\uB0B4\uAE30",
323
+ helpful: "\uB3C4\uC6C0\uC774 \uB428",
324
+ not_helpful: "\uB3C4\uC6C0\uC774 \uC548 \uB428",
325
+ menu: "\uBA54\uB274",
326
+ action_approve: "\uC2B9\uC778",
327
+ action_cancel: "\uCDE8\uC18C",
328
+ action_what_will_happen: "\uBB34\uC2A8 \uC77C\uC774 \uC77C\uC5B4\uB098\uB098\uC694?",
329
+ action_already_resolved: "\uC774 \uC791\uC5C5\uC740 \uC774\uBBF8 \uCC98\uB9AC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
330
+ action_failed: "\uC791\uC5C5\uC744 \uC644\uB8CC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
331
+ status_sending: "\uBCF4\uB0B4\uB294 \uC911",
332
+ status_sent: "\uBCF4\uB0C8\uC74C",
333
+ status_failed: "\uBCF4\uB0B4\uAE30 \uC2E4\uD328",
334
+ screenshot_capture: "\uC2A4\uD06C\uB9B0\uC0F7 \uCC0D\uAE30",
335
+ attachment_remove: "\uCCA8\uBD80 \uD30C\uC77C \uC81C\uAC70"
336
+ };
337
+
338
+ // src/i18n/locales/zh-CN.ts
339
+ var zhCN = {
340
+ online: "\u5728\u7EBF",
341
+ typing: "\u6B63\u5728\u8F93\u5165...",
342
+ unable_to_load: "\u65E0\u6CD5\u52A0\u8F7D\u804A\u5929",
343
+ powered_by: "\u6280\u672F\u652F\u6301\uFF1A",
344
+ new_conversation: "\u65B0\u5BF9\u8BDD",
345
+ open_chat: "\u6253\u5F00\u804A\u5929",
346
+ close_chat: "\u5173\u95ED\u804A\u5929",
347
+ send_message: "\u53D1\u9001\u6D88\u606F",
348
+ helpful: "\u6709\u7528",
349
+ not_helpful: "\u6CA1\u7528",
350
+ menu: "\u83DC\u5355",
351
+ action_approve: "\u6279\u51C6",
352
+ action_cancel: "\u53D6\u6D88",
353
+ action_what_will_happen: "\u4F1A\u53D1\u751F\u4EC0\u4E48\uFF1F",
354
+ action_already_resolved: "\u6B64\u64CD\u4F5C\u5DF2\u5904\u7406\u3002",
355
+ action_failed: "\u64CD\u4F5C\u672A\u80FD\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u3002",
356
+ status_sending: "\u53D1\u9001\u4E2D",
357
+ status_sent: "\u5DF2\u53D1\u9001",
358
+ status_failed: "\u53D1\u9001\u5931\u8D25",
359
+ screenshot_capture: "\u622A\u5C4F",
360
+ attachment_remove: "\u79FB\u9664\u9644\u4EF6"
361
+ };
362
+
363
+ // src/i18n/locales/zh-TW.ts
364
+ var zhTW = {
365
+ online: "\u5728\u7DDA",
366
+ typing: "\u6B63\u5728\u8F38\u5165...",
367
+ unable_to_load: "\u7121\u6CD5\u8F09\u5165\u804A\u5929",
368
+ powered_by: "\u6280\u8853\u652F\u63F4\uFF1A",
369
+ new_conversation: "\u65B0\u5C0D\u8A71",
370
+ open_chat: "\u958B\u555F\u804A\u5929",
371
+ close_chat: "\u95DC\u9589\u804A\u5929",
372
+ send_message: "\u50B3\u9001\u8A0A\u606F",
373
+ helpful: "\u6709\u7528",
374
+ not_helpful: "\u6C92\u7528",
375
+ menu: "\u9078\u55AE",
376
+ action_approve: "\u6838\u51C6",
377
+ action_cancel: "\u53D6\u6D88",
378
+ action_what_will_happen: "\u6703\u767C\u751F\u4EC0\u9EBC\uFF1F",
379
+ action_already_resolved: "\u6B64\u64CD\u4F5C\u5DF2\u8655\u7406\u3002",
380
+ action_failed: "\u64CD\u4F5C\u672A\u80FD\u5B8C\u6210\uFF0C\u8ACB\u91CD\u8A66\u3002",
381
+ status_sending: "\u50B3\u9001\u4E2D",
382
+ status_sent: "\u5DF2\u50B3\u9001",
383
+ status_failed: "\u50B3\u9001\u5931\u6557",
384
+ screenshot_capture: "\u64F7\u53D6\u756B\u9762",
385
+ attachment_remove: "\u79FB\u9664\u9644\u4EF6"
386
+ };
387
+
388
+ // src/i18n/index.ts
389
+ var SUPPORTED_LOCALES = [
390
+ "en",
391
+ "es",
392
+ "pt-BR",
393
+ "pt-PT",
394
+ "fr",
395
+ "de",
396
+ "it",
397
+ "nl",
398
+ "pl",
399
+ "tr",
400
+ "ar",
401
+ "ja",
402
+ "ko",
403
+ "zh-CN",
404
+ "zh-TW"
405
+ ];
406
+ var catalog = {
407
+ en,
408
+ es,
409
+ "pt-BR": ptBR,
410
+ "pt-PT": ptPT,
411
+ fr,
412
+ de,
413
+ it,
414
+ nl,
415
+ pl,
416
+ tr,
417
+ ar,
418
+ ja,
419
+ ko,
420
+ "zh-CN": zhCN,
421
+ "zh-TW": zhTW
39
422
  };
40
- var locales = { en, es };
41
- function createTranslate(locale) {
42
- const resolved = resolveLocale(locale);
43
- const translations = locales[resolved] ?? en;
44
- return (key) => translations[key] ?? en[key] ?? key;
423
+ var RTL_PRIMARY_LANGS = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur"]);
424
+ function isRtlLocale(locale) {
425
+ return RTL_PRIMARY_LANGS.has(locale.split("-")[0].toLowerCase());
45
426
  }
46
427
  function resolveLocale(locale) {
47
- if (locale) {
48
- const base = locale.split("-")[0].toLowerCase();
49
- if (locales[base]) return base;
428
+ if (!locale) return "en";
429
+ if (SUPPORTED_LOCALES.includes(locale)) {
430
+ return locale;
431
+ }
432
+ const ciMatch = SUPPORTED_LOCALES.find(
433
+ (l) => l.toLowerCase() === locale.toLowerCase()
434
+ );
435
+ if (ciMatch) return ciMatch;
436
+ const base = locale.split("-")[0].toLowerCase();
437
+ if (base === "pt") return "pt-BR";
438
+ if (base === "zh") return "zh-CN";
439
+ if (SUPPORTED_LOCALES.includes(base)) {
440
+ return base;
441
+ }
442
+ return "en";
443
+ }
444
+ function detectLocale(explicit) {
445
+ if (explicit) return resolveLocale(explicit);
446
+ if (typeof window !== "undefined" && window.location?.search) {
447
+ try {
448
+ const params = new URLSearchParams(window.location.search);
449
+ const lang = params.get("lang");
450
+ if (lang) return resolveLocale(lang);
451
+ } catch {
452
+ }
50
453
  }
51
454
  if (typeof navigator !== "undefined" && navigator.language) {
52
- const base = navigator.language.split("-")[0].toLowerCase();
53
- if (locales[base]) return base;
455
+ return resolveLocale(navigator.language);
54
456
  }
55
457
  return "en";
56
458
  }
459
+ function createTranslator(locale, overrides) {
460
+ const translations = catalog[locale] ?? catalog.en;
461
+ return (key) => {
462
+ const ov = overrides?.[key];
463
+ if (ov) {
464
+ const localized = ov[locale];
465
+ if (typeof localized === "string") return localized;
466
+ const enOverride = ov.en;
467
+ if (typeof enOverride === "string") return enOverride;
468
+ }
469
+ return translations[key] ?? catalog.en[key] ?? key;
470
+ };
471
+ }
472
+
473
+ // src/sse.ts
474
+ async function* readSSEStream(stream) {
475
+ const reader = stream.getReader();
476
+ const decoder = new TextDecoder();
477
+ let buffer = "";
478
+ try {
479
+ while (true) {
480
+ const { done, value } = await reader.read();
481
+ if (done) break;
482
+ buffer += decoder.decode(value, { stream: true });
483
+ let sep = buffer.indexOf("\n\n");
484
+ while (sep !== -1) {
485
+ const raw = buffer.slice(0, sep);
486
+ buffer = buffer.slice(sep + 2);
487
+ const evt = parseSSERecord(raw);
488
+ if (evt) yield evt;
489
+ sep = buffer.indexOf("\n\n");
490
+ }
491
+ }
492
+ const remaining = buffer.trim();
493
+ if (remaining.length > 0) {
494
+ const evt = parseSSERecord(remaining);
495
+ if (evt) yield evt;
496
+ }
497
+ } finally {
498
+ reader.releaseLock();
499
+ }
500
+ }
501
+ function parseSSERecord(raw) {
502
+ let event = "";
503
+ let data = "";
504
+ for (const line of raw.split("\n")) {
505
+ if (line.startsWith("event:")) {
506
+ event = line.slice(6).trim();
507
+ } else if (line.startsWith("data:")) {
508
+ data = data.length > 0 ? `${data}
509
+ ${line.slice(5).trim()}` : line.slice(5).trim();
510
+ }
511
+ }
512
+ if (!event) return null;
513
+ return { event, data };
514
+ }
57
515
 
58
516
  // src/client.ts
59
517
  function resolveConfig(userConfig, fetched) {
@@ -68,7 +526,8 @@ function resolveConfig(userConfig, fetched) {
68
526
  welcomeMessage: userConfig.welcomeMessage ?? fetched?.welcomeMessage ?? DEFAULTS.welcomeMessage,
69
527
  title: userConfig.title ?? fetched?.title ?? DEFAULTS.title,
70
528
  avatarUrl: userConfig.avatarUrl ?? fetched?.avatarUrl,
71
- suggestedMessages: userConfig.suggestedMessages ?? fetched?.suggestedMessages ?? []
529
+ suggestedMessages: userConfig.suggestedMessages ?? fetched?.suggestedMessages ?? [],
530
+ stringOverrides: fetched?.stringOverrides
72
531
  };
73
532
  }
74
533
  function getStorage() {
@@ -84,12 +543,18 @@ var CustomerHeroChat = class {
84
543
  storage;
85
544
  userConfig;
86
545
  identityData = null;
546
+ // `t` is mutable: it gets rebuilt when locale changes or when the fetched
547
+ // widget config delivers `stringOverrides`. The React layer reads this
548
+ // property directly each render rather than caching it in a state snapshot,
549
+ // so a `setLocale` call propagates through `useSyncExternalStore` via the
550
+ // accompanying `setState({ locale, isRtl })` notification.
87
551
  t;
88
552
  constructor(config) {
89
553
  this.userConfig = config;
90
554
  this.storage = getStorage();
91
- this.t = createTranslate(config.locale);
555
+ const locale = detectLocale(config.locale);
92
556
  const resolved = resolveConfig(config);
557
+ this.t = createTranslator(locale, resolved.stringOverrides);
93
558
  const storedConvId = this.storage?.getItem(`ch_conv_${config.chatbotId}`);
94
559
  this.state = {
95
560
  messages: [],
@@ -100,9 +565,26 @@ var CustomerHeroChat = class {
100
565
  configLoaded: false,
101
566
  configError: null,
102
567
  error: null,
103
- identity: null
568
+ identity: null,
569
+ locale,
570
+ isRtl: isRtlLocale(locale)
104
571
  };
105
572
  }
573
+ // Switch the active locale at runtime. No-op when the resolved tag matches
574
+ // the current locale and `stringOverrides` is unchanged. Subscribers get a
575
+ // single state notification with the new `locale` / `isRtl`.
576
+ setLocale(tag) {
577
+ const next = detectLocale(tag);
578
+ if (next === this.state.locale) return;
579
+ this.t = createTranslator(next, this.state.config.stringOverrides);
580
+ this.setState({ locale: next, isRtl: isRtlLocale(next) });
581
+ }
582
+ rebuildTranslator() {
583
+ this.t = createTranslator(
584
+ this.state.locale,
585
+ this.state.config.stringOverrides
586
+ );
587
+ }
106
588
  subscribe(listener) {
107
589
  this.listeners.add(listener);
108
590
  return () => {
@@ -116,6 +598,18 @@ var CustomerHeroChat = class {
116
598
  this.state = { ...this.state, ...partial };
117
599
  this.notifyListeners();
118
600
  }
601
+ // Mutate the last message in place and notify. Used during streaming so
602
+ // listeners see tokens land without allocating a new messages array per
603
+ // token. The array itself is still replaced so consumers using
604
+ // structural equality (React's useSyncExternalStore) see a new reference.
605
+ patchLastMessage(patch) {
606
+ const { messages } = this.state;
607
+ if (messages.length === 0) return;
608
+ const next = messages.slice();
609
+ const last = next[next.length - 1];
610
+ next[next.length - 1] = { ...last, ...patch };
611
+ this.setState({ messages: next });
612
+ }
119
613
  notifyListeners() {
120
614
  for (const listener of this.listeners) {
121
615
  listener(this.state);
@@ -132,6 +626,7 @@ var CustomerHeroChat = class {
132
626
  const fetched = await response.json();
133
627
  const resolved = resolveConfig(this.userConfig, fetched);
134
628
  this.setState({ config: resolved, configLoaded: true });
629
+ if (resolved.stringOverrides) this.rebuildTranslator();
135
630
  } catch (error) {
136
631
  const errorMsg = error instanceof Error ? error.message : "Failed to load widget config";
137
632
  console.error("CustomerHero: Failed to fetch widget config", error);
@@ -159,68 +654,322 @@ var CustomerHeroChat = class {
159
654
  return;
160
655
  }
161
656
  const data = await response.json();
162
- const messages = (data.messages ?? []).map(
163
- (m) => ({
164
- id: m.id,
165
- role: m.role,
166
- content: m.content
167
- })
657
+ const raw = data.messages ?? [];
658
+ const messages = raw.map((m) => ({
659
+ id: m.id,
660
+ role: m.role,
661
+ content: m.content,
662
+ ...m.sources ? { sources: m.sources } : {},
663
+ ...m.blocks ? { blocks: m.blocks } : {},
664
+ ...m.suggestions ? { suggestions: m.suggestions } : {}
665
+ }));
666
+ const lastBotIndex = findLastIndex(
667
+ messages,
668
+ (m) => m.role === "bot" && !!m.suggestions?.length
168
669
  );
670
+ for (let i = 0; i < messages.length; i++) {
671
+ if (i !== lastBotIndex && messages[i].suggestions) {
672
+ const { suggestions: _s, ...rest } = messages[i];
673
+ void _s;
674
+ messages[i] = rest;
675
+ }
676
+ }
169
677
  if (messages.length > 0) {
170
678
  this.setState({ messages });
171
679
  }
172
680
  } catch {
173
681
  }
174
682
  }
175
- async sendMessage(message) {
683
+ async sendMessage(message, options) {
176
684
  const trimmed = message.trim();
685
+ const attachmentTokens = options?.attachmentTokens ?? [];
177
686
  if (!trimmed || this.state.isLoading) return;
178
- const userMsg = { role: "user", content: trimmed };
687
+ const userMsg = {
688
+ role: "user",
689
+ content: trimmed,
690
+ status: "sending"
691
+ };
692
+ const cleanedHistory = this.state.messages.map((m) => {
693
+ let next = m;
694
+ if (next.suggestions) next = stripSuggestions(next);
695
+ if (next.blocks?.some((b) => b.type === "action_confirmation")) {
696
+ next = stripActionConfirmationBlocks(next);
697
+ }
698
+ return next;
699
+ });
700
+ const userMsgIndex = cleanedHistory.length;
179
701
  this.setState({
180
- messages: [...this.state.messages, userMsg],
702
+ messages: [...cleanedHistory, userMsg],
181
703
  isLoading: true,
182
704
  error: null
183
705
  });
184
- const { chatbotId } = this.state.config;
185
- const apiBase = this.state.config.apiBase;
706
+ const { chatbotId, apiBase } = this.state.config;
707
+ let botMessageCreated = false;
186
708
  try {
187
709
  const response = await fetch(`${apiBase}/api/chat/${chatbotId}`, {
188
710
  method: "POST",
189
- headers: { "Content-Type": "application/json" },
711
+ headers: {
712
+ "Content-Type": "application/json",
713
+ Accept: "text/event-stream"
714
+ },
190
715
  body: JSON.stringify({
191
716
  message: trimmed,
192
717
  ...this.state.conversationId ? { conversationId: this.state.conversationId } : {},
193
- ...this.identityData ? { identity: this.identityData } : {}
718
+ ...this.identityData ? { identity: this.identityData } : {},
719
+ ...attachmentTokens.length > 0 ? { attachmentTokens } : {}
194
720
  })
195
721
  });
196
722
  if (!response.ok) {
197
- const data2 = await response.json().catch(() => null);
198
- const errorMsg = data2?.error ?? `Request failed: ${response.status}`;
723
+ const data = await response.json().catch(() => null);
724
+ const errorMsg = data?.error ?? `Request failed: ${response.status}`;
199
725
  throw new Error(errorMsg);
200
726
  }
201
- const data = await response.json();
202
- const botMsg = {
203
- id: data.messageId,
204
- role: "bot",
205
- content: data.message
206
- };
207
- const conversationId = data.conversationId ?? this.state.conversationId;
208
- if (conversationId) {
209
- this.storage?.setItem(`ch_conv_${chatbotId}`, conversationId);
727
+ if (!response.body) {
728
+ throw new Error("Empty response body");
210
729
  }
211
- this.setState({
212
- messages: [...this.state.messages, botMsg],
213
- isLoading: false,
214
- conversationId
215
- });
730
+ let fullContent = "";
731
+ let messageId;
732
+ for await (const evt of readSSEStream(response.body)) {
733
+ switch (evt.event) {
734
+ case "metadata": {
735
+ const meta = safeParse(evt.data);
736
+ if (meta?.conversationId) {
737
+ this.storage?.setItem(
738
+ `ch_conv_${chatbotId}`,
739
+ meta.conversationId
740
+ );
741
+ this.setState({ conversationId: meta.conversationId });
742
+ }
743
+ this.patchMessageAt(userMsgIndex, { status: "sent" });
744
+ if (meta?.messageId) {
745
+ messageId = meta.messageId;
746
+ }
747
+ break;
748
+ }
749
+ case "token": {
750
+ const tok = safeParse(evt.data);
751
+ const text = tok?.text ?? "";
752
+ fullContent += text;
753
+ if (!botMessageCreated) {
754
+ const botMsg = {
755
+ id: messageId,
756
+ role: "bot",
757
+ content: fullContent,
758
+ streaming: true
759
+ };
760
+ this.setState({
761
+ messages: [...this.state.messages, botMsg]
762
+ });
763
+ botMessageCreated = true;
764
+ } else {
765
+ this.patchLastMessage({ content: fullContent });
766
+ }
767
+ break;
768
+ }
769
+ case "sources": {
770
+ const payload = safeParse(evt.data);
771
+ if (payload?.sources?.length && botMessageCreated) {
772
+ this.patchLastMessage({ sources: payload.sources });
773
+ }
774
+ break;
775
+ }
776
+ case "block": {
777
+ const payload = safeParse(evt.data);
778
+ if (payload?.block && botMessageCreated) {
779
+ const existing = this.state.messages.at(-1)?.blocks ?? [];
780
+ this.patchLastMessage({
781
+ blocks: [...existing, payload.block]
782
+ });
783
+ }
784
+ break;
785
+ }
786
+ case "suggestions": {
787
+ const payload = safeParse(evt.data);
788
+ if (payload?.suggestions?.length && botMessageCreated) {
789
+ this.patchLastMessage({ suggestions: payload.suggestions });
790
+ }
791
+ break;
792
+ }
793
+ case "done": {
794
+ if (botMessageCreated) {
795
+ this.patchLastMessage({
796
+ id: messageId,
797
+ streaming: false
798
+ });
799
+ }
800
+ break;
801
+ }
802
+ case "error": {
803
+ const payload = safeParse(evt.data);
804
+ throw new Error(payload?.error ?? "Stream failed");
805
+ }
806
+ }
807
+ }
808
+ if (!botMessageCreated && !fullContent) {
809
+ }
810
+ this.setState({ isLoading: false });
216
811
  } catch (error) {
217
812
  const errorMsg = error instanceof Error ? error.message : "Something went wrong";
813
+ if (botMessageCreated) {
814
+ this.patchLastMessage({ streaming: false });
815
+ }
816
+ const userStatus = this.state.messages[userMsgIndex]?.status;
817
+ if (userStatus !== "sent") {
818
+ this.patchMessageAt(userMsgIndex, { status: "failed" });
819
+ }
218
820
  this.setState({
219
821
  isLoading: false,
220
822
  error: errorMsg
221
823
  });
222
824
  }
223
825
  }
826
+ // Upload a screenshot blob to the public attachments endpoint and return
827
+ // the token the caller should pass to `sendMessage(text, { attachmentTokens })`.
828
+ async uploadAttachment(blob, options) {
829
+ const { chatbotId, apiBase } = this.state.config;
830
+ const filename = options?.filename ?? `screenshot.${pickExtension(blob.type)}`;
831
+ const form = new FormData();
832
+ form.append("file", blob, filename);
833
+ const response = await fetch(
834
+ `${apiBase}/api/chat/${chatbotId}/attachments`,
835
+ { method: "POST", body: form }
836
+ );
837
+ if (!response.ok) {
838
+ const data = await response.json().catch(() => null);
839
+ const errorMsg = data?.error ?? `Upload failed: ${response.status}`;
840
+ throw new Error(errorMsg);
841
+ }
842
+ const json = await response.json();
843
+ return json;
844
+ }
845
+ approveAction(pendingId) {
846
+ return this.sendDecision(pendingId, "approve");
847
+ }
848
+ cancelAction(pendingId) {
849
+ return this.sendDecision(pendingId, "cancel");
850
+ }
851
+ // Locate the bot bubble that carries the action_confirmation block for
852
+ // `pendingId`, strip the block, mark the bubble streaming, then POST the
853
+ // decision and stream tokens back into the same bubble.
854
+ async sendDecision(pendingId, decision) {
855
+ const targetIndex = this.findActionConfirmationMessageIndex(pendingId);
856
+ if (targetIndex === -1) {
857
+ this.setState({ error: this.t("action_already_resolved") });
858
+ await this.loadHistory();
859
+ return;
860
+ }
861
+ const messages = this.state.messages.slice();
862
+ const original = messages[targetIndex];
863
+ messages[targetIndex] = {
864
+ ...stripActionConfirmationBlocks(original),
865
+ streaming: true
866
+ };
867
+ this.setState({ messages, error: null });
868
+ const { chatbotId, apiBase } = this.state.config;
869
+ const url = `${apiBase}/api/chat/${chatbotId}/tool-calls/${pendingId}/decision`;
870
+ try {
871
+ const response = await fetch(url, {
872
+ method: "POST",
873
+ headers: {
874
+ "Content-Type": "application/json",
875
+ Accept: "text/event-stream"
876
+ },
877
+ body: JSON.stringify({
878
+ decision,
879
+ ...this.identityData ? { identity: this.identityData } : {}
880
+ })
881
+ });
882
+ if (!response.ok) {
883
+ const data = await response.json().catch(() => null);
884
+ const errorMsg = data?.error ?? `Request failed: ${response.status}`;
885
+ throw new Error(errorMsg);
886
+ }
887
+ if (!response.body) throw new Error("Empty response body");
888
+ for await (const evt of readSSEStream(response.body)) {
889
+ switch (evt.event) {
890
+ case "metadata": {
891
+ const meta = safeParse(evt.data);
892
+ if (meta?.conversationId) {
893
+ this.storage?.setItem(
894
+ `ch_conv_${chatbotId}`,
895
+ meta.conversationId
896
+ );
897
+ this.setState({ conversationId: meta.conversationId });
898
+ }
899
+ break;
900
+ }
901
+ case "token": {
902
+ const tok = safeParse(evt.data);
903
+ const text = tok?.text ?? "";
904
+ if (text) {
905
+ this.appendToMessageAt(targetIndex, text);
906
+ }
907
+ break;
908
+ }
909
+ case "block": {
910
+ const payload = safeParse(evt.data);
911
+ if (payload?.block) {
912
+ this.appendBlockToMessageAt(targetIndex, payload.block);
913
+ }
914
+ break;
915
+ }
916
+ case "done": {
917
+ this.patchMessageAt(targetIndex, { streaming: false });
918
+ break;
919
+ }
920
+ case "error": {
921
+ const payload = safeParse(
922
+ evt.data
923
+ );
924
+ if (payload?.kind === "already_resolved") {
925
+ this.patchMessageAt(targetIndex, { streaming: false });
926
+ this.setState({ error: this.t("action_already_resolved") });
927
+ await this.loadHistory();
928
+ return;
929
+ }
930
+ throw new Error(payload?.error ?? "Stream failed");
931
+ }
932
+ }
933
+ }
934
+ } catch (error) {
935
+ const errorMsg = error instanceof Error ? error.message : this.t("action_failed");
936
+ this.patchMessageAt(targetIndex, { streaming: false });
937
+ this.setState({ error: errorMsg });
938
+ }
939
+ }
940
+ findActionConfirmationMessageIndex(pendingId) {
941
+ const { messages } = this.state;
942
+ for (let i = messages.length - 1; i >= 0; i--) {
943
+ const blocks = messages[i].blocks;
944
+ if (blocks?.some(
945
+ (b) => b.type === "action_confirmation" && b.pendingToolCallId === pendingId
946
+ )) {
947
+ return i;
948
+ }
949
+ }
950
+ return -1;
951
+ }
952
+ patchMessageAt(index, patch) {
953
+ const messages = this.state.messages.slice();
954
+ if (!messages[index]) return;
955
+ messages[index] = { ...messages[index], ...patch };
956
+ this.setState({ messages });
957
+ }
958
+ appendToMessageAt(index, text) {
959
+ const messages = this.state.messages.slice();
960
+ const target = messages[index];
961
+ if (!target) return;
962
+ messages[index] = { ...target, content: target.content + text };
963
+ this.setState({ messages });
964
+ }
965
+ appendBlockToMessageAt(index, block) {
966
+ const messages = this.state.messages.slice();
967
+ const target = messages[index];
968
+ if (!target) return;
969
+ const existing = target.blocks ?? [];
970
+ messages[index] = { ...target, blocks: [...existing, block] };
971
+ this.setState({ messages });
972
+ }
224
973
  async rateMessage(messageId, rating) {
225
974
  const { chatbotId, apiBase } = this.state.config;
226
975
  const { conversationId } = this.state;
@@ -290,7 +1039,185 @@ var CustomerHeroChat = class {
290
1039
  });
291
1040
  }
292
1041
  };
1042
+ function safeParse(data) {
1043
+ try {
1044
+ return JSON.parse(data);
1045
+ } catch {
1046
+ return null;
1047
+ }
1048
+ }
1049
+ function stripSuggestions(message) {
1050
+ const { suggestions: _s, ...rest } = message;
1051
+ void _s;
1052
+ return rest;
1053
+ }
1054
+ function stripActionConfirmationBlocks(message) {
1055
+ if (!message.blocks?.length) return message;
1056
+ const blocks = message.blocks.filter(
1057
+ (b) => b.type !== "action_confirmation"
1058
+ );
1059
+ if (blocks.length === message.blocks.length) return message;
1060
+ if (blocks.length === 0) {
1061
+ const { blocks: _b, ...rest } = message;
1062
+ void _b;
1063
+ return rest;
1064
+ }
1065
+ return { ...message, blocks };
1066
+ }
1067
+ function findLastIndex(arr, pred) {
1068
+ for (let i = arr.length - 1; i >= 0; i--) {
1069
+ if (pred(arr[i])) return i;
1070
+ }
1071
+ return -1;
1072
+ }
1073
+ function pickExtension(mime) {
1074
+ if (mime === "image/png") return "png";
1075
+ if (mime === "image/webp") return "webp";
1076
+ return "jpg";
1077
+ }
1078
+
1079
+ // src/screenshot.ts
1080
+ var ScreenshotCancelled = class extends Error {
1081
+ constructor(message = "Screenshot cancelled") {
1082
+ super(message);
1083
+ this.name = "ScreenshotCancelled";
1084
+ }
1085
+ };
1086
+ var ScreenshotUnavailable = class extends Error {
1087
+ constructor(message = "Screenshot unavailable") {
1088
+ super(message);
1089
+ this.name = "ScreenshotUnavailable";
1090
+ }
1091
+ };
1092
+ var MAX_DIMENSION = 2048;
1093
+ var SIZE_BUDGET = 5 * 1024 * 1024;
1094
+ function canCaptureScreenshot() {
1095
+ if (typeof navigator === "undefined" || typeof window === "undefined") {
1096
+ return false;
1097
+ }
1098
+ if (!navigator.mediaDevices?.getDisplayMedia) return false;
1099
+ if (typeof window.matchMedia !== "function") return false;
1100
+ return window.matchMedia("(pointer: fine)").matches;
1101
+ }
1102
+ async function captureScreenshot() {
1103
+ if (typeof navigator === "undefined" || !navigator.mediaDevices) {
1104
+ throw new ScreenshotUnavailable("Screen capture not supported");
1105
+ }
1106
+ let stream;
1107
+ try {
1108
+ stream = await navigator.mediaDevices.getDisplayMedia({
1109
+ video: true,
1110
+ audio: false
1111
+ });
1112
+ } catch (e) {
1113
+ if (e instanceof Error && (e.name === "NotAllowedError" || e.name === "AbortError")) {
1114
+ throw new ScreenshotCancelled(e.message);
1115
+ }
1116
+ const msg = e instanceof Error ? e.message : "Screen capture failed";
1117
+ throw new ScreenshotUnavailable(msg);
1118
+ }
1119
+ try {
1120
+ const bitmap = await grabFrame(stream);
1121
+ return await encodeJpeg(bitmap);
1122
+ } finally {
1123
+ for (const track of stream.getTracks()) track.stop();
1124
+ }
1125
+ }
1126
+ async function grabFrame(stream) {
1127
+ const [track] = stream.getVideoTracks();
1128
+ if (!track) throw new ScreenshotUnavailable("No video track");
1129
+ const ImageCaptureCtor = globalThis.ImageCapture;
1130
+ if (ImageCaptureCtor) {
1131
+ try {
1132
+ const cap = new ImageCaptureCtor(track);
1133
+ const bitmap = await cap.grabFrame();
1134
+ if (bitmap.width > 0 && bitmap.height > 0) return bitmap;
1135
+ } catch {
1136
+ }
1137
+ }
1138
+ return await grabFrameFromVideo(stream);
1139
+ }
1140
+ async function grabFrameFromVideo(stream) {
1141
+ const video = document.createElement("video");
1142
+ video.autoplay = true;
1143
+ video.muted = true;
1144
+ video.playsInline = true;
1145
+ video.srcObject = stream;
1146
+ try {
1147
+ await new Promise((resolve, reject) => {
1148
+ const onReady = () => {
1149
+ video.removeEventListener("loadedmetadata", onReady);
1150
+ resolve();
1151
+ };
1152
+ const onError = () => {
1153
+ video.removeEventListener("error", onError);
1154
+ reject(new ScreenshotUnavailable("Video element failed"));
1155
+ };
1156
+ video.addEventListener("loadedmetadata", onReady);
1157
+ video.addEventListener("error", onError);
1158
+ });
1159
+ await video.play().catch(() => {
1160
+ });
1161
+ const w = video.videoWidth;
1162
+ const h = video.videoHeight;
1163
+ if (w === 0 || h === 0) throw new ScreenshotUnavailable("Empty frame");
1164
+ const canvas = document.createElement("canvas");
1165
+ canvas.width = w;
1166
+ canvas.height = h;
1167
+ const ctx = canvas.getContext("2d");
1168
+ if (!ctx) throw new ScreenshotUnavailable("No 2D context");
1169
+ ctx.drawImage(video, 0, 0, w, h);
1170
+ return await createImageBitmap(canvas);
1171
+ } finally {
1172
+ video.srcObject = null;
1173
+ }
1174
+ }
1175
+ async function encodeJpeg(bitmap) {
1176
+ const { width, height } = scaleToFit(
1177
+ bitmap.width,
1178
+ bitmap.height,
1179
+ MAX_DIMENSION
1180
+ );
1181
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), { width, height });
1182
+ const ctx = canvas.getContext("2d");
1183
+ if (!ctx) throw new ScreenshotUnavailable("No 2D context");
1184
+ ctx.drawImage(bitmap, 0, 0, width, height);
1185
+ bitmap.close?.();
1186
+ const blob = await canvasToBlob(canvas, 0.85);
1187
+ if (blob.size <= SIZE_BUDGET) return blob;
1188
+ const slim = await canvasToBlob(canvas, 0.7);
1189
+ return slim;
1190
+ }
1191
+ function scaleToFit(w, h, max) {
1192
+ if (w <= max && h <= max) return { width: w, height: h };
1193
+ const ratio = w >= h ? max / w : max / h;
1194
+ return { width: Math.round(w * ratio), height: Math.round(h * ratio) };
1195
+ }
1196
+ async function canvasToBlob(canvas, quality) {
1197
+ if ("convertToBlob" in canvas) {
1198
+ return await canvas.convertToBlob({
1199
+ type: "image/jpeg",
1200
+ quality
1201
+ });
1202
+ }
1203
+ return await new Promise((resolve, reject) => {
1204
+ canvas.toBlob(
1205
+ (b) => b ? resolve(b) : reject(new ScreenshotUnavailable("Encode failed")),
1206
+ "image/jpeg",
1207
+ quality
1208
+ );
1209
+ });
1210
+ }
293
1211
  export {
294
1212
  CustomerHeroChat,
295
- DEFAULTS
1213
+ DEFAULTS,
1214
+ SUPPORTED_LOCALES,
1215
+ ScreenshotCancelled,
1216
+ ScreenshotUnavailable,
1217
+ canCaptureScreenshot,
1218
+ captureScreenshot,
1219
+ createTranslator,
1220
+ detectLocale,
1221
+ isRtlLocale,
1222
+ resolveLocale
296
1223
  };