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

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-DXPSPUWF.js +110 -0
  2. package/dist/chunk-GY472G6E.js +2316 -0
  3. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  4. package/dist/flows/index.d.ts +311 -4
  5. package/dist/flows/index.js +1322 -55
  6. package/dist/index.d.ts +1074 -42
  7. package/dist/index.js +3571 -742
  8. package/dist/preview/index.d.ts +328 -8
  9. package/dist/preview/index.js +902 -115
  10. package/dist/styles.css +819 -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 +543 -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 +1003 -0
  70. package/src/flows/flowCanvasModel.test.ts +293 -0
  71. package/src/flows/flowCanvasModel.ts +342 -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 +40 -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 +783 -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
@@ -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,243 @@ 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. */
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
+ * Pseudo-nó que representa um fluxo alvo ainda não mesclado no canvas.
357
+ *
358
+ * Um portal por (nó de origem, fluxo alvo): duas opções do mesmo nó indo para o mesmo fluxo
359
+ * compartilham o portal, senão o card ficaria cercado de caixas idênticas.
360
+ */
361
+ declare function portalNodeId(sourceNodeId: string, target: string): string;
362
+ declare function chainFrameNodeId(actionNodeId: string): string;
363
+ declare const GROUP_HEADER_NODE_ID = "__group_header__";
364
+ /**
365
+ * Quantas conversas estão paradas em cada nó de um fluxo.
366
+ *
367
+ * A raiz é caso à parte: o servidor guarda o passo do menu em `menuNodeId`, num campo próprio, e uma
368
+ * conversa no menu não carrega `flow`. Ler `nodeId` ali daria contagem zero na tela mais visitada do
369
+ * editor.
370
+ */
371
+ declare function countLiveByNode(params: {
372
+ readonly flowKey: string;
373
+ readonly rootFlowKey: string;
374
+ readonly positions: readonly FlowLivePosition[] | undefined;
375
+ }): Record<string, number>;
376
+ /**
377
+ * Um layout só para TODOS os nós de TODOS os fluxos abertos juntos.
378
+ *
379
+ * Posicionar cada fluxo à parte e deslocar não resolve: nada impede dois fluxos de ocuparem o mesmo
380
+ * espaço, e a altura real de cada card é ignorada. Aqui o ranqueamento por BFS roda sobre o grafo
381
+ * mesclado inteiro, com os saltos `flow:<key>` já resolvidos para o nó inicial do alvo.
382
+ */
383
+ declare function computeMergedLayout(params: {
384
+ readonly openKeys: readonly string[];
385
+ readonly graphs: Readonly<Record<string, FlowGraphData>>;
386
+ readonly primaryFlowKey: string;
387
+ }): Map<string, FlowNodePosition>;
388
+ /** Papel visual da aresta. O componente traduz em traço, cor e seta; o modelo só decide qual é. */
389
+ type FlowEdgeKind = 'linear' | 'branch' | 'fallback';
390
+ type FlowEdgeSpec = {
391
+ readonly id: string;
392
+ readonly source: string;
393
+ readonly target: string;
394
+ readonly sourceHandle?: string;
395
+ readonly kind: FlowEdgeKind;
396
+ /** Salto entre fluxos — tracejado, esteja o alvo mesclado ou num portal. */
397
+ readonly crossFlow: boolean;
398
+ readonly live: boolean;
399
+ };
400
+ /**
401
+ * Arestas de todos os fluxos abertos.
402
+ *
403
+ * A regra que não é óbvia: um salto `flow:<key>` vira ligação real até o nó inicial do alvo quando
404
+ * esse fluxo já está no canvas, e só cai no portal quando não está. Sem isso, abrir dois fluxos
405
+ * ligados mostrava uma caixa de portal entre dois cards que já estavam ali do lado.
406
+ */
407
+ declare function buildFlowEdges(params: {
408
+ readonly openKeys: readonly string[];
409
+ readonly graphs: Readonly<Record<string, FlowGraphData>>;
410
+ readonly rootFlowKey: string;
411
+ readonly livePositions?: readonly FlowLivePosition[] | undefined;
412
+ }): FlowEdgeSpec[];
413
+ /** Nós de um fluxo que ninguém aponta — o card ganha contorno tracejado para cobrar a ligação. */
414
+ declare function detachedNodeIds(graph: FlowGraphData): Set<string>;
415
+ /** Retângulo que envolve uma cadeia de coleta, em coordenadas do canvas. */
416
+ declare function chainFrameBounds(params: {
417
+ readonly nodeIds: readonly string[];
418
+ readonly graph: FlowGraphData;
419
+ readonly positionOf: (nodeId: string) => FlowNodePosition;
420
+ readonly padding: number;
421
+ }): {
422
+ x: number;
423
+ y: number;
424
+ width: number;
425
+ height: number;
426
+ };
427
+ /**
428
+ * O nó que nasce de cada item da paleta.
429
+ *
430
+ * `contextKey` igual ao id porque é o que o motor usa para guardar a resposta: deixá-lo vazio faria a
431
+ * pergunta ser feita e a resposta descartada, sem erro em lugar nenhum.
432
+ */
433
+ declare function newNodeFromSpec(spec: NewNodeSpec, existingIds: ReadonlySet<string>): FlowNodeData;
434
+
435
+ interface CreateFlowInput {
436
+ key: string;
437
+ label: string;
438
+ showInMenu: boolean;
439
+ /** Ausente quando `showInMenu` é falso — não há opção de menu para rotular. */
440
+ menuOptionLabel?: string;
441
+ }
442
+ /**
443
+ * Backend de fluxos do host. Funções cruas em vez de um cliente HTTP: o pacote roda em produtos com
444
+ * axios, fetch e react-query, e nenhum deles precisa entrar como dependência daqui.
445
+ */
446
+ interface FlowsWorkspaceApi {
447
+ getGraphs(): Promise<Record<string, FlowGraphData>>;
448
+ saveGraph(key: string, graph: FlowGraphData): Promise<void>;
449
+ /**
450
+ * Criar e excluir fluxo são **opcionais por capacidade**: produto cujos fluxos vêm de um seed
451
+ * versionado não expõe rota para isso, e a tela simplesmente não desenha os botões — em vez de
452
+ * oferecer uma ação que estoura no clique.
453
+ */
454
+ createFlow?(input: CreateFlowInput): Promise<void>;
455
+ deleteFlow?(key: string): Promise<void>;
456
+ /** Contagem de conversas vivas por nó. Ausente, os cards não pulsam e nada é consultado. */
457
+ getLivePositions?(): Promise<FlowLivePosition[]>;
458
+ }
459
+ interface FlowsWorkspaceProps {
460
+ readonly api: FlowsWorkspaceApi;
461
+ /** Fluxo raiz — o que abre por padrão e o único que não pode ser excluído. */
462
+ readonly rootFlowKey?: string;
463
+ readonly labels?: Partial<FlowEditorLabels>;
464
+ /** Kinds de ação do produto oferecidos na paleta (`trigger_simulation`, `abrir_comanda`…). */
465
+ readonly actionOptions?: readonly FlowPaletteActionOption[];
466
+ /** Seletor de arquivos do nó `send_media` — a biblioteca é do host, então entra por slot. */
467
+ readonly renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode;
468
+ /** Intervalo do polling de posições vivas. Só tem efeito com `getLivePositions`. */
469
+ readonly livePollIntervalMs?: number;
470
+ readonly className?: string;
471
+ }
472
+ /**
473
+ * Editor de fluxograma completo — barra de ações, abas de fluxo, paleta, canvas com fusão
474
+ * editável, painel de nó, mapa de fluxos e diálogos de criar/excluir.
475
+ *
476
+ * É a tela inteira, não as peças: cada produto que remontava esse grid à mão acabava com uma
477
+ * versão diferente do mesmo editor. Customização entra por `labels`, `actionOptions` e
478
+ * `renderMediaPicker` — nunca por cópia do arquivo.
479
+ */
480
+ declare function FlowsWorkspace({ api, rootFlowKey, labels: labelsOverride, actionOptions, renderMediaPicker, livePollIntervalMs, className, }: FlowsWorkspaceProps): react.JSX.Element;
481
+
482
+ /**
483
+ * Copyright (c) 2026 Ada Technology. MIT License.
484
+ *
485
+ * Operações puras de grafo que o editor de fluxo precisa, e que viviam soltas dentro da página de
486
+ * 973 linhas do financiamento.
487
+ *
488
+ * Puras e separadas do hook de propósito: são a parte que dá para testar sem navegador, sem React e
489
+ * sem estado — e três delas (`removeNodeAndCleanRefs`, `resolveConnection`, `mergedFlowKeysFrom`)
490
+ * decidem o que acontece com o fluxo que alguém desenhou. Errar ali não dá erro; dá aresta apontando
491
+ * para nó que não existe mais, ou fluxo que some do canvas.
492
+ */
493
+
494
+ /**
495
+ * Id de nó no canvas mesclado: vários fluxos dividem o mesmo espaço, e `boas-vindas` pode existir em
496
+ * dois deles. Sem o prefixo, arrastar um card moveria o homônimo do outro fluxo.
497
+ */
498
+ declare function namespaceNodeId(flowKey: string, nodeId: string): string;
499
+ declare function parseNamespacedId(value: string): {
500
+ flowKey: string;
501
+ nodeId: string;
502
+ };
503
+ /**
504
+ * Apaga o nó E as referências a ele.
505
+ *
506
+ * A limpeza não é cortesia: uma aresta apontando para nó inexistente faz o motor do bot parar a
507
+ * conversa no meio, e o sintoma aparece para o cliente, não para quem editou.
508
+ */
509
+ declare function removeNodeAndCleanRefs(nodes: Readonly<Record<string, FlowNodeData>>, removedId: string): Record<string, FlowNodeData>;
510
+ /**
511
+ * O fecho transitivo dos fluxos alcançáveis a partir de um — é o conjunto que o canvas abre junto.
512
+ *
513
+ * BFS e não recursão: fluxo que referencia a si mesmo (menu que volta ao menu) é comum, e recursão
514
+ * ingênua estouraria a pilha no caso mais banal que existe.
515
+ */
516
+ declare function mergedFlowKeysFrom(rootKey: string, graphs: Readonly<Record<string, FlowGraphData>>): readonly string[];
517
+ type ConnectionRequest = {
518
+ readonly source: string;
519
+ readonly target: string;
520
+ readonly sourceHandle?: string | null | undefined;
521
+ };
522
+ type ResolvedConnection = {
523
+ readonly flowKey: string;
524
+ readonly nodeId: string;
525
+ readonly handle: string;
526
+ /** O que gravar no `next`: id de nó local, ou `flow:<key>` quando atravessa fluxo. */
527
+ readonly targetValue: string;
528
+ };
529
+ /**
530
+ * Traduz um arraste de aresta no valor que vai para o `next` — e recusa o que o motor do bot não
531
+ * sabe executar.
532
+ *
533
+ * A regra que não é óbvia: conectar num nó de OUTRO fluxo só funciona se for o nó inicial dele,
534
+ * porque o motor só sabe pular para o começo de um fluxo, não para um nó do meio. Conectar no meio
535
+ * devolve `undefined` — recusa silenciosa é melhor que gravar um salto que o bot vai ignorar em
536
+ * produção, deixando a conversa parada sem ninguém entender por quê.
537
+ */
538
+ declare function resolveConnection(params: {
539
+ readonly connection: ConnectionRequest;
540
+ readonly graphs: Readonly<Record<string, FlowGraphData>>;
541
+ }): ResolvedConnection | undefined;
542
+ /** Aplica a conexão resolvida no nó. `next` string para saída única, objeto para ramificação. */
543
+ declare function applyConnection(node: FlowNodeData, resolved: ResolvedConnection): FlowNodeData;
544
+ /**
545
+ * Um fluxo está sujo quando o rascunho difere do publicado.
546
+ *
547
+ * Comparação estrutural por JSON: é grosseira, e é suficiente porque o grafo é dado serializável sem
548
+ * ordem significativa de chave — o servidor devolve o que gravou. Comparar campo a campo daria a
549
+ * mesma resposta com mais código para errar.
550
+ */
551
+ declare function isGraphDirty(working: FlowGraphData | undefined, published: FlowGraphData | undefined): boolean;
245
552
 
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 };
553
+ 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 FlowLivePosition, 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 };