@adatechnology/conversations-ui 0.1.0-rc.9 → 0.1.0

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