@adatechnology/conversations-ui 0.1.0-rc.3 → 0.1.0-rc.31

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.
Files changed (143) hide show
  1. package/dist/chunk-CUYYYZWD.js +2317 -0
  2. package/dist/chunk-DXPSPUWF.js +110 -0
  3. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  4. package/dist/flows/index.d.ts +338 -4
  5. package/dist/flows/index.js +1331 -55
  6. package/dist/index.d.ts +1074 -42
  7. package/dist/index.js +3572 -742
  8. package/dist/preview/index.d.ts +328 -8
  9. package/dist/preview/index.js +902 -115
  10. package/dist/styles.css +899 -0
  11. package/dist/types-De5aN-E_.d.ts +502 -0
  12. package/package.json +3 -3
  13. package/src/AudioPlayer.tsx +8 -0
  14. package/src/AudioRecorderButton.test.tsx +30 -0
  15. package/src/AudioRecorderButton.tsx +248 -0
  16. package/src/AudioTranscription.test.tsx +115 -0
  17. package/src/AudioTranscription.tsx +252 -0
  18. package/src/Avatar.tsx +14 -3
  19. package/src/ConversationContextPanel.tsx +218 -44
  20. package/src/ConversationDocumentsPanel.tsx +347 -24
  21. package/src/ConversationHeader.test.tsx +66 -0
  22. package/src/ConversationHeader.tsx +163 -45
  23. package/src/ConversationListItem.tsx +19 -2
  24. package/src/ConversationLocalesProvider.tsx +42 -0
  25. package/src/ConversationRow.tsx +31 -7
  26. package/src/DocumentsLibrary.tsx +382 -0
  27. package/src/EmojiPicker.tsx +70 -55
  28. package/src/FileIcon.test.ts +83 -0
  29. package/src/FileIcon.tsx +88 -11
  30. package/src/InteractiveMessage.test.tsx +41 -0
  31. package/src/InteractiveMessage.tsx +146 -0
  32. package/src/Lightbox.tsx +18 -3
  33. package/src/MediaRenderer.tsx +98 -22
  34. package/src/MessageBubble.test.tsx +41 -0
  35. package/src/MessageBubble.tsx +75 -6
  36. package/src/MessageComposer.test.tsx +35 -0
  37. package/src/MessageComposer.tsx +155 -19
  38. package/src/RichMessageComposer.test.tsx +113 -0
  39. package/src/RichMessageComposer.tsx +551 -0
  40. package/src/SimpleEmojiPicker.tsx +5 -3
  41. package/src/StatusTicks.tsx +1 -1
  42. package/src/Toast.tsx +4 -0
  43. package/src/Tooltip.test.ts +42 -0
  44. package/src/Tooltip.tsx +164 -0
  45. package/src/Wallpaper.test.tsx +21 -0
  46. package/src/Wallpaper.tsx +67 -7
  47. package/src/WhatsAppMessageEditor.tsx +28 -4
  48. package/src/WindowExpiredNotice.tsx +12 -4
  49. package/src/audioRecorderFormat.test.ts +67 -0
  50. package/src/buildOutput.test.ts +79 -0
  51. package/src/composer.constant.ts +33 -0
  52. package/src/conversationTranscript.test.ts +57 -0
  53. package/src/conversationTranscript.ts +29 -4
  54. package/src/conversationWindow.ts +7 -5
  55. package/src/documentTypeLabel.test.ts +57 -0
  56. package/src/documents/DocumentsWorkspace.tsx +550 -0
  57. package/src/documents/index.ts +8 -0
  58. package/src/documents/labels.ts +92 -0
  59. package/src/emojiCatalog.test.ts +35 -0
  60. package/src/emojiCatalog.ts +189 -0
  61. package/src/flows/FlowGroupHeader.tsx +12 -2
  62. package/src/flows/FlowMapCanvas.tsx +15 -12
  63. package/src/flows/FlowMapNode.tsx +4 -1
  64. package/src/flows/FlowNodeCard.tsx +35 -8
  65. package/src/flows/FlowNodePanel.tsx +149 -38
  66. package/src/flows/FlowPalette.tsx +13 -3
  67. package/src/flows/FlowPortalNode.tsx +1 -1
  68. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  69. package/src/flows/FlowsWorkspace.tsx +1023 -0
  70. package/src/flows/flowCanvasModel.test.ts +312 -0
  71. package/src/flows/flowCanvasModel.ts +368 -0
  72. package/src/flows/flowEditorOps.test.ts +241 -0
  73. package/src/flows/flowEditorOps.ts +177 -0
  74. package/src/flows/flowGraph.ts +6 -6
  75. package/src/flows/index.ts +47 -1
  76. package/src/flows/labels.ts +141 -0
  77. package/src/flows/workspaceContract.test.ts +95 -0
  78. package/src/hooks/useContainerWidth.ts +35 -0
  79. package/src/hooks/useConversationActions.ts +56 -0
  80. package/src/hooks/useConversationDocuments.ts +11 -7
  81. package/src/hooks/useConversationList.ts +15 -9
  82. package/src/hooks/useConversationMessages.ts +2 -2
  83. package/src/hooks/useConversationRealtime.ts +10 -8
  84. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  85. package/src/hooks/useUrlFilterState.ts +107 -0
  86. package/src/icon.constant.ts +12 -0
  87. package/src/index.ts +114 -13
  88. package/src/lib/cn.test.ts +29 -0
  89. package/src/lib/composer-formatting.test.ts +78 -0
  90. package/src/lib/composer-formatting.ts +145 -0
  91. package/src/lib/createMediaUrlResolver.ts +33 -0
  92. package/src/lib/paginated.test.ts +33 -0
  93. package/src/lib/paginated.ts +26 -0
  94. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  95. package/src/lib/whatsapp-formatting.tsx +28 -3
  96. package/src/listing/index.tsx +202 -0
  97. package/src/pagination.constant.ts +10 -0
  98. package/src/preview/ConversationPreview.tsx +199 -13
  99. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  100. package/src/preview/ConversationSimulatorPanel.tsx +89 -0
  101. package/src/preview/MediaTypesPreview.tsx +87 -0
  102. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  103. package/src/preview/createMockConversationsApi.ts +175 -15
  104. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  105. package/src/preview/createPreviewBridgeClient.ts +124 -0
  106. package/src/preview/createPreviewMediaUploader.ts +82 -0
  107. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  108. package/src/preview/createPreviewWebhookClient.ts +127 -4
  109. package/src/preview/index.ts +33 -3
  110. package/src/preview/mediaTypeOf.test.ts +15 -0
  111. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  112. package/src/preview/preview.test.ts +5 -3
  113. package/src/preview/previewFileSamples.test.ts +151 -0
  114. package/src/preview/previewFileSamples.ts +74 -0
  115. package/src/preview/previewFixtures.ts +288 -1
  116. package/src/preview/previewMediaSource.test.ts +62 -0
  117. package/src/preview/previewMediaSource.ts +91 -0
  118. package/src/preview/previewMediaUploader.test.ts +61 -0
  119. package/src/providers/ConversationsProvider.tsx +8 -6
  120. package/src/providers/types.ts +185 -10
  121. package/src/quickReply.test.ts +58 -0
  122. package/src/settings/MessagesWorkspace.tsx +468 -0
  123. package/src/settings/TopicsForm.tsx +2 -0
  124. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  125. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  126. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  127. package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
  128. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  129. package/src/settings/WhatsAppTemplatesSettings.tsx +9 -1
  130. package/src/styles.css +862 -0
  131. package/src/types.ts +64 -1
  132. package/src/useWaitingNotifications.ts +74 -29
  133. package/src/workspace/BulkTemplateModal.tsx +132 -0
  134. package/src/workspace/ConversationPane.tsx +432 -0
  135. package/src/workspace/ConversationsInboxList.tsx +194 -0
  136. package/src/workspace/ConversationsWorkspace.tsx +346 -0
  137. package/src/workspace/index.ts +12 -0
  138. package/src/workspace/labels.test.ts +17 -0
  139. package/src/workspace/labels.ts +85 -0
  140. package/src/workspace/useConversationsInbox.ts +332 -0
  141. package/dist/chunk-N7B24WYD.js +0 -719
  142. package/dist/chunk-NV2RZ5KT.js +0 -56
  143. package/dist/types-C0PtaO7S.d.ts +0 -207
@@ -0,0 +1,110 @@
1
+ // src/Tooltip.tsx
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var TOOLTIP_ATTRIBUTE = "data-cv-tooltip";
6
+ var TOOLTIP_DELAY_MS = 120;
7
+ var TOOLTIP_GAP_PX = 8;
8
+ var TOOLTIP_EDGE_PX = 72;
9
+ function tooltipPositionOf({ targetRect, viewportWidth }) {
10
+ const placement = targetRect.top < TOOLTIP_EDGE_PX ? "bottom" : "top";
11
+ const centerX = targetRect.left + targetRect.width / 2;
12
+ return {
13
+ left: Math.min(Math.max(centerX, TOOLTIP_EDGE_PX), viewportWidth - TOOLTIP_EDGE_PX),
14
+ top: placement === "top" ? targetRect.top - TOOLTIP_GAP_PX : targetRect.bottom + TOOLTIP_GAP_PX,
15
+ placement
16
+ };
17
+ }
18
+ function tooltipStateFor(target, text) {
19
+ const position = tooltipPositionOf({
20
+ targetRect: target.getBoundingClientRect(),
21
+ viewportWidth: window.innerWidth
22
+ });
23
+ return { text, ...position };
24
+ }
25
+ var layerOwner;
26
+ function TooltipLayer() {
27
+ const identity = useRef({});
28
+ const [isOwner, setIsOwner] = useState(false);
29
+ const [tooltip, setTooltip] = useState(void 0);
30
+ useEffect(() => {
31
+ const owned = identity.current;
32
+ if (layerOwner && layerOwner !== owned) return void 0;
33
+ layerOwner = owned;
34
+ setIsOwner(true);
35
+ return () => {
36
+ if (layerOwner === owned) layerOwner = void 0;
37
+ };
38
+ }, []);
39
+ useEffect(() => {
40
+ if (!isOwner) return void 0;
41
+ let timer;
42
+ function cancelPending() {
43
+ if (timer) clearTimeout(timer);
44
+ timer = void 0;
45
+ }
46
+ function hide() {
47
+ cancelPending();
48
+ setTooltip(void 0);
49
+ }
50
+ function handleEnter(event) {
51
+ const origin = event.target;
52
+ if (!(origin instanceof Element)) return;
53
+ const target = origin.closest(`[${TOOLTIP_ATTRIBUTE}]`);
54
+ if (!(target instanceof HTMLElement)) {
55
+ hide();
56
+ return;
57
+ }
58
+ const text = target.getAttribute(TOOLTIP_ATTRIBUTE);
59
+ if (!text) {
60
+ hide();
61
+ return;
62
+ }
63
+ cancelPending();
64
+ timer = setTimeout(() => setTooltip(tooltipStateFor(target, text)), TOOLTIP_DELAY_MS);
65
+ }
66
+ function handleLeaveWindow(event) {
67
+ if (!event.relatedTarget) hide();
68
+ }
69
+ function handleKeyDown(event) {
70
+ if (event.key === "Escape") hide();
71
+ }
72
+ document.addEventListener("pointerover", handleEnter);
73
+ document.addEventListener("pointerout", handleLeaveWindow);
74
+ document.addEventListener("focusin", handleEnter);
75
+ document.addEventListener("pointerdown", hide);
76
+ document.addEventListener("focusout", hide);
77
+ document.addEventListener("keydown", handleKeyDown);
78
+ window.addEventListener("scroll", hide, true);
79
+ window.addEventListener("resize", hide);
80
+ return () => {
81
+ cancelPending();
82
+ document.removeEventListener("pointerover", handleEnter);
83
+ document.removeEventListener("pointerout", handleLeaveWindow);
84
+ document.removeEventListener("focusin", handleEnter);
85
+ document.removeEventListener("pointerdown", hide);
86
+ document.removeEventListener("focusout", hide);
87
+ document.removeEventListener("keydown", handleKeyDown);
88
+ window.removeEventListener("scroll", hide, true);
89
+ window.removeEventListener("resize", hide);
90
+ };
91
+ }, [isOwner]);
92
+ if (!tooltip) return null;
93
+ return createPortal(
94
+ /* @__PURE__ */ jsx(
95
+ "div",
96
+ {
97
+ role: "tooltip",
98
+ className: `cv-tooltip cv-tooltip--${tooltip.placement}`,
99
+ style: { left: tooltip.left, top: tooltip.top },
100
+ children: tooltip.text
101
+ }
102
+ ),
103
+ document.body
104
+ );
105
+ }
106
+
107
+ export {
108
+ TOOLTIP_ATTRIBUTE,
109
+ TooltipLayer
110
+ };
@@ -69,9 +69,12 @@ function parseInlineTokens(text) {
69
69
  if (remaining) result.push({ type: "text", content: remaining });
70
70
  break;
71
71
  }
72
- if (nextSpecial > 0) {
73
- result.push({ type: "text", content: remaining.slice(0, nextSpecial) });
72
+ if (nextSpecial === 0) {
73
+ result.push({ type: "text", content: remaining.slice(0, 1) });
74
+ remaining = remaining.slice(1);
75
+ continue;
74
76
  }
77
+ result.push({ type: "text", content: remaining.slice(0, nextSpecial) });
75
78
  remaining = remaining.slice(nextSpecial);
76
79
  }
77
80
  return result;
@@ -103,6 +106,9 @@ function unescapeHtml(text) {
103
106
  }
104
107
  var CODE_TOKEN_MARK = String.fromCharCode(57344);
105
108
  var CODE_TOKEN_REGEX = new RegExp(`${CODE_TOKEN_MARK}(\\d+)${CODE_TOKEN_MARK}`, "g");
109
+ var ZERO_WIDTH_SPACE = String.fromCharCode(8203);
110
+ var EMPTY_CODE_REGEX = new RegExp(`<code[^>]*>[${ZERO_WIDTH_SPACE}\\s]*</code>`, "gi");
111
+ var ZERO_WIDTH_SPACE_REGEX = new RegExp(ZERO_WIDTH_SPACE, "g");
106
112
  function waToHTML(text) {
107
113
  if (!text) return "";
108
114
  const codeTokens = [];
@@ -117,7 +123,7 @@ function waToHTML(text) {
117
123
  let html = escapeHtml(working);
118
124
  html = html.replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>");
119
125
  html = html.replace(/_([^_\n]+)_/g, "<em>$1</em>");
120
- html = html.replace(/~([^~\n]+)~/g, "<del>$1</del>");
126
+ html = html.replace(/~([^~\n]+)~/g, "<s>$1</s>");
121
127
  html = html.replace(/\n/g, "<br>");
122
128
  html = html.replace(CODE_TOKEN_REGEX, (_match, indexStr) => {
123
129
  const token = codeTokens[Number(indexStr)];
@@ -132,6 +138,7 @@ function waToHTML(text) {
132
138
  function htmlToWA(html) {
133
139
  if (!html) return "";
134
140
  let text = html;
141
+ text = text.replace(EMPTY_CODE_REGEX, "");
135
142
  text = text.replace(/<code data-wa="block"[^>]*>([\s\S]*?)<\/code>/gi, (_match, inner) => `\`\`\`${unescapeHtml(inner.replace(/<br\s*\/?>/gi, "\n"))}\`\`\``);
136
143
  text = text.replace(/<code data-wa="inline"[^>]*>([\s\S]*?)<\/code>/gi, (_match, inner) => `\`${unescapeHtml(inner)}\``);
137
144
  text = text.replace(/<br\s*\/?>/gi, "\n").replace(/<div>/gi, "\n").replace(/<\/div>/gi, "").replace(/<\/p>/gi, "\n").replace(/<p[^>]*>/gi, "");
@@ -141,9 +148,11 @@ function htmlToWA(html) {
141
148
  text = text.replace(/<i>(.*?)<\/i>/gi, "_$1_");
142
149
  text = text.replace(/<del>(.*?)<\/del>/gi, "~$1~");
143
150
  text = text.replace(/<s>(.*?)<\/s>/gi, "~$1~");
151
+ text = text.replace(/<strike>(.*?)<\/strike>/gi, "~$1~");
144
152
  text = text.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
145
153
  text = text.replace(/<[^>]+>/g, "");
146
154
  text = unescapeHtml(text);
155
+ text = text.replace(ZERO_WIDTH_SPACE_REGEX, "");
147
156
  return text.trim();
148
157
  }
149
158
  function waToHTMLInline(text) {
@@ -151,9 +160,64 @@ function waToHTMLInline(text) {
151
160
  return escapeHtml(text).replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>").replace(/_([^_\n]+)_/g, "<em>$1</em>").replace(/~([^~\n]+)~/g, "<del>$1</del>").replace(/`([^`\n]+)`/g, "<code>$1</code>");
152
161
  }
153
162
 
163
+ // src/useDarkMode.ts
164
+ import { useState, useEffect, useCallback } from "react";
165
+ var STORAGE_KEY = "conversations-ui-dark-mode";
166
+ function getInitialDark() {
167
+ if (typeof window === "undefined") return false;
168
+ const stored = localStorage.getItem(STORAGE_KEY);
169
+ if (stored !== null) {
170
+ return stored === "true";
171
+ }
172
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
173
+ }
174
+ function useIsDarkTheme() {
175
+ const [isDark, setIsDark] = useState(
176
+ () => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
177
+ );
178
+ useEffect(() => {
179
+ const root = document.documentElement;
180
+ const observer = new MutationObserver(() => setIsDark(root.classList.contains("dark")));
181
+ observer.observe(root, { attributes: true, attributeFilter: ["class"] });
182
+ setIsDark(root.classList.contains("dark"));
183
+ return () => observer.disconnect();
184
+ }, []);
185
+ return isDark;
186
+ }
187
+ function useDarkMode() {
188
+ const [isDark, setIsDark] = useState(getInitialDark);
189
+ useEffect(() => {
190
+ const root = document.documentElement;
191
+ if (isDark) {
192
+ root.classList.add("dark");
193
+ } else {
194
+ root.classList.remove("dark");
195
+ }
196
+ localStorage.setItem(STORAGE_KEY, String(isDark));
197
+ }, [isDark]);
198
+ useEffect(() => {
199
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
200
+ const handleChange = (event) => {
201
+ const stored = localStorage.getItem(STORAGE_KEY);
202
+ if (stored === null) {
203
+ setIsDark(event.matches);
204
+ }
205
+ };
206
+ mediaQuery.addEventListener("change", handleChange);
207
+ return () => mediaQuery.removeEventListener("change", handleChange);
208
+ }, []);
209
+ const toggle = useCallback(() => {
210
+ setIsDark((prev) => !prev);
211
+ }, []);
212
+ return { isDark, toggle };
213
+ }
214
+
154
215
  export {
155
216
  parseWhatsAppFormatting,
217
+ ZERO_WIDTH_SPACE,
156
218
  waToHTML,
157
219
  htmlToWA,
158
- waToHTMLInline
220
+ waToHTMLInline,
221
+ useIsDarkTheme,
222
+ useDarkMode
159
223
  };
@@ -1,4 +1,5 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode } from 'react';
2
3
  import { NodeProps } from '@xyflow/react';
3
4
  import { LucideIcon } from 'lucide-react';
4
5
  import { FlowConditionOperator, FlowGraphData, FlowNodeData, FlowNodeType, FlowQuestionType, FlowActionKind } from '@adatechnology/meta-whatsapp-contracts';
@@ -9,6 +10,7 @@ declare const BUILT_IN_ACTION_KINDS: {
9
10
  readonly HANDOFF: "handoff";
10
11
  readonly RATE_LIMITED_HANDOFF: "rate_limited_handoff";
11
12
  readonly SEND_PRODUCT_LIST: "send_product_list";
13
+ readonly SEND_MEDIA: "send_media";
12
14
  };
13
15
  declare const CROSS_FLOW_PREFIX = "flow:";
14
16
  declare const isCrossFlowTarget: (target: string) => boolean;
@@ -62,11 +64,12 @@ type CollectionChain = {
62
64
  actionNodeId: string;
63
65
  };
64
66
  declare function findCollectionChains(graph: FlowGraphData): CollectionChain[];
65
- declare function slugifyNodeId(label: string, existing: Set<string>): string;
67
+ declare function slugifyNodeId(label: string, existing: ReadonlySet<string>): string;
66
68
 
67
69
  interface FlowEditorLabels {
68
70
  legend: Record<FlowNodeType, string>;
69
71
  startNodeTooltip: string;
72
+ detachedNodeTooltip: string;
70
73
  liveCountTooltip: (count: number) => string;
71
74
  edgeFallbackLabel: string;
72
75
  actionKindLabels: Record<string, string>;
@@ -75,10 +78,15 @@ interface FlowEditorLabels {
75
78
  nodePanel: {
76
79
  title: string;
77
80
  contextKey: string;
81
+ nodeName: string;
82
+ nodeNamePlaceholder: string;
83
+ nodeNameHint: string;
78
84
  questionType: string;
79
85
  question: string;
80
86
  options: string;
81
87
  addOption: string;
88
+ removeOption: string;
89
+ close: string;
82
90
  optionId: string;
83
91
  optionLabel: string;
84
92
  next: string;
@@ -109,6 +117,8 @@ interface FlowEditorLabels {
109
117
  conditionTrue: string;
110
118
  conditionFalse: string;
111
119
  conditionVariableMissing: string;
120
+ media: string;
121
+ mediaUnavailable: string;
112
122
  };
113
123
  palette: {
114
124
  title: string;
@@ -121,6 +131,8 @@ interface FlowEditorLabels {
121
131
  flowMap: {
122
132
  nodeCount: (count: number) => string;
123
133
  openFlow: string;
134
+ toggleToMap: string;
135
+ toggleToDetail: string;
124
136
  };
125
137
  flowGroup: {
126
138
  focus: string;
@@ -130,6 +142,66 @@ interface FlowEditorLabels {
130
142
  tooltip: string;
131
143
  goesTo: (label: string) => string;
132
144
  };
145
+ collectionChain: {
146
+ feeds: (label: string) => string;
147
+ };
148
+ /**
149
+ * Texto dos problemas encontrados por `validateGraph`. Vive aqui, e não no host, porque a tela
150
+ * composta é quem valida — deixar de fora obrigaria todo produto a repassar o mesmo mapa de
151
+ * funções só para a barra de erros aparecer.
152
+ */
153
+ validation: FlowValidationLabels;
154
+ /** Barra de cima, estados de carregamento e ações do editor inteiro. */
155
+ workspace: {
156
+ title: string;
157
+ subtitle: string;
158
+ loading: string;
159
+ loadError: string;
160
+ saveGraph: string;
161
+ saving: string;
162
+ saveSuccess: string;
163
+ saveError: string;
164
+ organize: string;
165
+ organizeTooltip: string;
166
+ discardChanges: string;
167
+ discardTooltip: string;
168
+ discardConfirm: string;
169
+ unsavedChangesConfirm: string;
170
+ };
171
+ flowManager: {
172
+ newFlow: string;
173
+ createTitle: string;
174
+ key: string;
175
+ keyHint: string;
176
+ keyInvalid: string;
177
+ label: string;
178
+ showInMenu: string;
179
+ menuOptionLabel: string;
180
+ create: string;
181
+ creating: string;
182
+ deleteFlow: string;
183
+ deleteConfirm: (label: string) => string;
184
+ createError: string;
185
+ deleteError: string;
186
+ };
187
+ }
188
+ interface FlowValidationLabels {
189
+ title: string;
190
+ errors: (count: number) => string;
191
+ warnings: (count: number) => string;
192
+ noStart: string;
193
+ brokenRef: (from: string, to: string) => string;
194
+ choiceWithoutOptions: (id: string) => string;
195
+ duplicatedOptionId: (id: string, optionId: string) => string;
196
+ optionWithoutTarget: (id: string, optionLabel: string) => string;
197
+ tooManyOptions: (id: string, count: number) => string;
198
+ buttonTitleTooLong: (id: string, label: string) => string;
199
+ listTitleTooLong: (id: string, label: string) => string;
200
+ bodyTooLong: (id: string) => string;
201
+ unreachable: (id: string) => string;
202
+ deadEndQuestion: (id: string) => string;
203
+ conditionIncomplete: (id: string) => string;
204
+ conditionBranchMissing: (id: string, branch: string) => string;
133
205
  }
134
206
  declare const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels;
135
207
  declare function mergeFlowEditorLabels(override?: Partial<FlowEditorLabels>): FlowEditorLabels;
@@ -140,6 +212,12 @@ type FlowNodeCardData = {
140
212
  liveCount: number;
141
213
  isStart: boolean;
142
214
  isSelected: boolean;
215
+ /**
216
+ * Nó sem nenhuma ligação de entrada — o bot nunca chega nele. Contorno tracejado e pulso: sem
217
+ * isso, desconectar um fio ou criar um card solto passa despercebido até o fluxo quebrar em
218
+ * produção.
219
+ */
220
+ isDetached?: boolean;
143
221
  issues: GraphIssue[];
144
222
  labels: FlowEditorLabels;
145
223
  actionKindIcons?: Record<string, LucideIcon>;
@@ -233,14 +311,270 @@ interface FlowNodePanelProps {
233
311
  onChange: (updated: FlowNodeData) => void;
234
312
  onDelete: (nodeId: string) => void;
235
313
  labels?: Partial<FlowEditorLabels>;
314
+ /**
315
+ * Seletor de arquivos do nó `send_media`, renderizado no lugar da mensagem direta.
316
+ *
317
+ * Slot, e não uma lista de arquivos por prop, porque a biblioteca é do host: upload, permissão e
318
+ * URL assinada são dele, e o painel não tem como buscar nada. Ausente, o nó continua editável —
319
+ * só não dá para anexar por aqui.
320
+ */
321
+ renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode;
236
322
  }
237
- declare function FlowNodePanel({ graph, node, issues, otherFlows, onClose, onChange, onDelete, labels: labelsOverride, }: FlowNodePanelProps): react.JSX.Element;
323
+ declare function FlowNodePanel({ graph, node, issues, otherFlows, onClose, onChange, onDelete, labels: labelsOverride, renderMediaPicker, }: FlowNodePanelProps): react.JSX.Element;
238
324
 
239
325
  interface FlowWhatsAppPreviewProps {
240
326
  body: string;
241
327
  options?: [string, string][];
242
328
  labels?: FlowEditorLabels['nodePanel'];
243
329
  }
244
- declare function FlowWhatsAppPreview({ body, options, labels }: FlowWhatsAppPreviewProps): react.JSX.Element;
330
+ declare function FlowWhatsAppPreview({ body, options, labels, }: FlowWhatsAppPreviewProps): react.JSX.Element;
331
+
332
+ /**
333
+ * Derivações puras do canvas do editor: posições, arestas, contagem de conversas vivas e o nó que
334
+ * nasce de cada item da paleta.
335
+ *
336
+ * Viviam dentro do componente, e é ali que estava o risco. Uma aresta com destino errado não pinta
337
+ * errado — ela leva a conversa do cliente para o lugar errado, e o sintoma aparece longe de quem
338
+ * editou. Um card que o layout joga para fora da área visível se lê como "apaguei sem querer".
339
+ *
340
+ * A topologia sai daqui em forma neutra (`FlowEdgeSpec`) e o estilo fica no componente: o teste cobra
341
+ * o que importa e roda sem navegador nem `@xyflow/react`. Cor de traço ninguém quebra sem ver.
342
+ */
343
+
344
+ type FlowNodePosition = {
345
+ readonly x: number;
346
+ readonly y: number;
347
+ };
348
+ /** Onde cada conversa viva está parada agora — uma linha por sessão. */
349
+ type FlowLivePosition = {
350
+ readonly currentState: string;
351
+ readonly flow: string | null;
352
+ readonly nodeId: string | null;
353
+ readonly menuNodeId: string | null;
354
+ };
355
+ /**
356
+ * Quantas conversas estão paradas num nó — o formato que o servidor já agrega.
357
+ *
358
+ * `meta-whatsapp-module` responde `GROUP BY (flowKey, currentNodeId)`, e trazer uma linha por
359
+ * sessão só para o editor recontar em memória seria pior em toda base grande. Os dois formatos
360
+ * valem porque produto que já implementou o de sessão não deve quebrar numa release de correção.
361
+ */
362
+ type FlowLiveNodeCount = {
363
+ readonly flowKey: string;
364
+ readonly nodeId: string;
365
+ readonly count: number;
366
+ };
367
+ type FlowLivePositionInput = FlowLivePosition | FlowLiveNodeCount;
368
+ /**
369
+ * Pseudo-nó que representa um fluxo alvo ainda não mesclado no canvas.
370
+ *
371
+ * Um portal por (nó de origem, fluxo alvo): duas opções do mesmo nó indo para o mesmo fluxo
372
+ * compartilham o portal, senão o card ficaria cercado de caixas idênticas.
373
+ */
374
+ declare function portalNodeId(sourceNodeId: string, target: string): string;
375
+ declare function chainFrameNodeId(actionNodeId: string): string;
376
+ declare const GROUP_HEADER_NODE_ID = "__group_header__";
377
+ /**
378
+ * Quantas conversas estão paradas em cada nó de um fluxo.
379
+ *
380
+ * A raiz é caso à parte no formato por sessão: o servidor guarda o passo do menu em `menuNodeId`,
381
+ * num campo próprio, e uma conversa no menu não carrega `flow`. Ler `nodeId` ali daria contagem
382
+ * zero na tela mais visitada do editor. No formato agregado a chave do fluxo já vem na linha,
383
+ * então a raiz não tem exceção.
384
+ */
385
+ declare function countLiveByNode(params: {
386
+ readonly flowKey: string;
387
+ readonly rootFlowKey: string;
388
+ readonly positions: readonly FlowLivePositionInput[] | undefined;
389
+ }): Record<string, number>;
390
+ /**
391
+ * Um layout só para TODOS os nós de TODOS os fluxos abertos juntos.
392
+ *
393
+ * Posicionar cada fluxo à parte e deslocar não resolve: nada impede dois fluxos de ocuparem o mesmo
394
+ * espaço, e a altura real de cada card é ignorada. Aqui o ranqueamento por BFS roda sobre o grafo
395
+ * mesclado inteiro, com os saltos `flow:<key>` já resolvidos para o nó inicial do alvo.
396
+ */
397
+ declare function computeMergedLayout(params: {
398
+ readonly openKeys: readonly string[];
399
+ readonly graphs: Readonly<Record<string, FlowGraphData>>;
400
+ readonly primaryFlowKey: string;
401
+ }): Map<string, FlowNodePosition>;
402
+ /** Papel visual da aresta. O componente traduz em traço, cor e seta; o modelo só decide qual é. */
403
+ type FlowEdgeKind = 'linear' | 'branch' | 'fallback';
404
+ type FlowEdgeSpec = {
405
+ readonly id: string;
406
+ readonly source: string;
407
+ readonly target: string;
408
+ readonly sourceHandle?: string;
409
+ readonly kind: FlowEdgeKind;
410
+ /** Salto entre fluxos — tracejado, esteja o alvo mesclado ou num portal. */
411
+ readonly crossFlow: boolean;
412
+ readonly live: boolean;
413
+ };
414
+ /**
415
+ * Arestas de todos os fluxos abertos.
416
+ *
417
+ * A regra que não é óbvia: um salto `flow:<key>` vira ligação real até o nó inicial do alvo quando
418
+ * esse fluxo já está no canvas, e só cai no portal quando não está. Sem isso, abrir dois fluxos
419
+ * ligados mostrava uma caixa de portal entre dois cards que já estavam ali do lado.
420
+ */
421
+ declare function buildFlowEdges(params: {
422
+ readonly openKeys: readonly string[];
423
+ readonly graphs: Readonly<Record<string, FlowGraphData>>;
424
+ readonly rootFlowKey: string;
425
+ readonly livePositions?: readonly FlowLivePositionInput[] | undefined;
426
+ }): FlowEdgeSpec[];
427
+ /** Nós de um fluxo que ninguém aponta — o card ganha contorno tracejado para cobrar a ligação. */
428
+ declare function detachedNodeIds(graph: FlowGraphData): Set<string>;
429
+ /** Retângulo que envolve uma cadeia de coleta, em coordenadas do canvas. */
430
+ declare function chainFrameBounds(params: {
431
+ readonly nodeIds: readonly string[];
432
+ readonly graph: FlowGraphData;
433
+ readonly positionOf: (nodeId: string) => FlowNodePosition;
434
+ readonly padding: number;
435
+ }): {
436
+ x: number;
437
+ y: number;
438
+ width: number;
439
+ height: number;
440
+ };
441
+ /**
442
+ * O nó que nasce de cada item da paleta.
443
+ *
444
+ * `contextKey` igual ao id porque é o que o motor usa para guardar a resposta: deixá-lo vazio faria a
445
+ * pergunta ser feita e a resposta descartada, sem erro em lugar nenhum.
446
+ */
447
+ declare function newNodeFromSpec(spec: NewNodeSpec, existingIds: ReadonlySet<string>): FlowNodeData;
448
+
449
+ interface CreateFlowInput {
450
+ key: string;
451
+ label: string;
452
+ showInMenu: boolean;
453
+ /** Ausente quando `showInMenu` é falso — não há opção de menu para rotular. */
454
+ menuOptionLabel?: string;
455
+ }
456
+ /**
457
+ * Backend de fluxos do host. Funções cruas em vez de um cliente HTTP: o pacote roda em produtos com
458
+ * axios, fetch e react-query, e nenhum deles precisa entrar como dependência daqui.
459
+ */
460
+ interface FlowsWorkspaceApi {
461
+ getGraphs(): Promise<Record<string, FlowGraphData>>;
462
+ saveGraph(key: string, graph: FlowGraphData): Promise<void>;
463
+ /**
464
+ * Criar e excluir fluxo são **opcionais por capacidade**: produto cujos fluxos vêm de um seed
465
+ * versionado não expõe rota para isso, e a tela simplesmente não desenha os botões — em vez de
466
+ * oferecer uma ação que estoura no clique.
467
+ */
468
+ createFlow?(input: CreateFlowInput): Promise<void>;
469
+ deleteFlow?(key: string): Promise<void>;
470
+ /**
471
+ * Onde estão as conversas vivas. Ausente, os cards não pulsam e nada é consultado.
472
+ *
473
+ * Aceita a linha por sessão e a linha já agregada por nó — `meta-whatsapp-module` responde a
474
+ * segunda, e exigir a primeira deixava os cards parados em todo produto que usa o módulo.
475
+ */
476
+ getLivePositions?(): Promise<readonly FlowLivePositionInput[]>;
477
+ }
478
+ interface FlowsWorkspaceProps {
479
+ readonly api: FlowsWorkspaceApi;
480
+ /** Fluxo raiz — o que abre por padrão e o único que não pode ser excluído. */
481
+ readonly rootFlowKey?: string;
482
+ readonly labels?: Partial<FlowEditorLabels>;
483
+ /** Kinds de ação do produto oferecidos na paleta (`trigger_simulation`, `abrir_comanda`…). */
484
+ readonly actionOptions?: readonly FlowPaletteActionOption[];
485
+ /** Seletor de arquivos do nó `send_media` — a biblioteca é do host, então entra por slot. */
486
+ readonly renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode;
487
+ /** Intervalo do polling de posições vivas. Só tem efeito com `getLivePositions`. */
488
+ readonly livePollIntervalMs?: number;
489
+ /**
490
+ * Título e subtítulo do editor. `false` deixa só a barra de ações.
491
+ *
492
+ * Produto cuja navegação já nomeia a tela mostrava o nome duas vezes, em dois tamanhos, porque a
493
+ * tipografia daqui é do pacote e a de lá é do host. Esconder é a única saída que não força o
494
+ * pacote a adivinhar a escala tipográfica de cada produto.
495
+ */
496
+ readonly showHeader?: boolean;
497
+ readonly className?: string;
498
+ }
499
+ /**
500
+ * Editor de fluxograma completo — barra de ações, abas de fluxo, paleta, canvas com fusão
501
+ * editável, painel de nó, mapa de fluxos e diálogos de criar/excluir.
502
+ *
503
+ * É a tela inteira, não as peças: cada produto que remontava esse grid à mão acabava com uma
504
+ * versão diferente do mesmo editor. Customização entra por `labels`, `actionOptions` e
505
+ * `renderMediaPicker` — nunca por cópia do arquivo.
506
+ */
507
+ declare function FlowsWorkspace({ api, rootFlowKey, labels: labelsOverride, actionOptions, renderMediaPicker, livePollIntervalMs, showHeader, className, }: FlowsWorkspaceProps): react.JSX.Element;
508
+
509
+ /**
510
+ * Copyright (c) 2026 Ada Technology. MIT License.
511
+ *
512
+ * Operações puras de grafo que o editor de fluxo precisa, e que viviam soltas dentro da página de
513
+ * 973 linhas do financiamento.
514
+ *
515
+ * Puras e separadas do hook de propósito: são a parte que dá para testar sem navegador, sem React e
516
+ * sem estado — e três delas (`removeNodeAndCleanRefs`, `resolveConnection`, `mergedFlowKeysFrom`)
517
+ * decidem o que acontece com o fluxo que alguém desenhou. Errar ali não dá erro; dá aresta apontando
518
+ * para nó que não existe mais, ou fluxo que some do canvas.
519
+ */
520
+
521
+ /**
522
+ * Id de nó no canvas mesclado: vários fluxos dividem o mesmo espaço, e `boas-vindas` pode existir em
523
+ * dois deles. Sem o prefixo, arrastar um card moveria o homônimo do outro fluxo.
524
+ */
525
+ declare function namespaceNodeId(flowKey: string, nodeId: string): string;
526
+ declare function parseNamespacedId(value: string): {
527
+ flowKey: string;
528
+ nodeId: string;
529
+ };
530
+ /**
531
+ * Apaga o nó E as referências a ele.
532
+ *
533
+ * A limpeza não é cortesia: uma aresta apontando para nó inexistente faz o motor do bot parar a
534
+ * conversa no meio, e o sintoma aparece para o cliente, não para quem editou.
535
+ */
536
+ declare function removeNodeAndCleanRefs(nodes: Readonly<Record<string, FlowNodeData>>, removedId: string): Record<string, FlowNodeData>;
537
+ /**
538
+ * O fecho transitivo dos fluxos alcançáveis a partir de um — é o conjunto que o canvas abre junto.
539
+ *
540
+ * BFS e não recursão: fluxo que referencia a si mesmo (menu que volta ao menu) é comum, e recursão
541
+ * ingênua estouraria a pilha no caso mais banal que existe.
542
+ */
543
+ declare function mergedFlowKeysFrom(rootKey: string, graphs: Readonly<Record<string, FlowGraphData>>): readonly string[];
544
+ type ConnectionRequest = {
545
+ readonly source: string;
546
+ readonly target: string;
547
+ readonly sourceHandle?: string | null | undefined;
548
+ };
549
+ type ResolvedConnection = {
550
+ readonly flowKey: string;
551
+ readonly nodeId: string;
552
+ readonly handle: string;
553
+ /** O que gravar no `next`: id de nó local, ou `flow:<key>` quando atravessa fluxo. */
554
+ readonly targetValue: string;
555
+ };
556
+ /**
557
+ * Traduz um arraste de aresta no valor que vai para o `next` — e recusa o que o motor do bot não
558
+ * sabe executar.
559
+ *
560
+ * A regra que não é óbvia: conectar num nó de OUTRO fluxo só funciona se for o nó inicial dele,
561
+ * porque o motor só sabe pular para o começo de um fluxo, não para um nó do meio. Conectar no meio
562
+ * devolve `undefined` — recusa silenciosa é melhor que gravar um salto que o bot vai ignorar em
563
+ * produção, deixando a conversa parada sem ninguém entender por quê.
564
+ */
565
+ declare function resolveConnection(params: {
566
+ readonly connection: ConnectionRequest;
567
+ readonly graphs: Readonly<Record<string, FlowGraphData>>;
568
+ }): ResolvedConnection | undefined;
569
+ /** Aplica a conexão resolvida no nó. `next` string para saída única, objeto para ramificação. */
570
+ declare function applyConnection(node: FlowNodeData, resolved: ResolvedConnection): FlowNodeData;
571
+ /**
572
+ * Um fluxo está sujo quando o rascunho difere do publicado.
573
+ *
574
+ * Comparação estrutural por JSON: é grosseira, e é suficiente porque o grafo é dado serializável sem
575
+ * ordem significativa de chave — o servidor devolve o que gravou. Comparar campo a campo daria a
576
+ * mesma resposta com mais código para errar.
577
+ */
578
+ declare function isGraphDirty(working: FlowGraphData | undefined, published: FlowGraphData | undefined): boolean;
245
579
 
246
- export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, DEFAULT_FLOW_EDITOR_LABELS, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, WHATSAPP_LIMITS, computeAutoLayout, computeFlowMapLayout, crossFlowKey, crossFlowTargetsOf, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, mergeFlowEditorLabels, nodeLabel, rendersAsButtons, slugifyNodeId, targetsOf, validateGraph };
580
+ export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, type ConnectionRequest, type CreateFlowInput, DEFAULT_FLOW_EDITOR_LABELS, type FlowEdgeKind, type FlowEdgeSpec, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, type FlowLiveNodeCount, type FlowLivePosition, type FlowLivePositionInput, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, type FlowNodePosition, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, type FlowValidationLabels, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, FlowsWorkspace, type FlowsWorkspaceApi, type FlowsWorkspaceProps, GROUP_HEADER_NODE_ID, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, type ResolvedConnection, WHATSAPP_LIMITS, applyConnection, buildFlowEdges, chainFrameBounds, chainFrameNodeId, computeAutoLayout, computeFlowMapLayout, computeMergedLayout, countLiveByNode, crossFlowKey, crossFlowTargetsOf, detachedNodeIds, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, isGraphDirty, mergeFlowEditorLabels, mergedFlowKeysFrom, namespaceNodeId, newNodeFromSpec, nodeLabel, parseNamespacedId, portalNodeId, removeNodeAndCleanRefs, rendersAsButtons, resolveConnection, slugifyNodeId, targetsOf, validateGraph };