@erlancarreira/evolution-chat 0.1.2 → 0.1.4
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 +41 -1
- 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 +41 -1
- package/api/index.js.map +1 -1
- package/bridge/index.cjs +90 -0
- 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 +90 -0
- 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/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
|