@erlancarreira/evolution-chat 0.1.1 → 0.1.3
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/README.md +69 -13
- package/api/index.cjs +40 -0
- package/api/index.cjs.map +1 -1
- package/api/index.d.cts +56 -30
- package/api/index.d.ts +56 -30
- package/api/index.js +40 -0
- package/api/index.js.map +1 -1
- package/bridge/index.cjs +108 -15
- package/bridge/index.cjs.map +1 -1
- package/bridge/index.d.cts +37 -0
- package/bridge/index.d.ts +37 -0
- package/bridge/index.js +108 -15
- package/bridge/index.js.map +1 -1
- package/next/index.d.cts +37 -0
- package/next/index.d.ts +37 -0
- package/package.json +1 -1
- package/transports/supabase/index.d.cts +8 -0
- package/transports/supabase/index.d.ts +8 -0
- package/widget/index.cjs +115 -28
- package/widget/index.cjs.map +1 -1
- package/widget/index.d.cts +19 -3
- package/widget/index.d.ts +19 -3
- package/widget/index.js +115 -28
- package/widget/index.js.map +1 -1
- package/widget/styles.css +33 -0
- package/widget/styles.css.map +1 -1
- package/widget-embed/evolution-chat.iife.js +113 -27
package/next/index.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ interface ChatConfig {
|
|
|
46
46
|
/** URL pública da imagem de capa do grupo (opcional; ignora se vazia). */
|
|
47
47
|
groupImage?: string;
|
|
48
48
|
}
|
|
49
|
+
/** Estado de presença (digitação) normalizado da Evolution. */
|
|
50
|
+
type PresenceState = "composing" | "paused" | "recording";
|
|
49
51
|
/** Evento de tempo real publicado no canal broadcast `chat:<realtimeToken>`. */
|
|
50
52
|
type ChatEvent = {
|
|
51
53
|
type: "message";
|
|
@@ -53,6 +55,14 @@ type ChatEvent = {
|
|
|
53
55
|
} | {
|
|
54
56
|
type: "session";
|
|
55
57
|
status: ChatSessionStatus;
|
|
58
|
+
}
|
|
59
|
+
/** Indicador de digitação: `from` diz quem está digitando (dona da plataforma
|
|
60
|
+
* ou visitante) para o widget exibir os "3 pontinhos" apenas quando a OUTRA
|
|
61
|
+
* ponta digita. */
|
|
62
|
+
| {
|
|
63
|
+
type: "typing";
|
|
64
|
+
isTyping: boolean;
|
|
65
|
+
from: "owner" | "visitor";
|
|
56
66
|
};
|
|
57
67
|
|
|
58
68
|
/** Persistência de sessões/mensagens vista pela bridge. */
|
|
@@ -120,6 +130,10 @@ interface CreateGroupResult {
|
|
|
120
130
|
}
|
|
121
131
|
interface EvolutionClient {
|
|
122
132
|
sendText(instance: string, number: string, text: string): Promise<SendTextResult>;
|
|
133
|
+
/** Envia presença de digitação ("digitando…") para o número/grupo. `presence` =
|
|
134
|
+
* "composing" | "recording" (digitando) ou "paused" (parou). Best-effort: a
|
|
135
|
+
* presença é anunciada pela CONTA CONECTADA à instância, não por terceiros. */
|
|
136
|
+
sendPresence(instance: string, number: string, presence: PresenceState): Promise<void>;
|
|
123
137
|
createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;
|
|
124
138
|
setGroupPicture(instance: string, groupJid: string, image: string): Promise<void>;
|
|
125
139
|
leaveGroup(instance: string, groupJid: string): Promise<void>;
|
|
@@ -193,6 +207,29 @@ declare class ChatBridge {
|
|
|
193
207
|
* não reenviar.
|
|
194
208
|
*/
|
|
195
209
|
private handleGroupParticipants;
|
|
210
|
+
/**
|
|
211
|
+
* Trata `PRESENCE_UPDATE` (Evolution): quando a OUTRA ponta está digitando, publica
|
|
212
|
+
* um evento de tempo real para o widget exibir os "3 pontinhos". O `from` distingue
|
|
213
|
+
* quem digita (dona da plataforma vs. visitante) para o widget mostrar só quando a
|
|
214
|
+
* ponta OPOSTA digita.
|
|
215
|
+
* NUNCA lança: erros são logados e devolvidos como tratado (200) para a Evolution não
|
|
216
|
+
* reenviar.
|
|
217
|
+
*/
|
|
218
|
+
private handlePresence;
|
|
219
|
+
/**
|
|
220
|
+
* Sinaliza que o visitante está (ou parou de) digitar no widget. Publica um evento
|
|
221
|
+
* `typing` no canal em tempo real da sessão para que uma eventual INTERFACE DE
|
|
222
|
+
* ATENDENTE exiba o indicador.
|
|
223
|
+
*
|
|
224
|
+
* NOTA sobre WhatsApp: a Evolution só permite anunciar a presença da CONTA CONECTADA
|
|
225
|
+
* à instância. Como o atendente desta plataforma OPERA a própria instância, enviar
|
|
226
|
+
* `sendPresence` faria o widget exibir um "digitando" FALSO do atendente (eco do
|
|
227
|
+
* próprio evento de volta pelo webhook). Por isso, aqui, apenas publicamos no canal
|
|
228
|
+
* em tempo real — a presença WhatsApp do visitante não é representável por terceiros.
|
|
229
|
+
*/
|
|
230
|
+
setVisitorTyping(token: string, isTyping: boolean): Promise<void>;
|
|
231
|
+
/** Verdadeiro se `jid` é a conta da plataforma (dona da instância). */
|
|
232
|
+
private isPlatformParticipant;
|
|
196
233
|
/**
|
|
197
234
|
* Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do
|
|
198
235
|
* lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na
|
package/package.json
CHANGED
|
@@ -20,6 +20,14 @@ type ChatEvent = {
|
|
|
20
20
|
} | {
|
|
21
21
|
type: "session";
|
|
22
22
|
status: ChatSessionStatus;
|
|
23
|
+
}
|
|
24
|
+
/** Indicador de digitação: `from` diz quem está digitando (dona da plataforma
|
|
25
|
+
* ou visitante) para o widget exibir os "3 pontinhos" apenas quando a OUTRA
|
|
26
|
+
* ponta digita. */
|
|
27
|
+
| {
|
|
28
|
+
type: "typing";
|
|
29
|
+
isTyping: boolean;
|
|
30
|
+
from: "owner" | "visitor";
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
/** Publicação server-side de eventos para o canal de uma sessão (fire-and-forget). */
|
|
@@ -20,6 +20,14 @@ type ChatEvent = {
|
|
|
20
20
|
} | {
|
|
21
21
|
type: "session";
|
|
22
22
|
status: ChatSessionStatus;
|
|
23
|
+
}
|
|
24
|
+
/** Indicador de digitação: `from` diz quem está digitando (dona da plataforma
|
|
25
|
+
* ou visitante) para o widget exibir os "3 pontinhos" apenas quando a OUTRA
|
|
26
|
+
* ponta digita. */
|
|
27
|
+
| {
|
|
28
|
+
type: "typing";
|
|
29
|
+
isTyping: boolean;
|
|
30
|
+
from: "owner" | "visitor";
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
/** Publicação server-side de eventos para o canal de uma sessão (fire-and-forget). */
|
package/widget/index.cjs
CHANGED
|
@@ -58,7 +58,8 @@ var WIDGET_KEYS = [
|
|
|
58
58
|
"retry",
|
|
59
59
|
"sessionClosed",
|
|
60
60
|
"newConversation",
|
|
61
|
-
"poweredBy"
|
|
61
|
+
"poweredBy",
|
|
62
|
+
"typing"
|
|
62
63
|
];
|
|
63
64
|
var pt = {
|
|
64
65
|
openChat: "Abrir chat",
|
|
@@ -75,7 +76,8 @@ var pt = {
|
|
|
75
76
|
retry: "Tentar novamente",
|
|
76
77
|
sessionClosed: "Esta conversa foi encerrada. Abra uma nova para continuar.",
|
|
77
78
|
newConversation: "Iniciar nova conversa",
|
|
78
|
-
poweredBy: "Powered by"
|
|
79
|
+
poweredBy: "Powered by",
|
|
80
|
+
typing: "digitando\u2026"
|
|
79
81
|
};
|
|
80
82
|
var en = {
|
|
81
83
|
openChat: "Open chat",
|
|
@@ -92,7 +94,8 @@ var en = {
|
|
|
92
94
|
retry: "Try again",
|
|
93
95
|
sessionClosed: "This conversation was closed. Start a new one to continue.",
|
|
94
96
|
newConversation: "Start a new conversation",
|
|
95
|
-
poweredBy: "Powered by"
|
|
97
|
+
poweredBy: "Powered by",
|
|
98
|
+
typing: "typing\u2026"
|
|
96
99
|
};
|
|
97
100
|
var es = {
|
|
98
101
|
openChat: "Abrir chat",
|
|
@@ -109,7 +112,8 @@ var es = {
|
|
|
109
112
|
retry: "Reintentar",
|
|
110
113
|
sessionClosed: "Esta conversaci\xF3n fue cerrada. Abre una nueva para continuar.",
|
|
111
114
|
newConversation: "Iniciar nueva conversaci\xF3n",
|
|
112
|
-
poweredBy: "Powered by"
|
|
115
|
+
poweredBy: "Powered by",
|
|
116
|
+
typing: "escribiendo\u2026"
|
|
113
117
|
};
|
|
114
118
|
var dictionaries = { pt, en, es };
|
|
115
119
|
function t(locale, key, overrides) {
|
|
@@ -358,6 +362,31 @@ var WIDGET_CSS = `/*
|
|
|
358
362
|
.ecw-send:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
359
363
|
.ecw-send svg { width: 18px; height: 18px; }
|
|
360
364
|
|
|
365
|
+
/* \u2500\u2500 indicador "digitando\u2026" (3 pontinhos) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
366
|
+
.ecw-typing {
|
|
367
|
+
display: inline-flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
gap: 4px;
|
|
370
|
+
padding: 10px 14px;
|
|
371
|
+
border-radius: 12px;
|
|
372
|
+
border-bottom-left-radius: 4px;
|
|
373
|
+
background: var(--ecw-surface);
|
|
374
|
+
border: 1px solid var(--ecw-border);
|
|
375
|
+
}
|
|
376
|
+
.ecw-typing-dot {
|
|
377
|
+
width: 7px;
|
|
378
|
+
height: 7px;
|
|
379
|
+
border-radius: 50%;
|
|
380
|
+
background: var(--ecw-muted);
|
|
381
|
+
animation: ecw-typing-bounce 1.2s infinite ease-in-out;
|
|
382
|
+
}
|
|
383
|
+
.ecw-typing-dot:nth-child(2) { animation-delay: 0.18s; }
|
|
384
|
+
.ecw-typing-dot:nth-child(3) { animation-delay: 0.36s; }
|
|
385
|
+
@keyframes ecw-typing-bounce {
|
|
386
|
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
|
|
387
|
+
30% { transform: translateY(-4px); opacity: 1; }
|
|
388
|
+
}
|
|
389
|
+
|
|
361
390
|
/* \u2500\u2500 sess\xE3o encerrada / falha \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
362
391
|
.ecw-closed { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }
|
|
363
392
|
|
|
@@ -422,17 +451,23 @@ var initialState = {
|
|
|
422
451
|
messages: [],
|
|
423
452
|
unread: 0,
|
|
424
453
|
sending: false,
|
|
425
|
-
error: null
|
|
454
|
+
error: null,
|
|
455
|
+
ownerTyping: false,
|
|
456
|
+
visitorTyping: false
|
|
426
457
|
};
|
|
427
458
|
function withMessage(state, message) {
|
|
428
459
|
const index = state.messages.findIndex((m) => m.id === message.id);
|
|
460
|
+
let messages;
|
|
429
461
|
if (index >= 0) {
|
|
430
|
-
|
|
462
|
+
messages = [...state.messages];
|
|
431
463
|
messages[index] = message;
|
|
432
|
-
|
|
464
|
+
} else {
|
|
465
|
+
messages = [...state.messages, message];
|
|
433
466
|
}
|
|
434
467
|
const bump = state.open || message.direction !== "owner" ? 0 : 1;
|
|
435
|
-
|
|
468
|
+
const next = { ...state, messages, unread: state.unread + bump };
|
|
469
|
+
if (message.direction === "owner") next.ownerTyping = false;
|
|
470
|
+
return next;
|
|
436
471
|
}
|
|
437
472
|
function chatReducer(state, action) {
|
|
438
473
|
switch (action.type) {
|
|
@@ -496,6 +531,8 @@ function chatReducer(state, action) {
|
|
|
496
531
|
return { ...state, sending: action.value };
|
|
497
532
|
case "error":
|
|
498
533
|
return { ...state, error: action.value };
|
|
534
|
+
case "typing":
|
|
535
|
+
return action.from === "owner" ? state.ownerTyping === action.isTyping ? state : { ...state, ownerTyping: action.isTyping } : state.visitorTyping === action.isTyping ? state : { ...state, visitorTyping: action.isTyping };
|
|
499
536
|
}
|
|
500
537
|
}
|
|
501
538
|
function normalizePhone(value) {
|
|
@@ -566,8 +603,9 @@ function tmpMessage(body) {
|
|
|
566
603
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
567
604
|
};
|
|
568
605
|
}
|
|
569
|
-
function useChat({ endpoint, realtime }) {
|
|
606
|
+
function useChat({ endpoint, realtime, typingEndpoint }) {
|
|
570
607
|
const [state, dispatch] = (0, import_react.useReducer)(chatReducer, initialState);
|
|
608
|
+
const typingUrl = typingEndpoint ?? `${endpoint.replace(/\/+$/, "")}/typing`;
|
|
571
609
|
const stateRef = (0, import_react.useRef)(state);
|
|
572
610
|
(0, import_react.useEffect)(() => {
|
|
573
611
|
stateRef.current = state;
|
|
@@ -622,6 +660,8 @@ function useChat({ endpoint, realtime }) {
|
|
|
622
660
|
dispatch({ type: "append", message: e.message });
|
|
623
661
|
} else if (e.type === "session" && e.status !== void 0) {
|
|
624
662
|
dispatch({ type: "session-status", status: e.status });
|
|
663
|
+
} else if (e.type === "typing" && e.from !== void 0) {
|
|
664
|
+
dispatch({ type: "typing", from: e.from, isTyping: e.isTyping });
|
|
625
665
|
}
|
|
626
666
|
}, []);
|
|
627
667
|
(0, import_react.useEffect)(() => {
|
|
@@ -728,6 +768,21 @@ function useChat({ endpoint, realtime }) {
|
|
|
728
768
|
clearStoredSession();
|
|
729
769
|
dispatch({ type: "reset-form" });
|
|
730
770
|
}, []);
|
|
771
|
+
const notifyTyping = (0, import_react.useCallback)(
|
|
772
|
+
async (isTyping) => {
|
|
773
|
+
const token = stateRef.current.token;
|
|
774
|
+
if (token === null) return;
|
|
775
|
+
try {
|
|
776
|
+
await fetch(typingUrl, {
|
|
777
|
+
method: "POST",
|
|
778
|
+
headers: { "content-type": "application/json" },
|
|
779
|
+
body: JSON.stringify({ token, isTyping })
|
|
780
|
+
});
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
[typingUrl]
|
|
785
|
+
);
|
|
731
786
|
const submitForm = (0, import_react.useCallback)(
|
|
732
787
|
async ({ name, phone, message, honeypot }) => {
|
|
733
788
|
if (honeypot.trim() !== "") return;
|
|
@@ -775,7 +830,7 @@ function useChat({ endpoint, realtime }) {
|
|
|
775
830
|
},
|
|
776
831
|
[endpoint]
|
|
777
832
|
);
|
|
778
|
-
return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation };
|
|
833
|
+
return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping };
|
|
779
834
|
}
|
|
780
835
|
|
|
781
836
|
// src/widget/chat-widget.tsx
|
|
@@ -799,6 +854,14 @@ function StatusMark({ status }) {
|
|
|
799
854
|
const glyph = status === "pending" ? "\u2713" : status === "sent" ? "\u2713\u2713" : "\u26A0";
|
|
800
855
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: status === "failed" ? "ecw-status ecw-status--failed" : "ecw-status", "aria-hidden": "true", children: glyph });
|
|
801
856
|
}
|
|
857
|
+
function TypingIndicator({ label }) {
|
|
858
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "ecw-item ecw-item--owner", "aria-live": "polite", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-typing", role: "status", "aria-label": label, children: [
|
|
859
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
860
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
861
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
862
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-sr-only", children: label })
|
|
863
|
+
] }) });
|
|
864
|
+
}
|
|
802
865
|
function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
803
866
|
const uid = (0, import_react2.useId)();
|
|
804
867
|
const [name, setName] = (0, import_react2.useState)("");
|
|
@@ -898,29 +961,53 @@ function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
|
898
961
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
899
962
|
] });
|
|
900
963
|
}
|
|
901
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, onSend, onRetry, onNewConversation }) {
|
|
964
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
902
965
|
const uid = (0, import_react2.useId)();
|
|
903
966
|
const [draft, setDraft] = (0, import_react2.useState)("");
|
|
967
|
+
const typingTimer = (0, import_react2.useRef)(null);
|
|
968
|
+
(0, import_react2.useEffect)(() => {
|
|
969
|
+
return () => {
|
|
970
|
+
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
971
|
+
};
|
|
972
|
+
}, []);
|
|
973
|
+
const handleDraftChange = (e) => {
|
|
974
|
+
setDraft(e.target.value);
|
|
975
|
+
if (!canCompose) return;
|
|
976
|
+
void onTyping(true);
|
|
977
|
+
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
978
|
+
typingTimer.current = setTimeout(() => {
|
|
979
|
+
void onTyping(false);
|
|
980
|
+
typingTimer.current = null;
|
|
981
|
+
}, 2500);
|
|
982
|
+
};
|
|
904
983
|
const submit = (e) => {
|
|
905
984
|
e.preventDefault();
|
|
906
985
|
const text = draft.trim();
|
|
907
986
|
if (text === "" || !canCompose) return;
|
|
987
|
+
if (typingTimer.current !== null) {
|
|
988
|
+
clearTimeout(typingTimer.current);
|
|
989
|
+
typingTimer.current = null;
|
|
990
|
+
}
|
|
991
|
+
void onTyping(false);
|
|
908
992
|
setDraft("");
|
|
909
993
|
void onSend(text);
|
|
910
994
|
};
|
|
911
995
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
912
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
913
|
-
|
|
914
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `ecw-
|
|
915
|
-
|
|
916
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
996
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { ref: listRef, className: "ecw-list", children: [
|
|
997
|
+
messages.map(
|
|
998
|
+
(m) => m.direction === "system" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "ecw-item ecw-item--system", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ecw-system", children: m.body }) }, m.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: `ecw-item ecw-item--${m.direction}`, children: [
|
|
999
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `ecw-bubble ecw-bubble--${m.direction}`, children: m.body }),
|
|
1000
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-meta", children: [
|
|
1001
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("time", { dateTime: m.createdAt, children: formatTime(m.createdAt, locale) }),
|
|
1002
|
+
m.direction === "visitor" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusMark, { status: m.status }),
|
|
1003
|
+
m.status === "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "ecw-retry", onClick: () => {
|
|
1004
|
+
void onRetry(m.id);
|
|
1005
|
+
}, children: tr("retry") })
|
|
1006
|
+
] })
|
|
1007
|
+
] }, m.id)
|
|
1008
|
+
),
|
|
1009
|
+
ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { label: tr("typing") })
|
|
1010
|
+
] }),
|
|
924
1011
|
!canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
|
|
925
1012
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|
|
926
1013
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "ecw-submit", onClick: onNewConversation, children: tr("newConversation") })
|
|
@@ -937,9 +1024,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
937
1024
|
autoComplete: "off",
|
|
938
1025
|
placeholder: tr("message"),
|
|
939
1026
|
value: draft,
|
|
940
|
-
onChange:
|
|
941
|
-
setDraft(e.target.value);
|
|
942
|
-
},
|
|
1027
|
+
onChange: handleDraftChange,
|
|
943
1028
|
disabled: !canCompose
|
|
944
1029
|
}
|
|
945
1030
|
),
|
|
@@ -951,7 +1036,7 @@ function ChatWidget(props) {
|
|
|
951
1036
|
const { endpoint, locale, welcome, projectName, realtime, labels } = props;
|
|
952
1037
|
const accentColor = props.accentColor ?? DEFAULT_ACCENT;
|
|
953
1038
|
const tr = (0, import_react2.useCallback)((key) => t(locale, key, labels), [locale, labels]);
|
|
954
|
-
const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation } = useChat({ endpoint, realtime });
|
|
1039
|
+
const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping } = useChat({ endpoint, realtime });
|
|
955
1040
|
const bubbleRef = (0, import_react2.useRef)(null);
|
|
956
1041
|
const listRef = (0, import_react2.useRef)(null);
|
|
957
1042
|
const firstFieldRef = (0, import_react2.useRef)(null);
|
|
@@ -976,7 +1061,7 @@ function ChatWidget(props) {
|
|
|
976
1061
|
(0, import_react2.useEffect)(() => {
|
|
977
1062
|
const list = listRef.current;
|
|
978
1063
|
if (list !== null) list.scrollTop = list.scrollHeight;
|
|
979
|
-
}, [state.messages, state.open]);
|
|
1064
|
+
}, [state.messages, state.ownerTyping, state.open]);
|
|
980
1065
|
const handleClose = (0, import_react2.useCallback)(() => {
|
|
981
1066
|
closePanel();
|
|
982
1067
|
bubbleRef.current?.focus();
|
|
@@ -1019,6 +1104,8 @@ function ChatWidget(props) {
|
|
|
1019
1104
|
tr,
|
|
1020
1105
|
listRef,
|
|
1021
1106
|
firstFieldRef,
|
|
1107
|
+
ownerTyping: state.ownerTyping,
|
|
1108
|
+
onTyping: notifyTyping,
|
|
1022
1109
|
onSend: sendMessage,
|
|
1023
1110
|
onRetry: retryMessage,
|
|
1024
1111
|
onNewConversation: startNewConversation
|