@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.40

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 (149) hide show
  1. package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
  2. package/dist/chunk-BJNRLLDO.js +2708 -0
  3. package/dist/chunk-DKPXKQGC.js +110 -0
  4. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  5. package/dist/flows/index.d.ts +422 -5
  6. package/dist/flows/index.js +2372 -678
  7. package/dist/index.d.ts +1171 -42
  8. package/dist/index.js +3755 -843
  9. package/dist/preview/index.d.ts +157 -42
  10. package/dist/preview/index.js +772 -191
  11. package/dist/styles.css +893 -0
  12. package/package.json +9 -8
  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 +28 -0
  25. package/src/ConversationRow.tsx +31 -7
  26. package/src/DarkModeToggle.test.tsx +76 -0
  27. package/src/DarkModeToggle.tsx +92 -0
  28. package/src/DocumentsLibrary.tsx +382 -0
  29. package/src/EmojiPicker.tsx +70 -55
  30. package/src/FileIcon.test.ts +83 -0
  31. package/src/FileIcon.tsx +88 -11
  32. package/src/InteractiveMessage.test.tsx +41 -0
  33. package/src/InteractiveMessage.tsx +146 -0
  34. package/src/Lightbox.tsx +18 -3
  35. package/src/MediaRenderer.tsx +92 -16
  36. package/src/MessageBubble.test.tsx +41 -0
  37. package/src/MessageBubble.tsx +75 -6
  38. package/src/MessageComposer.test.tsx +35 -0
  39. package/src/MessageComposer.tsx +155 -19
  40. package/src/RichMessageComposer.test.tsx +113 -0
  41. package/src/RichMessageComposer.tsx +551 -0
  42. package/src/SimpleEmojiPicker.tsx +5 -3
  43. package/src/StatusTicks.tsx +1 -1
  44. package/src/Toast.tsx +4 -0
  45. package/src/Tooltip.test.ts +42 -0
  46. package/src/Tooltip.tsx +167 -0
  47. package/src/Wallpaper.test.tsx +21 -0
  48. package/src/Wallpaper.tsx +67 -7
  49. package/src/WhatsAppMessageEditor.tsx +34 -7
  50. package/src/WindowExpiredNotice.tsx +12 -4
  51. package/src/audioRecorderFormat.test.ts +67 -0
  52. package/src/buildOutput.test.ts +79 -0
  53. package/src/composer.constant.ts +33 -0
  54. package/src/conversationTranscript.test.ts +57 -0
  55. package/src/conversationTranscript.ts +29 -4
  56. package/src/conversationWindow.ts +7 -5
  57. package/src/documentTypeLabel.test.ts +57 -0
  58. package/src/documents/DocumentsWorkspace.tsx +550 -0
  59. package/src/documents/index.ts +8 -0
  60. package/src/documents/labels.ts +92 -0
  61. package/src/emojiCatalog.test.ts +35 -0
  62. package/src/emojiCatalog.ts +189 -0
  63. package/src/flows/FlowConnectionEdge.tsx +104 -0
  64. package/src/flows/FlowGroupHeader.tsx +12 -2
  65. package/src/flows/FlowLegend.tsx +125 -0
  66. package/src/flows/FlowMapCanvas.tsx +15 -12
  67. package/src/flows/FlowMapNode.tsx +4 -1
  68. package/src/flows/FlowNodeCard.tsx +219 -34
  69. package/src/flows/FlowNodePanel.tsx +153 -39
  70. package/src/flows/FlowPalette.tsx +106 -69
  71. package/src/flows/FlowPortalNode.tsx +1 -1
  72. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  73. package/src/flows/FlowsWorkspace.tsx +1193 -0
  74. package/src/flows/flowCanvasModel.test.ts +456 -0
  75. package/src/flows/flowCanvasModel.ts +378 -0
  76. package/src/flows/flowEditorOps.test.ts +276 -0
  77. package/src/flows/flowEditorOps.ts +202 -0
  78. package/src/flows/flowGraph.ts +78 -53
  79. package/src/flows/index.ts +51 -2
  80. package/src/flows/labels.ts +180 -0
  81. package/src/flows/workspaceContract.test.ts +95 -0
  82. package/src/hooks/useContainerWidth.ts +35 -0
  83. package/src/hooks/useConversationActions.ts +56 -0
  84. package/src/hooks/useConversationDocuments.ts +11 -7
  85. package/src/hooks/useConversationList.ts +15 -9
  86. package/src/hooks/useConversationMessages.ts +2 -2
  87. package/src/hooks/useConversationRealtime.ts +10 -8
  88. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  89. package/src/hooks/useUrlFilterState.ts +107 -0
  90. package/src/icon.constant.ts +12 -0
  91. package/src/index.ts +129 -13
  92. package/src/lib/cn.test.ts +29 -0
  93. package/src/lib/composer-formatting.test.ts +78 -0
  94. package/src/lib/composer-formatting.ts +145 -0
  95. package/src/lib/createMediaUrlResolver.ts +33 -0
  96. package/src/lib/paginated.test.ts +33 -0
  97. package/src/lib/paginated.ts +26 -0
  98. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  99. package/src/lib/whatsapp-formatting.tsx +28 -3
  100. package/src/listing/index.tsx +202 -0
  101. package/src/pagination.constant.ts +10 -0
  102. package/src/preview/ConversationPreview.tsx +225 -17
  103. package/src/preview/ConversationSimulatorClient.ts +143 -0
  104. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  105. package/src/preview/ConversationSimulatorPanel.tsx +131 -0
  106. package/src/preview/MediaTypesPreview.tsx +87 -0
  107. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  108. package/src/preview/createMockConversationsApi.ts +175 -15
  109. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  110. package/src/preview/createPreviewBridgeClient.ts +124 -0
  111. package/src/preview/createPreviewMediaUploader.ts +82 -0
  112. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  113. package/src/preview/createPreviewWebhookClient.ts +127 -4
  114. package/src/preview/index.ts +51 -3
  115. package/src/preview/mediaTypeOf.test.ts +15 -0
  116. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  117. package/src/preview/preview.test.ts +5 -3
  118. package/src/preview/previewFileSamples.test.ts +151 -0
  119. package/src/preview/previewFileSamples.ts +74 -0
  120. package/src/preview/previewFixtures.ts +288 -1
  121. package/src/preview/previewMediaSource.test.ts +62 -0
  122. package/src/preview/previewMediaSource.ts +91 -0
  123. package/src/preview/previewMediaUploader.test.ts +61 -0
  124. package/src/providers/ConversationsProvider.tsx +8 -6
  125. package/src/providers/types.ts +185 -10
  126. package/src/quickReply.test.ts +58 -0
  127. package/src/settings/MessagesWorkspace.tsx +571 -0
  128. package/src/settings/TopicsForm.tsx +2 -0
  129. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  130. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  131. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  132. package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
  133. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  134. package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
  135. package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
  136. package/src/styles.css +858 -0
  137. package/src/types.ts +64 -1
  138. package/src/useWaitingNotifications.ts +74 -29
  139. package/src/workspace/BulkTemplateModal.tsx +132 -0
  140. package/src/workspace/ConversationPane.tsx +432 -0
  141. package/src/workspace/ConversationsInboxList.tsx +194 -0
  142. package/src/workspace/ConversationsWorkspace.tsx +423 -0
  143. package/src/workspace/index.ts +17 -0
  144. package/src/workspace/labels.test.ts +17 -0
  145. package/src/workspace/labels.ts +85 -0
  146. package/src/workspace/useConversationsInbox.ts +332 -0
  147. package/dist/chunk-4R6Y43DQ.js +0 -726
  148. package/dist/chunk-NV2RZ5KT.js +0 -56
  149. package/dist/types-C0PtaO7S.d.ts +0 -207
@@ -1,279 +1,34 @@
1
1
  import {
2
- useIsDarkTheme
3
- } from "../chunk-NV2RZ5KT.js";
2
+ TooltipLayer
3
+ } from "../chunk-DKPXKQGC.js";
4
4
  import {
5
- parseWhatsAppFormatting
6
- } from "../chunk-OGRRHQQW.js";
5
+ parseWhatsAppFormatting,
6
+ useIsDarkTheme
7
+ } from "../chunk-WCBDXZ3X.js";
7
8
 
8
9
  // src/flows/FlowNodeCard.tsx
10
+ import { useState } from "react";
9
11
  import { Handle, Position } from "@xyflow/react";
10
- import { MessageCircleQuestion, GitBranch, Zap, ListTree, Diamond, AlertTriangle, AlertCircle, Headset, Clock3, ShoppingBag } from "lucide-react";
11
- import { jsx, jsxs } from "react/jsx-runtime";
12
- var NODE_TYPE_COLOR = {
13
- question: "border-blue-300 bg-blue-50 dark:bg-blue-950/40 dark:border-blue-800",
14
- entrada_choice: "border-purple-300 bg-purple-50 dark:bg-purple-950/40 dark:border-purple-800",
15
- action: "border-orange-300 bg-orange-50 dark:bg-orange-950/40 dark:border-orange-800",
16
- menu: "border-emerald-300 bg-emerald-50 dark:bg-emerald-950/40 dark:border-emerald-800",
17
- condition: "border-cyan-300 bg-cyan-50 dark:bg-cyan-950/40 dark:border-cyan-800"
18
- };
19
- var NODE_TYPE_ICON = {
20
- question: MessageCircleQuestion,
21
- entrada_choice: ListTree,
22
- action: Zap,
23
- menu: GitBranch,
24
- condition: Diamond
25
- };
26
- var DEFAULT_ACTION_KIND_ICON = {
27
- handoff: Headset,
28
- rate_limited_handoff: Clock3,
29
- send_product_list: ShoppingBag
30
- };
31
- function nodeLabel(node, labels) {
32
- if (!node) return "\u2014";
33
- if (node.type === "action") {
34
- const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
35
- return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
36
- }
37
- if (node.type === "condition") {
38
- if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return node.id;
39
- const operatorLabel = labels.conditionOperatorLabels[node.conditionOperator] ?? node.conditionOperator;
40
- return `${node.conditionContextKey} ${operatorLabel} ${node.conditionValue}`;
41
- }
42
- return node.question || node.contextKey || node.id;
43
- }
44
- function sourceRows(node, labels) {
45
- if (node.type === "action") return [];
46
- if (node.type === "condition") {
47
- return [
48
- { id: "true", label: labels.nodePanel.conditionTrue, isDefault: false },
49
- { id: "false", label: labels.nodePanel.conditionFalse, isDefault: false }
50
- ];
51
- }
52
- const isChoice = node.type === "menu" || node.questionType === "choice";
53
- if (!isChoice) return [{ id: "next", label: labels.nodePanel.nextRowLabel, isDefault: false }];
54
- const options = node.options ?? [];
55
- return [
56
- ...options.map(([id, label]) => ({ id, label, isDefault: false })),
57
- { id: "__default", label: labels.edgeFallbackLabel, isDefault: true }
58
- ];
59
- }
60
- function SourceRow({ label, isDefault, handleId }) {
61
- return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-600 bg-white/70 dark:bg-gray-900/40 px-2 py-1 pr-3", children: [
62
- /* @__PURE__ */ jsx("span", { className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`, children: label }),
63
- /* @__PURE__ */ jsx(
64
- Handle,
65
- {
66
- type: "source",
67
- position: Position.Right,
68
- id: handleId,
69
- style: { position: "absolute", right: -7, top: "50%", transform: "translateY(-50%)" },
70
- className: isDefault ? "!bg-gray-400 dark:!bg-gray-500" : "!bg-purple-500"
71
- }
72
- )
73
- ] });
74
- }
75
- function FlowNodeCard({ data }) {
76
- const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } = data;
77
- const label = nodeLabel(node, labels);
78
- const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
79
- const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
80
- const rows = sourceRows(node, labels);
81
- const hasError = issues.some((i) => i.severity === "error");
82
- const hasWarning = !hasError && issues.some((i) => i.severity === "warning");
83
- return /* @__PURE__ */ jsxs(
84
- "div",
85
- {
86
- title: label,
87
- className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""}`,
88
- onClick: () => onSelect(node.id),
89
- children: [
90
- /* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
91
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
92
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs", children: [
93
- /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
94
- isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", title: labels.startNodeTooltip }),
95
- labels.legend[node.type]
96
- ] }),
97
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
98
- hasError && /* @__PURE__ */ jsx(AlertCircle, { size: 13, className: "text-red-600 dark:text-red-400" }),
99
- hasWarning && /* @__PURE__ */ jsx(AlertTriangle, { size: 13, className: "text-amber-500 dark:text-amber-400" }),
100
- liveCount > 0 && /* @__PURE__ */ jsx(
101
- "span",
102
- {
103
- title: labels.liveCountTooltip(liveCount),
104
- className: "font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs",
105
- children: liveCount
106
- }
107
- )
108
- ] })
109
- ] }),
110
- /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-900 dark:text-gray-100 mt-1 line-clamp-3", children: label }),
111
- rows.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-2 space-y-1", children: rows.map((row) => /* @__PURE__ */ jsx(SourceRow, { label: row.label, isDefault: row.isDefault, handleId: row.id }, row.id)) })
112
- ]
113
- }
114
- );
115
- }
116
- var flowNodeTypes = { flowNode: FlowNodeCard };
117
-
118
- // src/flows/FlowMapNode.tsx
119
- import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
120
- import { GitBranch as GitBranch2, Maximize2 } from "lucide-react";
121
-
122
- // src/flows/labels.ts
123
- var DEFAULT_FLOW_EDITOR_LABELS = {
124
- legend: {
125
- question: "Pergunta",
126
- entrada_choice: "Escolha calculada",
127
- action: "A\xE7\xE3o (simula\xE7\xE3o/atendimento)",
128
- menu: "Menu",
129
- condition: "Condi\xE7\xE3o"
130
- },
131
- startNodeTooltip: "In\xEDcio do fluxo",
132
- liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
133
- edgeFallbackLabel: "outro",
134
- actionKindLabels: {
135
- handoff: "Encaminhar para atendimento",
136
- rate_limited_handoff: "Encaminhar (limite de simula\xE7\xF5es atingido)",
137
- send_product_list: "Enviar cat\xE1logo de produtos"
138
- },
139
- conditionOperatorLabels: {
140
- ">": "maior que",
141
- ">=": "maior ou igual a",
142
- "<": "menor que",
143
- "<=": "menor ou igual a",
144
- "==": "igual a",
145
- "!=": "diferente de",
146
- contains: "cont\xE9m"
147
- },
148
- questionTypeLabels: {
149
- text: "Texto livre",
150
- money: "Valor em R$",
151
- date: "Data",
152
- int: "N\xFAmero inteiro",
153
- cpf: "CPF",
154
- choice: "Escolha (bot\xF5es/lista)"
155
- },
156
- nodePanel: {
157
- title: "Editar n\xF3",
158
- contextKey: "Chave (contexto)",
159
- questionType: "Tipo de resposta",
160
- question: "Texto da pergunta",
161
- options: "Op\xE7\xF5es (choice)",
162
- addOption: "Adicionar op\xE7\xE3o",
163
- optionId: "Valor",
164
- optionLabel: "Texto exibido",
165
- next: "Pr\xF3ximo n\xF3",
166
- nextHint: "Tamb\xE9m \xE9 poss\xEDvel arrastar um fio no canvas para conectar.",
167
- nextRowLabel: "Pr\xF3ximo",
168
- otherFlowsGroup: "Outros fluxos (salto)",
169
- nextByAnswer: (id) => `Se responder "${id}" \u2192`,
170
- nextDefault: "Caso contr\xE1rio \u2192",
171
- save: "Salvar altera\xE7\xF5es",
172
- cancel: "Cancelar",
173
- fixedLogicNotice: "Este n\xF3 tem l\xF3gica fixa (valida\xE7\xF5es/c\xE1lculos do sistema) \u2014 s\xF3 o texto e o destino s\xE3o edit\xE1veis.",
174
- actionNotice: "N\xF3 de a\xE7\xE3o \u2014 dispara a a\xE7\xE3o registrada ou encaminha para atendimento humano.",
175
- preview: "Como o cliente v\xEA no WhatsApp",
176
- previewPlaceholder: "Pr\xE9-visualiza\xE7\xE3o\u2026",
177
- previewEmptyBody: "(escreva o texto da mensagem)",
178
- previewEmptyOption: "(sem texto)",
179
- previewListButton: "Ver op\xE7\xF5es",
180
- previewModeButtons: "Enviado como bot\xF5es (at\xE9 3 op\xE7\xF5es)",
181
- previewModeList: "Enviado como lista (4+ op\xE7\xF5es)",
182
- delete: "Excluir n\xF3",
183
- deleteConfirm: "Excluir este n\xF3? Liga\xE7\xF5es que apontam para ele ficar\xE3o quebradas.",
184
- directMessage: "Mensagem (opcional)",
185
- fallbackMessage: "Mensagem de fallback (quando o cat\xE1logo n\xE3o est\xE1 dispon\xEDvel)",
186
- conditionNotice: "N\xF3 de condi\xE7\xE3o \u2014 n\xE3o pergunta nada ao cliente, s\xF3 decide automaticamente entre Verdadeiro/Falso.",
187
- conditionVariable: "Vari\xE1vel (chave j\xE1 coletada por uma pergunta anterior)",
188
- conditionOperator: "Operador",
189
- conditionValue: "Valor de compara\xE7\xE3o",
190
- conditionTrue: "Se verdadeiro \u2192",
191
- conditionFalse: "Se falso \u2192",
192
- conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192"
193
- },
194
- palette: {
195
- title: "Adicionar ao fluxo",
196
- question: "Pergunta",
197
- decision: "Decis\xE3o",
198
- condition: "Condi\xE7\xE3o",
199
- conditionHint: "Compara uma vari\xE1vel j\xE1 coletada e segue automaticamente, sem perguntar nada",
200
- action: "A\xE7\xE3o"
201
- },
202
- flowMap: {
203
- nodeCount: (count) => `${count} n\xF3(s)`,
204
- openFlow: "Abrir fluxo"
205
- },
206
- flowGroup: {
207
- focus: "Focar neste fluxo",
208
- close: "Fechar"
209
- },
210
- crossFlowPortal: {
211
- tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
212
- goesTo: (label) => `\u21AA Vai para: ${label}`
213
- }
214
- };
215
- function mergeFlowEditorLabels(override) {
216
- if (!override) return DEFAULT_FLOW_EDITOR_LABELS;
217
- return {
218
- ...DEFAULT_FLOW_EDITOR_LABELS,
219
- ...override,
220
- legend: { ...DEFAULT_FLOW_EDITOR_LABELS.legend, ...override.legend },
221
- actionKindLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.actionKindLabels, ...override.actionKindLabels },
222
- conditionOperatorLabels: {
223
- ...DEFAULT_FLOW_EDITOR_LABELS.conditionOperatorLabels,
224
- ...override.conditionOperatorLabels
225
- },
226
- questionTypeLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.questionTypeLabels, ...override.questionTypeLabels },
227
- nodePanel: { ...DEFAULT_FLOW_EDITOR_LABELS.nodePanel, ...override.nodePanel },
228
- palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
229
- flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
230
- flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
231
- crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal }
232
- };
233
- }
234
-
235
- // src/flows/FlowMapNode.tsx
236
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
237
- function FlowMapNode({ data }) {
238
- const { label, nodeCount, isRoot, labels = DEFAULT_FLOW_EDITOR_LABELS, onOpen } = data;
239
- return /* @__PURE__ */ jsxs2("div", { className: "relative rounded-xl border-2 border-emerald-300 dark:border-emerald-700 bg-emerald-50 dark:bg-emerald-950/40 px-4 py-3 w-56 shadow-sm", children: [
240
- /* @__PURE__ */ jsx2(Handle2, { type: "target", position: Position2.Top, className: "!bg-gray-400 dark:!bg-gray-500" }),
241
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
242
- /* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
243
- /* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
244
- isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", title: labels.startNodeTooltip })
245
- ] }),
246
- /* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
247
- /* @__PURE__ */ jsxs2(
248
- "button",
249
- {
250
- type: "button",
251
- onClick: onOpen,
252
- className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
253
- children: [
254
- /* @__PURE__ */ jsx2(Maximize2, { size: 11 }),
255
- " ",
256
- labels.flowMap.openFlow
257
- ]
258
- }
259
- ),
260
- /* @__PURE__ */ jsx2(Handle2, { type: "source", position: Position2.Bottom, className: "!bg-gray-400 dark:!bg-gray-500" })
261
- ] });
262
- }
263
- var flowMapNodeTypes = { flowMapNode: FlowMapNode };
264
-
265
- // src/flows/FlowMapCanvas.tsx
266
- import { useMemo } from "react";
267
- import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
268
- import "@xyflow/react/dist/style.css";
12
+ import {
13
+ MessageCircleQuestion,
14
+ Plus,
15
+ GitBranch,
16
+ Zap,
17
+ ListTree,
18
+ Diamond,
19
+ AlertTriangle,
20
+ AlertCircle,
21
+ Headset,
22
+ Clock3,
23
+ ShoppingBag,
24
+ RotateCcw
25
+ } from "lucide-react";
269
26
 
270
27
  // src/flows/flowGraph.ts
28
+ import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
271
29
  var CONDITION_OPERATORS = [">", ">=", "<", "<=", "==", "!=", "contains"];
272
- var BUILT_IN_ACTION_KINDS = {
273
- HANDOFF: "handoff",
274
- RATE_LIMITED_HANDOFF: "rate_limited_handoff",
275
- SEND_PRODUCT_LIST: "send_product_list"
276
- };
30
+ var BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND;
31
+ var PASS_THROUGH_ACTION_KINDS = ["send_media"];
277
32
  var CROSS_FLOW_PREFIX = "flow:";
278
33
  var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
279
34
  var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
@@ -283,7 +38,10 @@ function estimateNodeHeight(node) {
283
38
  const BODY_HEIGHT = 56;
284
39
  const PADDING = 16;
285
40
  const ROW_HEIGHT = 34;
286
- const rowCount = node.type === "action" ? 0 : node.type === "condition" ? 2 : node.type === "menu" || node.questionType === "choice" ? (node.options?.length ?? 0) + 1 : 1;
41
+ const rowCount = node.type === "action" ? (
42
+ // Ação de passagem desenha uma linha de saída; terminal não desenha nenhuma.
43
+ node.actionKind && PASS_THROUGH_ACTION_KINDS.includes(node.actionKind) ? 1 : 0
44
+ ) : node.type === "condition" ? 2 : node.type === "menu" || node.questionType === "choice" ? (node.options?.length ?? 0) + 1 : 1;
287
45
  return HEADER_HEIGHT + BODY_HEIGHT + PADDING + rowCount * ROW_HEIGHT;
288
46
  }
289
47
  var WHATSAPP_LIMITS = {
@@ -389,45 +147,34 @@ function findUnreachable(graph) {
389
147
  }
390
148
  return Object.keys(graph.nodes).filter((id) => !reachable.has(id));
391
149
  }
392
- function computeAutoLayout(graph) {
393
- const H_GAP = 300;
394
- const V_GAP = 90;
395
- const rank = {};
396
- const queue = [graph.startNodeId];
397
- rank[graph.startNodeId] = 0;
398
- while (queue.length > 0) {
399
- const id = queue.shift();
400
- const node = graph.nodes[id];
401
- if (!node) continue;
402
- for (const { target } of targetsOf(node)) {
403
- if (isCrossFlowTarget(target) || !graph.nodes[target]) continue;
404
- if (rank[target] === void 0) {
405
- rank[target] = rank[id] + 1;
406
- queue.push(target);
407
- }
408
- }
409
- }
410
- const maxRank = Math.max(0, ...Object.values(rank));
411
- let strayRank = maxRank + 1;
412
- for (const id of Object.keys(graph.nodes)) {
413
- if (rank[id] === void 0) rank[id] = strayRank++;
150
+ var LAYOUT_COLUMN_GAP = 300;
151
+ var LAYOUT_ROW_GAP = 40;
152
+ function cascadeOrder(params) {
153
+ const placed = /* @__PURE__ */ new Map();
154
+ function visit(id, depth) {
155
+ if (placed.has(id)) return;
156
+ placed.set(id, { depth, order: placed.size });
157
+ for (const next of params.forwardEdges(id)) visit(next, depth + 1);
414
158
  }
415
- const layers = {};
416
- for (const [id, r] of Object.entries(rank)) {
417
- layers[r] = [...layers[r] ?? [], id];
159
+ if (params.allIds.includes(params.rootId)) visit(params.rootId, 0);
160
+ const strayDepth = Math.max(0, ...[...placed.values()].map((each) => each.depth)) + 1;
161
+ for (const id of params.allIds) {
162
+ if (!placed.has(id)) placed.set(id, { depth: strayDepth, order: placed.size });
418
163
  }
164
+ return placed;
165
+ }
166
+ function computeAutoLayout(graph) {
167
+ const placed = cascadeOrder({
168
+ rootId: graph.startNodeId,
169
+ allIds: Object.keys(graph.nodes),
170
+ forwardEdges: (id) => targetsOf(graph.nodes[id] ?? { id, type: "action" }).map((edge) => edge.target).filter((target) => !isCrossFlowTarget(target) && Boolean(graph.nodes[target]))
171
+ });
172
+ const byOrder = [...placed.entries()].sort((a, b) => a[1].order - b[1].order);
419
173
  const positions = {};
420
- const sortedRanks = Object.keys(layers).map(Number).sort((a, b) => a - b);
421
- let cumulativeY = 0;
422
- for (const r of sortedRanks) {
423
- const ids = layers[r];
424
- const width = (ids.length - 1) * H_GAP;
425
- let maxHeight = 0;
426
- ids.forEach((id, index) => {
427
- maxHeight = Math.max(maxHeight, estimateNodeHeight(graph.nodes[id]));
428
- positions[id] = { x: index * H_GAP - width / 2, y: cumulativeY };
429
- });
430
- cumulativeY += maxHeight + V_GAP;
174
+ let cursorY = 0;
175
+ for (const [id, { depth }] of byOrder) {
176
+ positions[id] = { x: depth * LAYOUT_COLUMN_GAP, y: cursorY };
177
+ cursorY += estimateNodeHeight(graph.nodes[id]) + LAYOUT_ROW_GAP;
431
178
  }
432
179
  return positions;
433
180
  }
@@ -518,29 +265,497 @@ function slugifyNodeId(label, existing) {
518
265
  return candidate;
519
266
  }
520
267
 
521
- // src/flows/FlowMapCanvas.tsx
522
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
523
- var MAP_NODE_TYPES = { ...flowMapNodeTypes };
524
- var EDGE_COLOR = "#06b6d4";
525
- var BACKGROUND_COLOR_LIGHT = "#cbd5e1";
526
- var BACKGROUND_COLOR_DARK = "#334155";
527
- function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverride }) {
268
+ // src/flows/FlowNodeCard.tsx
269
+ import { jsx, jsxs } from "react/jsx-runtime";
270
+ var HANDLE_SIZE_PX = 14;
271
+ var NODE_TYPE_COLOR = {
272
+ question: "border-blue-300 bg-blue-50 dark:bg-blue-950/40 dark:border-blue-800",
273
+ entrada_choice: "border-purple-300 bg-purple-50 dark:bg-purple-950/40 dark:border-purple-800",
274
+ action: "border-orange-300 bg-orange-50 dark:bg-orange-950/40 dark:border-orange-800",
275
+ menu: "border-emerald-300 bg-emerald-50 dark:bg-emerald-950/40 dark:border-emerald-800",
276
+ condition: "border-cyan-300 bg-cyan-50 dark:bg-cyan-950/40 dark:border-cyan-800"
277
+ };
278
+ var NODE_TYPE_ICON = {
279
+ question: MessageCircleQuestion,
280
+ entrada_choice: ListTree,
281
+ action: Zap,
282
+ menu: GitBranch,
283
+ condition: Diamond
284
+ };
285
+ var DEFAULT_ACTION_KIND_ICON = {
286
+ handoff: Headset,
287
+ rate_limited_handoff: Clock3,
288
+ send_product_list: ShoppingBag
289
+ };
290
+ function nodeLabel(node, labels) {
291
+ if (!node) return "\u2014";
292
+ if (node.label) return node.label;
293
+ if (node.type === "action") {
294
+ const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
295
+ return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
296
+ }
297
+ if (node.type === "condition") {
298
+ if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return node.id;
299
+ const operatorLabel = labels.conditionOperatorLabels[node.conditionOperator] ?? node.conditionOperator;
300
+ return `${node.conditionContextKey} ${operatorLabel} ${node.conditionValue}`;
301
+ }
302
+ return node.question || node.contextKey || node.id;
303
+ }
304
+ function selfLoopHandles(node) {
305
+ const loops = /* @__PURE__ */ new Set();
306
+ if (typeof node.next === "string") {
307
+ if (node.next === node.id) loops.add("next");
308
+ return loops;
309
+ }
310
+ if (!node.next) return loops;
311
+ for (const [optionId, target] of Object.entries(node.next.byAnswer ?? {})) {
312
+ if (target === node.id) loops.add(optionId);
313
+ }
314
+ if (node.next.default === node.id) loops.add("__default");
315
+ return loops;
316
+ }
317
+ function sourceRows(node, labels) {
318
+ if (node.type === "action") {
319
+ return node.actionKind && PASS_THROUGH_ACTION_KINDS.includes(node.actionKind) ? [{ id: "next", label: labels.nodePanel.nextRowLabel, isDefault: false, isSelfLoop: false }] : [];
320
+ }
321
+ const loops = selfLoopHandles(node);
322
+ if (node.type === "condition") {
323
+ return [
324
+ { id: "true", label: labels.nodePanel.conditionTrue, isDefault: false, isSelfLoop: loops.has("true") },
325
+ { id: "false", label: labels.nodePanel.conditionFalse, isDefault: false, isSelfLoop: loops.has("false") }
326
+ ];
327
+ }
328
+ const isChoice = node.type === "menu" || node.questionType === "choice";
329
+ if (!isChoice)
330
+ return [{ id: "next", label: labels.nodePanel.nextRowLabel, isDefault: false, isSelfLoop: loops.has("next") }];
331
+ const options = node.options ?? [];
332
+ return [
333
+ ...options.map(([id, label]) => ({ id, label, isDefault: false, isSelfLoop: loops.has(id) })),
334
+ { id: "__default", label: labels.edgeFallbackLabel, isDefault: true, isSelfLoop: loops.has("__default") }
335
+ ];
336
+ }
337
+ function SourceRow({
338
+ label,
339
+ isDefault,
340
+ handleId,
341
+ addLabel,
342
+ isSelfLoop,
343
+ selfLoopLabel,
344
+ onQuickAdd
345
+ }) {
346
+ const [hovered, setHovered] = useState(false);
347
+ return /* @__PURE__ */ jsxs(
348
+ "div",
349
+ {
350
+ className: `relative ${onQuickAdd ? "pr-9" : ""}`,
351
+ onMouseEnter: () => setHovered(true),
352
+ onMouseLeave: () => setHovered(false),
353
+ children: [
354
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-600 bg-white/70 dark:bg-gray-900/40 px-2 py-1 pr-3", children: [
355
+ /* @__PURE__ */ jsx(
356
+ "span",
357
+ {
358
+ className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`,
359
+ children: label
360
+ }
361
+ ),
362
+ isSelfLoop && /* @__PURE__ */ jsx(
363
+ RotateCcw,
364
+ {
365
+ size: 12,
366
+ strokeWidth: 2.5,
367
+ "data-cv-tooltip": selfLoopLabel,
368
+ "aria-label": selfLoopLabel,
369
+ className: "shrink-0 text-gray-400 dark:text-gray-500"
370
+ }
371
+ ),
372
+ /* @__PURE__ */ jsx(
373
+ Handle,
374
+ {
375
+ type: "source",
376
+ position: Position.Right,
377
+ id: handleId,
378
+ style: {
379
+ position: "absolute",
380
+ right: -(HANDLE_SIZE_PX / 2 + 1),
381
+ top: "50%",
382
+ transform: "translateY(-50%)",
383
+ width: HANDLE_SIZE_PX,
384
+ height: HANDLE_SIZE_PX
385
+ },
386
+ className: `!border-2 !border-white dark:!border-gray-800 ${isDefault ? "!bg-gray-400 dark:!bg-gray-500" : "!bg-purple-500"}`
387
+ }
388
+ )
389
+ ] }),
390
+ onQuickAdd && /* @__PURE__ */ jsx(
391
+ "button",
392
+ {
393
+ type: "button",
394
+ "data-cv-tooltip": addLabel,
395
+ "aria-label": addLabel,
396
+ onClick: (event) => {
397
+ event.stopPropagation();
398
+ const rect = event.currentTarget.getBoundingClientRect();
399
+ onQuickAdd({ handle: handleId, anchor: { x: rect.right, y: rect.top } });
400
+ },
401
+ style: { opacity: hovered ? 1 : 0.35 },
402
+ className: "nodrag absolute right-0 top-1/2 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded-full border border-blue-300 bg-white text-blue-600 shadow-sm transition-opacity hover:bg-blue-50 dark:border-blue-700 dark:bg-gray-800 dark:text-blue-400 dark:hover:bg-gray-700",
403
+ children: /* @__PURE__ */ jsx(Plus, { size: 12, strokeWidth: 3 })
404
+ }
405
+ )
406
+ ]
407
+ }
408
+ );
409
+ }
410
+ function FlowNodeCard({ data }) {
411
+ const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect, onQuickAdd } = data;
412
+ const label = nodeLabel(node, labels);
413
+ const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
414
+ const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
415
+ const rows = sourceRows(node, labels);
416
+ const nodeIssues = issues.filter((issue) => issue.nodeId === node.id);
417
+ const errors = nodeIssues.filter((issue) => issue.severity === "error");
418
+ const warnings = nodeIssues.filter((issue) => issue.severity === "warning");
419
+ const hasError = errors.length > 0;
420
+ const hasWarning = !hasError && warnings.length > 0;
421
+ const issueTooltip = (hasError ? errors : warnings).map((issue) => issue.message).join(" \xB7 ");
422
+ return /* @__PURE__ */ jsxs(
423
+ "div",
424
+ {
425
+ "data-cv-tooltip": isDetached ? labels.detachedNodeTooltip : label,
426
+ className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""} ${isDetached ? "border-dashed !border-amber-400 animate-pulse" : ""}`,
427
+ onClick: () => onSelect(node.id),
428
+ children: [
429
+ /* @__PURE__ */ jsx(
430
+ Handle,
431
+ {
432
+ type: "target",
433
+ position: Position.Top,
434
+ id: "target",
435
+ style: { width: HANDLE_SIZE_PX, height: HANDLE_SIZE_PX },
436
+ className: "!bg-gray-400 !border-2 !border-white dark:!bg-gray-500 dark:!border-gray-800"
437
+ }
438
+ ),
439
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
440
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs", children: [
441
+ /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
442
+ isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", "data-cv-tooltip": labels.startNodeTooltip }),
443
+ labels.legend[node.type]
444
+ ] }),
445
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
446
+ hasError && /* @__PURE__ */ jsx(
447
+ AlertCircle,
448
+ {
449
+ size: 13,
450
+ "data-cv-tooltip": issueTooltip,
451
+ "aria-label": issueTooltip,
452
+ className: "text-red-600 dark:text-red-400"
453
+ }
454
+ ),
455
+ hasWarning && /* @__PURE__ */ jsx(
456
+ AlertTriangle,
457
+ {
458
+ size: 13,
459
+ "data-cv-tooltip": issueTooltip,
460
+ "aria-label": issueTooltip,
461
+ className: "text-amber-500 dark:text-amber-400"
462
+ }
463
+ ),
464
+ liveCount > 0 && /* @__PURE__ */ jsx(
465
+ "span",
466
+ {
467
+ "data-cv-tooltip": labels.liveCountTooltip(liveCount),
468
+ className: "font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs",
469
+ children: liveCount
470
+ }
471
+ )
472
+ ] })
473
+ ] }),
474
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-900 dark:text-gray-100 mt-1 line-clamp-3", children: label }),
475
+ rows.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-2 space-y-1", children: rows.map((row) => /* @__PURE__ */ jsx(
476
+ SourceRow,
477
+ {
478
+ label: row.label,
479
+ isDefault: row.isDefault,
480
+ handleId: row.id,
481
+ addLabel: labels.quickAdd.fromHandle,
482
+ isSelfLoop: row.isSelfLoop,
483
+ selfLoopLabel: labels.legendPanel.selfLoop,
484
+ ...onQuickAdd ? { onQuickAdd: (params) => onQuickAdd({ nodeId: node.id, ...params }) } : {}
485
+ },
486
+ row.id
487
+ )) })
488
+ ]
489
+ }
490
+ );
491
+ }
492
+ var flowNodeTypes = { flowNode: FlowNodeCard };
493
+
494
+ // src/flows/FlowMapNode.tsx
495
+ import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
496
+ import { GitBranch as GitBranch2, Maximize2 } from "lucide-react";
497
+
498
+ // src/flows/labels.ts
499
+ var DEFAULT_FLOW_EDITOR_LABELS = {
500
+ legend: {
501
+ question: "Pergunta",
502
+ entrada_choice: "Escolha calculada",
503
+ action: "A\xE7\xE3o (simula\xE7\xE3o/atendimento)",
504
+ menu: "Menu",
505
+ condition: "Condi\xE7\xE3o"
506
+ },
507
+ startNodeTooltip: "In\xEDcio do fluxo",
508
+ detachedNodeTooltip: "Sem liga\xE7\xE3o de entrada \u2014 o bot n\xE3o chega neste n\xF3. Puxe um fio de outro card at\xE9 ele.",
509
+ liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
510
+ edgeFallbackLabel: "outro",
511
+ actionKindLabels: {
512
+ handoff: "Encaminhar para atendimento",
513
+ rate_limited_handoff: "Encaminhar (limite de simula\xE7\xF5es atingido)",
514
+ send_product_list: "Enviar cat\xE1logo de produtos",
515
+ send_media: "Enviar arquivos da biblioteca"
516
+ },
517
+ conditionOperatorLabels: {
518
+ ">": "maior que",
519
+ ">=": "maior ou igual a",
520
+ "<": "menor que",
521
+ "<=": "menor ou igual a",
522
+ "==": "igual a",
523
+ "!=": "diferente de",
524
+ contains: "cont\xE9m"
525
+ },
526
+ questionTypeLabels: {
527
+ text: "Texto livre",
528
+ money: "Valor em R$",
529
+ date: "Data",
530
+ int: "N\xFAmero inteiro",
531
+ cpf: "CPF",
532
+ choice: "Escolha (bot\xF5es/lista)"
533
+ },
534
+ nodePanel: {
535
+ title: "Editar n\xF3",
536
+ contextKey: "Chave (contexto)",
537
+ nodeName: "Nome do n\xF3 (opcional)",
538
+ nodeNamePlaceholder: "Ex.: Enviar tabela de pre\xE7os",
539
+ nodeNameHint: "S\xF3 aparece no editor \u2014 o cliente n\xE3o v\xEA.",
540
+ questionType: "Tipo de resposta",
541
+ question: "Texto da pergunta",
542
+ options: "Op\xE7\xF5es (choice)",
543
+ addOption: "Adicionar op\xE7\xE3o",
544
+ removeOption: "Remover op\xE7\xE3o",
545
+ close: "Fechar painel",
546
+ optionId: "Valor",
547
+ optionLabel: "Texto exibido",
548
+ next: "Pr\xF3ximo n\xF3",
549
+ nextHint: "Tamb\xE9m \xE9 poss\xEDvel arrastar um fio no canvas para conectar.",
550
+ nextRowLabel: "Pr\xF3ximo",
551
+ otherFlowsGroup: "Outros fluxos (salto)",
552
+ nextByAnswer: (id) => `Se responder "${id}" \u2192`,
553
+ nextDefault: "Caso contr\xE1rio \u2192",
554
+ save: "Salvar altera\xE7\xF5es",
555
+ cancel: "Cancelar",
556
+ fixedLogicNotice: "Este n\xF3 tem l\xF3gica fixa (valida\xE7\xF5es/c\xE1lculos do sistema) \u2014 s\xF3 o texto e o destino s\xE3o edit\xE1veis.",
557
+ actionNotice: "N\xF3 de a\xE7\xE3o \u2014 dispara a a\xE7\xE3o registrada ou encaminha para atendimento humano.",
558
+ preview: "Como o cliente v\xEA no WhatsApp",
559
+ previewPlaceholder: "Pr\xE9-visualiza\xE7\xE3o\u2026",
560
+ previewEmptyBody: "(escreva o texto da mensagem)",
561
+ previewEmptyOption: "(sem texto)",
562
+ previewListButton: "Ver op\xE7\xF5es",
563
+ previewModeButtons: "Enviado como bot\xF5es (at\xE9 3 op\xE7\xF5es)",
564
+ previewModeList: "Enviado como lista (4+ op\xE7\xF5es)",
565
+ delete: "Excluir n\xF3",
566
+ deleteConfirm: "Excluir este n\xF3? Liga\xE7\xF5es que apontam para ele ficar\xE3o quebradas.",
567
+ directMessage: "Mensagem (opcional)",
568
+ fallbackMessage: "Mensagem de fallback (quando o cat\xE1logo n\xE3o est\xE1 dispon\xEDvel)",
569
+ conditionNotice: "N\xF3 de condi\xE7\xE3o \u2014 n\xE3o pergunta nada ao cliente, s\xF3 decide automaticamente entre Verdadeiro/Falso.",
570
+ conditionVariable: "Vari\xE1vel (chave j\xE1 coletada por uma pergunta anterior)",
571
+ conditionOperator: "Operador",
572
+ conditionValue: "Valor de compara\xE7\xE3o",
573
+ conditionTrue: "Se verdadeiro \u2192",
574
+ conditionFalse: "Se falso \u2192",
575
+ conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192",
576
+ media: "Arquivos enviados neste ponto",
577
+ mediaUnavailable: "A biblioteca de arquivos n\xE3o est\xE1 dispon\xEDvel neste painel."
578
+ },
579
+ quickAdd: {
580
+ fromHandle: "Criar o pr\xF3ximo n\xF3 j\xE1 ligado aqui",
581
+ title: "Ligar em um n\xF3 novo",
582
+ disconnect: "Desligar este fio (o n\xF3 continua no fluxo)"
583
+ },
584
+ legendPanel: {
585
+ title: "Legenda",
586
+ nodes: "Cards",
587
+ connections: "Liga\xE7\xF5es",
588
+ linear: "Segue direto para o pr\xF3ximo",
589
+ branch: "Caminho de uma op\xE7\xE3o escolhida",
590
+ fallback: "Quando a resposta n\xE3o casa com nenhuma op\xE7\xE3o",
591
+ crossFlow: "Salta para outro fluxo",
592
+ live: "Tem conversa passando por aqui agora",
593
+ selfLoop: "Volta ao mesmo card \u2014 repete a pergunta",
594
+ detached: "Ningu\xE9m aponta para este card: o bot n\xE3o chega nele",
595
+ startNode: "Onde o fluxo come\xE7a"
596
+ },
597
+ palette: {
598
+ title: "Adicionar ao fluxo",
599
+ question: "Pergunta",
600
+ decision: "Decis\xE3o",
601
+ condition: "Condi\xE7\xE3o",
602
+ conditionHint: "Compara uma vari\xE1vel j\xE1 coletada e segue automaticamente, sem perguntar nada",
603
+ action: "A\xE7\xE3o"
604
+ },
605
+ flowMap: {
606
+ nodeCount: (count) => `${count} n\xF3(s)`,
607
+ openFlow: "Abrir fluxo",
608
+ toggleToMap: "Mapa de fluxos",
609
+ toggleToDetail: "Voltar ao editor"
610
+ },
611
+ flowGroup: {
612
+ focus: "Focar neste fluxo",
613
+ close: "Fechar"
614
+ },
615
+ crossFlowPortal: {
616
+ tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
617
+ goesTo: (label) => `\u21AA Vai para: ${label}`
618
+ },
619
+ collectionChain: {
620
+ feeds: (label) => `Alimenta: ${label}`
621
+ },
622
+ validation: {
623
+ title: "Antes de publicar",
624
+ errors: (count) => `${count} erro(s) \u2014 corrija antes de salvar`,
625
+ warnings: (count) => `${count} aviso(s)`,
626
+ noStart: "O fluxo precisa de um n\xF3 inicial v\xE1lido.",
627
+ brokenRef: (from, to) => `N\xF3 "${from}": liga\xE7\xE3o aponta para "${to}", que n\xE3o existe.`,
628
+ choiceWithoutOptions: (id) => `N\xF3 "${id}": escolha sem nenhuma op\xE7\xE3o.`,
629
+ duplicatedOptionId: (id, optionId) => `N\xF3 "${id}": valor de op\xE7\xE3o "${optionId}" duplicado.`,
630
+ optionWithoutTarget: (id, label) => `N\xF3 "${id}": op\xE7\xE3o "${label}" n\xE3o tem destino definido.`,
631
+ tooManyOptions: (id, count) => `N\xF3 "${id}": ${count} op\xE7\xF5es \u2014 o WhatsApp aceita no m\xE1ximo 10 em lista.`,
632
+ buttonTitleTooLong: (id, label) => `N\xF3 "${id}": bot\xE3o "${label}" passa de 20 caracteres.`,
633
+ listTitleTooLong: (id, label) => `N\xF3 "${id}": item de lista "${label}" passa de 24 caracteres.`,
634
+ bodyTooLong: (id) => `N\xF3 "${id}": texto passa de 1024 caracteres.`,
635
+ unreachable: (id) => `N\xF3 "${id}" \xE9 inalcan\xE7\xE1vel a partir do in\xEDcio do fluxo.`,
636
+ deadEndQuestion: (id) => `N\xF3 "${id}": pergunta sem pr\xF3ximo passo definido.`,
637
+ conditionIncomplete: (id) => `N\xF3 "${id}": condi\xE7\xE3o incompleta \u2014 defina vari\xE1vel, operador e valor.`,
638
+ conditionBranchMissing: (id, branch) => `N\xF3 "${id}": ramo "${branch === "true" ? "Verdadeiro" : "Falso"}" sem destino definido.`
639
+ },
640
+ workspace: {
641
+ title: "Fluxos do Bot",
642
+ subtitle: "Blueprint visual dos fluxos de conversa, sincronizado com o que est\xE1 em produ\xE7\xE3o.",
643
+ loading: "Carregando fluxos\u2026",
644
+ loadError: "N\xE3o foi poss\xEDvel carregar os fluxos.",
645
+ saveGraph: "Publicar altera\xE7\xF5es",
646
+ saving: "Publicando\u2026",
647
+ saveSuccess: "Fluxo publicado! O bot j\xE1 est\xE1 usando a vers\xE3o nova.",
648
+ saveError: "N\xE3o foi poss\xEDvel salvar \u2014 verifique se todos os destinos apontam para n\xF3s existentes.",
649
+ organize: "Organizar",
650
+ organizeTooltip: "Reorganiza os n\xF3s automaticamente e salva as novas posi\xE7\xF5es",
651
+ discardChanges: "Desfazer altera\xE7\xF5es",
652
+ discardTooltip: "Devolve os fluxos abertos ao que est\xE1 publicado, descartando o que n\xE3o foi salvo",
653
+ discardConfirm: "Descartar todas as altera\xE7\xF5es n\xE3o publicadas e voltar ao fluxo que est\xE1 no ar?",
654
+ unsavedChangesConfirm: "Voc\xEA tem altera\xE7\xF5es n\xE3o publicadas neste fluxo. Trocar de fluxo agora descarta essas edi\xE7\xF5es. Continuar?"
655
+ },
656
+ flowManager: {
657
+ newFlow: "Novo fluxo",
658
+ createTitle: "Criar novo fluxo",
659
+ key: "Identificador \xFAnico",
660
+ keyHint: "letras min\xFAsculas, n\xFAmeros e _ (ex.: promocoes_semana)",
661
+ keyInvalid: "Use apenas letras min\xFAsculas, n\xFAmeros e _ (2 a 40 caracteres)",
662
+ label: "Nome exibido",
663
+ showInMenu: "Exibir como op\xE7\xE3o no menu principal do bot",
664
+ menuOptionLabel: "Texto da op\xE7\xE3o no menu",
665
+ create: "Criar fluxo",
666
+ creating: "Criando\u2026",
667
+ deleteFlow: "Excluir fluxo",
668
+ deleteConfirm: (label) => `Excluir o fluxo "${label}"? Esta a\xE7\xE3o n\xE3o pode ser desfeita.`,
669
+ createError: "N\xE3o foi poss\xEDvel criar o fluxo.",
670
+ deleteError: "N\xE3o foi poss\xEDvel excluir o fluxo."
671
+ }
672
+ };
673
+ function mergeFlowEditorLabels(override) {
674
+ if (!override) return DEFAULT_FLOW_EDITOR_LABELS;
675
+ return {
676
+ ...DEFAULT_FLOW_EDITOR_LABELS,
677
+ ...override,
678
+ legend: { ...DEFAULT_FLOW_EDITOR_LABELS.legend, ...override.legend },
679
+ actionKindLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.actionKindLabels, ...override.actionKindLabels },
680
+ conditionOperatorLabels: {
681
+ ...DEFAULT_FLOW_EDITOR_LABELS.conditionOperatorLabels,
682
+ ...override.conditionOperatorLabels
683
+ },
684
+ questionTypeLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.questionTypeLabels, ...override.questionTypeLabels },
685
+ nodePanel: { ...DEFAULT_FLOW_EDITOR_LABELS.nodePanel, ...override.nodePanel },
686
+ quickAdd: { ...DEFAULT_FLOW_EDITOR_LABELS.quickAdd, ...override.quickAdd },
687
+ legendPanel: { ...DEFAULT_FLOW_EDITOR_LABELS.legendPanel, ...override.legendPanel },
688
+ palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
689
+ flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
690
+ flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
691
+ crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal },
692
+ collectionChain: { ...DEFAULT_FLOW_EDITOR_LABELS.collectionChain, ...override.collectionChain },
693
+ validation: { ...DEFAULT_FLOW_EDITOR_LABELS.validation, ...override.validation },
694
+ workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, ...override.workspace },
695
+ flowManager: { ...DEFAULT_FLOW_EDITOR_LABELS.flowManager, ...override.flowManager }
696
+ };
697
+ }
698
+
699
+ // src/flows/FlowMapNode.tsx
700
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
701
+ function FlowMapNode({ data }) {
702
+ const { label, nodeCount, isRoot, labels = DEFAULT_FLOW_EDITOR_LABELS, onOpen } = data;
703
+ return /* @__PURE__ */ jsxs2("div", { className: "relative rounded-xl border-2 border-emerald-300 dark:border-emerald-700 bg-emerald-50 dark:bg-emerald-950/40 px-4 py-3 w-56 shadow-sm", children: [
704
+ /* @__PURE__ */ jsx2(Handle2, { type: "target", position: Position2.Top, className: "!bg-gray-400 dark:!bg-gray-500" }),
705
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
706
+ /* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
707
+ /* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
708
+ isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", "data-cv-tooltip": labels.startNodeTooltip })
709
+ ] }),
710
+ /* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
711
+ /* @__PURE__ */ jsxs2(
712
+ "button",
713
+ {
714
+ "data-cv-tooltip": labels.flowMap.openFlow,
715
+ "aria-label": labels.flowMap.openFlow,
716
+ type: "button",
717
+ onClick: onOpen,
718
+ className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
719
+ children: [
720
+ /* @__PURE__ */ jsx2(Maximize2, { size: 11 }),
721
+ " ",
722
+ labels.flowMap.openFlow
723
+ ]
724
+ }
725
+ ),
726
+ /* @__PURE__ */ jsx2(Handle2, { type: "source", position: Position2.Bottom, className: "!bg-gray-400 dark:!bg-gray-500" })
727
+ ] });
728
+ }
729
+ var flowMapNodeTypes = { flowMapNode: FlowMapNode };
730
+
731
+ // src/flows/FlowMapCanvas.tsx
732
+ import { useMemo } from "react";
733
+ import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
734
+ import "@xyflow/react/dist/style.css";
735
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
736
+ var MAP_NODE_TYPES = { ...flowMapNodeTypes };
737
+ var EDGE_COLOR = "#06b6d4";
738
+ var BACKGROUND_COLOR_LIGHT = "#cbd5e1";
739
+ var BACKGROUND_COLOR_DARK = "#334155";
740
+ function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverride }) {
528
741
  const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
529
742
  const isDark = useIsDarkTheme();
530
743
  const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey]);
531
744
  const nodes = useMemo(
532
- () => Object.values(graphs).map((g) => ({
533
- id: g.key,
534
- type: "flowMapNode",
535
- position: positions[g.key] ?? { x: 0, y: 0 },
536
- data: {
537
- label: g.label,
538
- nodeCount: Object.keys(g.nodes).length,
539
- isRoot: g.key === rootKey,
540
- labels,
541
- onOpen: () => onOpenFlow(g.key)
542
- }
543
- })),
745
+ () => Object.values(graphs).map(
746
+ (g) => ({
747
+ id: g.key,
748
+ type: "flowMapNode",
749
+ position: positions[g.key] ?? { x: 0, y: 0 },
750
+ data: {
751
+ label: g.label,
752
+ nodeCount: Object.keys(g.nodes).length,
753
+ isRoot: g.key === rootKey,
754
+ labels,
755
+ onOpen: () => onOpenFlow(g.key)
756
+ }
757
+ })
758
+ ),
544
759
  [graphs, positions, rootKey, onOpenFlow, labels]
545
760
  );
546
761
  const edges = useMemo(() => {
@@ -592,8 +807,28 @@ function FlowGroupHeader({ data }) {
592
807
  const { label, labels = DEFAULT_FLOW_EDITOR_LABELS, onFocus, onClose } = data;
593
808
  return /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 rounded-full border border-cyan-300 dark:border-cyan-700 bg-cyan-50 dark:bg-cyan-950/50 px-3 py-1 text-xs font-medium text-cyan-800 dark:text-cyan-200 shadow-sm whitespace-nowrap", children: [
594
809
  /* @__PURE__ */ jsx5("span", { children: label }),
595
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: onFocus, title: labels.flowGroup.focus, className: "hover:text-blue-600 dark:hover:text-blue-400", children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 }) }),
596
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: onClose, title: labels.flowGroup.close, className: "hover:text-red-600 dark:hover:text-red-400", children: /* @__PURE__ */ jsx5(X, { size: 12 }) })
810
+ /* @__PURE__ */ jsx5(
811
+ "button",
812
+ {
813
+ type: "button",
814
+ onClick: onFocus,
815
+ "data-cv-tooltip": labels.flowGroup.focus,
816
+ "aria-label": labels.flowGroup.focus,
817
+ className: "hover:text-blue-600 dark:hover:text-blue-400",
818
+ children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 })
819
+ }
820
+ ),
821
+ /* @__PURE__ */ jsx5(
822
+ "button",
823
+ {
824
+ type: "button",
825
+ onClick: onClose,
826
+ "data-cv-tooltip": labels.flowGroup.close,
827
+ "aria-label": labels.flowGroup.close,
828
+ className: "hover:text-red-600 dark:hover:text-red-400",
829
+ children: /* @__PURE__ */ jsx5(X, { size: 12 })
830
+ }
831
+ )
597
832
  ] });
598
833
  }
599
834
  var flowGroupHeaderNodeTypes = { flowGroupHeader: FlowGroupHeader };
@@ -608,7 +843,8 @@ function FlowPortalNode({ data }) {
608
843
  "button",
609
844
  {
610
845
  type: "button",
611
- title: labels.crossFlowPortal.tooltip,
846
+ "data-cv-tooltip": labels.crossFlowPortal.tooltip,
847
+ "aria-label": labels.crossFlowPortal.tooltip,
612
848
  onClick: onNavigate,
613
849
  className: "relative flex items-center gap-1.5 rounded-full border-2 border-dashed border-cyan-400 dark:border-cyan-600 bg-cyan-50 dark:bg-cyan-950/40 px-3 py-1.5 text-xs font-medium text-cyan-700 dark:text-cyan-300 hover:bg-cyan-100 dark:hover:bg-cyan-950/70 transition-colors cursor-pointer",
614
850
  children: [
@@ -622,23 +858,122 @@ function FlowPortalNode({ data }) {
622
858
  var flowPortalNodeTypes = { flowPortal: FlowPortalNode };
623
859
 
624
860
  // src/flows/FlowPalette.tsx
625
- import { useEffect, useRef, useState } from "react";
626
- import { Plus, MessageCircleQuestion as MessageCircleQuestion2, GitBranch as GitBranch3, Zap as Zap2, Diamond as Diamond2, ChevronRight } from "lucide-react";
627
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
861
+ import { useEffect, useRef, useState as useState2 } from "react";
862
+ import { Plus as Plus2, MessageCircleQuestion as MessageCircleQuestion2, GitBranch as GitBranch3, Zap as Zap2, Diamond as Diamond2, ChevronRight } from "lucide-react";
863
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
628
864
  var QUESTION_TYPES = ["text", "money", "date", "int", "cpf"];
629
- function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
630
- const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
865
+ function FlowPaletteMenu({ onSelect, labels, actionOptions }) {
631
866
  const resolvedActionOptions = actionOptions ?? [
632
867
  { actionKind: "handoff", label: labels.actionKindLabels.handoff ?? "Encaminhar para atendimento" }
633
868
  ];
634
- const [open, setOpen] = useState(false);
635
- const [submenu, setSubmenu] = useState(null);
869
+ const [submenu, setSubmenu] = useState2(null);
870
+ function select(spec) {
871
+ onSelect(spec);
872
+ setSubmenu(null);
873
+ }
874
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
875
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
876
+ /* @__PURE__ */ jsxs6(
877
+ "button",
878
+ {
879
+ "data-cv-tooltip": labels.palette.question,
880
+ "aria-label": labels.palette.question,
881
+ onMouseEnter: () => setSubmenu("question"),
882
+ onClick: () => setSubmenu(submenu === "question" ? null : "question"),
883
+ className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
884
+ children: [
885
+ /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
886
+ /* @__PURE__ */ jsx7(MessageCircleQuestion2, { size: 15, className: "text-blue-500" }),
887
+ " ",
888
+ labels.palette.question
889
+ ] }),
890
+ /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
891
+ ]
892
+ }
893
+ ),
894
+ submenu === "question" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: QUESTION_TYPES.map((qt) => /* @__PURE__ */ jsx7(
895
+ "button",
896
+ {
897
+ "data-cv-tooltip": labels.questionTypeLabels[qt],
898
+ "aria-label": labels.questionTypeLabels[qt],
899
+ onClick: () => select({ kind: "question", questionType: qt }),
900
+ className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
901
+ children: labels.questionTypeLabels[qt]
902
+ },
903
+ qt
904
+ )) })
905
+ ] }),
906
+ /* @__PURE__ */ jsxs6(
907
+ "button",
908
+ {
909
+ "data-cv-tooltip": labels.palette.decision,
910
+ "aria-label": labels.palette.decision,
911
+ onMouseEnter: () => setSubmenu(null),
912
+ onClick: () => select({ kind: "decision" }),
913
+ className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
914
+ children: [
915
+ /* @__PURE__ */ jsx7(GitBranch3, { size: 15, className: "text-purple-500" }),
916
+ " ",
917
+ labels.palette.decision
918
+ ]
919
+ }
920
+ ),
921
+ /* @__PURE__ */ jsxs6(
922
+ "button",
923
+ {
924
+ onMouseEnter: () => setSubmenu(null),
925
+ onClick: () => select({ kind: "condition" }),
926
+ className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
927
+ "data-cv-tooltip": labels.palette.conditionHint,
928
+ "aria-label": labels.palette.conditionHint,
929
+ children: [
930
+ /* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
931
+ " ",
932
+ labels.palette.condition
933
+ ]
934
+ }
935
+ ),
936
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
937
+ /* @__PURE__ */ jsxs6(
938
+ "button",
939
+ {
940
+ "data-cv-tooltip": labels.palette.action,
941
+ "aria-label": labels.palette.action,
942
+ onMouseEnter: () => setSubmenu("action"),
943
+ onClick: () => setSubmenu(submenu === "action" ? null : "action"),
944
+ className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
945
+ children: [
946
+ /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
947
+ /* @__PURE__ */ jsx7(Zap2, { size: 15, className: "text-orange-500" }),
948
+ " ",
949
+ labels.palette.action
950
+ ] }),
951
+ /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
952
+ ]
953
+ }
954
+ ),
955
+ submenu === "action" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: resolvedActionOptions.map((option) => /* @__PURE__ */ jsx7(
956
+ "button",
957
+ {
958
+ "data-cv-tooltip": option.label,
959
+ "aria-label": option.label,
960
+ onClick: () => select({ kind: "action", actionKind: option.actionKind }),
961
+ className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
962
+ children: option.label
963
+ },
964
+ option.actionKind
965
+ )) })
966
+ ] })
967
+ ] });
968
+ }
969
+ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
970
+ const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
971
+ const [open, setOpen] = useState2(false);
636
972
  const containerRef = useRef(null);
637
973
  useEffect(() => {
638
974
  function handleClickOutside(event) {
639
975
  if (containerRef.current && !containerRef.current.contains(event.target)) {
640
976
  setOpen(false);
641
- setSubmenu(null);
642
977
  }
643
978
  }
644
979
  document.addEventListener("mousedown", handleClickOutside);
@@ -647,465 +982,1824 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
647
982
  function select(spec) {
648
983
  onAdd(spec);
649
984
  setOpen(false);
650
- setSubmenu(null);
651
985
  }
652
986
  return /* @__PURE__ */ jsxs6("div", { ref: containerRef, className: "relative", children: [
653
987
  /* @__PURE__ */ jsxs6(
654
988
  "button",
655
989
  {
990
+ "data-cv-tooltip": labels.palette.title,
991
+ "aria-label": labels.palette.title,
656
992
  onClick: () => setOpen((v) => !v),
657
993
  className: "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700",
658
994
  children: [
659
- /* @__PURE__ */ jsx7(Plus, { size: 14 }),
995
+ /* @__PURE__ */ jsx7(Plus2, { size: 14 }),
660
996
  " ",
661
997
  labels.palette.title
662
998
  ]
663
999
  }
664
1000
  ),
665
- open && /* @__PURE__ */ jsxs6("div", { className: "absolute left-0 top-full mt-1.5 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg z-50 py-1", children: [
666
- /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
667
- /* @__PURE__ */ jsxs6(
668
- "button",
1001
+ open && /* @__PURE__ */ jsx7("div", { className: "absolute left-0 top-full mt-1.5 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg z-50 py-1", children: /* @__PURE__ */ jsx7(
1002
+ FlowPaletteMenu,
1003
+ {
1004
+ onSelect: select,
1005
+ labels,
1006
+ ...actionOptions ? { actionOptions } : {}
1007
+ }
1008
+ ) })
1009
+ ] });
1010
+ }
1011
+
1012
+ // src/flows/FlowLegend.tsx
1013
+ import { useState as useState3 } from "react";
1014
+ import { ChevronDown, ChevronUp, RotateCcw as RotateCcw2 } from "lucide-react";
1015
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1016
+ function EdgeSample({ sample }) {
1017
+ return /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1018
+ /* @__PURE__ */ jsx8("svg", { width: "28", height: "12", viewBox: "0 0 28 12", "aria-hidden": "true", className: "shrink-0", children: /* @__PURE__ */ jsx8(
1019
+ "line",
1020
+ {
1021
+ x1: "2",
1022
+ y1: "6",
1023
+ x2: "26",
1024
+ y2: "6",
1025
+ stroke: sample.color,
1026
+ strokeWidth: "1.75",
1027
+ ...sample.dash ? { strokeDasharray: sample.dash } : {}
1028
+ }
1029
+ ) }),
1030
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: sample.label })
1031
+ ] });
1032
+ }
1033
+ function FlowLegend({ labels, edgeSamples, nodeSwatches }) {
1034
+ const [open, setOpen] = useState3(false);
1035
+ return /* @__PURE__ */ jsxs7("div", { className: "rounded-lg border border-gray-200 bg-white/95 shadow-sm backdrop-blur dark:border-gray-700 dark:bg-gray-800/95", children: [
1036
+ /* @__PURE__ */ jsxs7(
1037
+ "button",
1038
+ {
1039
+ type: "button",
1040
+ onClick: () => setOpen((current) => !current),
1041
+ "data-cv-tooltip": labels.legendPanel.title,
1042
+ "aria-label": labels.legendPanel.title,
1043
+ "aria-expanded": open,
1044
+ className: "flex w-full items-center justify-between gap-2 px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400",
1045
+ children: [
1046
+ labels.legendPanel.title,
1047
+ open ? /* @__PURE__ */ jsx8(ChevronDown, { size: 13 }) : /* @__PURE__ */ jsx8(ChevronUp, { size: 13 })
1048
+ ]
1049
+ }
1050
+ ),
1051
+ open && /* @__PURE__ */ jsxs7("div", { className: "max-h-72 w-64 overflow-y-auto border-t border-gray-100 px-2.5 py-2 dark:border-gray-700", children: [
1052
+ /* @__PURE__ */ jsx8("p", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500", children: labels.legendPanel.connections }),
1053
+ /* @__PURE__ */ jsx8("ul", { className: "space-y-1.5", children: edgeSamples.map((sample) => /* @__PURE__ */ jsx8(EdgeSample, { sample }, sample.label)) }),
1054
+ /* @__PURE__ */ jsx8("p", { className: "mb-1 mt-3 text-[10px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500", children: labels.legendPanel.nodes }),
1055
+ /* @__PURE__ */ jsxs7("ul", { className: "space-y-1.5", children: [
1056
+ nodeSwatches.map((swatch) => /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1057
+ /* @__PURE__ */ jsx8("span", { className: `h-3 w-5 shrink-0 rounded border-2 ${swatch.className}`, "aria-hidden": "true" }),
1058
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legend[swatch.type] })
1059
+ ] }, swatch.type)),
1060
+ /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1061
+ /* @__PURE__ */ jsx8(
1062
+ "span",
1063
+ {
1064
+ className: "h-3 w-5 shrink-0 rounded border-2 border-dashed border-amber-400",
1065
+ "aria-hidden": "true"
1066
+ }
1067
+ ),
1068
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legendPanel.detached })
1069
+ ] }),
1070
+ /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1071
+ /* @__PURE__ */ jsx8("span", { className: "flex h-3 w-5 shrink-0 items-center justify-center", "aria-hidden": "true", children: /* @__PURE__ */ jsx8("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }) }),
1072
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legendPanel.startNode })
1073
+ ] }),
1074
+ /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1075
+ /* @__PURE__ */ jsx8("span", { className: "flex h-3 w-5 shrink-0 items-center justify-center text-gray-400", "aria-hidden": "true", children: /* @__PURE__ */ jsx8(RotateCcw2, { size: 12, strokeWidth: 2.5 }) }),
1076
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legendPanel.selfLoop })
1077
+ ] })
1078
+ ] })
1079
+ ] })
1080
+ ] });
1081
+ }
1082
+
1083
+ // src/flows/FlowConnectionEdge.tsx
1084
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
1085
+ import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@xyflow/react";
1086
+ import { X as X2 } from "lucide-react";
1087
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1088
+ var HIDE_DELAY_MS = 320;
1089
+ function FlowConnectionEdge({
1090
+ id,
1091
+ sourceX,
1092
+ sourceY,
1093
+ targetX,
1094
+ targetY,
1095
+ sourcePosition,
1096
+ targetPosition,
1097
+ markerEnd,
1098
+ style,
1099
+ data,
1100
+ interactionWidth
1101
+ }) {
1102
+ const [hovered, setHovered] = useState4(false);
1103
+ const hideTimer = useRef2(void 0);
1104
+ const { onDisconnect, disconnectLabel } = data ?? {};
1105
+ function show() {
1106
+ if (hideTimer.current) clearTimeout(hideTimer.current);
1107
+ setHovered(true);
1108
+ }
1109
+ function scheduleHide() {
1110
+ if (hideTimer.current) clearTimeout(hideTimer.current);
1111
+ hideTimer.current = setTimeout(() => setHovered(false), HIDE_DELAY_MS);
1112
+ }
1113
+ useEffect2(() => () => clearTimeout(hideTimer.current), []);
1114
+ const [path, labelX, labelY] = getBezierPath({
1115
+ sourceX,
1116
+ sourceY,
1117
+ sourcePosition,
1118
+ targetX,
1119
+ targetY,
1120
+ targetPosition
1121
+ });
1122
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
1123
+ /* @__PURE__ */ jsx9(BaseEdge, { id, path, markerEnd, style, interactionWidth: interactionWidth ?? 20 }),
1124
+ /* @__PURE__ */ jsx9(
1125
+ "path",
1126
+ {
1127
+ d: path,
1128
+ fill: "none",
1129
+ strokeWidth: 22,
1130
+ stroke: "transparent",
1131
+ className: "react-flow__edge-interaction",
1132
+ onMouseEnter: show,
1133
+ onMouseLeave: scheduleHide
1134
+ }
1135
+ ),
1136
+ onDisconnect && hovered && /* @__PURE__ */ jsx9(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx9(
1137
+ "button",
1138
+ {
1139
+ type: "button",
1140
+ "data-cv-tooltip": disconnectLabel,
1141
+ "aria-label": disconnectLabel,
1142
+ onMouseEnter: show,
1143
+ onMouseLeave: scheduleHide,
1144
+ onClick: (event) => {
1145
+ event.stopPropagation();
1146
+ onDisconnect();
1147
+ },
1148
+ style: { transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` },
1149
+ className: "nodrag nopan pointer-events-auto absolute flex h-5 w-5 items-center justify-center rounded-full border border-gray-300 bg-white text-gray-500 shadow-sm hover:border-red-400 hover:text-red-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400",
1150
+ children: /* @__PURE__ */ jsx9(X2, { size: 12, strokeWidth: 2.5 })
1151
+ }
1152
+ ) })
1153
+ ] });
1154
+ }
1155
+ var flowEdgeTypes = { flowConnection: FlowConnectionEdge };
1156
+
1157
+ // src/flows/FlowNodePanel.tsx
1158
+ import { useState as useState5 } from "react";
1159
+ import { Plus as Plus3, Trash2, Save, X as X3, AlertTriangle as AlertTriangle2, AlertCircle as AlertCircle2 } from "lucide-react";
1160
+
1161
+ // src/flows/FlowWhatsAppPreview.tsx
1162
+ import { List } from "lucide-react";
1163
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1164
+ function FlowWhatsAppPreview({
1165
+ body,
1166
+ options,
1167
+ labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel
1168
+ }) {
1169
+ if (!body && (!options || options.length === 0)) {
1170
+ return /* @__PURE__ */ jsx10("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
1171
+ }
1172
+ const usesButtons = rendersAsButtons(options);
1173
+ const hasOptions = (options?.length ?? 0) > 0;
1174
+ return /* @__PURE__ */ jsxs9("div", { className: "rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5", children: [
1175
+ /* @__PURE__ */ jsxs9("div", { className: "max-w-[85%]", children: [
1176
+ /* @__PURE__ */ jsxs9("div", { className: "rounded-lg rounded-tl-none bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm whitespace-pre-wrap break-words", children: [
1177
+ body ? parseWhatsAppFormatting(body) : /* @__PURE__ */ jsx10("span", { className: "italic text-gray-400", children: labels.previewEmptyBody }),
1178
+ hasOptions && !usesButtons && /* @__PURE__ */ jsx10("div", { className: "mt-2 -mx-3 -mb-2 border-t border-gray-100 dark:border-gray-600", children: /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-center gap-1.5 py-2 text-sm font-medium text-cyan-600 dark:text-cyan-400", children: [
1179
+ /* @__PURE__ */ jsx10(List, { size: 15 }),
1180
+ " ",
1181
+ labels.previewListButton
1182
+ ] }) })
1183
+ ] }),
1184
+ hasOptions && usesButtons && /* @__PURE__ */ jsx10("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx10(
1185
+ "div",
1186
+ {
1187
+ className: "rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm",
1188
+ children: label || /* @__PURE__ */ jsx10("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
1189
+ },
1190
+ id
1191
+ )) }),
1192
+ hasOptions && !usesButtons && /* @__PURE__ */ jsx10("div", { className: "mt-1.5 rounded-lg bg-white dark:bg-gray-700 shadow-sm divide-y divide-gray-100 dark:divide-gray-600 overflow-hidden", children: options.slice(0, WHATSAPP_LIMITS.MAX_LIST_ROWS).map(([id, label]) => /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-3 py-1.5 text-sm text-gray-800 dark:text-gray-100", children: [
1193
+ /* @__PURE__ */ jsx10("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
1194
+ label || /* @__PURE__ */ jsx10("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
1195
+ ] }, id)) })
1196
+ ] }),
1197
+ hasOptions && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-gray-500 dark:text-gray-400 px-1", children: usesButtons ? labels.previewModeButtons : labels.previewModeList })
1198
+ ] });
1199
+ }
1200
+
1201
+ // src/flows/FlowNodePanel.tsx
1202
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1203
+ var SELECT_CLASSNAME = "border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all";
1204
+ var INPUT_CLASSNAME = "border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all";
1205
+ function truncateLabel(value, max = 60) {
1206
+ if (!value) return "\u2014";
1207
+ return value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
1208
+ }
1209
+ function WhatsAppTextField({
1210
+ label,
1211
+ value,
1212
+ onValueChange,
1213
+ options,
1214
+ placeholder,
1215
+ labels
1216
+ }) {
1217
+ return /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
1218
+ /* @__PURE__ */ jsxs10("div", { children: [
1219
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: label }),
1220
+ /* @__PURE__ */ jsx11(
1221
+ "textarea",
1222
+ {
1223
+ value,
1224
+ onChange: (e) => onValueChange(e.target.value),
1225
+ rows: 3,
1226
+ placeholder,
1227
+ className: `w-full mt-1 ${INPUT_CLASSNAME}`
1228
+ }
1229
+ )
1230
+ ] }),
1231
+ /* @__PURE__ */ jsxs10("div", { children: [
1232
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.preview }),
1233
+ /* @__PURE__ */ jsx11("div", { className: "mt-1", children: /* @__PURE__ */ jsx11(FlowWhatsAppPreview, { body: value, options, labels: labels.nodePanel }) })
1234
+ ] })
1235
+ ] });
1236
+ }
1237
+ function IssueRow({ issue }) {
1238
+ const Icon = issue.severity === "error" ? AlertCircle2 : AlertTriangle2;
1239
+ const color = issue.severity === "error" ? "text-red-600 dark:text-red-400" : "text-amber-600 dark:text-amber-400";
1240
+ return /* @__PURE__ */ jsxs10("div", { className: `flex items-start gap-1.5 text-xs ${color}`, children: [
1241
+ /* @__PURE__ */ jsx11(Icon, { size: 13, className: "mt-0.5 shrink-0" }),
1242
+ /* @__PURE__ */ jsx11("span", { children: issue.message })
1243
+ ] });
1244
+ }
1245
+ function FlowNodePanel({
1246
+ graph,
1247
+ node,
1248
+ issues,
1249
+ otherFlows,
1250
+ onClose,
1251
+ onChange,
1252
+ onDelete,
1253
+ labels: labelsOverride,
1254
+ renderMediaPicker
1255
+ }) {
1256
+ const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
1257
+ const [draft, setDraft] = useState5(node);
1258
+ const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id);
1259
+ const isFixedLogic = draft.type === "entrada_choice";
1260
+ const isAction = draft.type === "action";
1261
+ const isSendMedia = isAction && draft.actionKind === BUILT_IN_ACTION_KINDS.SEND_MEDIA;
1262
+ const isPassThroughAction = isAction && Boolean(draft.actionKind && PASS_THROUGH_ACTION_KINDS.includes(draft.actionKind));
1263
+ const isCondition = draft.type === "condition";
1264
+ const isStart = graph.startNodeId === node.id;
1265
+ const nodeIssues = issues.filter((i) => i.nodeId === node.id);
1266
+ const knownContextKeys = [
1267
+ ...new Set(
1268
+ Object.values(graph.nodes).map((n) => n.contextKey).filter((key) => !!key)
1269
+ )
1270
+ ];
1271
+ const conditionAnswerIds = isCondition ? ["true", "false"] : (draft.options ?? []).map(([id]) => id);
1272
+ function updateNextString(value) {
1273
+ setDraft((prev) => ({ ...prev, next: value }));
1274
+ }
1275
+ function updateNextByAnswer(answerId, value) {
1276
+ setDraft((prev) => {
1277
+ const current = typeof prev.next === "object" && prev.next ? prev.next : { byAnswer: {}, default: otherNodeIds[0] ?? "" };
1278
+ return { ...prev, next: { ...current, byAnswer: { ...current.byAnswer, [answerId]: value } } };
1279
+ });
1280
+ }
1281
+ function updateNextDefault(value) {
1282
+ setDraft((prev) => {
1283
+ const current = typeof prev.next === "object" && prev.next ? prev.next : { byAnswer: {}, default: value };
1284
+ return { ...prev, next: { ...current, default: value } };
1285
+ });
1286
+ }
1287
+ function updateOption(index, field, value) {
1288
+ setDraft((prev) => {
1289
+ const opts = [...prev.options ?? []];
1290
+ const opt = [...opts[index]];
1291
+ opt[field] = value;
1292
+ opts[index] = opt;
1293
+ return { ...prev, options: opts };
1294
+ });
1295
+ }
1296
+ function addOption() {
1297
+ setDraft((prev) => ({
1298
+ ...prev,
1299
+ options: [...prev.options ?? [], [String((prev.options?.length ?? 0) + 1), "Nova op\xE7\xE3o"]]
1300
+ }));
1301
+ }
1302
+ function removeOption(index) {
1303
+ setDraft((prev) => ({ ...prev, options: (prev.options ?? []).filter((_, i) => i !== index) }));
1304
+ }
1305
+ function nextNodeOptions() {
1306
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1307
+ otherNodeIds.map((id) => /* @__PURE__ */ jsx11("option", { value: id, children: truncateLabel(nodeLabel(graph.nodes[id], labels)) }, id)),
1308
+ otherFlows.length > 0 && /* @__PURE__ */ jsx11("optgroup", { label: labels.nodePanel.otherFlowsGroup, children: otherFlows.map((flow) => /* @__PURE__ */ jsx11("option", { value: `${CROSS_FLOW_PREFIX}${flow.key}`, children: flow.label }, flow.key)) })
1309
+ ] });
1310
+ }
1311
+ return /* @__PURE__ */ jsxs10("div", { className: "fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col", children: [
1312
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
1313
+ /* @__PURE__ */ jsx11("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
1314
+ /* @__PURE__ */ jsx11("button", { "data-cv-tooltip": labels.nodePanel.close, "aria-label": labels.nodePanel.close, onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx11(X3, { size: 18 }) })
1315
+ ] }),
1316
+ /* @__PURE__ */ jsxs10("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
1317
+ nodeIssues.length > 0 && /* @__PURE__ */ jsx11("div", { className: "rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-2.5 space-y-1.5", children: nodeIssues.map((issue, i) => /* @__PURE__ */ jsx11(IssueRow, { issue }, i)) }),
1318
+ isFixedLogic && /* @__PURE__ */ jsx11("p", { className: "text-xs text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30 rounded-lg p-2", children: labels.nodePanel.fixedLogicNotice }),
1319
+ isAction && /* @__PURE__ */ jsx11("p", { className: "text-xs text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/30 rounded-lg p-2", children: labels.nodePanel.actionNotice }),
1320
+ isCondition && /* @__PURE__ */ jsx11("p", { className: "text-xs text-cyan-700 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/30 rounded-lg p-2", children: labels.nodePanel.conditionNotice }),
1321
+ /* @__PURE__ */ jsxs10("div", { children: [
1322
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.nodeName }),
1323
+ /* @__PURE__ */ jsx11(
1324
+ "input",
1325
+ {
1326
+ value: draft.label ?? "",
1327
+ placeholder: labels.nodePanel.nodeNamePlaceholder,
1328
+ onChange: (event) => setDraft((prev) => ({ ...prev, label: event.target.value || void 0 })),
1329
+ className: `w-full mt-1 ${INPUT_CLASSNAME}`
1330
+ }
1331
+ ),
1332
+ /* @__PURE__ */ jsx11("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mt-1", children: labels.nodePanel.nodeNameHint })
1333
+ ] }),
1334
+ draft.contextKey && /* @__PURE__ */ jsxs10("div", { children: [
1335
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
1336
+ /* @__PURE__ */ jsx11(
1337
+ "input",
1338
+ {
1339
+ value: draft.contextKey,
1340
+ disabled: true,
1341
+ className: "w-full mt-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-3 py-2.5 text-sm text-gray-500"
1342
+ }
1343
+ )
1344
+ ] }),
1345
+ !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs10("div", { children: [
1346
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
1347
+ /* @__PURE__ */ jsx11(
1348
+ "select",
1349
+ {
1350
+ value: draft.questionType ?? "text",
1351
+ onChange: (e) => setDraft((prev) => ({ ...prev, questionType: e.target.value })),
1352
+ className: `w-full mt-1 ${SELECT_CLASSNAME}`,
1353
+ children: Object.entries(labels.questionTypeLabels).map(([key, label]) => /* @__PURE__ */ jsx11("option", { value: key, children: label }, key))
1354
+ }
1355
+ )
1356
+ ] }),
1357
+ !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsx11(
1358
+ WhatsAppTextField,
1359
+ {
1360
+ label: labels.nodePanel.question,
1361
+ value: draft.question ?? "",
1362
+ options: draft.questionType === "choice" ? draft.options : void 0,
1363
+ onValueChange: (value) => setDraft((prev) => ({ ...prev, question: value })),
1364
+ labels
1365
+ }
1366
+ ),
1367
+ isCondition && /* @__PURE__ */ jsxs10("div", { className: "space-y-3", children: [
1368
+ /* @__PURE__ */ jsxs10("div", { children: [
1369
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionVariable }),
1370
+ /* @__PURE__ */ jsx11(
1371
+ "input",
1372
+ {
1373
+ value: draft.conditionContextKey ?? "",
1374
+ onChange: (e) => setDraft((prev) => ({ ...prev, conditionContextKey: e.target.value })),
1375
+ list: "condition-context-keys",
1376
+ className: `w-full mt-1 ${INPUT_CLASSNAME}`
1377
+ }
1378
+ ),
1379
+ /* @__PURE__ */ jsx11("datalist", { id: "condition-context-keys", children: knownContextKeys.map((key) => /* @__PURE__ */ jsx11("option", { value: key }, key)) })
1380
+ ] }),
1381
+ /* @__PURE__ */ jsxs10("div", { children: [
1382
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionOperator }),
1383
+ /* @__PURE__ */ jsx11(
1384
+ "select",
1385
+ {
1386
+ value: draft.conditionOperator ?? ">",
1387
+ onChange: (e) => setDraft((prev) => ({
1388
+ ...prev,
1389
+ conditionOperator: e.target.value
1390
+ })),
1391
+ className: `w-full mt-1 ${SELECT_CLASSNAME}`,
1392
+ children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx11("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
1393
+ }
1394
+ )
1395
+ ] }),
1396
+ /* @__PURE__ */ jsxs10("div", { children: [
1397
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionValue }),
1398
+ /* @__PURE__ */ jsx11(
1399
+ "input",
1400
+ {
1401
+ value: draft.conditionValue ?? "",
1402
+ onChange: (e) => setDraft((prev) => ({ ...prev, conditionValue: e.target.value })),
1403
+ className: `w-full mt-1 ${INPUT_CLASSNAME}`
1404
+ }
1405
+ )
1406
+ ] })
1407
+ ] }),
1408
+ isAction && draft.actionKind === "send_product_list" && /* @__PURE__ */ jsx11(
1409
+ WhatsAppTextField,
1410
+ {
1411
+ label: labels.nodePanel.fallbackMessage,
1412
+ value: draft.fallbackMessage ?? "",
1413
+ onValueChange: (value) => setDraft((prev) => ({ ...prev, fallbackMessage: value })),
1414
+ placeholder: "(usa a mensagem padr\xE3o de fallback se vazio)",
1415
+ labels
1416
+ }
1417
+ ),
1418
+ isSendMedia && /* @__PURE__ */ jsxs10("div", { children: [
1419
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.media }),
1420
+ /* @__PURE__ */ jsx11("div", { className: "mt-1", children: renderMediaPicker?.(node, graph) ?? /* @__PURE__ */ jsx11("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
1421
+ ] }),
1422
+ isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx11(
1423
+ WhatsAppTextField,
1424
+ {
1425
+ label: labels.nodePanel.directMessage,
1426
+ value: draft.directMessage ?? "",
1427
+ onValueChange: (value) => setDraft((prev) => ({ ...prev, directMessage: value })),
1428
+ placeholder: "(usa a mensagem padr\xE3o de encaminhamento se vazio)",
1429
+ labels
1430
+ }
1431
+ ),
1432
+ (draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs10("div", { children: [
1433
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between mb-1.5", children: [
1434
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
1435
+ /* @__PURE__ */ jsxs10("button", { "data-cv-tooltip": labels.nodePanel.addOption, "aria-label": labels.nodePanel.addOption, onClick: addOption, className: "text-xs text-blue-600 hover:underline flex items-center gap-1", children: [
1436
+ /* @__PURE__ */ jsx11(Plus3, { size: 12 }),
1437
+ " ",
1438
+ labels.nodePanel.addOption
1439
+ ] })
1440
+ ] }),
1441
+ /* @__PURE__ */ jsx11("div", { className: "space-y-2", children: (draft.options ?? []).map(([id, label], i) => /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
1442
+ /* @__PURE__ */ jsx11(
1443
+ "input",
1444
+ {
1445
+ value: id,
1446
+ onChange: (e) => updateOption(i, 0, e.target.value),
1447
+ placeholder: labels.nodePanel.optionId,
1448
+ className: `w-16 ${INPUT_CLASSNAME}`
1449
+ }
1450
+ ),
1451
+ /* @__PURE__ */ jsx11(
1452
+ "input",
1453
+ {
1454
+ value: label,
1455
+ onChange: (e) => updateOption(i, 1, e.target.value),
1456
+ placeholder: labels.nodePanel.optionLabel,
1457
+ className: `flex-1 ${INPUT_CLASSNAME}`
1458
+ }
1459
+ ),
1460
+ /* @__PURE__ */ jsx11("button", { "data-cv-tooltip": labels.nodePanel.removeOption, "aria-label": labels.nodePanel.removeOption, onClick: () => removeOption(i), className: "text-gray-400 hover:text-red-600", children: /* @__PURE__ */ jsx11(Trash2, { size: 14 }) })
1461
+ ] }, i)) })
1462
+ ] }),
1463
+ (!isAction || isPassThroughAction) && /* @__PURE__ */ jsxs10("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
1464
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.next }),
1465
+ /* @__PURE__ */ jsx11("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mb-1", children: labels.nodePanel.nextHint }),
1466
+ typeof draft.next !== "object" && !isCondition ? /* @__PURE__ */ jsxs10(
1467
+ "select",
669
1468
  {
670
- onMouseEnter: () => setSubmenu("question"),
671
- onClick: () => setSubmenu(submenu === "question" ? null : "question"),
672
- className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
1469
+ value: typeof draft.next === "string" ? draft.next : "",
1470
+ onChange: (e) => updateNextString(e.target.value),
1471
+ className: `w-full mt-1 ${SELECT_CLASSNAME}`,
673
1472
  children: [
674
- /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
675
- /* @__PURE__ */ jsx7(MessageCircleQuestion2, { size: 15, className: "text-blue-500" }),
676
- " ",
677
- labels.palette.question
678
- ] }),
679
- /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
1473
+ /* @__PURE__ */ jsx11("option", { value: "", children: "\u2014" }),
1474
+ nextNodeOptions()
680
1475
  ]
681
1476
  }
682
- ),
683
- submenu === "question" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: QUESTION_TYPES.map((qt) => /* @__PURE__ */ jsx7(
684
- "button",
685
- {
686
- onClick: () => select({ kind: "question", questionType: qt }),
687
- className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
688
- children: labels.questionTypeLabels[qt]
689
- },
690
- qt
691
- )) })
692
- ] }),
693
- /* @__PURE__ */ jsxs6(
1477
+ ) : /* @__PURE__ */ jsxs10("div", { className: "space-y-2 mt-1", children: [
1478
+ conditionAnswerIds.map((id) => /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
1479
+ /* @__PURE__ */ jsx11("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? id === "true" ? labels.nodePanel.conditionTrue : labels.nodePanel.conditionFalse : labels.nodePanel.nextByAnswer(id) }),
1480
+ /* @__PURE__ */ jsxs10(
1481
+ "select",
1482
+ {
1483
+ value: draft.next && typeof draft.next === "object" ? draft.next.byAnswer[id] ?? "" : "",
1484
+ onChange: (e) => updateNextByAnswer(id, e.target.value),
1485
+ className: `flex-1 ${SELECT_CLASSNAME}`,
1486
+ children: [
1487
+ /* @__PURE__ */ jsx11("option", { value: "", children: "\u2014" }),
1488
+ nextNodeOptions()
1489
+ ]
1490
+ }
1491
+ )
1492
+ ] }, id)),
1493
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
1494
+ /* @__PURE__ */ jsx11("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault }),
1495
+ /* @__PURE__ */ jsxs10(
1496
+ "select",
1497
+ {
1498
+ value: draft.next && typeof draft.next === "object" ? draft.next.default : "",
1499
+ onChange: (e) => updateNextDefault(e.target.value),
1500
+ className: `flex-1 ${SELECT_CLASSNAME}`,
1501
+ children: [
1502
+ /* @__PURE__ */ jsx11("option", { value: "", children: "\u2014" }),
1503
+ nextNodeOptions()
1504
+ ]
1505
+ }
1506
+ )
1507
+ ] })
1508
+ ] })
1509
+ ] })
1510
+ ] }),
1511
+ /* @__PURE__ */ jsxs10("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
1512
+ /* @__PURE__ */ jsxs10(
694
1513
  "button",
695
1514
  {
696
- onMouseEnter: () => setSubmenu(null),
697
- onClick: () => select({ kind: "decision" }),
698
- className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
1515
+ "data-cv-tooltip": labels.nodePanel.save,
1516
+ "aria-label": labels.nodePanel.save,
1517
+ onClick: () => onChange(draft),
1518
+ className: "flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
699
1519
  children: [
700
- /* @__PURE__ */ jsx7(GitBranch3, { size: 15, className: "text-purple-500" }),
1520
+ /* @__PURE__ */ jsx11(Save, { size: 14 }),
701
1521
  " ",
702
- labels.palette.decision
1522
+ labels.nodePanel.save
703
1523
  ]
704
1524
  }
705
1525
  ),
706
- /* @__PURE__ */ jsxs6(
1526
+ !isStart && /* @__PURE__ */ jsx11(
707
1527
  "button",
708
1528
  {
709
- onMouseEnter: () => setSubmenu(null),
710
- onClick: () => select({ kind: "condition" }),
711
- className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
712
- title: labels.palette.conditionHint,
713
- children: [
714
- /* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
715
- " ",
716
- labels.palette.condition
717
- ]
1529
+ onClick: () => {
1530
+ if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id);
1531
+ },
1532
+ "data-cv-tooltip": labels.nodePanel.delete,
1533
+ "aria-label": labels.nodePanel.delete,
1534
+ className: "px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg",
1535
+ children: /* @__PURE__ */ jsx11(Trash2, { size: 14 })
718
1536
  }
719
1537
  ),
720
- /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
721
- /* @__PURE__ */ jsxs6(
722
- "button",
723
- {
724
- onMouseEnter: () => setSubmenu("action"),
725
- onClick: () => setSubmenu(submenu === "action" ? null : "action"),
726
- className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
727
- children: [
728
- /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
729
- /* @__PURE__ */ jsx7(Zap2, { size: 15, className: "text-orange-500" }),
730
- " ",
731
- labels.palette.action
732
- ] }),
733
- /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
734
- ]
735
- }
736
- ),
737
- submenu === "action" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: resolvedActionOptions.map((option) => /* @__PURE__ */ jsx7(
738
- "button",
739
- {
740
- onClick: () => select({ kind: "action", actionKind: option.actionKind }),
741
- className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
742
- children: option.label
743
- },
744
- option.actionKind
745
- )) })
746
- ] })
1538
+ /* @__PURE__ */ jsx11("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
747
1539
  ] })
748
1540
  ] });
749
1541
  }
750
1542
 
751
- // src/flows/FlowNodePanel.tsx
752
- import { useState as useState2 } from "react";
753
- import { Plus as Plus2, Trash2, Save, X as X2, AlertTriangle as AlertTriangle2, AlertCircle as AlertCircle2 } from "lucide-react";
1543
+ // src/flows/FlowsWorkspace.tsx
1544
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState6 } from "react";
1545
+ import {
1546
+ ReactFlow as ReactFlow2,
1547
+ Background as Background2,
1548
+ Controls as Controls2,
1549
+ MiniMap,
1550
+ MarkerType as MarkerType2,
1551
+ Panel,
1552
+ applyNodeChanges
1553
+ } from "@xyflow/react";
1554
+ import "@xyflow/react/dist/style.css";
1555
+ import { Plus as Plus4, Trash2 as Trash22, LayoutGrid, AlertTriangle as AlertTriangle3, AlertCircle as AlertCircle3, Save as Save2, Undo2, Map as MapIcon, Workflow } from "lucide-react";
754
1556
 
755
- // src/flows/FlowWhatsAppPreview.tsx
756
- import { List } from "lucide-react";
757
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
758
- function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel }) {
759
- if (!body && (!options || options.length === 0)) {
760
- return /* @__PURE__ */ jsx8("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
1557
+ // src/flows/flowEditorOps.ts
1558
+ var NAMESPACE_SEPARATOR = "::";
1559
+ function namespaceNodeId(flowKey, nodeId) {
1560
+ return `${flowKey}${NAMESPACE_SEPARATOR}${nodeId}`;
1561
+ }
1562
+ function parseNamespacedId(value) {
1563
+ const index = value.indexOf(NAMESPACE_SEPARATOR);
1564
+ if (index === -1) return { flowKey: "", nodeId: value };
1565
+ return { flowKey: value.slice(0, index), nodeId: value.slice(index + NAMESPACE_SEPARATOR.length) };
1566
+ }
1567
+ function removeNodeAndCleanRefs(nodes, removedId) {
1568
+ const remaining = {};
1569
+ for (const [id, node] of Object.entries(nodes)) {
1570
+ if (id === removedId) continue;
1571
+ remaining[id] = { ...node, next: cleanNext(node.next, removedId) };
761
1572
  }
762
- const usesButtons = rendersAsButtons(options);
763
- const hasOptions = (options?.length ?? 0) > 0;
764
- return /* @__PURE__ */ jsxs7("div", { className: "rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5", children: [
765
- /* @__PURE__ */ jsxs7("div", { className: "max-w-[85%]", children: [
766
- /* @__PURE__ */ jsxs7("div", { className: "rounded-lg rounded-tl-none bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm whitespace-pre-wrap break-words", children: [
767
- body ? parseWhatsAppFormatting(body) : /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyBody }),
768
- hasOptions && !usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-2 -mx-3 -mb-2 border-t border-gray-100 dark:border-gray-600", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-center gap-1.5 py-2 text-sm font-medium text-cyan-600 dark:text-cyan-400", children: [
769
- /* @__PURE__ */ jsx8(List, { size: 15 }),
770
- " ",
771
- labels.previewListButton
772
- ] }) })
773
- ] }),
774
- hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8("div", { className: "rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm", children: label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption }) }, id)) }),
775
- hasOptions && !usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1.5 rounded-lg bg-white dark:bg-gray-700 shadow-sm divide-y divide-gray-100 dark:divide-gray-600 overflow-hidden", children: options.slice(0, WHATSAPP_LIMITS.MAX_LIST_ROWS).map(([id, label]) => /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 px-3 py-1.5 text-sm text-gray-800 dark:text-gray-100", children: [
776
- /* @__PURE__ */ jsx8("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
777
- label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
778
- ] }, id)) })
779
- ] }),
780
- hasOptions && /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-gray-500 dark:text-gray-400 px-1", children: usesButtons ? labels.previewModeButtons : labels.previewModeList })
781
- ] });
1573
+ return remaining;
1574
+ }
1575
+ function cleanNext(next, removedId) {
1576
+ if (next === void 0) return void 0;
1577
+ if (typeof next === "string") return next === removedId ? "" : next;
1578
+ const byAnswer = {};
1579
+ for (const [answer, target] of Object.entries(next.byAnswer ?? {})) {
1580
+ byAnswer[answer] = target === removedId ? "" : target;
1581
+ }
1582
+ return { byAnswer, default: next.default === removedId ? "" : next.default ?? "" };
1583
+ }
1584
+ function mergedFlowKeysFrom(rootKey, graphs) {
1585
+ const visited = /* @__PURE__ */ new Set();
1586
+ const queue = [rootKey];
1587
+ while (queue.length > 0) {
1588
+ const key = queue.shift();
1589
+ if (visited.has(key) || !graphs[key]) continue;
1590
+ visited.add(key);
1591
+ for (const target of crossFlowTargetsOf(graphs[key])) {
1592
+ if (!visited.has(target) && graphs[target]) queue.push(target);
1593
+ }
1594
+ }
1595
+ return [...visited];
1596
+ }
1597
+ function resolveConnection(params) {
1598
+ const { source, target, sourceHandle } = params.connection;
1599
+ if (!source || !target || source === target) return void 0;
1600
+ const sourceRef = parseNamespacedId(source);
1601
+ const targetRef = parseNamespacedId(target);
1602
+ let targetValue;
1603
+ if (targetRef.flowKey === sourceRef.flowKey) {
1604
+ targetValue = targetRef.nodeId;
1605
+ } else {
1606
+ const targetGraph = params.graphs[targetRef.flowKey];
1607
+ if (!targetGraph || targetGraph.startNodeId !== targetRef.nodeId) return void 0;
1608
+ targetValue = `${CROSS_FLOW_PREFIX}${targetRef.flowKey}`;
1609
+ }
1610
+ return {
1611
+ flowKey: sourceRef.flowKey,
1612
+ nodeId: sourceRef.nodeId,
1613
+ handle: sourceHandle ?? "next",
1614
+ targetValue
1615
+ };
1616
+ }
1617
+ function applyConnection(node, resolved) {
1618
+ const currentNext = typeof node.next === "object" && node.next ? node.next : void 0;
1619
+ if (resolved.handle === "next") return { ...node, next: resolved.targetValue };
1620
+ if (resolved.handle === "__default") {
1621
+ return { ...node, next: { byAnswer: currentNext?.byAnswer ?? {}, default: resolved.targetValue } };
1622
+ }
1623
+ return {
1624
+ ...node,
1625
+ next: {
1626
+ byAnswer: { ...currentNext?.byAnswer ?? {}, [resolved.handle]: resolved.targetValue },
1627
+ default: currentNext?.default ?? ""
1628
+ }
1629
+ };
1630
+ }
1631
+ function isGraphDirty(working, published) {
1632
+ if (!working || !published) return false;
1633
+ return JSON.stringify(working) !== JSON.stringify(published);
1634
+ }
1635
+ function clearConnection(node, handle) {
1636
+ if (handle === "next" || typeof node.next === "string" || node.next === void 0) {
1637
+ return { ...node, next: "" };
1638
+ }
1639
+ if (handle === "__default") {
1640
+ return { ...node, next: { byAnswer: node.next.byAnswer ?? {}, default: "" } };
1641
+ }
1642
+ return {
1643
+ ...node,
1644
+ next: {
1645
+ byAnswer: { ...node.next.byAnswer ?? {}, [handle]: "" },
1646
+ default: node.next.default ?? ""
1647
+ }
1648
+ };
782
1649
  }
783
1650
 
784
- // src/flows/FlowNodePanel.tsx
785
- import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
786
- var SELECT_CLASSNAME = "border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all";
787
- var INPUT_CLASSNAME = "border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all";
788
- function truncateLabel(value, max = 60) {
789
- if (!value) return "\u2014";
790
- return value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
1651
+ // src/flows/flowCanvasModel.ts
1652
+ var COLUMN_GAP = 360;
1653
+ var ROW_GAP = 40;
1654
+ var CHAIN_LABEL_ROOM = 24;
1655
+ function isNodeCount(position) {
1656
+ return typeof position.count === "number";
791
1657
  }
792
- function WhatsAppTextField({
793
- label,
794
- value,
795
- onValueChange,
796
- options,
797
- placeholder,
798
- labels
1658
+ function portalNodeId(sourceNodeId, target) {
1659
+ return `__portal__${sourceNodeId}__${target}`;
1660
+ }
1661
+ function chainFrameNodeId(actionNodeId) {
1662
+ return `__chain__${actionNodeId}`;
1663
+ }
1664
+ var GROUP_HEADER_NODE_ID = "__group_header__";
1665
+ function countLiveByNode(params) {
1666
+ const { flowKey, rootFlowKey, positions } = params;
1667
+ const isRoot = flowKey === rootFlowKey;
1668
+ const counts = {};
1669
+ for (const position of positions ?? []) {
1670
+ if (isNodeCount(position)) {
1671
+ if (position.flowKey !== flowKey) continue;
1672
+ counts[position.nodeId] = (counts[position.nodeId] ?? 0) + position.count;
1673
+ continue;
1674
+ }
1675
+ if (position.flow !== flowKey && !isRoot) continue;
1676
+ const nodeId = isRoot ? position.menuNodeId : position.nodeId;
1677
+ if (!nodeId) continue;
1678
+ counts[nodeId] = (counts[nodeId] ?? 0) + 1;
1679
+ }
1680
+ return counts;
1681
+ }
1682
+ function computeMergedLayout(params) {
1683
+ const { openKeys, graphs, primaryFlowKey } = params;
1684
+ const open = new Set(openKeys);
1685
+ const nodeById = /* @__PURE__ */ new Map();
1686
+ for (const key of openKeys) {
1687
+ for (const node of Object.values(graphs[key]?.nodes ?? {})) {
1688
+ nodeById.set(namespaceNodeId(key, node.id), node);
1689
+ }
1690
+ }
1691
+ function forwardEdges(id) {
1692
+ const node = nodeById.get(id);
1693
+ if (!node) return [];
1694
+ const flowKey = parseNamespacedId(id).flowKey;
1695
+ const result = [];
1696
+ for (const { target } of targetsOf(node)) {
1697
+ if (isCrossFlowTarget(target)) {
1698
+ const targetKey = crossFlowKey(target);
1699
+ const targetGraph = open.has(targetKey) ? graphs[targetKey] : void 0;
1700
+ if (targetGraph) result.push(namespaceNodeId(targetKey, targetGraph.startNodeId));
1701
+ } else if (graphs[flowKey]?.nodes[target]) {
1702
+ result.push(namespaceNodeId(flowKey, target));
1703
+ }
1704
+ }
1705
+ return result;
1706
+ }
1707
+ const primaryGraph = graphs[primaryFlowKey];
1708
+ const placed = cascadeOrder({
1709
+ rootId: primaryGraph ? namespaceNodeId(primaryFlowKey, primaryGraph.startNodeId) : "",
1710
+ allIds: [...nodeById.keys()],
1711
+ forwardEdges
1712
+ });
1713
+ const positions = /* @__PURE__ */ new Map();
1714
+ let cursorY = 0;
1715
+ for (const [id, { depth }] of [...placed.entries()].sort((a, b) => a[1].order - b[1].order)) {
1716
+ positions.set(id, { x: depth * COLUMN_GAP, y: cursorY });
1717
+ cursorY += estimateNodeHeight(nodeById.get(id)) + ROW_GAP;
1718
+ }
1719
+ return positions;
1720
+ }
1721
+ function buildFlowEdges(params) {
1722
+ const { openKeys, graphs, rootFlowKey, livePositions } = params;
1723
+ const open = new Set(openKeys);
1724
+ const edges = [];
1725
+ for (const flowKey of openKeys) {
1726
+ const graph = graphs[flowKey];
1727
+ if (!graph) continue;
1728
+ const liveCounts = countLiveByNode({ flowKey, rootFlowKey, positions: livePositions });
1729
+ for (const [nodeId, node] of Object.entries(graph.nodes)) {
1730
+ const live = (liveCounts[nodeId] ?? 0) > 0;
1731
+ for (const { target, optionId, isDefault } of targetsOf(node)) {
1732
+ if (!target) continue;
1733
+ const crossFlow = isCrossFlowTarget(target);
1734
+ let targetFlowKey = flowKey;
1735
+ let targetNodeId = target;
1736
+ if (crossFlow) {
1737
+ const wantedKey = crossFlowKey(target);
1738
+ const targetGraph = open.has(wantedKey) ? graphs[wantedKey] : void 0;
1739
+ if (targetGraph) {
1740
+ targetFlowKey = wantedKey;
1741
+ targetNodeId = targetGraph.startNodeId;
1742
+ } else {
1743
+ targetNodeId = portalNodeId(nodeId, target);
1744
+ }
1745
+ }
1746
+ const source = namespaceNodeId(flowKey, nodeId);
1747
+ const edgeTarget = namespaceNodeId(targetFlowKey, targetNodeId);
1748
+ if (source === edgeTarget) continue;
1749
+ if (isDefault) {
1750
+ edges.push({
1751
+ id: `${source}->${edgeTarget}-default`,
1752
+ source,
1753
+ target: edgeTarget,
1754
+ sourceHandle: "__default",
1755
+ kind: "fallback",
1756
+ crossFlow,
1757
+ // Fallback não anima: é o caminho que ninguém escolheu, e piscar sugeria que a conversa
1758
+ // estava passando por ali.
1759
+ live: false
1760
+ });
1761
+ continue;
1762
+ }
1763
+ if (optionId === void 0) {
1764
+ edges.push({ id: `${source}->${edgeTarget}`, source, target: edgeTarget, kind: "linear", crossFlow, live });
1765
+ continue;
1766
+ }
1767
+ edges.push({
1768
+ id: `${source}->${edgeTarget}-${optionId}`,
1769
+ source,
1770
+ target: edgeTarget,
1771
+ sourceHandle: optionId,
1772
+ kind: "branch",
1773
+ crossFlow,
1774
+ live
1775
+ });
1776
+ }
1777
+ }
1778
+ }
1779
+ return edges;
1780
+ }
1781
+ function detachedNodeIds(graph) {
1782
+ const connected = /* @__PURE__ */ new Set();
1783
+ for (const node of Object.values(graph.nodes)) {
1784
+ for (const { target } of targetsOf(node)) {
1785
+ if (!isCrossFlowTarget(target)) connected.add(target);
1786
+ }
1787
+ }
1788
+ const detached = /* @__PURE__ */ new Set();
1789
+ for (const nodeId of Object.keys(graph.nodes)) {
1790
+ if (nodeId !== graph.startNodeId && !connected.has(nodeId)) detached.add(nodeId);
1791
+ }
1792
+ return detached;
1793
+ }
1794
+ function chainFrameBounds(params) {
1795
+ const { nodeIds, graph, positionOf, padding } = params;
1796
+ const positions = nodeIds.map(positionOf);
1797
+ const minX = Math.min(...positions.map((each) => each.x));
1798
+ const maxX = Math.max(...positions.map((each) => each.x)) + NODE_CARD_WIDTH;
1799
+ const minY = Math.min(...positions.map((each) => each.y));
1800
+ const maxY = Math.max(...nodeIds.map((id, index) => positions[index].y + estimateNodeHeight(graph.nodes[id])));
1801
+ return {
1802
+ x: minX - padding,
1803
+ y: minY - padding - CHAIN_LABEL_ROOM,
1804
+ width: maxX - minX + padding * 2,
1805
+ height: maxY - minY + padding * 2 + CHAIN_LABEL_ROOM
1806
+ };
1807
+ }
1808
+ function newNodeFromSpec(spec, existingIds) {
1809
+ if (spec.kind === "question") {
1810
+ const id2 = slugifyNodeId("nova_pergunta", existingIds);
1811
+ return { id: id2, type: "question", questionType: spec.questionType, contextKey: id2, question: "" };
1812
+ }
1813
+ if (spec.kind === "decision") {
1814
+ const id2 = slugifyNodeId("nova_decisao", existingIds);
1815
+ return {
1816
+ id: id2,
1817
+ type: "question",
1818
+ questionType: "choice",
1819
+ contextKey: id2,
1820
+ question: "",
1821
+ options: [
1822
+ ["1", "Op\xE7\xE3o 1"],
1823
+ ["2", "Op\xE7\xE3o 2"]
1824
+ ]
1825
+ };
1826
+ }
1827
+ if (spec.kind === "condition") {
1828
+ const id2 = slugifyNodeId("nova_condicao", existingIds);
1829
+ return { id: id2, type: "condition", conditionOperator: ">" };
1830
+ }
1831
+ const id = slugifyNodeId("nova_acao", existingIds);
1832
+ return { id, type: "action", actionKind: spec.actionKind };
1833
+ }
1834
+ var FREE_SLOT_STEP = 120;
1835
+ function findFreeSlot(params) {
1836
+ const step = params.step ?? FREE_SLOT_STEP;
1837
+ const isOccupied = (candidate) => params.taken.some(
1838
+ (each) => Math.abs(each.x - candidate.x) < NODE_CARD_WIDTH && Math.abs(each.y - candidate.y) < step
1839
+ );
1840
+ let slot = params.desired;
1841
+ for (let attempt = 0; attempt <= params.taken.length && isOccupied(slot); attempt += 1) {
1842
+ slot = { x: slot.x, y: slot.y + step };
1843
+ }
1844
+ return slot;
1845
+ }
1846
+
1847
+ // src/flows/FlowsWorkspace.tsx
1848
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1849
+ var RF_NODE_TYPES = {
1850
+ ...flowNodeTypes,
1851
+ ...flowPortalNodeTypes,
1852
+ ...flowGroupHeaderNodeTypes,
1853
+ ...flowGroupFrameNodeTypes
1854
+ };
1855
+ function legendEdgeSamples(labels) {
1856
+ return [
1857
+ { color: EDGE_COLOR_LINEAR, label: labels.legendPanel.linear },
1858
+ { color: EDGE_COLOR_BRANCH, label: labels.legendPanel.branch },
1859
+ { color: EDGE_COLOR_FALLBACK, dash: "5 4", label: labels.legendPanel.fallback },
1860
+ { color: EDGE_COLOR_CROSS_FLOW, dash: "3 3", label: labels.legendPanel.crossFlow },
1861
+ { color: EDGE_COLOR_LIVE, label: labels.legendPanel.live }
1862
+ ];
1863
+ }
1864
+ var LEGEND_NODE_SWATCHES = Object.keys(NODE_TYPE_COLOR).map((type) => ({
1865
+ type,
1866
+ className: NODE_TYPE_COLOR[type]
1867
+ }));
1868
+ var CHAIN_FRAME_PADDING = 36;
1869
+ var QUICK_ADD_COLUMN_GAP = 320;
1870
+ var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
1871
+ var EDGE_COLOR_LINEAR = "#94a3b8";
1872
+ var EDGE_COLOR_BRANCH = "#8b5cf6";
1873
+ var EDGE_COLOR_FALLBACK = "#cbd5e1";
1874
+ var EDGE_COLOR_LIVE = "#3b82f6";
1875
+ var EDGE_COLOR_CROSS_FLOW = "#06b6d4";
1876
+ var BACKGROUND_COLOR_LIGHT2 = "#cbd5e1";
1877
+ var BACKGROUND_COLOR_DARK2 = "#334155";
1878
+ function styleEdge(spec, params) {
1879
+ const color = spec.crossFlow ? EDGE_COLOR_CROSS_FLOW : spec.kind === "fallback" ? EDGE_COLOR_FALLBACK : spec.live ? EDGE_COLOR_LIVE : spec.kind === "branch" ? EDGE_COLOR_BRANCH : EDGE_COLOR_LINEAR;
1880
+ const baseWidth = spec.kind === "branch" ? 1.75 : 1.5;
1881
+ const dash = spec.crossFlow ? "3 3" : spec.kind === "fallback" ? "5 4" : void 0;
1882
+ const markerSize = spec.kind === "fallback" ? 16 : 18;
1883
+ return {
1884
+ id: spec.id,
1885
+ source: spec.source,
1886
+ target: spec.target,
1887
+ ...spec.sourceHandle === void 0 ? {} : { sourceHandle: spec.sourceHandle },
1888
+ type: "flowConnection",
1889
+ reconnectable: "target",
1890
+ data: {
1891
+ disconnectLabel: params.disconnectLabel,
1892
+ onDisconnect: () => params.onDisconnect(spec)
1893
+ },
1894
+ animated: spec.live,
1895
+ style: {
1896
+ stroke: color,
1897
+ strokeWidth: spec.live && !spec.crossFlow ? 2.5 : baseWidth,
1898
+ ...dash === void 0 ? {} : { strokeDasharray: dash }
1899
+ },
1900
+ markerEnd: { type: MarkerType2.ArrowClosed, color, width: markerSize, height: markerSize }
1901
+ };
1902
+ }
1903
+ function extractErrorMessage(error) {
1904
+ if (error instanceof Error) return error.message;
1905
+ return void 0;
1906
+ }
1907
+ var OUTLINE_BUTTON = "inline-flex items-center gap-1.5 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300 hover:border-blue-300 disabled:opacity-40 disabled:cursor-not-allowed";
1908
+ var PRIMARY_BUTTON = "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed";
1909
+ var DIALOG_INPUT = "w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400";
1910
+ function FlowsWorkspace({
1911
+ api,
1912
+ rootFlowKey = "menu",
1913
+ labels: labelsOverride,
1914
+ actionOptions,
1915
+ renderMediaPicker,
1916
+ livePollIntervalMs = 5e3,
1917
+ showHeader = true,
1918
+ className
799
1919
  }) {
800
- return /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
801
- /* @__PURE__ */ jsxs8("div", { children: [
802
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: label }),
803
- /* @__PURE__ */ jsx9(
804
- "textarea",
805
- {
806
- value,
807
- onChange: (e) => onValueChange(e.target.value),
808
- rows: 3,
809
- placeholder,
810
- className: `w-full mt-1 ${INPUT_CLASSNAME}`
1920
+ const labels = useMemo2(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride]);
1921
+ const isDark = useIsDarkTheme();
1922
+ const [graphs, setGraphs] = useState6(void 0);
1923
+ const [loadState, setLoadState] = useState6("loading");
1924
+ const [livePositions, setLivePositions] = useState6(void 0);
1925
+ const [viewMode, setViewMode] = useState6("detail");
1926
+ const [openFlowKeys, setOpenFlowKeys] = useState6([rootFlowKey]);
1927
+ const [hasAutoMerged, setHasAutoMerged] = useState6(false);
1928
+ const [workingGraphs, setWorkingGraphs] = useState6({});
1929
+ const [editingRef, setEditingRef] = useState6(null);
1930
+ const [saveState, setSaveState] = useState6("idle");
1931
+ const [saveErrorMessage, setSaveErrorMessage] = useState6(void 0);
1932
+ const [showCreateDialog, setShowCreateDialog] = useState6(false);
1933
+ const [showDeleteDialog, setShowDeleteDialog] = useState6(false);
1934
+ const [quickAddFrom, setQuickAddFrom] = useState6(null);
1935
+ const [newFlow, setNewFlow] = useState6({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
1936
+ const [flowMutationState, setFlowMutationState] = useState6({ pending: false });
1937
+ const [rfNodes, setRfNodes] = useState6([]);
1938
+ const [flowInstance, setFlowInstance] = useState6(null);
1939
+ const [pendingFocusNodeId, setPendingFocusNodeId] = useState6(null);
1940
+ const reloadGraphs = useCallback(async () => {
1941
+ try {
1942
+ const loaded = await api.getGraphs();
1943
+ setGraphs(loaded);
1944
+ setLoadState("ready");
1945
+ return loaded;
1946
+ } catch {
1947
+ setLoadState("error");
1948
+ return void 0;
1949
+ }
1950
+ }, [api]);
1951
+ useEffect3(() => {
1952
+ void reloadGraphs();
1953
+ }, [reloadGraphs]);
1954
+ useEffect3(() => {
1955
+ const fetchLive = api.getLivePositions;
1956
+ if (!fetchLive) return;
1957
+ let active = true;
1958
+ async function poll() {
1959
+ try {
1960
+ const positions = await fetchLive();
1961
+ if (active) setLivePositions(positions);
1962
+ } catch {
1963
+ }
1964
+ }
1965
+ void poll();
1966
+ const timer = setInterval(() => void poll(), livePollIntervalMs);
1967
+ return () => {
1968
+ active = false;
1969
+ clearInterval(timer);
1970
+ };
1971
+ }, [api, livePollIntervalMs]);
1972
+ const primaryFlowKey = openFlowKeys[0] ?? rootFlowKey;
1973
+ const primaryGraph = workingGraphs[primaryFlowKey];
1974
+ useEffect3(() => {
1975
+ if (!graphs || hasAutoMerged) return;
1976
+ setOpenFlowKeys(mergedFlowKeysFrom(rootFlowKey, graphs));
1977
+ setHasAutoMerged(true);
1978
+ }, [graphs, hasAutoMerged, rootFlowKey]);
1979
+ useEffect3(() => {
1980
+ if (!graphs) return;
1981
+ setWorkingGraphs((prev) => {
1982
+ let changed = false;
1983
+ const next = { ...prev };
1984
+ for (const key of openFlowKeys) {
1985
+ if (!next[key] && graphs[key]) {
1986
+ next[key] = graphs[key];
1987
+ changed = true;
1988
+ }
1989
+ }
1990
+ return changed ? next : prev;
1991
+ });
1992
+ }, [graphs, openFlowKeys]);
1993
+ const isFlowDirty = useCallback(
1994
+ (key) => {
1995
+ const working = workingGraphs[key];
1996
+ const server = graphs?.[key];
1997
+ if (!working || !server) return false;
1998
+ return JSON.stringify(working) !== JSON.stringify(server);
1999
+ },
2000
+ [workingGraphs, graphs]
2001
+ );
2002
+ const issuesByFlow = useMemo2(() => {
2003
+ const map = {};
2004
+ for (const key of openFlowKeys) {
2005
+ const graph = workingGraphs[key];
2006
+ if (graph) map[key] = validateGraph(graph, labels.validation);
2007
+ }
2008
+ return map;
2009
+ }, [openFlowKeys, workingGraphs, labels]);
2010
+ const errorCount = Object.values(issuesByFlow).reduce(
2011
+ (sum, list) => sum + list.filter((issue) => issue.severity === "error").length,
2012
+ 0
2013
+ );
2014
+ const warningCount = Object.values(issuesByFlow).reduce(
2015
+ (sum, list) => sum + list.filter((issue) => issue.severity === "warning").length,
2016
+ 0
2017
+ );
2018
+ const dirtyKeys = openFlowKeys.filter(isFlowDirty);
2019
+ const isDirty = dirtyKeys.length > 0;
2020
+ const updateFlow = useCallback((flowKey, updater) => {
2021
+ setWorkingGraphs((prev) => {
2022
+ const graph = prev[flowKey];
2023
+ if (!graph) return prev;
2024
+ return { ...prev, [flowKey]: updater(graph) };
2025
+ });
2026
+ }, []);
2027
+ const focusFlow = useCallback(
2028
+ (key) => {
2029
+ const others = openFlowKeys.filter((each) => each !== key);
2030
+ if (others.some(isFlowDirty) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
2031
+ setOpenFlowKeys(graphs ? mergedFlowKeysFrom(key, graphs) : [key]);
2032
+ if (editingRef && editingRef.flowKey !== key) setEditingRef(null);
2033
+ },
2034
+ [openFlowKeys, isFlowDirty, editingRef, graphs, labels]
2035
+ );
2036
+ const closeFlow = useCallback(
2037
+ (key) => {
2038
+ if (isFlowDirty(key) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
2039
+ setOpenFlowKeys((prev) => prev.filter((each) => each !== key));
2040
+ setWorkingGraphs((prev) => {
2041
+ const { [key]: _removed, ...rest } = prev;
2042
+ return rest;
2043
+ });
2044
+ if (editingRef?.flowKey === key) setEditingRef(null);
2045
+ },
2046
+ [isFlowDirty, editingRef, labels]
2047
+ );
2048
+ const mergeFlow = useCallback((targetFlowKey) => {
2049
+ setOpenFlowKeys((prev) => prev.includes(targetFlowKey) ? prev : [...prev, targetFlowKey]);
2050
+ }, []);
2051
+ const isMerged = openFlowKeys.length > 1;
2052
+ const mergedPositions = useMemo2(
2053
+ () => isMerged ? computeMergedLayout({ openKeys: openFlowKeys, graphs: workingGraphs, primaryFlowKey }) : null,
2054
+ [isMerged, openFlowKeys, workingGraphs, primaryFlowKey]
2055
+ );
2056
+ const renderedPositionsRef = useRef3(/* @__PURE__ */ new Map());
2057
+ const derivedNodes = useMemo2(() => {
2058
+ const allNodes = [];
2059
+ for (const flowKey of openFlowKeys) {
2060
+ let resolvePosition2 = function(nodeId) {
2061
+ if (mergedPositions) {
2062
+ const nsId2 = namespaceNodeId(flowKey, nodeId);
2063
+ return renderedPositionsRef.current.get(nsId2) ?? mergedPositions.get(nsId2) ?? { x: 0, y: 0 };
811
2064
  }
2065
+ const nsId = namespaceNodeId(flowKey, nodeId);
2066
+ return graph.nodes[nodeId]?.position ?? renderedPositionsRef.current.get(nsId) ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
2067
+ };
2068
+ var resolvePosition = resolvePosition2;
2069
+ const graph = workingGraphs[flowKey];
2070
+ if (!graph) continue;
2071
+ const fallbackPositions = mergedPositions ? {} : computeAutoLayout(graph);
2072
+ const liveCounts = countLiveByNode({ flowKey, rootFlowKey, positions: livePositions });
2073
+ const flowIssues = issuesByFlow[flowKey] ?? [];
2074
+ const isPrimary = flowKey === primaryFlowKey;
2075
+ const connectedTargets = /* @__PURE__ */ new Set();
2076
+ for (const candidate of Object.values(graph.nodes)) {
2077
+ for (const { target } of targetsOf(candidate)) {
2078
+ if (!isCrossFlowTarget(target)) connectedTargets.add(target);
2079
+ }
2080
+ }
2081
+ for (const node of Object.values(graph.nodes)) {
2082
+ const position = resolvePosition2(node.id);
2083
+ allNodes.push({
2084
+ id: namespaceNodeId(flowKey, node.id),
2085
+ type: "flowNode",
2086
+ position,
2087
+ draggable: true,
2088
+ data: {
2089
+ node,
2090
+ liveCount: liveCounts[node.id] ?? 0,
2091
+ isStart: node.id === graph.startNodeId,
2092
+ isSelected: editingRef?.flowKey === flowKey && editingRef?.nodeId === node.id,
2093
+ isDetached: node.id !== graph.startNodeId && !connectedTargets.has(node.id),
2094
+ issues: flowIssues,
2095
+ labels,
2096
+ onSelect: (nodeId) => setEditingRef({ flowKey, nodeId }),
2097
+ onQuickAdd: ({ nodeId, handle, anchor }) => setQuickAddFrom({ flowKey, nodeId, handle, anchor })
2098
+ }
2099
+ });
2100
+ const crossFlowTargets = [...new Set(targetsOf(node).map((edge) => edge.target).filter(isCrossFlowTarget))];
2101
+ crossFlowTargets.forEach((target, index) => {
2102
+ const targetFlowKey = crossFlowKey(target);
2103
+ if (openFlowKeys.includes(targetFlowKey)) return;
2104
+ allNodes.push({
2105
+ id: namespaceNodeId(flowKey, portalNodeId(node.id, target)),
2106
+ type: "flowPortal",
2107
+ draggable: false,
2108
+ selectable: false,
2109
+ position: { x: position.x + 320, y: position.y + index * 70 },
2110
+ data: {
2111
+ label: graphs?.[targetFlowKey]?.label ?? targetFlowKey,
2112
+ onNavigate: () => mergeFlow(targetFlowKey)
2113
+ }
2114
+ });
2115
+ });
2116
+ }
2117
+ for (const chain of findCollectionChains(graph)) {
2118
+ const chainNodeIds = [...chain.nodeIds, chain.actionNodeId];
2119
+ const positions = chainNodeIds.map((id) => resolvePosition2(id));
2120
+ const minX = Math.min(...positions.map((point) => point.x));
2121
+ const maxX = Math.max(...positions.map((point) => point.x)) + NODE_CARD_WIDTH;
2122
+ const minY = Math.min(...positions.map((point) => point.y));
2123
+ const maxY = Math.max(...positions.map((point) => point.y)) + estimateNodeHeight(graph.nodes[chain.actionNodeId]);
2124
+ allNodes.push({
2125
+ id: namespaceNodeId(flowKey, `__chain__${chain.actionNodeId}`),
2126
+ type: "flowGroupFrame",
2127
+ draggable: false,
2128
+ selectable: false,
2129
+ zIndex: -1,
2130
+ position: { x: minX - CHAIN_FRAME_PADDING, y: minY - CHAIN_FRAME_PADDING - 24 },
2131
+ style: {
2132
+ width: maxX - minX + CHAIN_FRAME_PADDING * 2,
2133
+ height: maxY - minY + CHAIN_FRAME_PADDING * 2 + 24
2134
+ },
2135
+ data: {
2136
+ label: labels.collectionChain.feeds(nodeLabel(graph.nodes[chain.actionNodeId], labels))
2137
+ }
2138
+ });
2139
+ }
2140
+ if (!isPrimary) {
2141
+ const startPosition = resolvePosition2(graph.startNodeId);
2142
+ allNodes.push({
2143
+ id: namespaceNodeId(flowKey, "__group_header__"),
2144
+ type: "flowGroupHeader",
2145
+ draggable: false,
2146
+ selectable: false,
2147
+ position: { x: startPosition.x, y: startPosition.y - 60 },
2148
+ data: {
2149
+ label: graph.label,
2150
+ onFocus: () => focusFlow(flowKey),
2151
+ onClose: () => closeFlow(flowKey)
2152
+ }
2153
+ });
2154
+ }
2155
+ }
2156
+ return allNodes;
2157
+ }, [
2158
+ openFlowKeys,
2159
+ workingGraphs,
2160
+ mergedPositions,
2161
+ livePositions,
2162
+ issuesByFlow,
2163
+ primaryFlowKey,
2164
+ rootFlowKey,
2165
+ editingRef,
2166
+ graphs,
2167
+ mergeFlow,
2168
+ focusFlow,
2169
+ closeFlow,
2170
+ labels
2171
+ ]);
2172
+ const disconnectEdge = useCallback(
2173
+ (spec) => {
2174
+ const { flowKey, nodeId } = parseNamespacedId(spec.source);
2175
+ updateFlow(flowKey, (graph) => {
2176
+ const node = graph.nodes[nodeId];
2177
+ if (!node) return graph;
2178
+ return { ...graph, nodes: { ...graph.nodes, [nodeId]: clearConnection(node, spec.sourceHandle ?? "next") } };
2179
+ });
2180
+ },
2181
+ [updateFlow]
2182
+ );
2183
+ const edges = useMemo2(
2184
+ () => buildFlowEdges({ openKeys: openFlowKeys, graphs: workingGraphs, rootFlowKey, livePositions }).map(
2185
+ (spec) => (
2186
+ // Salto entre fluxos desenhado como portal não se desliga daqui: quem manda nele é o `next`
2187
+ // do nó de origem, e o portal é só a caixa que representa o fluxo alvo ausente.
2188
+ styleEdge(spec, { disconnectLabel: labels.quickAdd.disconnect, onDisconnect: disconnectEdge })
812
2189
  )
813
- ] }),
814
- /* @__PURE__ */ jsxs8("div", { children: [
815
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.preview }),
816
- /* @__PURE__ */ jsx9("div", { className: "mt-1", children: /* @__PURE__ */ jsx9(FlowWhatsAppPreview, { body: value, options, labels: labels.nodePanel }) })
817
- ] })
818
- ] });
819
- }
820
- function IssueRow({ issue }) {
821
- const Icon = issue.severity === "error" ? AlertCircle2 : AlertTriangle2;
822
- const color = issue.severity === "error" ? "text-red-600 dark:text-red-400" : "text-amber-600 dark:text-amber-400";
823
- return /* @__PURE__ */ jsxs8("div", { className: `flex items-start gap-1.5 text-xs ${color}`, children: [
824
- /* @__PURE__ */ jsx9(Icon, { size: 13, className: "mt-0.5 shrink-0" }),
825
- /* @__PURE__ */ jsx9("span", { children: issue.message })
826
- ] });
827
- }
828
- function FlowNodePanel({
829
- graph,
830
- node,
831
- issues,
832
- otherFlows,
833
- onClose,
834
- onChange,
835
- onDelete,
836
- labels: labelsOverride
837
- }) {
838
- const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
839
- const [draft, setDraft] = useState2(node);
840
- const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id);
841
- const isFixedLogic = draft.type === "entrada_choice";
842
- const isAction = draft.type === "action";
843
- const isCondition = draft.type === "condition";
844
- const isStart = graph.startNodeId === node.id;
845
- const nodeIssues = issues.filter((i) => i.nodeId === node.id);
846
- const knownContextKeys = [...new Set(Object.values(graph.nodes).map((n) => n.contextKey).filter((key) => !!key))];
847
- const conditionAnswerIds = isCondition ? ["true", "false"] : (draft.options ?? []).map(([id]) => id);
848
- function updateNextString(value) {
849
- setDraft((prev) => ({ ...prev, next: value }));
2190
+ ),
2191
+ [openFlowKeys, workingGraphs, rootFlowKey, livePositions, labels, disconnectEdge]
2192
+ );
2193
+ const onReconnect = useCallback(
2194
+ (oldEdge, connection) => {
2195
+ const resolved = resolveConnection({
2196
+ connection: {
2197
+ source: connection.source,
2198
+ target: connection.target,
2199
+ sourceHandle: connection.sourceHandle ?? oldEdge.sourceHandle
2200
+ },
2201
+ graphs: workingGraphs
2202
+ });
2203
+ if (!resolved) return;
2204
+ updateFlow(resolved.flowKey, (graph) => {
2205
+ const node = graph.nodes[resolved.nodeId];
2206
+ if (!node) return graph;
2207
+ return { ...graph, nodes: { ...graph.nodes, [resolved.nodeId]: applyConnection(node, resolved) } };
2208
+ });
2209
+ },
2210
+ [workingGraphs, updateFlow]
2211
+ );
2212
+ useEffect3(() => {
2213
+ setRfNodes(derivedNodes);
2214
+ for (const node of derivedNodes) {
2215
+ if (node.type === "flowNode") renderedPositionsRef.current.set(node.id, node.position);
2216
+ }
2217
+ }, [derivedNodes]);
2218
+ useEffect3(() => {
2219
+ if (!pendingFocusNodeId || !flowInstance) return;
2220
+ const target = rfNodes.find((node) => node.id === pendingFocusNodeId);
2221
+ if (!target) return;
2222
+ flowInstance.setCenter(target.position.x + NODE_CARD_WIDTH / 2, target.position.y, { zoom: 1, duration: 400 });
2223
+ setPendingFocusNodeId(null);
2224
+ }, [pendingFocusNodeId, flowInstance, rfNodes]);
2225
+ const onNodesChange = useCallback((changes) => {
2226
+ setRfNodes((current) => applyNodeChanges(changes, current));
2227
+ }, []);
2228
+ const onNodeDragStop = useCallback(
2229
+ (_event, node) => {
2230
+ renderedPositionsRef.current.set(node.id, node.position);
2231
+ const { flowKey, nodeId } = parseNamespacedId(node.id);
2232
+ if (!openFlowKeys.includes(flowKey)) return;
2233
+ updateFlow(
2234
+ flowKey,
2235
+ (graph) => graph.nodes[nodeId] ? { ...graph, nodes: { ...graph.nodes, [nodeId]: { ...graph.nodes[nodeId], position: node.position } } } : graph
2236
+ );
2237
+ },
2238
+ [openFlowKeys, updateFlow]
2239
+ );
2240
+ const onConnect = useCallback(
2241
+ (connection) => {
2242
+ const resolved = resolveConnection({
2243
+ connection: { source: connection.source, target: connection.target, sourceHandle: connection.sourceHandle },
2244
+ graphs: workingGraphs
2245
+ });
2246
+ if (!resolved) return;
2247
+ updateFlow(resolved.flowKey, (graph) => {
2248
+ const node = graph.nodes[resolved.nodeId];
2249
+ if (!node) return graph;
2250
+ return { ...graph, nodes: { ...graph.nodes, [resolved.nodeId]: applyConnection(node, resolved) } };
2251
+ });
2252
+ },
2253
+ [workingGraphs, updateFlow]
2254
+ );
2255
+ function handleAddNode(spec) {
2256
+ if (!primaryGraph) return;
2257
+ const newNode = newNodeFromSpec(spec, new Set(Object.keys(primaryGraph.nodes)));
2258
+ const maxY = Math.max(0, ...Object.values(primaryGraph.nodes).map((node) => node.position?.y ?? 0));
2259
+ newNode.position = { x: 0, y: maxY + 170 };
2260
+ updateFlow(primaryFlowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [newNode.id]: newNode } }));
2261
+ setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
2262
+ setPendingFocusNodeId(namespaceNodeId(primaryFlowKey, newNode.id));
850
2263
  }
851
- function updateNextByAnswer(answerId, value) {
852
- setDraft((prev) => {
853
- const current = typeof prev.next === "object" && prev.next ? prev.next : { byAnswer: {}, default: otherNodeIds[0] ?? "" };
854
- return { ...prev, next: { ...current, byAnswer: { ...current.byAnswer, [answerId]: value } } };
2264
+ function handleQuickAdd(spec) {
2265
+ const origin = quickAddFrom;
2266
+ setQuickAddFrom(null);
2267
+ if (!origin) return;
2268
+ const originGraph = workingGraphs[origin.flowKey];
2269
+ if (!originGraph) return;
2270
+ const newNode = newNodeFromSpec(spec, new Set(Object.keys(originGraph.nodes)));
2271
+ const originPosition = renderedPositionsRef.current.get(namespaceNodeId(origin.flowKey, origin.nodeId)) ?? originGraph.nodes[origin.nodeId]?.position ?? { x: 0, y: 0 };
2272
+ const taken = openFlowKeys.flatMap(
2273
+ (key) => Object.entries(workingGraphs[key]?.nodes ?? {}).map(
2274
+ ([id, node]) => renderedPositionsRef.current.get(namespaceNodeId(key, id)) ?? node.position ?? { x: 0, y: 0 }
2275
+ )
2276
+ );
2277
+ newNode.position = findFreeSlot({
2278
+ desired: { x: originPosition.x + QUICK_ADD_COLUMN_GAP, y: originPosition.y },
2279
+ taken
855
2280
  });
856
- }
857
- function updateNextDefault(value) {
858
- setDraft((prev) => {
859
- const current = typeof prev.next === "object" && prev.next ? prev.next : { byAnswer: {}, default: value };
860
- return { ...prev, next: { ...current, default: value } };
2281
+ renderedPositionsRef.current.set(namespaceNodeId(origin.flowKey, newNode.id), newNode.position);
2282
+ updateFlow(origin.flowKey, (graph) => {
2283
+ const sourceNode = graph.nodes[origin.nodeId];
2284
+ if (!sourceNode) return graph;
2285
+ const connected = applyConnection(sourceNode, {
2286
+ flowKey: origin.flowKey,
2287
+ nodeId: origin.nodeId,
2288
+ handle: origin.handle,
2289
+ targetValue: newNode.id
2290
+ });
2291
+ return { ...graph, nodes: { ...graph.nodes, [origin.nodeId]: connected, [newNode.id]: newNode } };
861
2292
  });
2293
+ setEditingRef({ flowKey: origin.flowKey, nodeId: newNode.id });
862
2294
  }
863
- function updateOption(index, field, value) {
864
- setDraft((prev) => {
865
- const opts = [...prev.options ?? []];
866
- const opt = [...opts[index]];
867
- opt[field] = value;
868
- opts[index] = opt;
869
- return { ...prev, options: opts };
2295
+ function handleNodePanelChange(updated) {
2296
+ if (!editingRef) return;
2297
+ updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [updated.id]: updated } }));
2298
+ setEditingRef(null);
2299
+ }
2300
+ function handleNodeDelete(nodeId) {
2301
+ if (!editingRef) return;
2302
+ updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: removeNodeAndCleanRefs(graph.nodes, nodeId) }));
2303
+ setEditingRef(null);
2304
+ }
2305
+ function handleOrganize() {
2306
+ if (!primaryGraph) return;
2307
+ renderedPositionsRef.current.clear();
2308
+ if (isMerged) {
2309
+ const positions2 = computeMergedLayout({ openKeys: openFlowKeys, graphs: workingGraphs, primaryFlowKey });
2310
+ for (const key of openFlowKeys) {
2311
+ updateFlow(key, (graph) => ({
2312
+ ...graph,
2313
+ nodes: Object.fromEntries(
2314
+ Object.entries(graph.nodes).map(([id, node]) => {
2315
+ const position = positions2.get(namespaceNodeId(key, id));
2316
+ return [id, position ? { ...node, position } : node];
2317
+ })
2318
+ )
2319
+ }));
2320
+ }
2321
+ return;
2322
+ }
2323
+ const positions = computeAutoLayout(primaryGraph);
2324
+ updateFlow(primaryFlowKey, (graph) => ({
2325
+ ...graph,
2326
+ nodes: Object.fromEntries(
2327
+ Object.entries(graph.nodes).map(([id, node]) => [id, { ...node, position: positions[id] ?? node.position }])
2328
+ )
2329
+ }));
2330
+ }
2331
+ function handleDiscardChanges() {
2332
+ if (!graphs || !isDirty) return;
2333
+ if (!window.confirm(labels.workspace.discardConfirm)) return;
2334
+ setWorkingGraphs((prev) => {
2335
+ const next = { ...prev };
2336
+ for (const key of dirtyKeys) {
2337
+ const published = graphs[key];
2338
+ if (published) next[key] = published;
2339
+ }
2340
+ return next;
870
2341
  });
2342
+ renderedPositionsRef.current.clear();
2343
+ setEditingRef(null);
871
2344
  }
872
- function addOption() {
873
- setDraft((prev) => ({ ...prev, options: [...prev.options ?? [], [String((prev.options?.length ?? 0) + 1), "Nova op\xE7\xE3o"]] }));
2345
+ async function handlePublish() {
2346
+ if (dirtyKeys.length === 0 || errorCount > 0) return;
2347
+ setSaveState("saving");
2348
+ setSaveErrorMessage(void 0);
2349
+ try {
2350
+ await Promise.all(dirtyKeys.map((key) => api.saveGraph(key, workingGraphs[key])));
2351
+ await reloadGraphs();
2352
+ setSaveState("success");
2353
+ setTimeout(() => setSaveState("idle"), 3e3);
2354
+ } catch (error) {
2355
+ setSaveState("error");
2356
+ setSaveErrorMessage(extractErrorMessage(error));
2357
+ }
874
2358
  }
875
- function removeOption(index) {
876
- setDraft((prev) => ({ ...prev, options: (prev.options ?? []).filter((_, i) => i !== index) }));
2359
+ async function handleCreateFlow() {
2360
+ const createFlow = api.createFlow;
2361
+ if (!createFlow) return;
2362
+ setFlowMutationState({ pending: true });
2363
+ try {
2364
+ await createFlow({
2365
+ key: newFlow.key,
2366
+ label: newFlow.label,
2367
+ showInMenu: newFlow.showInMenu,
2368
+ ...newFlow.showInMenu ? { menuOptionLabel: newFlow.menuOptionLabel || newFlow.label } : {}
2369
+ });
2370
+ await reloadGraphs();
2371
+ setOpenFlowKeys([newFlow.key]);
2372
+ setShowCreateDialog(false);
2373
+ setNewFlow({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
2374
+ setFlowMutationState({ pending: false });
2375
+ } catch (error) {
2376
+ setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.createError });
2377
+ }
877
2378
  }
878
- function nextNodeOptions() {
879
- return /* @__PURE__ */ jsxs8(Fragment, { children: [
880
- otherNodeIds.map((id) => /* @__PURE__ */ jsx9("option", { value: id, children: truncateLabel(nodeLabel(graph.nodes[id], labels)) }, id)),
881
- otherFlows.length > 0 && /* @__PURE__ */ jsx9("optgroup", { label: labels.nodePanel.otherFlowsGroup, children: otherFlows.map((flow) => /* @__PURE__ */ jsx9("option", { value: `${CROSS_FLOW_PREFIX}${flow.key}`, children: flow.label }, flow.key)) })
882
- ] });
2379
+ async function handleDeleteFlow() {
2380
+ const deleteFlow = api.deleteFlow;
2381
+ if (!deleteFlow || !primaryGraph) return;
2382
+ setFlowMutationState({ pending: true });
2383
+ try {
2384
+ await deleteFlow(primaryGraph.key);
2385
+ const reloaded = await reloadGraphs();
2386
+ setOpenFlowKeys(reloaded ? mergedFlowKeysFrom(rootFlowKey, reloaded) : [rootFlowKey]);
2387
+ setShowDeleteDialog(false);
2388
+ setFlowMutationState({ pending: false });
2389
+ } catch (error) {
2390
+ setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.deleteError });
2391
+ }
883
2392
  }
884
- return /* @__PURE__ */ jsxs8("div", { className: "fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col", children: [
885
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
886
- /* @__PURE__ */ jsx9("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
887
- /* @__PURE__ */ jsx9("button", { onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx9(X2, { size: 18 }) })
888
- ] }),
889
- /* @__PURE__ */ jsxs8("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
890
- nodeIssues.length > 0 && /* @__PURE__ */ jsx9("div", { className: "rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-2.5 space-y-1.5", children: nodeIssues.map((issue, i) => /* @__PURE__ */ jsx9(IssueRow, { issue }, i)) }),
891
- isFixedLogic && /* @__PURE__ */ jsx9("p", { className: "text-xs text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30 rounded-lg p-2", children: labels.nodePanel.fixedLogicNotice }),
892
- isAction && /* @__PURE__ */ jsx9("p", { className: "text-xs text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/30 rounded-lg p-2", children: labels.nodePanel.actionNotice }),
893
- isCondition && /* @__PURE__ */ jsx9("p", { className: "text-xs text-cyan-700 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/30 rounded-lg p-2", children: labels.nodePanel.conditionNotice }),
894
- draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
895
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
896
- /* @__PURE__ */ jsx9("input", { value: draft.contextKey, disabled: true, className: "w-full mt-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-3 py-2.5 text-sm text-gray-500" })
2393
+ const editingGraph = editingRef ? workingGraphs[editingRef.flowKey] : null;
2394
+ const editingNode = editingRef && editingGraph ? editingGraph.nodes[editingRef.nodeId] : null;
2395
+ const otherFlows = useMemo2(
2396
+ () => Object.values(graphs ?? {}).filter((graph) => graph.key !== editingRef?.flowKey).map((graph) => ({ key: graph.key, label: graph.label })),
2397
+ [graphs, editingRef?.flowKey]
2398
+ );
2399
+ const keyIsValid = FLOW_KEY_PATTERN.test(newFlow.key);
2400
+ const canCreateFlow = Boolean(api.createFlow);
2401
+ const canDeleteFlow = Boolean(api.deleteFlow) && primaryFlowKey !== rootFlowKey;
2402
+ return /* @__PURE__ */ jsxs11("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
2403
+ /* @__PURE__ */ jsx12(TooltipLayer, {}),
2404
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
2405
+ showHeader && /* @__PURE__ */ jsxs11("div", { className: "min-w-0", children: [
2406
+ /* @__PURE__ */ jsx12("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
2407
+ /* @__PURE__ */ jsx12("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
897
2408
  ] }),
898
- !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs8("div", { children: [
899
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
900
- /* @__PURE__ */ jsx9(
901
- "select",
2409
+ /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap items-center justify-end gap-3 ml-auto", children: [
2410
+ saveState === "success" && /* @__PURE__ */ jsx12("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
2411
+ saveState === "error" && /* @__PURE__ */ jsx12("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
2412
+ /* @__PURE__ */ jsxs11(
2413
+ "button",
902
2414
  {
903
- value: draft.questionType ?? "text",
904
- onChange: (e) => setDraft((prev) => ({ ...prev, questionType: e.target.value })),
905
- className: `w-full mt-1 ${SELECT_CLASSNAME}`,
906
- children: Object.entries(labels.questionTypeLabels).map(([key, label]) => /* @__PURE__ */ jsx9("option", { value: key, children: label }, key))
2415
+ "data-cv-tooltip": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
2416
+ "aria-label": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
2417
+ type: "button",
2418
+ onClick: () => setViewMode((mode) => mode === "map" ? "detail" : "map"),
2419
+ className: OUTLINE_BUTTON,
2420
+ children: [
2421
+ viewMode === "map" ? /* @__PURE__ */ jsx12(Workflow, { size: 14, "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(MapIcon, { size: 14, "aria-hidden": "true" }),
2422
+ viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap
2423
+ ]
907
2424
  }
908
- )
909
- ] }),
910
- !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsx9(
911
- WhatsAppTextField,
912
- {
913
- label: labels.nodePanel.question,
914
- value: draft.question ?? "",
915
- options: draft.questionType === "choice" ? draft.options : void 0,
916
- onValueChange: (value) => setDraft((prev) => ({ ...prev, question: value })),
917
- labels
918
- }
919
- ),
920
- isCondition && /* @__PURE__ */ jsxs8("div", { className: "space-y-3", children: [
921
- /* @__PURE__ */ jsxs8("div", { children: [
922
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionVariable }),
923
- /* @__PURE__ */ jsx9(
924
- "input",
2425
+ ),
2426
+ viewMode === "detail" && /* @__PURE__ */ jsxs11(Fragment4, { children: [
2427
+ /* @__PURE__ */ jsxs11(
2428
+ "button",
925
2429
  {
926
- value: draft.conditionContextKey ?? "",
927
- onChange: (e) => setDraft((prev) => ({ ...prev, conditionContextKey: e.target.value })),
928
- list: "condition-context-keys",
929
- className: `w-full mt-1 ${INPUT_CLASSNAME}`
2430
+ type: "button",
2431
+ onClick: handleOrganize,
2432
+ className: OUTLINE_BUTTON,
2433
+ "data-cv-tooltip": labels.workspace.organizeTooltip,
2434
+ "aria-label": labels.workspace.organizeTooltip,
2435
+ disabled: !primaryGraph,
2436
+ children: [
2437
+ /* @__PURE__ */ jsx12(LayoutGrid, { size: 14, "aria-hidden": "true" }),
2438
+ " ",
2439
+ labels.workspace.organize
2440
+ ]
930
2441
  }
931
2442
  ),
932
- /* @__PURE__ */ jsx9("datalist", { id: "condition-context-keys", children: knownContextKeys.map((key) => /* @__PURE__ */ jsx9("option", { value: key }, key)) })
933
- ] }),
934
- /* @__PURE__ */ jsxs8("div", { children: [
935
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionOperator }),
936
- /* @__PURE__ */ jsx9(
937
- "select",
2443
+ /* @__PURE__ */ jsxs11(
2444
+ "button",
938
2445
  {
939
- value: draft.conditionOperator ?? ">",
940
- onChange: (e) => setDraft((prev) => ({ ...prev, conditionOperator: e.target.value })),
941
- className: `w-full mt-1 ${SELECT_CLASSNAME}`,
942
- children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx9("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
2446
+ type: "button",
2447
+ onClick: handleDiscardChanges,
2448
+ className: OUTLINE_BUTTON,
2449
+ "data-cv-tooltip": labels.workspace.discardTooltip,
2450
+ "aria-label": labels.workspace.discardTooltip,
2451
+ disabled: !isDirty || saveState === "saving",
2452
+ children: [
2453
+ /* @__PURE__ */ jsx12(Undo2, { size: 14, "aria-hidden": "true" }),
2454
+ " ",
2455
+ labels.workspace.discardChanges
2456
+ ]
943
2457
  }
944
- )
945
- ] }),
946
- /* @__PURE__ */ jsxs8("div", { children: [
947
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionValue }),
948
- /* @__PURE__ */ jsx9(
949
- "input",
2458
+ ),
2459
+ /* @__PURE__ */ jsxs11(
2460
+ "button",
950
2461
  {
951
- value: draft.conditionValue ?? "",
952
- onChange: (e) => setDraft((prev) => ({ ...prev, conditionValue: e.target.value })),
953
- className: `w-full mt-1 ${INPUT_CLASSNAME}`
2462
+ "data-cv-tooltip": labels.workspace.saveGraph,
2463
+ "aria-label": labels.workspace.saveGraph,
2464
+ type: "button",
2465
+ onClick: () => void handlePublish(),
2466
+ className: PRIMARY_BUTTON,
2467
+ disabled: !isDirty || errorCount > 0 || saveState === "saving",
2468
+ children: [
2469
+ /* @__PURE__ */ jsx12(Save2, { size: 14, "aria-hidden": "true" }),
2470
+ saveState === "saving" ? labels.workspace.saving : labels.workspace.saveGraph
2471
+ ]
954
2472
  }
955
2473
  )
956
2474
  ] })
2475
+ ] })
2476
+ ] }),
2477
+ viewMode === "detail" && /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
2478
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 flex-wrap", children: [
2479
+ graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx12(
2480
+ "button",
2481
+ {
2482
+ "data-cv-tooltip": graph.label,
2483
+ "aria-label": graph.label,
2484
+ type: "button",
2485
+ onClick: () => focusFlow(graph.key),
2486
+ className: `px-3 py-1.5 rounded-full text-xs font-medium border transition-colors ${primaryFlowKey === graph.key ? "bg-blue-600 text-white border-blue-600" : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-700 hover:border-blue-300"}`,
2487
+ children: graph.label
2488
+ },
2489
+ graph.key
2490
+ )),
2491
+ canCreateFlow && /* @__PURE__ */ jsxs11(
2492
+ "button",
2493
+ {
2494
+ "data-cv-tooltip": labels.flowManager.newFlow,
2495
+ "aria-label": labels.flowManager.newFlow,
2496
+ type: "button",
2497
+ onClick: () => {
2498
+ setFlowMutationState({ pending: false });
2499
+ setShowCreateDialog(true);
2500
+ },
2501
+ className: "inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium border border-dashed border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:border-blue-400 hover:text-blue-600",
2502
+ children: [
2503
+ /* @__PURE__ */ jsx12(Plus4, { size: 12, "aria-hidden": "true" }),
2504
+ " ",
2505
+ labels.flowManager.newFlow
2506
+ ]
2507
+ }
2508
+ )
957
2509
  ] }),
958
- isAction && draft.actionKind === "send_product_list" && /* @__PURE__ */ jsx9(
959
- WhatsAppTextField,
2510
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
2511
+ primaryGraph && /* @__PURE__ */ jsx12(
2512
+ FlowPalette,
2513
+ {
2514
+ onAdd: handleAddNode,
2515
+ labels,
2516
+ ...actionOptions ? { actionOptions: [...actionOptions] } : {}
2517
+ }
2518
+ ),
2519
+ primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs11(
2520
+ "button",
2521
+ {
2522
+ "data-cv-tooltip": labels.flowManager.deleteFlow,
2523
+ "aria-label": labels.flowManager.deleteFlow,
2524
+ type: "button",
2525
+ onClick: () => {
2526
+ setFlowMutationState({ pending: false });
2527
+ setShowDeleteDialog(true);
2528
+ },
2529
+ className: "inline-flex items-center gap-1.5 rounded-lg border border-red-200 dark:border-red-900 px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30",
2530
+ children: [
2531
+ /* @__PURE__ */ jsx12(Trash22, { size: 13, "aria-hidden": "true" }),
2532
+ " ",
2533
+ labels.flowManager.deleteFlow
2534
+ ]
2535
+ }
2536
+ )
2537
+ ] })
2538
+ ] }),
2539
+ viewMode === "detail" && (errorCount > 0 || warningCount > 0) && /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/60 px-3 py-2 text-xs", children: [
2540
+ /* @__PURE__ */ jsxs11("span", { className: "font-medium text-gray-600 dark:text-gray-300", children: [
2541
+ labels.validation.title,
2542
+ ":"
2543
+ ] }),
2544
+ errorCount > 0 && /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-1 text-red-600 dark:text-red-400 font-medium", children: [
2545
+ /* @__PURE__ */ jsx12(AlertCircle3, { size: 13, "aria-hidden": "true" }),
2546
+ " ",
2547
+ labels.validation.errors(errorCount)
2548
+ ] }),
2549
+ warningCount > 0 && /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [
2550
+ /* @__PURE__ */ jsx12(AlertTriangle3, { size: 13, "aria-hidden": "true" }),
2551
+ " ",
2552
+ labels.validation.warnings(warningCount)
2553
+ ] })
2554
+ ] }),
2555
+ /* @__PURE__ */ jsxs11("div", { className: "flex-1 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative", children: [
2556
+ loadState === "loading" && /* @__PURE__ */ jsx12("p", { className: "text-center text-gray-400 py-12", children: labels.workspace.loading }),
2557
+ loadState === "error" && /* @__PURE__ */ jsx12("p", { className: "text-center text-red-500 py-12", children: labels.workspace.loadError }),
2558
+ loadState === "ready" && graphs && viewMode === "map" && /* @__PURE__ */ jsx12(
2559
+ FlowMapCanvas,
960
2560
  {
961
- label: labels.nodePanel.fallbackMessage,
962
- value: draft.fallbackMessage ?? "",
963
- onValueChange: (value) => setDraft((prev) => ({ ...prev, fallbackMessage: value })),
964
- placeholder: "(usa a mensagem padr\xE3o de fallback se vazio)",
965
- labels
2561
+ graphs,
2562
+ rootKey: rootFlowKey,
2563
+ labels,
2564
+ onOpenFlow: (key) => {
2565
+ focusFlow(key);
2566
+ setViewMode("detail");
2567
+ }
966
2568
  }
967
2569
  ),
968
- isAction && draft.actionKind !== "send_product_list" && /* @__PURE__ */ jsx9(
969
- WhatsAppTextField,
2570
+ loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs11(
2571
+ ReactFlow2,
970
2572
  {
971
- label: labels.nodePanel.directMessage,
972
- value: draft.directMessage ?? "",
973
- onValueChange: (value) => setDraft((prev) => ({ ...prev, directMessage: value })),
974
- placeholder: "(usa a mensagem padr\xE3o de encaminhamento se vazio)",
975
- labels
2573
+ nodes: rfNodes,
2574
+ edges,
2575
+ nodeTypes: RF_NODE_TYPES,
2576
+ edgeTypes: flowEdgeTypes,
2577
+ onNodesChange,
2578
+ onNodeDragStop,
2579
+ onConnect,
2580
+ onReconnect,
2581
+ onInit: setFlowInstance,
2582
+ fitView: true,
2583
+ proOptions: { hideAttribution: true },
2584
+ colorMode: isDark ? "dark" : "light",
2585
+ children: [
2586
+ /* @__PURE__ */ jsx12(Background2, { color: isDark ? BACKGROUND_COLOR_DARK2 : BACKGROUND_COLOR_LIGHT2 }),
2587
+ /* @__PURE__ */ jsx12(Controls2, {}),
2588
+ /* @__PURE__ */ jsx12(Panel, { position: "top-right", children: /* @__PURE__ */ jsx12(
2589
+ FlowLegend,
2590
+ {
2591
+ labels,
2592
+ edgeSamples: legendEdgeSamples(labels),
2593
+ nodeSwatches: LEGEND_NODE_SWATCHES
2594
+ }
2595
+ ) }),
2596
+ /* @__PURE__ */ jsx12(MiniMap, { pannable: true, zoomable: true, className: "!bg-white dark:!bg-gray-800" })
2597
+ ]
976
2598
  }
977
- ),
978
- (draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs8("div", { children: [
979
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between mb-1.5", children: [
980
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
981
- /* @__PURE__ */ jsxs8("button", { onClick: addOption, className: "text-xs text-blue-600 hover:underline flex items-center gap-1", children: [
982
- /* @__PURE__ */ jsx9(Plus2, { size: 12 }),
983
- " ",
984
- labels.nodePanel.addOption
985
- ] })
2599
+ )
2600
+ ] }),
2601
+ quickAddFrom && /* @__PURE__ */ jsxs11(Fragment4, { children: [
2602
+ /* @__PURE__ */ jsx12("div", { className: "fixed inset-0 z-40", onClick: () => setQuickAddFrom(null) }),
2603
+ /* @__PURE__ */ jsxs11(
2604
+ "div",
2605
+ {
2606
+ className: "fixed z-50 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1",
2607
+ style: { left: quickAddFrom.anchor.x + 12, top: quickAddFrom.anchor.y },
2608
+ children: [
2609
+ /* @__PURE__ */ jsx12("p", { className: "px-3 py-1.5 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500", children: labels.quickAdd.title }),
2610
+ /* @__PURE__ */ jsx12(
2611
+ FlowPaletteMenu,
2612
+ {
2613
+ onSelect: handleQuickAdd,
2614
+ labels,
2615
+ ...actionOptions ? { actionOptions: [...actionOptions] } : {}
2616
+ }
2617
+ )
2618
+ ]
2619
+ }
2620
+ )
2621
+ ] }),
2622
+ editingNode && editingGraph && /* @__PURE__ */ jsx12(
2623
+ FlowNodePanel,
2624
+ {
2625
+ graph: editingGraph,
2626
+ node: editingNode,
2627
+ issues: editingRef ? issuesByFlow[editingRef.flowKey] ?? [] : [],
2628
+ otherFlows,
2629
+ labels,
2630
+ onClose: () => setEditingRef(null),
2631
+ onChange: handleNodePanelChange,
2632
+ onDelete: handleNodeDelete,
2633
+ ...renderMediaPicker ? { renderMediaPicker } : {}
2634
+ },
2635
+ `${editingRef?.flowKey}:${editingRef?.nodeId}`
2636
+ ),
2637
+ showCreateDialog && canCreateFlow && /* @__PURE__ */ jsxs11(FlowDialog, { title: labels.flowManager.createTitle, onClose: () => setShowCreateDialog(false), children: [
2638
+ /* @__PURE__ */ jsxs11("div", { className: "space-y-3", children: [
2639
+ /* @__PURE__ */ jsxs11("div", { children: [
2640
+ /* @__PURE__ */ jsx12("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.label }),
2641
+ /* @__PURE__ */ jsx12(
2642
+ "input",
2643
+ {
2644
+ value: newFlow.label,
2645
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, label: event.target.value })),
2646
+ className: `mt-1 ${DIALOG_INPUT}`
2647
+ }
2648
+ )
986
2649
  ] }),
987
- /* @__PURE__ */ jsx9("div", { className: "space-y-2", children: (draft.options ?? []).map(([id, label], i) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
988
- /* @__PURE__ */ jsx9(
2650
+ /* @__PURE__ */ jsxs11("div", { children: [
2651
+ /* @__PURE__ */ jsx12("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.key }),
2652
+ /* @__PURE__ */ jsx12(
989
2653
  "input",
990
2654
  {
991
- value: id,
992
- onChange: (e) => updateOption(i, 0, e.target.value),
993
- placeholder: labels.nodePanel.optionId,
994
- className: `w-16 ${INPUT_CLASSNAME}`
2655
+ value: newFlow.key,
2656
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, key: event.target.value.toLowerCase() })),
2657
+ className: `mt-1 ${DIALOG_INPUT}`
995
2658
  }
996
2659
  ),
997
- /* @__PURE__ */ jsx9(
2660
+ /* @__PURE__ */ jsx12("p", { className: "text-[11px] text-gray-400 mt-1", children: newFlow.key && !keyIsValid ? labels.flowManager.keyInvalid : labels.flowManager.keyHint })
2661
+ ] }),
2662
+ /* @__PURE__ */ jsxs11("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200", children: [
2663
+ /* @__PURE__ */ jsx12(
998
2664
  "input",
999
2665
  {
1000
- value: label,
1001
- onChange: (e) => updateOption(i, 1, e.target.value),
1002
- placeholder: labels.nodePanel.optionLabel,
1003
- className: `flex-1 ${INPUT_CLASSNAME}`
2666
+ type: "checkbox",
2667
+ checked: newFlow.showInMenu,
2668
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, showInMenu: event.target.checked }))
1004
2669
  }
1005
2670
  ),
1006
- /* @__PURE__ */ jsx9("button", { onClick: () => removeOption(i), className: "text-gray-400 hover:text-red-600", children: /* @__PURE__ */ jsx9(Trash2, { size: 14 }) })
1007
- ] }, i)) })
2671
+ labels.flowManager.showInMenu
2672
+ ] }),
2673
+ newFlow.showInMenu && /* @__PURE__ */ jsxs11("div", { children: [
2674
+ /* @__PURE__ */ jsx12("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.menuOptionLabel }),
2675
+ /* @__PURE__ */ jsx12(
2676
+ "input",
2677
+ {
2678
+ value: newFlow.menuOptionLabel,
2679
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, menuOptionLabel: event.target.value })),
2680
+ placeholder: newFlow.label,
2681
+ className: `mt-1 ${DIALOG_INPUT}`
2682
+ }
2683
+ )
2684
+ ] }),
2685
+ flowMutationState.error && /* @__PURE__ */ jsx12("p", { className: "text-xs text-red-600", children: flowMutationState.error })
1008
2686
  ] }),
1009
- !isAction && /* @__PURE__ */ jsxs8("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
1010
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.next }),
1011
- /* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mb-1", children: labels.nodePanel.nextHint }),
1012
- typeof draft.next !== "object" && !isCondition ? /* @__PURE__ */ jsxs8(
1013
- "select",
2687
+ /* @__PURE__ */ jsxs11("div", { className: "flex justify-end gap-2 mt-4", children: [
2688
+ /* @__PURE__ */ jsx12("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowCreateDialog(false), children: labels.nodePanel.cancel }),
2689
+ /* @__PURE__ */ jsx12(
2690
+ "button",
1014
2691
  {
1015
- value: typeof draft.next === "string" ? draft.next : "",
1016
- onChange: (e) => updateNextString(e.target.value),
1017
- className: `w-full mt-1 ${SELECT_CLASSNAME}`,
2692
+ "data-cv-tooltip": labels.flowManager.create,
2693
+ "aria-label": labels.flowManager.create,
2694
+ type: "button",
2695
+ className: PRIMARY_BUTTON,
2696
+ onClick: () => void handleCreateFlow(),
2697
+ disabled: !keyIsValid || !newFlow.label || flowMutationState.pending,
2698
+ children: flowMutationState.pending ? labels.flowManager.creating : labels.flowManager.create
2699
+ }
2700
+ )
2701
+ ] })
2702
+ ] }),
2703
+ showDeleteDialog && canDeleteFlow && /* @__PURE__ */ jsxs11(FlowDialog, { title: labels.flowManager.deleteFlow, onClose: () => setShowDeleteDialog(false), children: [
2704
+ /* @__PURE__ */ jsx12("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
2705
+ flowMutationState.error && /* @__PURE__ */ jsx12("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
2706
+ /* @__PURE__ */ jsxs11("div", { className: "flex justify-end gap-2 mt-4", children: [
2707
+ /* @__PURE__ */ jsx12("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowDeleteDialog(false), children: labels.nodePanel.cancel }),
2708
+ /* @__PURE__ */ jsxs11(
2709
+ "button",
2710
+ {
2711
+ "data-cv-tooltip": labels.flowManager.deleteFlow,
2712
+ "aria-label": labels.flowManager.deleteFlow,
2713
+ type: "button",
2714
+ className: "inline-flex items-center gap-1.5 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 disabled:opacity-40",
2715
+ onClick: () => void handleDeleteFlow(),
2716
+ disabled: flowMutationState.pending,
1018
2717
  children: [
1019
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
1020
- nextNodeOptions()
2718
+ /* @__PURE__ */ jsx12(Trash22, { size: 13, "aria-hidden": "true" }),
2719
+ " ",
2720
+ labels.flowManager.deleteFlow
1021
2721
  ]
1022
2722
  }
1023
- ) : /* @__PURE__ */ jsxs8("div", { className: "space-y-2 mt-1", children: [
1024
- conditionAnswerIds.map((id) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1025
- /* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? id === "true" ? labels.nodePanel.conditionTrue : labels.nodePanel.conditionFalse : labels.nodePanel.nextByAnswer(id) }),
1026
- /* @__PURE__ */ jsxs8(
1027
- "select",
1028
- {
1029
- value: draft.next && typeof draft.next === "object" ? draft.next.byAnswer[id] ?? "" : "",
1030
- onChange: (e) => updateNextByAnswer(id, e.target.value),
1031
- className: `flex-1 ${SELECT_CLASSNAME}`,
1032
- children: [
1033
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
1034
- nextNodeOptions()
1035
- ]
1036
- }
1037
- )
1038
- ] }, id)),
1039
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1040
- /* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault }),
1041
- /* @__PURE__ */ jsxs8(
1042
- "select",
1043
- {
1044
- value: draft.next && typeof draft.next === "object" ? draft.next.default : "",
1045
- onChange: (e) => updateNextDefault(e.target.value),
1046
- className: `flex-1 ${SELECT_CLASSNAME}`,
1047
- children: [
1048
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
1049
- nextNodeOptions()
1050
- ]
1051
- }
1052
- )
1053
- ] })
1054
- ] })
2723
+ )
1055
2724
  ] })
1056
- ] }),
1057
- /* @__PURE__ */ jsxs8("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
1058
- /* @__PURE__ */ jsxs8("button", { onClick: () => onChange(draft), className: "flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700", children: [
1059
- /* @__PURE__ */ jsx9(Save, { size: 14 }),
1060
- " ",
1061
- labels.nodePanel.save
1062
- ] }),
1063
- !isStart && /* @__PURE__ */ jsx9(
1064
- "button",
1065
- {
1066
- onClick: () => {
1067
- if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id);
1068
- },
1069
- title: labels.nodePanel.delete,
1070
- className: "px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg",
1071
- children: /* @__PURE__ */ jsx9(Trash2, { size: 14 })
1072
- }
1073
- ),
1074
- /* @__PURE__ */ jsx9("button", { onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
1075
2725
  ] })
1076
2726
  ] });
1077
2727
  }
2728
+ function FlowDialog({
2729
+ title,
2730
+ onClose,
2731
+ children
2732
+ }) {
2733
+ return /* @__PURE__ */ jsxs11("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4", children: [
2734
+ /* @__PURE__ */ jsx12("div", { className: "absolute inset-0", onClick: onClose, "aria-hidden": "true" }),
2735
+ /* @__PURE__ */ jsxs11(
2736
+ "div",
2737
+ {
2738
+ role: "dialog",
2739
+ "aria-modal": "true",
2740
+ "aria-label": title,
2741
+ className: "relative w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-5 shadow-xl",
2742
+ children: [
2743
+ /* @__PURE__ */ jsx12("h3", { className: "text-base font-semibold text-gray-900 dark:text-gray-100 mb-3", children: title }),
2744
+ children
2745
+ ]
2746
+ }
2747
+ )
2748
+ ] });
2749
+ }
1078
2750
  export {
1079
2751
  BUILT_IN_ACTION_KINDS,
1080
2752
  CONDITION_OPERATORS,
1081
2753
  CROSS_FLOW_PREFIX,
1082
2754
  DEFAULT_FLOW_EDITOR_LABELS,
2755
+ FlowConnectionEdge,
1083
2756
  FlowGroupFrame,
1084
2757
  FlowGroupHeader,
2758
+ FlowLegend,
1085
2759
  FlowMapCanvas,
1086
2760
  FlowMapNode,
1087
2761
  FlowNodeCard,
1088
2762
  FlowNodePanel,
1089
2763
  FlowPalette,
2764
+ FlowPaletteMenu,
1090
2765
  FlowPortalNode,
1091
2766
  FlowWhatsAppPreview,
2767
+ FlowsWorkspace,
2768
+ GROUP_HEADER_NODE_ID,
1092
2769
  NODE_CARD_WIDTH,
2770
+ PASS_THROUGH_ACTION_KINDS,
1093
2771
  WHATSAPP_LIMITS,
2772
+ applyConnection,
2773
+ buildFlowEdges,
2774
+ chainFrameBounds,
2775
+ chainFrameNodeId,
1094
2776
  computeAutoLayout,
1095
2777
  computeFlowMapLayout,
2778
+ computeMergedLayout,
2779
+ countLiveByNode,
1096
2780
  crossFlowKey,
1097
2781
  crossFlowTargetsOf,
2782
+ detachedNodeIds,
1098
2783
  estimateNodeHeight,
1099
2784
  findCollectionChains,
2785
+ flowEdgeTypes,
1100
2786
  flowGroupFrameNodeTypes,
1101
2787
  flowGroupHeaderNodeTypes,
1102
2788
  flowMapNodeTypes,
1103
2789
  flowNodeTypes,
1104
2790
  flowPortalNodeTypes,
1105
2791
  isCrossFlowTarget,
2792
+ isGraphDirty,
1106
2793
  mergeFlowEditorLabels,
2794
+ mergedFlowKeysFrom,
2795
+ namespaceNodeId,
2796
+ newNodeFromSpec,
1107
2797
  nodeLabel,
2798
+ parseNamespacedId,
2799
+ portalNodeId,
2800
+ removeNodeAndCleanRefs,
1108
2801
  rendersAsButtons,
2802
+ resolveConnection,
1109
2803
  slugifyNodeId,
1110
2804
  targetsOf,
1111
2805
  validateGraph