@fayz-ai/plugin-conversations 0.23.15 → 0.24.1

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,102 +1,103 @@
1
- import { isWaitingOnUs, assinarChegada, ConversationsContextProvider, useChannelConnections, InboxView, useConversationsStore } from './chunk-LJKLHGFX.js';
2
- export { isWaitingOnUs } from './chunk-LJKLHGFX.js';
3
- import { T, listMessagingChannels, findDedicatedChannel, findFallbackChannel, isPendingChannelStatus, canClaimDedicated, CHANNEL_CLAIM_TIMELINE } from './chunk-3FZX3ZQ3.js';
4
- export { WhatsAppProviders, listMessagingChannels } from './chunk-3FZX3ZQ3.js';
1
+ import { isWaitingOnUs, assinarChegada, ConversationsContextProvider, useConversationsStore } from './chunk-X7BP2JH6.js';
2
+ export { isWaitingOnUs } from './chunk-X7BP2JH6.js';
3
+ import { T, listMessagingChannels, findDedicatedChannel, findFallbackChannel, isPendingChannelStatus, canClaimDedicated, CHANNEL_CLAIM_TIMELINE, WhatsAppProviders } from './chunk-2JV6B366.js';
4
+ export { WhatsAppProviders, listMessagingChannels } from './chunk-2JV6B366.js';
5
+ import { providersApi } from './chunk-HMWUU5O7.js';
5
6
  export { providersApi } from './chunk-HMWUU5O7.js';
6
- import * as React2 from 'react';
7
- import React2__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
8
- import { Loader2, ExternalLink, MessageCircle, SquarePen, CheckCircle2 } from 'lucide-react';
9
- import { createConnectionStore, connectionRuns, connectionStatus, tl, lazyComponent, useActiveTenantId, registerTranslations, getSupabaseClientOptional, describeError, getActiveTenantId, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, useTranslation, navigateApp } from '@fayz-ai/core';
10
- import { defineSettingsTab, useRightRailStore, useAdminPath, useRightRailPanel, useWidgetSurface, adminNavigateTo, playChime, notifySystem, useRightRailMenu, requestNotificationPermission } from '@fayz-ai/admin';
11
- import { Input, Button, toast, defineKpiWidget, defineTableWidget, KpiCard, TableWidget } from '@fayz-ai/ui';
7
+ import * as React from 'react';
8
+ import React__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
12
9
  import { jsxs, jsx } from 'react/jsx-runtime';
10
+ import { createConnectionStore, connectionRuns, connectionStatus, tl, lazyComponent, useActiveTenantId, registerTranslations, getSupabaseClientOptional, describeError, getActiveTenantId, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, useTranslation, navigateApp } from '@fayz-ai/core';
11
+ import { defineSettingsTab, useNavContribution, playChime, notifySystem, requestNotificationPermission } from '@fayz-ai/admin';
13
12
  import { createStore } from 'zustand/vanilla';
13
+ import { Input, Button, toast, defineKpiWidget, defineTableWidget, KpiCard, TableWidget } from '@fayz-ai/ui';
14
+ import { Loader2, ExternalLink } from 'lucide-react';
14
15
  import { MIGRATIONS } from '#migrations/main';
15
16
 
16
- var CONNECTORS_ROUTE = "/settings/conversations/connectors";
17
- var SPLIT_AT = 720;
18
- function ConversationsRailPanel({ store: store2, config }) {
19
- const t = useTranslation();
20
- const ref = React2.useRef(null);
21
- const [width, setWidth] = React2.useState(0);
22
- const [newSignal, setNewSignal] = React2.useState(0);
23
- const connected = useChannelConnections();
24
- const closeRail = useRightRailStore((s) => s.close);
25
- const wide = width >= SPLIT_AT;
26
- React2.useLayoutEffect(() => {
27
- const el = ref.current;
28
- if (!el) return;
29
- const ro = new ResizeObserver(([e]) => setWidth(e.contentRect.width));
30
- ro.observe(el);
31
- return () => ro.disconnect();
32
- }, []);
33
- React2.useEffect(() => {
34
- const narrow = (ref.current?.getBoundingClientRect().width ?? 0) < SPLIT_AT;
35
- if (narrow) store2.getState().deselect();
36
- void store2.getState().load({ autoSelect: !narrow });
37
- }, [store2]);
38
- const go = (route) => {
39
- closeRail();
40
- adminNavigateTo(route);
17
+ var EVOLUTION_CONNECTOR_ID = "evolution";
18
+ async function evolutionStatus() {
19
+ const row = (await providersApi.list()).find((r) => r.provider === EVOLUTION_CONNECTOR_ID);
20
+ if (!row || row.status === "unconfigured") return { connected: false };
21
+ if (row.status === "connected") {
22
+ return {
23
+ connected: true,
24
+ plane: "request",
25
+ detail: row.isActive ? "Valendo agora" : "Conectado, mas outro provedor est\xE1 valendo"
26
+ };
27
+ }
28
+ return {
29
+ connected: false,
30
+ detail: row.status === "error" ? row.statusDetail ?? "A inst\xE2ncia respondeu com erro" : "Configurado \u2014 falta parear o n\xFAmero pelo QR"
41
31
  };
42
- useRightRailMenu([
43
- { id: "new", label: t("conversations.list.new"), icon: SquarePen, onSelect: () => setNewSignal((n) => n + 1) },
44
- { id: "full", label: tl("Open full screen", "Abrir em tela cheia"), icon: ExternalLink, onSelect: () => go("/conversations") }
45
- ]);
46
- return /* @__PURE__ */ jsxs("div", { ref, className: "flex h-full min-h-0 flex-col", children: [
47
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(ConversationsContextProvider, { store: store2, config, children: /* @__PURE__ */ jsx(InboxView, { layout: wide ? "dock-wide" : "dock", newSignal }) }) }),
48
- /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border p-3", children: connected === void 0 ? /* @__PURE__ */ jsx("div", { className: "h-10", "aria-hidden": true }) : connected.whatsapp ? /* @__PURE__ */ jsxs(
49
- Button,
50
- {
51
- variant: "bare",
52
- size: "bare",
53
- type: "button",
54
- onClick: () => go(CONNECTORS_ROUTE),
55
- className: "font-normal whitespace-normal justify-start fz-tap flex h-10 w-full items-center gap-2 rounded-button px-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
56
- children: [
57
- /* @__PURE__ */ jsx(CheckCircle2, { className: "h-4 w-4 text-[#25D366]", "aria-hidden": true }),
58
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: tl("WhatsApp connected", "WhatsApp conectado") }),
59
- /* @__PURE__ */ jsx("span", { className: "text-xs text-primary", children: tl("Manage", "Gerenciar") })
60
- ]
61
- }
62
- ) : /* @__PURE__ */ jsxs(
63
- Button,
64
- {
65
- variant: "bare",
66
- size: "bare",
67
- type: "button",
68
- onClick: () => go(CONNECTORS_ROUTE),
69
- "data-testid": "inbox-connect-whatsapp",
70
- className: "font-normal whitespace-normal fz-tap flex h-11 w-full items-center justify-center gap-2 rounded-button bg-[#25D366] text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[#1EBE5A] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#25D366] focus-visible:ring-offset-2",
71
- children: [
72
- /* @__PURE__ */ jsx(WhatsAppMark, { className: "h-[18px] w-[18px]" }),
73
- tl("Connect WhatsApp", "Conectar WhatsApp")
74
- ]
75
- }
76
- ) })
77
- ] });
78
32
  }
79
- function WhatsAppMark({ className }) {
80
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", className, "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M17.47 14.38c-.3-.15-1.76-.87-2.03-.97-.27-.1-.47-.15-.67.15-.2.3-.77.96-.94 1.16-.17.2-.35.22-.65.07-.3-.15-1.26-.46-2.4-1.48-.89-.79-1.49-1.77-1.66-2.07-.17-.3-.02-.46.13-.61.13-.13.3-.35.45-.52.15-.17.2-.3.3-.5.1-.2.05-.37-.02-.52-.08-.15-.67-1.61-.92-2.2-.24-.58-.49-.5-.67-.51h-.57c-.2 0-.52.07-.79.37-.27.3-1.04 1.02-1.04 2.48s1.07 2.88 1.21 3.08c.15.2 2.1 3.2 5.08 4.49.71.31 1.26.49 1.69.63.71.22 1.36.19 1.87.12.57-.09 1.76-.72 2.01-1.41.25-.7.25-1.29.17-1.41-.07-.13-.27-.2-.57-.35zM12.05 21.5h-.01a9.4 9.4 0 0 1-4.8-1.32l-.34-.2-3.56.93.95-3.47-.22-.36a9.4 9.4 0 0 1-1.44-5.02c0-5.2 4.23-9.43 9.43-9.43 2.52 0 4.89.98 6.67 2.77a9.37 9.37 0 0 1 2.76 6.67c0 5.2-4.23 9.43-9.44 9.43zm8.03-17.46A11.3 11.3 0 0 0 12.05.72C5.79.72.7 5.81.7 12.07c0 2 .52 3.95 1.52 5.67L.6 23.6l6-1.57a11.32 11.32 0 0 0 5.44 1.39h.01c6.26 0 11.35-5.1 11.35-11.35 0-3.03-1.18-5.88-3.32-8.03z" }) });
33
+ function EvolutionPanel() {
34
+ return /* @__PURE__ */ jsx(WhatsAppProviders, { only: "evolution" });
81
35
  }
36
+ var evolutionConnectorDef = {
37
+ id: EVOLUTION_CONNECTOR_ID,
38
+ hostPluginId: "conversations",
39
+ name: "Evolution API",
40
+ description: "WhatsApp pelo n\xFAmero da pr\xF3pria casa: parea com um QR no celular e manda texto livre, sem template e sem janela de 24h.",
41
+ icon: "MessageCircle",
42
+ // The channel's mark: the store sees "WhatsApp", which is what it connects.
43
+ logoUrl: "https://assets.fayz.ai/connectors/whatsapp.png",
44
+ category: "messaging",
45
+ authKind: "api-key",
46
+ planes: ["request", "sync"],
47
+ direction: "outbound",
48
+ docsUrl: "https://doc.evolution-api.com",
49
+ // A message that does not go out must never take the order with it: past
50
+ // this budget the send gives up and the order carries on.
51
+ latencyBudgetMs: 3e3,
52
+ onFailure: "degrade",
53
+ permissions: [
54
+ {
55
+ kind: "data",
56
+ resource: "conversation",
57
+ actions: ["read", "write"],
58
+ reason: "Abrir a conversa do cliente no WhatsApp e mant\xEA-la em dia."
59
+ },
60
+ {
61
+ kind: "data",
62
+ resource: "message",
63
+ actions: ["read", "write"],
64
+ reason: "Guardar cada mensagem enviada e recebida, com fotos, \xE1udios e documentos."
65
+ }
66
+ ],
67
+ // The URL, the key and the instance are typed in the provider card below,
68
+ // which already saves them where the sender reads them.
69
+ fields: [],
70
+ invokedAt: [
71
+ {
72
+ key: "messaging.send",
73
+ label: "Uma automa\xE7\xE3o ou um pedido manda mensagem e ela sai pelo WhatsApp da casa",
74
+ touches: ["conversation", "message"]
75
+ },
76
+ {
77
+ key: "conversations.inbound",
78
+ label: "O cliente responde, e a resposta entra na caixa de entrada",
79
+ plane: "sync",
80
+ trigger: "webhook",
81
+ touches: ["conversation", "message"]
82
+ }
83
+ ],
84
+ howItSyncs: [
85
+ "As mensagens saem pelo n\xFAmero da pr\xF3pria casa, pareado lendo um QR com o WhatsApp do celular (Aparelhos conectados).",
86
+ "Manda texto livre para qualquer cliente, sem template aprovado e sem a janela de 24h do WhatsApp oficial.",
87
+ "\xC9 o WhatsApp Web por baixo: o n\xFAmero pode ser banido sem aviso e a sess\xE3o pode cair. Use um n\xFAmero j\xE1 ativo, e pareie de novo se cair."
88
+ ],
89
+ getStatus: evolutionStatus,
90
+ disconnectNote: "Para desconectar, desative o provedor ou desconecte o aparelho no WhatsApp do celular. As conversas que j\xE1 existem continuam aqui.",
91
+ ExtraPanel: EvolutionPanel
92
+ };
82
93
  var tocar = playChime;
83
94
  var pedirPermissao = requestNotificationPermission;
84
95
  var avisarNoSistema = (titulo, corpo, tag) => notifySystem(titulo, corpo, tag);
85
- var RAIL_ID = "conversations";
86
- function InboxWatcher({ label, route = "/conversations", store: store2, config, inHeader }) {
87
- const Panel = React2.useMemo(() => {
88
- if (!store2) return null;
89
- const C = () => React2.createElement(ConversationsRailPanel, { store: store2, config });
90
- C.displayName = "ConversationsRailPanel";
91
- return C;
92
- }, [store2, config]);
93
- const railOpen = useRightRailStore((s) => s.open && s.active === RAIL_ID);
94
- const togglePanel = useRightRailStore((s) => s.togglePanel);
96
+ function InboxWatcher({ label, route = "/conversations", section = "main", position = 1 }) {
95
97
  const tenantId = useActiveTenantId();
96
- const path = useAdminPath();
97
- const [naoLidas, setNaoLidas] = React2.useState(0);
98
- const podeTocar = React2.useRef(false);
99
- React2.useEffect(() => {
98
+ const [naoLidas, setNaoLidas] = React.useState(0);
99
+ const podeTocar = React.useRef(false);
100
+ React.useEffect(() => {
100
101
  const liberar = () => {
101
102
  podeTocar.current = true;
102
103
  void pedirPermissao();
@@ -108,7 +109,7 @@ function InboxWatcher({ label, route = "/conversations", store: store2, config,
108
109
  for (const ev of ["pointerdown", "keydown", "touchstart"]) window.removeEventListener(ev, liberar);
109
110
  };
110
111
  }, []);
111
- const contar = React2.useCallback(async () => {
112
+ const contar = React.useCallback(async () => {
112
113
  const supabase = getSupabaseClientOptional();
113
114
  if (!supabase) return;
114
115
  const { data } = await supabase.from(T.conversations).select("id, status, last_message_direction, unread_count").eq("status", "open");
@@ -120,10 +121,10 @@ function InboxWatcher({ label, route = "/conversations", store: store2, config,
120
121
  })).length;
121
122
  setNaoLidas(n);
122
123
  }, []);
123
- React2.useEffect(() => {
124
+ React.useEffect(() => {
124
125
  void contar();
125
126
  }, [contar]);
126
- React2.useEffect(() => {
127
+ React.useEffect(() => {
127
128
  if (!tenantId) return;
128
129
  return assinarChegada(tenantId, ({ conversationId }) => {
129
130
  void contar();
@@ -141,37 +142,8 @@ function InboxWatcher({ label, route = "/conversations", store: store2, config,
141
142
  })();
142
143
  });
143
144
  }, [contar, tenantId, label]);
144
- useRightRailPanel(React2.useMemo(() => Panel ? {
145
- id: RAIL_ID,
146
- label,
147
- icon: MessageCircle,
148
- order: 12,
149
- badge: { count: naoLidas, tone: "destructive" },
150
- Component: Panel
151
- } : null, [Panel, label, naoLidas]));
152
- const naPagina = path === route || path.startsWith(`${route}/`);
153
- const aberta = naPagina || railOpen;
154
- const surface = useWidgetSurface();
155
- const named = !inHeader && surface === "rail";
156
- return /* @__PURE__ */ jsxs(
157
- Button,
158
- {
159
- variant: "bare",
160
- size: "bare",
161
- type: "button",
162
- onClick: () => Panel && !naPagina ? togglePanel(RAIL_ID) : adminNavigateTo(route),
163
- "aria-expanded": Panel ? railOpen : void 0,
164
- "aria-label": naoLidas > 0 ? `${label} (${naoLidas})` : label,
165
- title: label,
166
- "aria-current": aberta ? "page" : void 0,
167
- className: `fz-tap relative inline-flex shrink-0 items-center rounded-button transition-colors hover:bg-accent ${named ? "w-full gap-3 px-2.5 py-2.5" : "justify-center p-2"} ${aberta ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
168
- children: [
169
- /* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4 shrink-0" }),
170
- named && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: label }),
171
- naoLidas > 0 && /* @__PURE__ */ jsx("span", { className: "absolute -right-0.5 -top-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-medium leading-none text-destructive-foreground tabular-nums", children: naoLidas > 9 ? "9+" : naoLidas })
172
- ]
173
- }
174
- );
145
+ useNavContribution("conversations:unread", naoLidas > 0 ? [{ id: "conversations", label, route, badge: naoLidas, badgeTone: "destructive", section, position }] : []);
146
+ return null;
175
147
  }
176
148
 
177
149
  // src/data/accents.ts
@@ -1811,7 +1783,7 @@ function createConversationsPlugin(options) {
1811
1783
  contactEntityDef: options?.contactEntityDef
1812
1784
  };
1813
1785
  const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
1814
- const PageComponent = () => React2__default.createElement(ConversationsPage, { store: store2, config });
1786
+ const PageComponent = () => React__default.createElement(ConversationsPage, { store: store2, config });
1815
1787
  PageComponent.displayName = "ConversationsPage";
1816
1788
  return {
1817
1789
  id: "conversations",
@@ -1828,11 +1800,11 @@ function createConversationsPlugin(options) {
1828
1800
  declaredLimits: [
1829
1801
  { key: "conversations_month", label: "Conversations this month", table: "plg_conversations", period: "month" }
1830
1802
  ],
1831
- // SEM linha de rail para `/conversations`. A caixa de entrada subiu para a
1832
- // faixa do aplicativo, como ícone com a conta do que espera resposta — ver
1833
- // o widget `conversations-inbox` abaixo. Ela não é um destino como o
1834
- // Financeiro: é um estado que se lê de canto de olho, e no rail a conta
1835
- // ficava escondida atrás de um menu que recolhe.
1803
+ // `/conversations` has its rail line and its own screen. From 09-28 the
1804
+ // inbox lived as a header icon and from 10-15 in the right column beside
1805
+ // the assistant; the column squeezed the thread into a sliver and lost
1806
+ // what the full screen does. The unread count is written on this line by
1807
+ // the watcher below.
1836
1808
  //
1837
1809
  // E SEM CAMPANHAS NEM TEMPLATES. As duas telas foram removidas — não
1838
1810
  // escondidas: elas tomavam duas linhas de rail num plugin que declara, na
@@ -1847,6 +1819,15 @@ function createConversationsPlugin(options) {
1847
1819
  // AtendeAI does get a rail line, as in V1: whoever runs the AI follows it
1848
1820
  // all day. Offered to every account — off, the page is the upsell.
1849
1821
  navigation: [
1822
+ {
1823
+ id: "conversations",
1824
+ section: options?.navSection ?? "main",
1825
+ position: options?.navPosition ?? 1,
1826
+ label: options?.navLabel ?? "Conversations",
1827
+ route: "/conversations",
1828
+ icon: "MessageCircle",
1829
+ permission: { feature: "conversations", action: "read" }
1830
+ },
1850
1831
  // AtendeAI é recurso de restaurante (ChefControl): só aparece no vertical
1851
1832
  // 'food'. Uma loja de outro ramo não vê a linha.
1852
1833
  { section: "main", position: 45, label: "AtendeAI", route: "/atendeai", icon: "Bot", before: ["/analytics", "/reports"], verticalId: "food", permission: { feature: "conversations", action: "read" } }
@@ -1864,23 +1845,20 @@ function createConversationsPlugin(options) {
1864
1845
  permission: { feature: "conversations", action: "read" }
1865
1846
  }
1866
1847
  ],
1867
- // In the shell's header, next to the bell (notifications sit at 100): both
1868
- // say "something is waiting on you", so they sit side by side. Header
1869
- // controls render outside the route switch, so the watcher's sound and
1870
- // count survive navigation.
1848
+ // The watcher renders nothing; it is a header control because header
1849
+ // controls live outside the route switch, so the chime, the system
1850
+ // notification and the rail badge survive navigation.
1871
1851
  headerControls: [{
1872
1852
  id: "conversations-inbox",
1873
1853
  component: (() => {
1874
- const W = () => React2__default.createElement(InboxWatcher, {
1854
+ const W = () => React__default.createElement(InboxWatcher, {
1875
1855
  label: options?.navLabel ?? "Conversations",
1876
- store: store2,
1877
- config,
1878
- inHeader: true
1856
+ section: options?.navSection ?? "main",
1857
+ position: options?.navPosition ?? 1
1879
1858
  });
1880
- W.displayName = "InboxButton";
1859
+ W.displayName = "InboxWatcher";
1881
1860
  return W;
1882
1861
  })(),
1883
- // After the cash drawer (90), right before the bell.
1884
1862
  order: 95
1885
1863
  }],
1886
1864
  dashboardWidgets,
@@ -2038,15 +2016,16 @@ function createConversationsPlugin(options) {
2038
2016
  }
2039
2017
  ],
2040
2018
  onboarding: { program: buildConversationsOnboarding() },
2041
- // WhatsApp through Tyxter. Declared here because this plugin IS the inbox
2042
- // the messages land in — the connector configures inside Conversas.
2043
- connectors: [tyxterConnectorDef],
2019
+ // WhatsApp through Tyxter or through the store's own number (Evolution).
2020
+ // Declared here because this plugin IS the inbox the messages land in;
2021
+ // both show on Configurações → Conectores like every other connector.
2022
+ connectors: [tyxterConnectorDef, evolutionConnectorDef],
2044
2023
  locales: conversationsLocales
2045
2024
  };
2046
2025
  }
2047
- var ConversationsPage = React2__default.lazy(() => import('./ConversationsPage-4V4VLM4D.js').then((m) => ({ default: m.ConversationsPage })));
2048
- var AtendeAIPage = React2__default.lazy(() => import('./AtendeAIPage-PBBMWIDW.js').then((m) => ({ default: m.AtendeAIPage })));
2049
- var ConversationsSettingsTab = lazyComponent(() => import('./ConversationsSettings-7UQYM26R.js').then((m) => m.ConversationsSettingsTab));
2026
+ var ConversationsPage = React__default.lazy(() => import('./ConversationsPage-BTRYZPM7.js').then((m) => ({ default: m.ConversationsPage })));
2027
+ var AtendeAIPage = React__default.lazy(() => import('./AtendeAIPage-PBBMWIDW.js').then((m) => ({ default: m.AtendeAIPage })));
2028
+ var ConversationsSettingsTab = lazyComponent(() => import('./ConversationsSettings-Q3PGQMB3.js').then((m) => m.ConversationsSettingsTab));
2050
2029
 
2051
2030
  export { PAYMENT_PREFLIGHT_FUNCTION, TYXTER_CONNECTOR_ID, TYXTER_LATENCY_BUDGET_MS, campaignsApi, canOfferPayment, createConversationsPlugin, createMockConversationsProvider, createSupabaseConversationsProvider, openPaymentSetupSession, readPaymentReadiness, tyxterConnectorDef };
2052
2031
  //# sourceMappingURL=index.js.map