@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.
@@ -7307,7 +7307,8 @@ var EvolutionChatEmbed = (() => {
7307
7307
  retry: "Tentar novamente",
7308
7308
  sessionClosed: "Esta conversa foi encerrada. Abra uma nova para continuar.",
7309
7309
  newConversation: "Iniciar nova conversa",
7310
- poweredBy: "Powered by"
7310
+ poweredBy: "Powered by",
7311
+ typing: "digitando\u2026"
7311
7312
  };
7312
7313
  var en = {
7313
7314
  openChat: "Open chat",
@@ -7324,7 +7325,8 @@ var EvolutionChatEmbed = (() => {
7324
7325
  retry: "Try again",
7325
7326
  sessionClosed: "This conversation was closed. Start a new one to continue.",
7326
7327
  newConversation: "Start a new conversation",
7327
- poweredBy: "Powered by"
7328
+ poweredBy: "Powered by",
7329
+ typing: "typing\u2026"
7328
7330
  };
7329
7331
  var es = {
7330
7332
  openChat: "Abrir chat",
@@ -7341,7 +7343,8 @@ var EvolutionChatEmbed = (() => {
7341
7343
  retry: "Reintentar",
7342
7344
  sessionClosed: "Esta conversaci\xF3n fue cerrada. Abre una nueva para continuar.",
7343
7345
  newConversation: "Iniciar nueva conversaci\xF3n",
7344
- poweredBy: "Powered by"
7346
+ poweredBy: "Powered by",
7347
+ typing: "escribiendo\u2026"
7345
7348
  };
7346
7349
  var dictionaries = { pt, en, es };
7347
7350
  function t(locale, key, overrides) {
@@ -7590,6 +7593,31 @@ var EvolutionChatEmbed = (() => {
7590
7593
  .ecw-send:disabled { opacity: 0.55; cursor: not-allowed; }
7591
7594
  .ecw-send svg { width: 18px; height: 18px; }
7592
7595
 
7596
+ /* \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 */
7597
+ .ecw-typing {
7598
+ display: inline-flex;
7599
+ align-items: center;
7600
+ gap: 4px;
7601
+ padding: 10px 14px;
7602
+ border-radius: 12px;
7603
+ border-bottom-left-radius: 4px;
7604
+ background: var(--ecw-surface);
7605
+ border: 1px solid var(--ecw-border);
7606
+ }
7607
+ .ecw-typing-dot {
7608
+ width: 7px;
7609
+ height: 7px;
7610
+ border-radius: 50%;
7611
+ background: var(--ecw-muted);
7612
+ animation: ecw-typing-bounce 1.2s infinite ease-in-out;
7613
+ }
7614
+ .ecw-typing-dot:nth-child(2) { animation-delay: 0.18s; }
7615
+ .ecw-typing-dot:nth-child(3) { animation-delay: 0.36s; }
7616
+ @keyframes ecw-typing-bounce {
7617
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
7618
+ 30% { transform: translateY(-4px); opacity: 1; }
7619
+ }
7620
+
7593
7621
  /* \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 */
7594
7622
  .ecw-closed { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }
7595
7623
 
@@ -7654,17 +7682,23 @@ var EvolutionChatEmbed = (() => {
7654
7682
  messages: [],
7655
7683
  unread: 0,
7656
7684
  sending: false,
7657
- error: null
7685
+ error: null,
7686
+ ownerTyping: false,
7687
+ visitorTyping: false
7658
7688
  };
7659
7689
  function withMessage(state, message) {
7660
7690
  const index = state.messages.findIndex((m) => m.id === message.id);
7691
+ let messages;
7661
7692
  if (index >= 0) {
7662
- const messages = [...state.messages];
7693
+ messages = [...state.messages];
7663
7694
  messages[index] = message;
7664
- return { ...state, messages };
7695
+ } else {
7696
+ messages = [...state.messages, message];
7665
7697
  }
7666
7698
  const bump = state.open || message.direction !== "owner" ? 0 : 1;
7667
- return { ...state, messages: [...state.messages, message], unread: state.unread + bump };
7699
+ const next = { ...state, messages, unread: state.unread + bump };
7700
+ if (message.direction === "owner") next.ownerTyping = false;
7701
+ return next;
7668
7702
  }
7669
7703
  function chatReducer(state, action) {
7670
7704
  switch (action.type) {
@@ -7728,6 +7762,8 @@ var EvolutionChatEmbed = (() => {
7728
7762
  return { ...state, sending: action.value };
7729
7763
  case "error":
7730
7764
  return { ...state, error: action.value };
7765
+ case "typing":
7766
+ return action.from === "owner" ? state.ownerTyping === action.isTyping ? state : { ...state, ownerTyping: action.isTyping } : state.visitorTyping === action.isTyping ? state : { ...state, visitorTyping: action.isTyping };
7731
7767
  }
7732
7768
  }
7733
7769
  function normalizePhone(value) {
@@ -7798,8 +7834,9 @@ var EvolutionChatEmbed = (() => {
7798
7834
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
7799
7835
  };
7800
7836
  }
7801
- function useChat({ endpoint, realtime }) {
7837
+ function useChat({ endpoint, realtime, typingEndpoint }) {
7802
7838
  const [state, dispatch] = (0, import_react.useReducer)(chatReducer, initialState);
7839
+ const typingUrl = typingEndpoint ?? `${endpoint.replace(/\/+$/, "")}/typing`;
7803
7840
  const stateRef = (0, import_react.useRef)(state);
7804
7841
  (0, import_react.useEffect)(() => {
7805
7842
  stateRef.current = state;
@@ -7854,6 +7891,8 @@ var EvolutionChatEmbed = (() => {
7854
7891
  dispatch({ type: "append", message: e.message });
7855
7892
  } else if (e.type === "session" && e.status !== void 0) {
7856
7893
  dispatch({ type: "session-status", status: e.status });
7894
+ } else if (e.type === "typing" && e.from !== void 0) {
7895
+ dispatch({ type: "typing", from: e.from, isTyping: e.isTyping });
7857
7896
  }
7858
7897
  }, []);
7859
7898
  (0, import_react.useEffect)(() => {
@@ -7960,6 +7999,21 @@ var EvolutionChatEmbed = (() => {
7960
7999
  clearStoredSession();
7961
8000
  dispatch({ type: "reset-form" });
7962
8001
  }, []);
8002
+ const notifyTyping = (0, import_react.useCallback)(
8003
+ async (isTyping) => {
8004
+ const token = stateRef.current.token;
8005
+ if (token === null) return;
8006
+ try {
8007
+ await fetch(typingUrl, {
8008
+ method: "POST",
8009
+ headers: { "content-type": "application/json" },
8010
+ body: JSON.stringify({ token, isTyping })
8011
+ });
8012
+ } catch {
8013
+ }
8014
+ },
8015
+ [typingUrl]
8016
+ );
7963
8017
  const submitForm = (0, import_react.useCallback)(
7964
8018
  async ({ name, phone, message, honeypot }) => {
7965
8019
  if (honeypot.trim() !== "") return;
@@ -8007,7 +8061,7 @@ var EvolutionChatEmbed = (() => {
8007
8061
  },
8008
8062
  [endpoint]
8009
8063
  );
8010
- return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation };
8064
+ return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping };
8011
8065
  }
8012
8066
 
8013
8067
  // src/widget/chat-widget.tsx
@@ -8031,6 +8085,14 @@ var EvolutionChatEmbed = (() => {
8031
8085
  const glyph = status === "pending" ? "\u2713" : status === "sent" ? "\u2713\u2713" : "\u26A0";
8032
8086
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: status === "failed" ? "ecw-status ecw-status--failed" : "ecw-status", "aria-hidden": "true", children: glyph });
8033
8087
  }
8088
+ function TypingIndicator({ label }) {
8089
+ 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: [
8090
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
8091
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
8092
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
8093
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-sr-only", children: label })
8094
+ ] }) });
8095
+ }
8034
8096
  function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
8035
8097
  const uid = (0, import_react2.useId)();
8036
8098
  const [name, setName] = (0, import_react2.useState)("");
@@ -8130,29 +8192,53 @@ var EvolutionChatEmbed = (() => {
8130
8192
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", children: tr("privacyNote") })
8131
8193
  ] });
8132
8194
  }
8133
- function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, onSend, onRetry, onNewConversation }) {
8195
+ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
8134
8196
  const uid = (0, import_react2.useId)();
8135
8197
  const [draft, setDraft] = (0, import_react2.useState)("");
8198
+ const typingTimer = (0, import_react2.useRef)(null);
8199
+ (0, import_react2.useEffect)(() => {
8200
+ return () => {
8201
+ if (typingTimer.current !== null) clearTimeout(typingTimer.current);
8202
+ };
8203
+ }, []);
8204
+ const handleDraftChange = (e) => {
8205
+ setDraft(e.target.value);
8206
+ if (!canCompose) return;
8207
+ void onTyping(true);
8208
+ if (typingTimer.current !== null) clearTimeout(typingTimer.current);
8209
+ typingTimer.current = setTimeout(() => {
8210
+ void onTyping(false);
8211
+ typingTimer.current = null;
8212
+ }, 2500);
8213
+ };
8136
8214
  const submit = (e) => {
8137
8215
  e.preventDefault();
8138
8216
  const text = draft.trim();
8139
8217
  if (text === "" || !canCompose) return;
8218
+ if (typingTimer.current !== null) {
8219
+ clearTimeout(typingTimer.current);
8220
+ typingTimer.current = null;
8221
+ }
8222
+ void onTyping(false);
8140
8223
  setDraft("");
8141
8224
  void onSend(text);
8142
8225
  };
8143
8226
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
8144
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { ref: listRef, className: "ecw-list", children: messages.map(
8145
- (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: [
8146
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `ecw-bubble ecw-bubble--${m.direction}`, children: m.body }),
8147
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-meta", children: [
8148
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("time", { dateTime: m.createdAt, children: formatTime(m.createdAt, locale) }),
8149
- m.direction === "visitor" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusMark, { status: m.status }),
8150
- m.status === "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "ecw-retry", onClick: () => {
8151
- void onRetry(m.id);
8152
- }, children: tr("retry") })
8153
- ] })
8154
- ] }, m.id)
8155
- ) }),
8227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { ref: listRef, className: "ecw-list", children: [
8228
+ messages.map(
8229
+ (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: [
8230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `ecw-bubble ecw-bubble--${m.direction}`, children: m.body }),
8231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-meta", children: [
8232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("time", { dateTime: m.createdAt, children: formatTime(m.createdAt, locale) }),
8233
+ m.direction === "visitor" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusMark, { status: m.status }),
8234
+ m.status === "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "ecw-retry", onClick: () => {
8235
+ void onRetry(m.id);
8236
+ }, children: tr("retry") })
8237
+ ] })
8238
+ ] }, m.id)
8239
+ ),
8240
+ ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { label: tr("typing") })
8241
+ ] }),
8156
8242
  !canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
8157
8243
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
8158
8244
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "ecw-submit", onClick: onNewConversation, children: tr("newConversation") })
@@ -8169,9 +8255,7 @@ var EvolutionChatEmbed = (() => {
8169
8255
  autoComplete: "off",
8170
8256
  placeholder: tr("message"),
8171
8257
  value: draft,
8172
- onChange: (e) => {
8173
- setDraft(e.target.value);
8174
- },
8258
+ onChange: handleDraftChange,
8175
8259
  disabled: !canCompose
8176
8260
  }
8177
8261
  ),
@@ -8183,7 +8267,7 @@ var EvolutionChatEmbed = (() => {
8183
8267
  const { endpoint, locale, welcome, projectName, realtime, labels } = props;
8184
8268
  const accentColor = props.accentColor ?? DEFAULT_ACCENT;
8185
8269
  const tr = (0, import_react2.useCallback)((key) => t(locale, key, labels), [locale, labels]);
8186
- const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation } = useChat({ endpoint, realtime });
8270
+ const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping } = useChat({ endpoint, realtime });
8187
8271
  const bubbleRef = (0, import_react2.useRef)(null);
8188
8272
  const listRef = (0, import_react2.useRef)(null);
8189
8273
  const firstFieldRef = (0, import_react2.useRef)(null);
@@ -8208,7 +8292,7 @@ var EvolutionChatEmbed = (() => {
8208
8292
  (0, import_react2.useEffect)(() => {
8209
8293
  const list = listRef.current;
8210
8294
  if (list !== null) list.scrollTop = list.scrollHeight;
8211
- }, [state.messages, state.open]);
8295
+ }, [state.messages, state.ownerTyping, state.open]);
8212
8296
  const handleClose = (0, import_react2.useCallback)(() => {
8213
8297
  closePanel();
8214
8298
  bubbleRef.current?.focus();
@@ -8251,6 +8335,8 @@ var EvolutionChatEmbed = (() => {
8251
8335
  tr,
8252
8336
  listRef,
8253
8337
  firstFieldRef,
8338
+ ownerTyping: state.ownerTyping,
8339
+ onTyping: notifyTyping,
8254
8340
  onSend: sendMessage,
8255
8341
  onRetry: retryMessage,
8256
8342
  onNewConversation: startNewConversation