@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.js
CHANGED
|
@@ -23,7 +23,8 @@ var WIDGET_KEYS = [
|
|
|
23
23
|
"retry",
|
|
24
24
|
"sessionClosed",
|
|
25
25
|
"newConversation",
|
|
26
|
-
"poweredBy"
|
|
26
|
+
"poweredBy",
|
|
27
|
+
"typing"
|
|
27
28
|
];
|
|
28
29
|
var pt = {
|
|
29
30
|
openChat: "Abrir chat",
|
|
@@ -40,7 +41,8 @@ var pt = {
|
|
|
40
41
|
retry: "Tentar novamente",
|
|
41
42
|
sessionClosed: "Esta conversa foi encerrada. Abra uma nova para continuar.",
|
|
42
43
|
newConversation: "Iniciar nova conversa",
|
|
43
|
-
poweredBy: "Powered by"
|
|
44
|
+
poweredBy: "Powered by",
|
|
45
|
+
typing: "digitando\u2026"
|
|
44
46
|
};
|
|
45
47
|
var en = {
|
|
46
48
|
openChat: "Open chat",
|
|
@@ -57,7 +59,8 @@ var en = {
|
|
|
57
59
|
retry: "Try again",
|
|
58
60
|
sessionClosed: "This conversation was closed. Start a new one to continue.",
|
|
59
61
|
newConversation: "Start a new conversation",
|
|
60
|
-
poweredBy: "Powered by"
|
|
62
|
+
poweredBy: "Powered by",
|
|
63
|
+
typing: "typing\u2026"
|
|
61
64
|
};
|
|
62
65
|
var es = {
|
|
63
66
|
openChat: "Abrir chat",
|
|
@@ -74,7 +77,8 @@ var es = {
|
|
|
74
77
|
retry: "Reintentar",
|
|
75
78
|
sessionClosed: "Esta conversaci\xF3n fue cerrada. Abre una nueva para continuar.",
|
|
76
79
|
newConversation: "Iniciar nueva conversaci\xF3n",
|
|
77
|
-
poweredBy: "Powered by"
|
|
80
|
+
poweredBy: "Powered by",
|
|
81
|
+
typing: "escribiendo\u2026"
|
|
78
82
|
};
|
|
79
83
|
var dictionaries = { pt, en, es };
|
|
80
84
|
function t(locale, key, overrides) {
|
|
@@ -323,6 +327,31 @@ var WIDGET_CSS = `/*
|
|
|
323
327
|
.ecw-send:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
324
328
|
.ecw-send svg { width: 18px; height: 18px; }
|
|
325
329
|
|
|
330
|
+
/* \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 */
|
|
331
|
+
.ecw-typing {
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
gap: 4px;
|
|
335
|
+
padding: 10px 14px;
|
|
336
|
+
border-radius: 12px;
|
|
337
|
+
border-bottom-left-radius: 4px;
|
|
338
|
+
background: var(--ecw-surface);
|
|
339
|
+
border: 1px solid var(--ecw-border);
|
|
340
|
+
}
|
|
341
|
+
.ecw-typing-dot {
|
|
342
|
+
width: 7px;
|
|
343
|
+
height: 7px;
|
|
344
|
+
border-radius: 50%;
|
|
345
|
+
background: var(--ecw-muted);
|
|
346
|
+
animation: ecw-typing-bounce 1.2s infinite ease-in-out;
|
|
347
|
+
}
|
|
348
|
+
.ecw-typing-dot:nth-child(2) { animation-delay: 0.18s; }
|
|
349
|
+
.ecw-typing-dot:nth-child(3) { animation-delay: 0.36s; }
|
|
350
|
+
@keyframes ecw-typing-bounce {
|
|
351
|
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
|
|
352
|
+
30% { transform: translateY(-4px); opacity: 1; }
|
|
353
|
+
}
|
|
354
|
+
|
|
326
355
|
/* \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 */
|
|
327
356
|
.ecw-closed { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }
|
|
328
357
|
|
|
@@ -387,17 +416,23 @@ var initialState = {
|
|
|
387
416
|
messages: [],
|
|
388
417
|
unread: 0,
|
|
389
418
|
sending: false,
|
|
390
|
-
error: null
|
|
419
|
+
error: null,
|
|
420
|
+
ownerTyping: false,
|
|
421
|
+
visitorTyping: false
|
|
391
422
|
};
|
|
392
423
|
function withMessage(state, message) {
|
|
393
424
|
const index = state.messages.findIndex((m) => m.id === message.id);
|
|
425
|
+
let messages;
|
|
394
426
|
if (index >= 0) {
|
|
395
|
-
|
|
427
|
+
messages = [...state.messages];
|
|
396
428
|
messages[index] = message;
|
|
397
|
-
|
|
429
|
+
} else {
|
|
430
|
+
messages = [...state.messages, message];
|
|
398
431
|
}
|
|
399
432
|
const bump = state.open || message.direction !== "owner" ? 0 : 1;
|
|
400
|
-
|
|
433
|
+
const next = { ...state, messages, unread: state.unread + bump };
|
|
434
|
+
if (message.direction === "owner") next.ownerTyping = false;
|
|
435
|
+
return next;
|
|
401
436
|
}
|
|
402
437
|
function chatReducer(state, action) {
|
|
403
438
|
switch (action.type) {
|
|
@@ -461,6 +496,8 @@ function chatReducer(state, action) {
|
|
|
461
496
|
return { ...state, sending: action.value };
|
|
462
497
|
case "error":
|
|
463
498
|
return { ...state, error: action.value };
|
|
499
|
+
case "typing":
|
|
500
|
+
return action.from === "owner" ? state.ownerTyping === action.isTyping ? state : { ...state, ownerTyping: action.isTyping } : state.visitorTyping === action.isTyping ? state : { ...state, visitorTyping: action.isTyping };
|
|
464
501
|
}
|
|
465
502
|
}
|
|
466
503
|
function normalizePhone(value) {
|
|
@@ -531,8 +568,9 @@ function tmpMessage(body) {
|
|
|
531
568
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
532
569
|
};
|
|
533
570
|
}
|
|
534
|
-
function useChat({ endpoint, realtime }) {
|
|
571
|
+
function useChat({ endpoint, realtime, typingEndpoint }) {
|
|
535
572
|
const [state, dispatch] = useReducer(chatReducer, initialState);
|
|
573
|
+
const typingUrl = typingEndpoint ?? `${endpoint.replace(/\/+$/, "")}/typing`;
|
|
536
574
|
const stateRef = useRef(state);
|
|
537
575
|
useEffect(() => {
|
|
538
576
|
stateRef.current = state;
|
|
@@ -587,6 +625,8 @@ function useChat({ endpoint, realtime }) {
|
|
|
587
625
|
dispatch({ type: "append", message: e.message });
|
|
588
626
|
} else if (e.type === "session" && e.status !== void 0) {
|
|
589
627
|
dispatch({ type: "session-status", status: e.status });
|
|
628
|
+
} else if (e.type === "typing" && e.from !== void 0) {
|
|
629
|
+
dispatch({ type: "typing", from: e.from, isTyping: e.isTyping });
|
|
590
630
|
}
|
|
591
631
|
}, []);
|
|
592
632
|
useEffect(() => {
|
|
@@ -693,6 +733,21 @@ function useChat({ endpoint, realtime }) {
|
|
|
693
733
|
clearStoredSession();
|
|
694
734
|
dispatch({ type: "reset-form" });
|
|
695
735
|
}, []);
|
|
736
|
+
const notifyTyping = useCallback(
|
|
737
|
+
async (isTyping) => {
|
|
738
|
+
const token = stateRef.current.token;
|
|
739
|
+
if (token === null) return;
|
|
740
|
+
try {
|
|
741
|
+
await fetch(typingUrl, {
|
|
742
|
+
method: "POST",
|
|
743
|
+
headers: { "content-type": "application/json" },
|
|
744
|
+
body: JSON.stringify({ token, isTyping })
|
|
745
|
+
});
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
[typingUrl]
|
|
750
|
+
);
|
|
696
751
|
const submitForm = useCallback(
|
|
697
752
|
async ({ name, phone, message, honeypot }) => {
|
|
698
753
|
if (honeypot.trim() !== "") return;
|
|
@@ -740,7 +795,7 @@ function useChat({ endpoint, realtime }) {
|
|
|
740
795
|
},
|
|
741
796
|
[endpoint]
|
|
742
797
|
);
|
|
743
|
-
return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation };
|
|
798
|
+
return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping };
|
|
744
799
|
}
|
|
745
800
|
|
|
746
801
|
// src/widget/chat-widget.tsx
|
|
@@ -764,6 +819,14 @@ function StatusMark({ status }) {
|
|
|
764
819
|
const glyph = status === "pending" ? "\u2713" : status === "sent" ? "\u2713\u2713" : "\u26A0";
|
|
765
820
|
return /* @__PURE__ */ jsx("span", { className: status === "failed" ? "ecw-status ecw-status--failed" : "ecw-status", "aria-hidden": "true", children: glyph });
|
|
766
821
|
}
|
|
822
|
+
function TypingIndicator({ label }) {
|
|
823
|
+
return /* @__PURE__ */ jsx("li", { className: "ecw-item ecw-item--owner", "aria-live": "polite", children: /* @__PURE__ */ jsxs("div", { className: "ecw-typing", role: "status", "aria-label": label, children: [
|
|
824
|
+
/* @__PURE__ */ jsx("span", { className: "ecw-typing-dot" }),
|
|
825
|
+
/* @__PURE__ */ jsx("span", { className: "ecw-typing-dot" }),
|
|
826
|
+
/* @__PURE__ */ jsx("span", { className: "ecw-typing-dot" }),
|
|
827
|
+
/* @__PURE__ */ jsx("span", { className: "ecw-sr-only", children: label })
|
|
828
|
+
] }) });
|
|
829
|
+
}
|
|
767
830
|
function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
768
831
|
const uid = useId();
|
|
769
832
|
const [name, setName] = useState("");
|
|
@@ -863,29 +926,53 @@ function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
|
863
926
|
/* @__PURE__ */ jsx("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
864
927
|
] });
|
|
865
928
|
}
|
|
866
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, onSend, onRetry, onNewConversation }) {
|
|
929
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
867
930
|
const uid = useId();
|
|
868
931
|
const [draft, setDraft] = useState("");
|
|
932
|
+
const typingTimer = useRef2(null);
|
|
933
|
+
useEffect2(() => {
|
|
934
|
+
return () => {
|
|
935
|
+
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
936
|
+
};
|
|
937
|
+
}, []);
|
|
938
|
+
const handleDraftChange = (e) => {
|
|
939
|
+
setDraft(e.target.value);
|
|
940
|
+
if (!canCompose) return;
|
|
941
|
+
void onTyping(true);
|
|
942
|
+
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
943
|
+
typingTimer.current = setTimeout(() => {
|
|
944
|
+
void onTyping(false);
|
|
945
|
+
typingTimer.current = null;
|
|
946
|
+
}, 2500);
|
|
947
|
+
};
|
|
869
948
|
const submit = (e) => {
|
|
870
949
|
e.preventDefault();
|
|
871
950
|
const text = draft.trim();
|
|
872
951
|
if (text === "" || !canCompose) return;
|
|
952
|
+
if (typingTimer.current !== null) {
|
|
953
|
+
clearTimeout(typingTimer.current);
|
|
954
|
+
typingTimer.current = null;
|
|
955
|
+
}
|
|
956
|
+
void onTyping(false);
|
|
873
957
|
setDraft("");
|
|
874
958
|
void onSend(text);
|
|
875
959
|
};
|
|
876
960
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
877
|
-
/* @__PURE__ */
|
|
878
|
-
|
|
879
|
-
/* @__PURE__ */ jsx("div", { className: `ecw-
|
|
880
|
-
|
|
881
|
-
/* @__PURE__ */
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
961
|
+
/* @__PURE__ */ jsxs("ul", { ref: listRef, className: "ecw-list", children: [
|
|
962
|
+
messages.map(
|
|
963
|
+
(m) => m.direction === "system" ? /* @__PURE__ */ jsx("li", { className: "ecw-item ecw-item--system", role: "status", children: /* @__PURE__ */ jsx("div", { className: "ecw-system", children: m.body }) }, m.id) : /* @__PURE__ */ jsxs("li", { className: `ecw-item ecw-item--${m.direction}`, children: [
|
|
964
|
+
/* @__PURE__ */ jsx("div", { className: `ecw-bubble ecw-bubble--${m.direction}`, children: m.body }),
|
|
965
|
+
/* @__PURE__ */ jsxs("div", { className: "ecw-meta", children: [
|
|
966
|
+
/* @__PURE__ */ jsx("time", { dateTime: m.createdAt, children: formatTime(m.createdAt, locale) }),
|
|
967
|
+
m.direction === "visitor" && /* @__PURE__ */ jsx(StatusMark, { status: m.status }),
|
|
968
|
+
m.status === "failed" && /* @__PURE__ */ jsx("button", { type: "button", className: "ecw-retry", onClick: () => {
|
|
969
|
+
void onRetry(m.id);
|
|
970
|
+
}, children: tr("retry") })
|
|
971
|
+
] })
|
|
972
|
+
] }, m.id)
|
|
973
|
+
),
|
|
974
|
+
ownerTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { label: tr("typing") })
|
|
975
|
+
] }),
|
|
889
976
|
!canCompose && /* @__PURE__ */ jsxs("div", { className: "ecw-closed", children: [
|
|
890
977
|
/* @__PURE__ */ jsx("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|
|
891
978
|
/* @__PURE__ */ jsx("button", { type: "button", className: "ecw-submit", onClick: onNewConversation, children: tr("newConversation") })
|
|
@@ -902,9 +989,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
902
989
|
autoComplete: "off",
|
|
903
990
|
placeholder: tr("message"),
|
|
904
991
|
value: draft,
|
|
905
|
-
onChange:
|
|
906
|
-
setDraft(e.target.value);
|
|
907
|
-
},
|
|
992
|
+
onChange: handleDraftChange,
|
|
908
993
|
disabled: !canCompose
|
|
909
994
|
}
|
|
910
995
|
),
|
|
@@ -916,7 +1001,7 @@ function ChatWidget(props) {
|
|
|
916
1001
|
const { endpoint, locale, welcome, projectName, realtime, labels } = props;
|
|
917
1002
|
const accentColor = props.accentColor ?? DEFAULT_ACCENT;
|
|
918
1003
|
const tr = useCallback2((key) => t(locale, key, labels), [locale, labels]);
|
|
919
|
-
const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation } = useChat({ endpoint, realtime });
|
|
1004
|
+
const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping } = useChat({ endpoint, realtime });
|
|
920
1005
|
const bubbleRef = useRef2(null);
|
|
921
1006
|
const listRef = useRef2(null);
|
|
922
1007
|
const firstFieldRef = useRef2(null);
|
|
@@ -941,7 +1026,7 @@ function ChatWidget(props) {
|
|
|
941
1026
|
useEffect2(() => {
|
|
942
1027
|
const list = listRef.current;
|
|
943
1028
|
if (list !== null) list.scrollTop = list.scrollHeight;
|
|
944
|
-
}, [state.messages, state.open]);
|
|
1029
|
+
}, [state.messages, state.ownerTyping, state.open]);
|
|
945
1030
|
const handleClose = useCallback2(() => {
|
|
946
1031
|
closePanel();
|
|
947
1032
|
bubbleRef.current?.focus();
|
|
@@ -984,6 +1069,8 @@ function ChatWidget(props) {
|
|
|
984
1069
|
tr,
|
|
985
1070
|
listRef,
|
|
986
1071
|
firstFieldRef,
|
|
1072
|
+
ownerTyping: state.ownerTyping,
|
|
1073
|
+
onTyping: notifyTyping,
|
|
987
1074
|
onSend: sendMessage,
|
|
988
1075
|
onRetry: retryMessage,
|
|
989
1076
|
onNewConversation: startNewConversation
|