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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/chunk-DXPSPUWF.js +110 -0
  2. package/dist/chunk-GY472G6E.js +2316 -0
  3. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  4. package/dist/flows/index.d.ts +311 -4
  5. package/dist/flows/index.js +1322 -55
  6. package/dist/index.d.ts +1074 -42
  7. package/dist/index.js +3571 -742
  8. package/dist/preview/index.d.ts +328 -8
  9. package/dist/preview/index.js +902 -115
  10. package/dist/styles.css +819 -0
  11. package/dist/types-De5aN-E_.d.ts +502 -0
  12. package/package.json +3 -3
  13. package/src/AudioPlayer.tsx +8 -0
  14. package/src/AudioRecorderButton.test.tsx +30 -0
  15. package/src/AudioRecorderButton.tsx +248 -0
  16. package/src/AudioTranscription.test.tsx +115 -0
  17. package/src/AudioTranscription.tsx +252 -0
  18. package/src/Avatar.tsx +14 -3
  19. package/src/ConversationContextPanel.tsx +218 -44
  20. package/src/ConversationDocumentsPanel.tsx +347 -24
  21. package/src/ConversationHeader.test.tsx +66 -0
  22. package/src/ConversationHeader.tsx +163 -45
  23. package/src/ConversationListItem.tsx +19 -2
  24. package/src/ConversationLocalesProvider.tsx +42 -0
  25. package/src/ConversationRow.tsx +31 -7
  26. package/src/DocumentsLibrary.tsx +382 -0
  27. package/src/EmojiPicker.tsx +70 -55
  28. package/src/FileIcon.test.ts +83 -0
  29. package/src/FileIcon.tsx +88 -11
  30. package/src/InteractiveMessage.test.tsx +41 -0
  31. package/src/InteractiveMessage.tsx +146 -0
  32. package/src/Lightbox.tsx +18 -3
  33. package/src/MediaRenderer.tsx +98 -22
  34. package/src/MessageBubble.test.tsx +41 -0
  35. package/src/MessageBubble.tsx +75 -6
  36. package/src/MessageComposer.test.tsx +35 -0
  37. package/src/MessageComposer.tsx +155 -19
  38. package/src/RichMessageComposer.test.tsx +113 -0
  39. package/src/RichMessageComposer.tsx +551 -0
  40. package/src/SimpleEmojiPicker.tsx +5 -3
  41. package/src/StatusTicks.tsx +1 -1
  42. package/src/Toast.tsx +4 -0
  43. package/src/Tooltip.test.ts +42 -0
  44. package/src/Tooltip.tsx +164 -0
  45. package/src/Wallpaper.test.tsx +21 -0
  46. package/src/Wallpaper.tsx +67 -7
  47. package/src/WhatsAppMessageEditor.tsx +28 -4
  48. package/src/WindowExpiredNotice.tsx +12 -4
  49. package/src/audioRecorderFormat.test.ts +67 -0
  50. package/src/buildOutput.test.ts +79 -0
  51. package/src/composer.constant.ts +33 -0
  52. package/src/conversationTranscript.test.ts +57 -0
  53. package/src/conversationTranscript.ts +29 -4
  54. package/src/conversationWindow.ts +7 -5
  55. package/src/documentTypeLabel.test.ts +57 -0
  56. package/src/documents/DocumentsWorkspace.tsx +543 -0
  57. package/src/documents/index.ts +8 -0
  58. package/src/documents/labels.ts +92 -0
  59. package/src/emojiCatalog.test.ts +35 -0
  60. package/src/emojiCatalog.ts +189 -0
  61. package/src/flows/FlowGroupHeader.tsx +12 -2
  62. package/src/flows/FlowMapCanvas.tsx +15 -12
  63. package/src/flows/FlowMapNode.tsx +4 -1
  64. package/src/flows/FlowNodeCard.tsx +35 -8
  65. package/src/flows/FlowNodePanel.tsx +149 -38
  66. package/src/flows/FlowPalette.tsx +13 -3
  67. package/src/flows/FlowPortalNode.tsx +1 -1
  68. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  69. package/src/flows/FlowsWorkspace.tsx +1003 -0
  70. package/src/flows/flowCanvasModel.test.ts +293 -0
  71. package/src/flows/flowCanvasModel.ts +342 -0
  72. package/src/flows/flowEditorOps.test.ts +241 -0
  73. package/src/flows/flowEditorOps.ts +177 -0
  74. package/src/flows/flowGraph.ts +6 -6
  75. package/src/flows/index.ts +40 -1
  76. package/src/flows/labels.ts +141 -0
  77. package/src/flows/workspaceContract.test.ts +95 -0
  78. package/src/hooks/useContainerWidth.ts +35 -0
  79. package/src/hooks/useConversationActions.ts +56 -0
  80. package/src/hooks/useConversationDocuments.ts +11 -7
  81. package/src/hooks/useConversationList.ts +15 -9
  82. package/src/hooks/useConversationMessages.ts +2 -2
  83. package/src/hooks/useConversationRealtime.ts +10 -8
  84. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  85. package/src/hooks/useUrlFilterState.ts +107 -0
  86. package/src/icon.constant.ts +12 -0
  87. package/src/index.ts +114 -13
  88. package/src/lib/cn.test.ts +29 -0
  89. package/src/lib/composer-formatting.test.ts +78 -0
  90. package/src/lib/composer-formatting.ts +145 -0
  91. package/src/lib/createMediaUrlResolver.ts +33 -0
  92. package/src/lib/paginated.test.ts +33 -0
  93. package/src/lib/paginated.ts +26 -0
  94. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  95. package/src/lib/whatsapp-formatting.tsx +28 -3
  96. package/src/listing/index.tsx +202 -0
  97. package/src/pagination.constant.ts +10 -0
  98. package/src/preview/ConversationPreview.tsx +199 -13
  99. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  100. package/src/preview/ConversationSimulatorPanel.tsx +89 -0
  101. package/src/preview/MediaTypesPreview.tsx +87 -0
  102. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  103. package/src/preview/createMockConversationsApi.ts +175 -15
  104. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  105. package/src/preview/createPreviewBridgeClient.ts +124 -0
  106. package/src/preview/createPreviewMediaUploader.ts +82 -0
  107. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  108. package/src/preview/createPreviewWebhookClient.ts +127 -4
  109. package/src/preview/index.ts +33 -3
  110. package/src/preview/mediaTypeOf.test.ts +15 -0
  111. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  112. package/src/preview/preview.test.ts +5 -3
  113. package/src/preview/previewFileSamples.test.ts +151 -0
  114. package/src/preview/previewFileSamples.ts +74 -0
  115. package/src/preview/previewFixtures.ts +288 -1
  116. package/src/preview/previewMediaSource.test.ts +62 -0
  117. package/src/preview/previewMediaSource.ts +91 -0
  118. package/src/preview/previewMediaUploader.test.ts +61 -0
  119. package/src/providers/ConversationsProvider.tsx +8 -6
  120. package/src/providers/types.ts +185 -10
  121. package/src/quickReply.test.ts +58 -0
  122. package/src/settings/MessagesWorkspace.tsx +468 -0
  123. package/src/settings/TopicsForm.tsx +2 -0
  124. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  125. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  126. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  127. package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
  128. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  129. package/src/settings/WhatsAppTemplatesSettings.tsx +9 -1
  130. package/src/styles.css +783 -0
  131. package/src/types.ts +64 -1
  132. package/src/useWaitingNotifications.ts +74 -29
  133. package/src/workspace/BulkTemplateModal.tsx +132 -0
  134. package/src/workspace/ConversationPane.tsx +432 -0
  135. package/src/workspace/ConversationsInboxList.tsx +194 -0
  136. package/src/workspace/ConversationsWorkspace.tsx +346 -0
  137. package/src/workspace/index.ts +12 -0
  138. package/src/workspace/labels.test.ts +17 -0
  139. package/src/workspace/labels.ts +85 -0
  140. package/src/workspace/useConversationsInbox.ts +332 -0
  141. package/dist/chunk-N7B24WYD.js +0 -719
  142. package/dist/chunk-NV2RZ5KT.js +0 -56
  143. package/dist/types-C0PtaO7S.d.ts +0 -207
@@ -1,13 +1,25 @@
1
1
  import {
2
- useIsDarkTheme
3
- } from "../chunk-NV2RZ5KT.js";
2
+ TooltipLayer
3
+ } from "../chunk-DXPSPUWF.js";
4
4
  import {
5
- parseWhatsAppFormatting
6
- } from "../chunk-OGRRHQQW.js";
5
+ parseWhatsAppFormatting,
6
+ useIsDarkTheme
7
+ } from "../chunk-WCBDXZ3X.js";
7
8
 
8
9
  // src/flows/FlowNodeCard.tsx
9
10
  import { Handle, Position } from "@xyflow/react";
10
- import { MessageCircleQuestion, GitBranch, Zap, ListTree, Diamond, AlertTriangle, AlertCircle, Headset, Clock3, ShoppingBag } from "lucide-react";
11
+ import {
12
+ MessageCircleQuestion,
13
+ GitBranch,
14
+ Zap,
15
+ ListTree,
16
+ Diamond,
17
+ AlertTriangle,
18
+ AlertCircle,
19
+ Headset,
20
+ Clock3,
21
+ ShoppingBag
22
+ } from "lucide-react";
11
23
  import { jsx, jsxs } from "react/jsx-runtime";
12
24
  var NODE_TYPE_COLOR = {
13
25
  question: "border-blue-300 bg-blue-50 dark:bg-blue-950/40 dark:border-blue-800",
@@ -30,6 +42,7 @@ var DEFAULT_ACTION_KIND_ICON = {
30
42
  };
31
43
  function nodeLabel(node, labels) {
32
44
  if (!node) return "\u2014";
45
+ if (node.label) return node.label;
33
46
  if (node.type === "action") {
34
47
  const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
35
48
  return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
@@ -59,7 +72,13 @@ function sourceRows(node, labels) {
59
72
  }
60
73
  function SourceRow({ label, isDefault, handleId }) {
61
74
  return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-600 bg-white/70 dark:bg-gray-900/40 px-2 py-1 pr-3", children: [
62
- /* @__PURE__ */ jsx("span", { className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`, children: label }),
75
+ /* @__PURE__ */ jsx(
76
+ "span",
77
+ {
78
+ className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`,
79
+ children: label
80
+ }
81
+ ),
63
82
  /* @__PURE__ */ jsx(
64
83
  Handle,
65
84
  {
@@ -73,7 +92,7 @@ function SourceRow({ label, isDefault, handleId }) {
73
92
  ] });
74
93
  }
75
94
  function FlowNodeCard({ data }) {
76
- const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } = data;
95
+ const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect } = data;
77
96
  const label = nodeLabel(node, labels);
78
97
  const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
79
98
  const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
@@ -83,15 +102,15 @@ function FlowNodeCard({ data }) {
83
102
  return /* @__PURE__ */ jsxs(
84
103
  "div",
85
104
  {
86
- title: label,
87
- className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""}`,
105
+ "data-cv-tooltip": isDetached ? labels.detachedNodeTooltip : label,
106
+ 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" : ""}`,
88
107
  onClick: () => onSelect(node.id),
89
108
  children: [
90
109
  /* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
91
110
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
92
111
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs", children: [
93
112
  /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
94
- isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", title: labels.startNodeTooltip }),
113
+ isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", "data-cv-tooltip": labels.startNodeTooltip }),
95
114
  labels.legend[node.type]
96
115
  ] }),
97
116
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -100,7 +119,7 @@ function FlowNodeCard({ data }) {
100
119
  liveCount > 0 && /* @__PURE__ */ jsx(
101
120
  "span",
102
121
  {
103
- title: labels.liveCountTooltip(liveCount),
122
+ "data-cv-tooltip": labels.liveCountTooltip(liveCount),
104
123
  className: "font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs",
105
124
  children: liveCount
106
125
  }
@@ -129,12 +148,14 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
129
148
  condition: "Condi\xE7\xE3o"
130
149
  },
131
150
  startNodeTooltip: "In\xEDcio do fluxo",
151
+ 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.",
132
152
  liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
133
153
  edgeFallbackLabel: "outro",
134
154
  actionKindLabels: {
135
155
  handoff: "Encaminhar para atendimento",
136
156
  rate_limited_handoff: "Encaminhar (limite de simula\xE7\xF5es atingido)",
137
- send_product_list: "Enviar cat\xE1logo de produtos"
157
+ send_product_list: "Enviar cat\xE1logo de produtos",
158
+ send_media: "Enviar arquivos da biblioteca"
138
159
  },
139
160
  conditionOperatorLabels: {
140
161
  ">": "maior que",
@@ -156,10 +177,15 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
156
177
  nodePanel: {
157
178
  title: "Editar n\xF3",
158
179
  contextKey: "Chave (contexto)",
180
+ nodeName: "Nome do n\xF3 (opcional)",
181
+ nodeNamePlaceholder: "Ex.: Enviar tabela de pre\xE7os",
182
+ nodeNameHint: "S\xF3 aparece no editor \u2014 o cliente n\xE3o v\xEA.",
159
183
  questionType: "Tipo de resposta",
160
184
  question: "Texto da pergunta",
161
185
  options: "Op\xE7\xF5es (choice)",
162
186
  addOption: "Adicionar op\xE7\xE3o",
187
+ removeOption: "Remover op\xE7\xE3o",
188
+ close: "Fechar painel",
163
189
  optionId: "Valor",
164
190
  optionLabel: "Texto exibido",
165
191
  next: "Pr\xF3ximo n\xF3",
@@ -189,7 +215,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
189
215
  conditionValue: "Valor de compara\xE7\xE3o",
190
216
  conditionTrue: "Se verdadeiro \u2192",
191
217
  conditionFalse: "Se falso \u2192",
192
- conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192"
218
+ conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192",
219
+ media: "Arquivos enviados neste ponto",
220
+ mediaUnavailable: "A biblioteca de arquivos n\xE3o est\xE1 dispon\xEDvel neste painel."
193
221
  },
194
222
  palette: {
195
223
  title: "Adicionar ao fluxo",
@@ -201,7 +229,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
201
229
  },
202
230
  flowMap: {
203
231
  nodeCount: (count) => `${count} n\xF3(s)`,
204
- openFlow: "Abrir fluxo"
232
+ openFlow: "Abrir fluxo",
233
+ toggleToMap: "Mapa de fluxos",
234
+ toggleToDetail: "Voltar ao editor"
205
235
  },
206
236
  flowGroup: {
207
237
  focus: "Focar neste fluxo",
@@ -210,6 +240,59 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
210
240
  crossFlowPortal: {
211
241
  tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
212
242
  goesTo: (label) => `\u21AA Vai para: ${label}`
243
+ },
244
+ collectionChain: {
245
+ feeds: (label) => `Alimenta: ${label}`
246
+ },
247
+ validation: {
248
+ title: "Antes de publicar",
249
+ errors: (count) => `${count} erro(s) \u2014 corrija antes de salvar`,
250
+ warnings: (count) => `${count} aviso(s)`,
251
+ noStart: "O fluxo precisa de um n\xF3 inicial v\xE1lido.",
252
+ brokenRef: (from, to) => `N\xF3 "${from}": liga\xE7\xE3o aponta para "${to}", que n\xE3o existe.`,
253
+ choiceWithoutOptions: (id) => `N\xF3 "${id}": escolha sem nenhuma op\xE7\xE3o.`,
254
+ duplicatedOptionId: (id, optionId) => `N\xF3 "${id}": valor de op\xE7\xE3o "${optionId}" duplicado.`,
255
+ optionWithoutTarget: (id, label) => `N\xF3 "${id}": op\xE7\xE3o "${label}" n\xE3o tem destino definido.`,
256
+ tooManyOptions: (id, count) => `N\xF3 "${id}": ${count} op\xE7\xF5es \u2014 o WhatsApp aceita no m\xE1ximo 10 em lista.`,
257
+ buttonTitleTooLong: (id, label) => `N\xF3 "${id}": bot\xE3o "${label}" passa de 20 caracteres.`,
258
+ listTitleTooLong: (id, label) => `N\xF3 "${id}": item de lista "${label}" passa de 24 caracteres.`,
259
+ bodyTooLong: (id) => `N\xF3 "${id}": texto passa de 1024 caracteres.`,
260
+ unreachable: (id) => `N\xF3 "${id}" \xE9 inalcan\xE7\xE1vel a partir do in\xEDcio do fluxo.`,
261
+ deadEndQuestion: (id) => `N\xF3 "${id}": pergunta sem pr\xF3ximo passo definido.`,
262
+ conditionIncomplete: (id) => `N\xF3 "${id}": condi\xE7\xE3o incompleta \u2014 defina vari\xE1vel, operador e valor.`,
263
+ conditionBranchMissing: (id, branch) => `N\xF3 "${id}": ramo "${branch === "true" ? "Verdadeiro" : "Falso"}" sem destino definido.`
264
+ },
265
+ workspace: {
266
+ title: "Fluxos do Bot",
267
+ subtitle: "Blueprint visual dos fluxos de conversa, sincronizado com o que est\xE1 em produ\xE7\xE3o.",
268
+ loading: "Carregando fluxos\u2026",
269
+ loadError: "N\xE3o foi poss\xEDvel carregar os fluxos.",
270
+ saveGraph: "Publicar altera\xE7\xF5es",
271
+ saving: "Publicando\u2026",
272
+ saveSuccess: "Fluxo publicado! O bot j\xE1 est\xE1 usando a vers\xE3o nova.",
273
+ saveError: "N\xE3o foi poss\xEDvel salvar \u2014 verifique se todos os destinos apontam para n\xF3s existentes.",
274
+ organize: "Organizar",
275
+ organizeTooltip: "Reorganiza os n\xF3s automaticamente e salva as novas posi\xE7\xF5es",
276
+ discardChanges: "Desfazer altera\xE7\xF5es",
277
+ discardTooltip: "Devolve os fluxos abertos ao que est\xE1 publicado, descartando o que n\xE3o foi salvo",
278
+ discardConfirm: "Descartar todas as altera\xE7\xF5es n\xE3o publicadas e voltar ao fluxo que est\xE1 no ar?",
279
+ unsavedChangesConfirm: "Voc\xEA tem altera\xE7\xF5es n\xE3o publicadas neste fluxo. Trocar de fluxo agora descarta essas edi\xE7\xF5es. Continuar?"
280
+ },
281
+ flowManager: {
282
+ newFlow: "Novo fluxo",
283
+ createTitle: "Criar novo fluxo",
284
+ key: "Identificador \xFAnico",
285
+ keyHint: "letras min\xFAsculas, n\xFAmeros e _ (ex.: promocoes_semana)",
286
+ keyInvalid: "Use apenas letras min\xFAsculas, n\xFAmeros e _ (2 a 40 caracteres)",
287
+ label: "Nome exibido",
288
+ showInMenu: "Exibir como op\xE7\xE3o no menu principal do bot",
289
+ menuOptionLabel: "Texto da op\xE7\xE3o no menu",
290
+ create: "Criar fluxo",
291
+ creating: "Criando\u2026",
292
+ deleteFlow: "Excluir fluxo",
293
+ deleteConfirm: (label) => `Excluir o fluxo "${label}"? Esta a\xE7\xE3o n\xE3o pode ser desfeita.`,
294
+ createError: "N\xE3o foi poss\xEDvel criar o fluxo.",
295
+ deleteError: "N\xE3o foi poss\xEDvel excluir o fluxo."
213
296
  }
214
297
  };
215
298
  function mergeFlowEditorLabels(override) {
@@ -228,7 +311,11 @@ function mergeFlowEditorLabels(override) {
228
311
  palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
229
312
  flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
230
313
  flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
231
- crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal }
314
+ crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal },
315
+ collectionChain: { ...DEFAULT_FLOW_EDITOR_LABELS.collectionChain, ...override.collectionChain },
316
+ validation: { ...DEFAULT_FLOW_EDITOR_LABELS.validation, ...override.validation },
317
+ workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, ...override.workspace },
318
+ flowManager: { ...DEFAULT_FLOW_EDITOR_LABELS.flowManager, ...override.flowManager }
232
319
  };
233
320
  }
234
321
 
@@ -241,12 +328,14 @@ function FlowMapNode({ data }) {
241
328
  /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
242
329
  /* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
243
330
  /* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
244
- isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", title: labels.startNodeTooltip })
331
+ isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", "data-cv-tooltip": labels.startNodeTooltip })
245
332
  ] }),
246
333
  /* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
247
334
  /* @__PURE__ */ jsxs2(
248
335
  "button",
249
336
  {
337
+ "data-cv-tooltip": labels.flowMap.openFlow,
338
+ "aria-label": labels.flowMap.openFlow,
250
339
  type: "button",
251
340
  onClick: onOpen,
252
341
  className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
@@ -268,12 +357,9 @@ import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
268
357
  import "@xyflow/react/dist/style.css";
269
358
 
270
359
  // src/flows/flowGraph.ts
360
+ import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
271
361
  var CONDITION_OPERATORS = [">", ">=", "<", "<=", "==", "!=", "contains"];
272
- var BUILT_IN_ACTION_KINDS = {
273
- HANDOFF: "handoff",
274
- RATE_LIMITED_HANDOFF: "rate_limited_handoff",
275
- SEND_PRODUCT_LIST: "send_product_list"
276
- };
362
+ var BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND;
277
363
  var CROSS_FLOW_PREFIX = "flow:";
278
364
  var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
279
365
  var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
@@ -529,18 +615,20 @@ function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverride })
529
615
  const isDark = useIsDarkTheme();
530
616
  const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey]);
531
617
  const nodes = useMemo(
532
- () => Object.values(graphs).map((g) => ({
533
- id: g.key,
534
- type: "flowMapNode",
535
- position: positions[g.key] ?? { x: 0, y: 0 },
536
- data: {
537
- label: g.label,
538
- nodeCount: Object.keys(g.nodes).length,
539
- isRoot: g.key === rootKey,
540
- labels,
541
- onOpen: () => onOpenFlow(g.key)
542
- }
543
- })),
618
+ () => Object.values(graphs).map(
619
+ (g) => ({
620
+ id: g.key,
621
+ type: "flowMapNode",
622
+ position: positions[g.key] ?? { x: 0, y: 0 },
623
+ data: {
624
+ label: g.label,
625
+ nodeCount: Object.keys(g.nodes).length,
626
+ isRoot: g.key === rootKey,
627
+ labels,
628
+ onOpen: () => onOpenFlow(g.key)
629
+ }
630
+ })
631
+ ),
544
632
  [graphs, positions, rootKey, onOpenFlow, labels]
545
633
  );
546
634
  const edges = useMemo(() => {
@@ -592,8 +680,28 @@ function FlowGroupHeader({ data }) {
592
680
  const { label, labels = DEFAULT_FLOW_EDITOR_LABELS, onFocus, onClose } = data;
593
681
  return /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 rounded-full border border-cyan-300 dark:border-cyan-700 bg-cyan-50 dark:bg-cyan-950/50 px-3 py-1 text-xs font-medium text-cyan-800 dark:text-cyan-200 shadow-sm whitespace-nowrap", children: [
594
682
  /* @__PURE__ */ jsx5("span", { children: label }),
595
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: onFocus, title: labels.flowGroup.focus, className: "hover:text-blue-600 dark:hover:text-blue-400", children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 }) }),
596
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: onClose, title: labels.flowGroup.close, className: "hover:text-red-600 dark:hover:text-red-400", children: /* @__PURE__ */ jsx5(X, { size: 12 }) })
683
+ /* @__PURE__ */ jsx5(
684
+ "button",
685
+ {
686
+ type: "button",
687
+ onClick: onFocus,
688
+ "data-cv-tooltip": labels.flowGroup.focus,
689
+ "aria-label": labels.flowGroup.focus,
690
+ className: "hover:text-blue-600 dark:hover:text-blue-400",
691
+ children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 })
692
+ }
693
+ ),
694
+ /* @__PURE__ */ jsx5(
695
+ "button",
696
+ {
697
+ type: "button",
698
+ onClick: onClose,
699
+ "data-cv-tooltip": labels.flowGroup.close,
700
+ "aria-label": labels.flowGroup.close,
701
+ className: "hover:text-red-600 dark:hover:text-red-400",
702
+ children: /* @__PURE__ */ jsx5(X, { size: 12 })
703
+ }
704
+ )
597
705
  ] });
598
706
  }
599
707
  var flowGroupHeaderNodeTypes = { flowGroupHeader: FlowGroupHeader };
@@ -608,7 +716,8 @@ function FlowPortalNode({ data }) {
608
716
  "button",
609
717
  {
610
718
  type: "button",
611
- title: labels.crossFlowPortal.tooltip,
719
+ "data-cv-tooltip": labels.crossFlowPortal.tooltip,
720
+ "aria-label": labels.crossFlowPortal.tooltip,
612
721
  onClick: onNavigate,
613
722
  className: "relative flex items-center gap-1.5 rounded-full border-2 border-dashed border-cyan-400 dark:border-cyan-600 bg-cyan-50 dark:bg-cyan-950/40 px-3 py-1.5 text-xs font-medium text-cyan-700 dark:text-cyan-300 hover:bg-cyan-100 dark:hover:bg-cyan-950/70 transition-colors cursor-pointer",
614
723
  children: [
@@ -653,6 +762,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
653
762
  /* @__PURE__ */ jsxs6(
654
763
  "button",
655
764
  {
765
+ "data-cv-tooltip": labels.palette.title,
766
+ "aria-label": labels.palette.title,
656
767
  onClick: () => setOpen((v) => !v),
657
768
  className: "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700",
658
769
  children: [
@@ -667,6 +778,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
667
778
  /* @__PURE__ */ jsxs6(
668
779
  "button",
669
780
  {
781
+ "data-cv-tooltip": labels.palette.question,
782
+ "aria-label": labels.palette.question,
670
783
  onMouseEnter: () => setSubmenu("question"),
671
784
  onClick: () => setSubmenu(submenu === "question" ? null : "question"),
672
785
  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",
@@ -683,6 +796,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
683
796
  submenu === "question" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: QUESTION_TYPES.map((qt) => /* @__PURE__ */ jsx7(
684
797
  "button",
685
798
  {
799
+ "data-cv-tooltip": labels.questionTypeLabels[qt],
800
+ "aria-label": labels.questionTypeLabels[qt],
686
801
  onClick: () => select({ kind: "question", questionType: qt }),
687
802
  className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
688
803
  children: labels.questionTypeLabels[qt]
@@ -693,6 +808,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
693
808
  /* @__PURE__ */ jsxs6(
694
809
  "button",
695
810
  {
811
+ "data-cv-tooltip": labels.palette.decision,
812
+ "aria-label": labels.palette.decision,
696
813
  onMouseEnter: () => setSubmenu(null),
697
814
  onClick: () => select({ kind: "decision" }),
698
815
  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",
@@ -709,7 +826,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
709
826
  onMouseEnter: () => setSubmenu(null),
710
827
  onClick: () => select({ kind: "condition" }),
711
828
  className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
712
- title: labels.palette.conditionHint,
829
+ "data-cv-tooltip": labels.palette.conditionHint,
830
+ "aria-label": labels.palette.conditionHint,
713
831
  children: [
714
832
  /* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
715
833
  " ",
@@ -721,6 +839,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
721
839
  /* @__PURE__ */ jsxs6(
722
840
  "button",
723
841
  {
842
+ "data-cv-tooltip": labels.palette.action,
843
+ "aria-label": labels.palette.action,
724
844
  onMouseEnter: () => setSubmenu("action"),
725
845
  onClick: () => setSubmenu(submenu === "action" ? null : "action"),
726
846
  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",
@@ -737,6 +857,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
737
857
  submenu === "action" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: resolvedActionOptions.map((option) => /* @__PURE__ */ jsx7(
738
858
  "button",
739
859
  {
860
+ "data-cv-tooltip": option.label,
861
+ "aria-label": option.label,
740
862
  onClick: () => select({ kind: "action", actionKind: option.actionKind }),
741
863
  className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
742
864
  children: option.label
@@ -755,7 +877,11 @@ import { Plus as Plus2, Trash2, Save, X as X2, AlertTriangle as AlertTriangle2,
755
877
  // src/flows/FlowWhatsAppPreview.tsx
756
878
  import { List } from "lucide-react";
757
879
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
758
- function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel }) {
880
+ function FlowWhatsAppPreview({
881
+ body,
882
+ options,
883
+ labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel
884
+ }) {
759
885
  if (!body && (!options || options.length === 0)) {
760
886
  return /* @__PURE__ */ jsx8("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
761
887
  }
@@ -771,7 +897,14 @@ function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITOR_LABEL
771
897
  labels.previewListButton
772
898
  ] }) })
773
899
  ] }),
774
- hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8("div", { className: "rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm", children: label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption }) }, id)) }),
900
+ hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8(
901
+ "div",
902
+ {
903
+ 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",
904
+ children: label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
905
+ },
906
+ id
907
+ )) }),
775
908
  hasOptions && !usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1.5 rounded-lg bg-white dark:bg-gray-700 shadow-sm divide-y divide-gray-100 dark:divide-gray-600 overflow-hidden", children: options.slice(0, WHATSAPP_LIMITS.MAX_LIST_ROWS).map(([id, label]) => /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 px-3 py-1.5 text-sm text-gray-800 dark:text-gray-100", children: [
776
909
  /* @__PURE__ */ jsx8("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
777
910
  label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
@@ -833,17 +966,23 @@ function FlowNodePanel({
833
966
  onClose,
834
967
  onChange,
835
968
  onDelete,
836
- labels: labelsOverride
969
+ labels: labelsOverride,
970
+ renderMediaPicker
837
971
  }) {
838
972
  const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
839
973
  const [draft, setDraft] = useState2(node);
840
974
  const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id);
841
975
  const isFixedLogic = draft.type === "entrada_choice";
842
976
  const isAction = draft.type === "action";
977
+ const isSendMedia = isAction && draft.actionKind === BUILT_IN_ACTION_KINDS.SEND_MEDIA;
843
978
  const isCondition = draft.type === "condition";
844
979
  const isStart = graph.startNodeId === node.id;
845
980
  const nodeIssues = issues.filter((i) => i.nodeId === node.id);
846
- const knownContextKeys = [...new Set(Object.values(graph.nodes).map((n) => n.contextKey).filter((key) => !!key))];
981
+ const knownContextKeys = [
982
+ ...new Set(
983
+ Object.values(graph.nodes).map((n) => n.contextKey).filter((key) => !!key)
984
+ )
985
+ ];
847
986
  const conditionAnswerIds = isCondition ? ["true", "false"] : (draft.options ?? []).map(([id]) => id);
848
987
  function updateNextString(value) {
849
988
  setDraft((prev) => ({ ...prev, next: value }));
@@ -870,7 +1009,10 @@ function FlowNodePanel({
870
1009
  });
871
1010
  }
872
1011
  function addOption() {
873
- setDraft((prev) => ({ ...prev, options: [...prev.options ?? [], [String((prev.options?.length ?? 0) + 1), "Nova op\xE7\xE3o"]] }));
1012
+ setDraft((prev) => ({
1013
+ ...prev,
1014
+ options: [...prev.options ?? [], [String((prev.options?.length ?? 0) + 1), "Nova op\xE7\xE3o"]]
1015
+ }));
874
1016
  }
875
1017
  function removeOption(index) {
876
1018
  setDraft((prev) => ({ ...prev, options: (prev.options ?? []).filter((_, i) => i !== index) }));
@@ -884,16 +1026,36 @@ function FlowNodePanel({
884
1026
  return /* @__PURE__ */ jsxs8("div", { className: "fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col", children: [
885
1027
  /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
886
1028
  /* @__PURE__ */ jsx9("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
887
- /* @__PURE__ */ jsx9("button", { onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx9(X2, { size: 18 }) })
1029
+ /* @__PURE__ */ jsx9("button", { "data-cv-tooltip": labels.nodePanel.close, "aria-label": labels.nodePanel.close, onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx9(X2, { size: 18 }) })
888
1030
  ] }),
889
1031
  /* @__PURE__ */ jsxs8("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
890
1032
  nodeIssues.length > 0 && /* @__PURE__ */ jsx9("div", { className: "rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-2.5 space-y-1.5", children: nodeIssues.map((issue, i) => /* @__PURE__ */ jsx9(IssueRow, { issue }, i)) }),
891
1033
  isFixedLogic && /* @__PURE__ */ jsx9("p", { className: "text-xs text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30 rounded-lg p-2", children: labels.nodePanel.fixedLogicNotice }),
892
1034
  isAction && /* @__PURE__ */ jsx9("p", { className: "text-xs text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/30 rounded-lg p-2", children: labels.nodePanel.actionNotice }),
893
1035
  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 }),
1036
+ /* @__PURE__ */ jsxs8("div", { children: [
1037
+ /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.nodeName }),
1038
+ /* @__PURE__ */ jsx9(
1039
+ "input",
1040
+ {
1041
+ value: draft.label ?? "",
1042
+ placeholder: labels.nodePanel.nodeNamePlaceholder,
1043
+ onChange: (event) => setDraft((prev) => ({ ...prev, label: event.target.value || void 0 })),
1044
+ className: `w-full mt-1 ${INPUT_CLASSNAME}`
1045
+ }
1046
+ ),
1047
+ /* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mt-1", children: labels.nodePanel.nodeNameHint })
1048
+ ] }),
894
1049
  draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
895
1050
  /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
896
- /* @__PURE__ */ jsx9("input", { value: draft.contextKey, disabled: true, className: "w-full mt-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-3 py-2.5 text-sm text-gray-500" })
1051
+ /* @__PURE__ */ jsx9(
1052
+ "input",
1053
+ {
1054
+ value: draft.contextKey,
1055
+ disabled: true,
1056
+ 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"
1057
+ }
1058
+ )
897
1059
  ] }),
898
1060
  !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs8("div", { children: [
899
1061
  /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
@@ -937,7 +1099,10 @@ function FlowNodePanel({
937
1099
  "select",
938
1100
  {
939
1101
  value: draft.conditionOperator ?? ">",
940
- onChange: (e) => setDraft((prev) => ({ ...prev, conditionOperator: e.target.value })),
1102
+ onChange: (e) => setDraft((prev) => ({
1103
+ ...prev,
1104
+ conditionOperator: e.target.value
1105
+ })),
941
1106
  className: `w-full mt-1 ${SELECT_CLASSNAME}`,
942
1107
  children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx9("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
943
1108
  }
@@ -965,7 +1130,11 @@ function FlowNodePanel({
965
1130
  labels
966
1131
  }
967
1132
  ),
968
- isAction && draft.actionKind !== "send_product_list" && /* @__PURE__ */ jsx9(
1133
+ isSendMedia && /* @__PURE__ */ jsxs8("div", { children: [
1134
+ /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.media }),
1135
+ /* @__PURE__ */ jsx9("div", { className: "mt-1", children: renderMediaPicker?.(node, graph) ?? /* @__PURE__ */ jsx9("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
1136
+ ] }),
1137
+ isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx9(
969
1138
  WhatsAppTextField,
970
1139
  {
971
1140
  label: labels.nodePanel.directMessage,
@@ -978,7 +1147,7 @@ function FlowNodePanel({
978
1147
  (draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs8("div", { children: [
979
1148
  /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between mb-1.5", children: [
980
1149
  /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
981
- /* @__PURE__ */ jsxs8("button", { onClick: addOption, className: "text-xs text-blue-600 hover:underline flex items-center gap-1", children: [
1150
+ /* @__PURE__ */ jsxs8("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: [
982
1151
  /* @__PURE__ */ jsx9(Plus2, { size: 12 }),
983
1152
  " ",
984
1153
  labels.nodePanel.addOption
@@ -1003,7 +1172,7 @@ function FlowNodePanel({
1003
1172
  className: `flex-1 ${INPUT_CLASSNAME}`
1004
1173
  }
1005
1174
  ),
1006
- /* @__PURE__ */ jsx9("button", { onClick: () => removeOption(i), className: "text-gray-400 hover:text-red-600", children: /* @__PURE__ */ jsx9(Trash2, { size: 14 }) })
1175
+ /* @__PURE__ */ jsx9("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__ */ jsx9(Trash2, { size: 14 }) })
1007
1176
  ] }, i)) })
1008
1177
  ] }),
1009
1178
  !isAction && /* @__PURE__ */ jsxs8("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
@@ -1055,26 +1224,1107 @@ function FlowNodePanel({
1055
1224
  ] })
1056
1225
  ] }),
1057
1226
  /* @__PURE__ */ jsxs8("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
1058
- /* @__PURE__ */ jsxs8("button", { onClick: () => onChange(draft), className: "flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700", children: [
1059
- /* @__PURE__ */ jsx9(Save, { size: 14 }),
1060
- " ",
1061
- labels.nodePanel.save
1062
- ] }),
1227
+ /* @__PURE__ */ jsxs8(
1228
+ "button",
1229
+ {
1230
+ "data-cv-tooltip": labels.nodePanel.save,
1231
+ "aria-label": labels.nodePanel.save,
1232
+ onClick: () => onChange(draft),
1233
+ 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",
1234
+ children: [
1235
+ /* @__PURE__ */ jsx9(Save, { size: 14 }),
1236
+ " ",
1237
+ labels.nodePanel.save
1238
+ ]
1239
+ }
1240
+ ),
1063
1241
  !isStart && /* @__PURE__ */ jsx9(
1064
1242
  "button",
1065
1243
  {
1066
1244
  onClick: () => {
1067
1245
  if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id);
1068
1246
  },
1069
- title: labels.nodePanel.delete,
1247
+ "data-cv-tooltip": labels.nodePanel.delete,
1248
+ "aria-label": labels.nodePanel.delete,
1070
1249
  className: "px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg",
1071
1250
  children: /* @__PURE__ */ jsx9(Trash2, { size: 14 })
1072
1251
  }
1073
1252
  ),
1074
- /* @__PURE__ */ jsx9("button", { onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
1253
+ /* @__PURE__ */ jsx9("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 })
1254
+ ] })
1255
+ ] });
1256
+ }
1257
+
1258
+ // src/flows/FlowsWorkspace.tsx
1259
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
1260
+ import {
1261
+ ReactFlow as ReactFlow2,
1262
+ Background as Background2,
1263
+ Controls as Controls2,
1264
+ MiniMap,
1265
+ MarkerType as MarkerType2,
1266
+ applyNodeChanges
1267
+ } from "@xyflow/react";
1268
+ import "@xyflow/react/dist/style.css";
1269
+ import { Plus as Plus3, Trash2 as Trash22, LayoutGrid, AlertTriangle as AlertTriangle3, AlertCircle as AlertCircle3, Save as Save2, Undo2, Map as MapIcon, Workflow } from "lucide-react";
1270
+
1271
+ // src/flows/flowEditorOps.ts
1272
+ var NAMESPACE_SEPARATOR = "::";
1273
+ function namespaceNodeId(flowKey, nodeId) {
1274
+ return `${flowKey}${NAMESPACE_SEPARATOR}${nodeId}`;
1275
+ }
1276
+ function parseNamespacedId(value) {
1277
+ const index = value.indexOf(NAMESPACE_SEPARATOR);
1278
+ if (index === -1) return { flowKey: "", nodeId: value };
1279
+ return { flowKey: value.slice(0, index), nodeId: value.slice(index + NAMESPACE_SEPARATOR.length) };
1280
+ }
1281
+ function removeNodeAndCleanRefs(nodes, removedId) {
1282
+ const remaining = {};
1283
+ for (const [id, node] of Object.entries(nodes)) {
1284
+ if (id === removedId) continue;
1285
+ remaining[id] = { ...node, next: cleanNext(node.next, removedId) };
1286
+ }
1287
+ return remaining;
1288
+ }
1289
+ function cleanNext(next, removedId) {
1290
+ if (next === void 0) return void 0;
1291
+ if (typeof next === "string") return next === removedId ? "" : next;
1292
+ const byAnswer = {};
1293
+ for (const [answer, target] of Object.entries(next.byAnswer ?? {})) {
1294
+ byAnswer[answer] = target === removedId ? "" : target;
1295
+ }
1296
+ return { byAnswer, default: next.default === removedId ? "" : next.default ?? "" };
1297
+ }
1298
+ function mergedFlowKeysFrom(rootKey, graphs) {
1299
+ const visited = /* @__PURE__ */ new Set();
1300
+ const queue = [rootKey];
1301
+ while (queue.length > 0) {
1302
+ const key = queue.shift();
1303
+ if (visited.has(key) || !graphs[key]) continue;
1304
+ visited.add(key);
1305
+ for (const target of crossFlowTargetsOf(graphs[key])) {
1306
+ if (!visited.has(target) && graphs[target]) queue.push(target);
1307
+ }
1308
+ }
1309
+ return [...visited];
1310
+ }
1311
+ function resolveConnection(params) {
1312
+ const { source, target, sourceHandle } = params.connection;
1313
+ if (!source || !target || source === target) return void 0;
1314
+ const sourceRef = parseNamespacedId(source);
1315
+ const targetRef = parseNamespacedId(target);
1316
+ let targetValue;
1317
+ if (targetRef.flowKey === sourceRef.flowKey) {
1318
+ targetValue = targetRef.nodeId;
1319
+ } else {
1320
+ const targetGraph = params.graphs[targetRef.flowKey];
1321
+ if (!targetGraph || targetGraph.startNodeId !== targetRef.nodeId) return void 0;
1322
+ targetValue = `${CROSS_FLOW_PREFIX}${targetRef.flowKey}`;
1323
+ }
1324
+ return {
1325
+ flowKey: sourceRef.flowKey,
1326
+ nodeId: sourceRef.nodeId,
1327
+ handle: sourceHandle ?? "next",
1328
+ targetValue
1329
+ };
1330
+ }
1331
+ function applyConnection(node, resolved) {
1332
+ const currentNext = typeof node.next === "object" && node.next ? node.next : void 0;
1333
+ if (resolved.handle === "next") return { ...node, next: resolved.targetValue };
1334
+ if (resolved.handle === "__default") {
1335
+ return { ...node, next: { byAnswer: currentNext?.byAnswer ?? {}, default: resolved.targetValue } };
1336
+ }
1337
+ return {
1338
+ ...node,
1339
+ next: {
1340
+ byAnswer: { ...currentNext?.byAnswer ?? {}, [resolved.handle]: resolved.targetValue },
1341
+ default: currentNext?.default ?? ""
1342
+ }
1343
+ };
1344
+ }
1345
+ function isGraphDirty(working, published) {
1346
+ if (!working || !published) return false;
1347
+ return JSON.stringify(working) !== JSON.stringify(published);
1348
+ }
1349
+
1350
+ // src/flows/flowCanvasModel.ts
1351
+ var COLUMN_GAP = 360;
1352
+ var ROW_GAP = 40;
1353
+ var CHAIN_LABEL_ROOM = 24;
1354
+ function portalNodeId(sourceNodeId, target) {
1355
+ return `__portal__${sourceNodeId}__${target}`;
1356
+ }
1357
+ function chainFrameNodeId(actionNodeId) {
1358
+ return `__chain__${actionNodeId}`;
1359
+ }
1360
+ var GROUP_HEADER_NODE_ID = "__group_header__";
1361
+ function countLiveByNode(params) {
1362
+ const { flowKey, rootFlowKey, positions } = params;
1363
+ const isRoot = flowKey === rootFlowKey;
1364
+ const counts = {};
1365
+ for (const position of positions ?? []) {
1366
+ if (position.flow !== flowKey && !isRoot) continue;
1367
+ const nodeId = isRoot ? position.menuNodeId : position.nodeId;
1368
+ if (!nodeId) continue;
1369
+ counts[nodeId] = (counts[nodeId] ?? 0) + 1;
1370
+ }
1371
+ return counts;
1372
+ }
1373
+ function computeMergedLayout(params) {
1374
+ const { openKeys, graphs, primaryFlowKey } = params;
1375
+ const open = new Set(openKeys);
1376
+ const nodeById = /* @__PURE__ */ new Map();
1377
+ for (const key of openKeys) {
1378
+ for (const node of Object.values(graphs[key]?.nodes ?? {})) {
1379
+ nodeById.set(namespaceNodeId(key, node.id), node);
1380
+ }
1381
+ }
1382
+ function forwardEdges(flowKey, node) {
1383
+ const result = [];
1384
+ for (const { target } of targetsOf(node)) {
1385
+ if (isCrossFlowTarget(target)) {
1386
+ const targetKey = crossFlowKey(target);
1387
+ const targetGraph = open.has(targetKey) ? graphs[targetKey] : void 0;
1388
+ if (targetGraph) result.push(namespaceNodeId(targetKey, targetGraph.startNodeId));
1389
+ } else if (graphs[flowKey]?.nodes[target]) {
1390
+ result.push(namespaceNodeId(flowKey, target));
1391
+ }
1392
+ }
1393
+ return result;
1394
+ }
1395
+ const rank = /* @__PURE__ */ new Map();
1396
+ const primaryGraph = graphs[primaryFlowKey];
1397
+ const rootId = primaryGraph ? namespaceNodeId(primaryFlowKey, primaryGraph.startNodeId) : void 0;
1398
+ if (rootId && nodeById.has(rootId)) {
1399
+ rank.set(rootId, 0);
1400
+ const queue = [rootId];
1401
+ while (queue.length > 0) {
1402
+ const id = queue.shift();
1403
+ const node = nodeById.get(id);
1404
+ if (!node) continue;
1405
+ for (const nextId of forwardEdges(parseNamespacedId(id).flowKey, node)) {
1406
+ if (!rank.has(nextId)) {
1407
+ rank.set(nextId, rank.get(id) + 1);
1408
+ queue.push(nextId);
1409
+ }
1410
+ }
1411
+ }
1412
+ }
1413
+ const strayRank = Math.max(0, ...rank.values()) + 1;
1414
+ for (const id of nodeById.keys()) {
1415
+ if (!rank.has(id)) rank.set(id, strayRank);
1416
+ }
1417
+ const layers = /* @__PURE__ */ new Map();
1418
+ for (const [id, value] of rank) {
1419
+ const layer = layers.get(value);
1420
+ if (layer) layer.push(id);
1421
+ else layers.set(value, [id]);
1422
+ }
1423
+ const positions = /* @__PURE__ */ new Map();
1424
+ for (const value of [...layers.keys()].sort((a, b) => a - b)) {
1425
+ let cursorY = 0;
1426
+ for (const id of layers.get(value)) {
1427
+ positions.set(id, { x: value * COLUMN_GAP, y: cursorY });
1428
+ cursorY += estimateNodeHeight(nodeById.get(id)) + ROW_GAP;
1429
+ }
1430
+ }
1431
+ return positions;
1432
+ }
1433
+ function buildFlowEdges(params) {
1434
+ const { openKeys, graphs, rootFlowKey, livePositions } = params;
1435
+ const open = new Set(openKeys);
1436
+ const edges = [];
1437
+ for (const flowKey of openKeys) {
1438
+ const graph = graphs[flowKey];
1439
+ if (!graph) continue;
1440
+ const liveCounts = countLiveByNode({ flowKey, rootFlowKey, positions: livePositions });
1441
+ for (const [nodeId, node] of Object.entries(graph.nodes)) {
1442
+ const live = (liveCounts[nodeId] ?? 0) > 0;
1443
+ for (const { target, optionId, isDefault } of targetsOf(node)) {
1444
+ if (!target) continue;
1445
+ const crossFlow = isCrossFlowTarget(target);
1446
+ let targetFlowKey = flowKey;
1447
+ let targetNodeId = target;
1448
+ if (crossFlow) {
1449
+ const wantedKey = crossFlowKey(target);
1450
+ const targetGraph = open.has(wantedKey) ? graphs[wantedKey] : void 0;
1451
+ if (targetGraph) {
1452
+ targetFlowKey = wantedKey;
1453
+ targetNodeId = targetGraph.startNodeId;
1454
+ } else {
1455
+ targetNodeId = portalNodeId(nodeId, target);
1456
+ }
1457
+ }
1458
+ const source = namespaceNodeId(flowKey, nodeId);
1459
+ const edgeTarget = namespaceNodeId(targetFlowKey, targetNodeId);
1460
+ if (isDefault) {
1461
+ edges.push({
1462
+ id: `${source}->${edgeTarget}-default`,
1463
+ source,
1464
+ target: edgeTarget,
1465
+ sourceHandle: "__default",
1466
+ kind: "fallback",
1467
+ crossFlow,
1468
+ // Fallback não anima: é o caminho que ninguém escolheu, e piscar sugeria que a conversa
1469
+ // estava passando por ali.
1470
+ live: false
1471
+ });
1472
+ continue;
1473
+ }
1474
+ if (optionId === void 0) {
1475
+ edges.push({ id: `${source}->${edgeTarget}`, source, target: edgeTarget, kind: "linear", crossFlow, live });
1476
+ continue;
1477
+ }
1478
+ edges.push({
1479
+ id: `${source}->${edgeTarget}-${optionId}`,
1480
+ source,
1481
+ target: edgeTarget,
1482
+ sourceHandle: optionId,
1483
+ kind: "branch",
1484
+ crossFlow,
1485
+ live
1486
+ });
1487
+ }
1488
+ }
1489
+ }
1490
+ return edges;
1491
+ }
1492
+ function detachedNodeIds(graph) {
1493
+ const connected = /* @__PURE__ */ new Set();
1494
+ for (const node of Object.values(graph.nodes)) {
1495
+ for (const { target } of targetsOf(node)) {
1496
+ if (!isCrossFlowTarget(target)) connected.add(target);
1497
+ }
1498
+ }
1499
+ const detached = /* @__PURE__ */ new Set();
1500
+ for (const nodeId of Object.keys(graph.nodes)) {
1501
+ if (nodeId !== graph.startNodeId && !connected.has(nodeId)) detached.add(nodeId);
1502
+ }
1503
+ return detached;
1504
+ }
1505
+ function chainFrameBounds(params) {
1506
+ const { nodeIds, graph, positionOf, padding } = params;
1507
+ const positions = nodeIds.map(positionOf);
1508
+ const minX = Math.min(...positions.map((each) => each.x));
1509
+ const maxX = Math.max(...positions.map((each) => each.x)) + NODE_CARD_WIDTH;
1510
+ const minY = Math.min(...positions.map((each) => each.y));
1511
+ const maxY = Math.max(...nodeIds.map((id, index) => positions[index].y + estimateNodeHeight(graph.nodes[id])));
1512
+ return {
1513
+ x: minX - padding,
1514
+ y: minY - padding - CHAIN_LABEL_ROOM,
1515
+ width: maxX - minX + padding * 2,
1516
+ height: maxY - minY + padding * 2 + CHAIN_LABEL_ROOM
1517
+ };
1518
+ }
1519
+ function newNodeFromSpec(spec, existingIds) {
1520
+ if (spec.kind === "question") {
1521
+ const id2 = slugifyNodeId("nova_pergunta", existingIds);
1522
+ return { id: id2, type: "question", questionType: spec.questionType, contextKey: id2, question: "" };
1523
+ }
1524
+ if (spec.kind === "decision") {
1525
+ const id2 = slugifyNodeId("nova_decisao", existingIds);
1526
+ return {
1527
+ id: id2,
1528
+ type: "question",
1529
+ questionType: "choice",
1530
+ contextKey: id2,
1531
+ question: "",
1532
+ options: [
1533
+ ["1", "Op\xE7\xE3o 1"],
1534
+ ["2", "Op\xE7\xE3o 2"]
1535
+ ]
1536
+ };
1537
+ }
1538
+ if (spec.kind === "condition") {
1539
+ const id2 = slugifyNodeId("nova_condicao", existingIds);
1540
+ return { id: id2, type: "condition", conditionOperator: ">" };
1541
+ }
1542
+ const id = slugifyNodeId("nova_acao", existingIds);
1543
+ return { id, type: "action", actionKind: spec.actionKind };
1544
+ }
1545
+
1546
+ // src/flows/FlowsWorkspace.tsx
1547
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1548
+ var RF_NODE_TYPES = {
1549
+ ...flowNodeTypes,
1550
+ ...flowPortalNodeTypes,
1551
+ ...flowGroupHeaderNodeTypes,
1552
+ ...flowGroupFrameNodeTypes
1553
+ };
1554
+ var CHAIN_FRAME_PADDING = 36;
1555
+ var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
1556
+ var EDGE_COLOR_LINEAR = "#94a3b8";
1557
+ var EDGE_COLOR_BRANCH = "#8b5cf6";
1558
+ var EDGE_COLOR_FALLBACK = "#cbd5e1";
1559
+ var EDGE_COLOR_LIVE = "#3b82f6";
1560
+ var EDGE_COLOR_CROSS_FLOW = "#06b6d4";
1561
+ var BACKGROUND_COLOR_LIGHT2 = "#cbd5e1";
1562
+ var BACKGROUND_COLOR_DARK2 = "#334155";
1563
+ function styleEdge(spec) {
1564
+ 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;
1565
+ const baseWidth = spec.kind === "branch" ? 1.75 : 1.5;
1566
+ const dash = spec.crossFlow ? "3 3" : spec.kind === "fallback" ? "5 4" : void 0;
1567
+ const markerSize = spec.kind === "fallback" ? 16 : 18;
1568
+ return {
1569
+ id: spec.id,
1570
+ source: spec.source,
1571
+ target: spec.target,
1572
+ ...spec.sourceHandle === void 0 ? {} : { sourceHandle: spec.sourceHandle },
1573
+ type: "bezier",
1574
+ animated: spec.live,
1575
+ style: {
1576
+ stroke: color,
1577
+ strokeWidth: spec.live && !spec.crossFlow ? 2.5 : baseWidth,
1578
+ ...dash === void 0 ? {} : { strokeDasharray: dash }
1579
+ },
1580
+ markerEnd: { type: MarkerType2.ArrowClosed, color, width: markerSize, height: markerSize }
1581
+ };
1582
+ }
1583
+ function extractErrorMessage(error) {
1584
+ if (error instanceof Error) return error.message;
1585
+ return void 0;
1586
+ }
1587
+ 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";
1588
+ 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";
1589
+ 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";
1590
+ function FlowsWorkspace({
1591
+ api,
1592
+ rootFlowKey = "menu",
1593
+ labels: labelsOverride,
1594
+ actionOptions,
1595
+ renderMediaPicker,
1596
+ livePollIntervalMs = 5e3,
1597
+ className
1598
+ }) {
1599
+ const labels = useMemo2(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride]);
1600
+ const isDark = useIsDarkTheme();
1601
+ const [graphs, setGraphs] = useState3(void 0);
1602
+ const [loadState, setLoadState] = useState3("loading");
1603
+ const [livePositions, setLivePositions] = useState3(void 0);
1604
+ const [viewMode, setViewMode] = useState3("detail");
1605
+ const [openFlowKeys, setOpenFlowKeys] = useState3([rootFlowKey]);
1606
+ const [hasAutoMerged, setHasAutoMerged] = useState3(false);
1607
+ const [workingGraphs, setWorkingGraphs] = useState3({});
1608
+ const [editingRef, setEditingRef] = useState3(null);
1609
+ const [saveState, setSaveState] = useState3("idle");
1610
+ const [saveErrorMessage, setSaveErrorMessage] = useState3(void 0);
1611
+ const [showCreateDialog, setShowCreateDialog] = useState3(false);
1612
+ const [showDeleteDialog, setShowDeleteDialog] = useState3(false);
1613
+ const [newFlow, setNewFlow] = useState3({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
1614
+ const [flowMutationState, setFlowMutationState] = useState3({ pending: false });
1615
+ const [rfNodes, setRfNodes] = useState3([]);
1616
+ const [flowInstance, setFlowInstance] = useState3(null);
1617
+ const [pendingFocusNodeId, setPendingFocusNodeId] = useState3(null);
1618
+ const reloadGraphs = useCallback(async () => {
1619
+ try {
1620
+ const loaded = await api.getGraphs();
1621
+ setGraphs(loaded);
1622
+ setLoadState("ready");
1623
+ return loaded;
1624
+ } catch {
1625
+ setLoadState("error");
1626
+ return void 0;
1627
+ }
1628
+ }, [api]);
1629
+ useEffect2(() => {
1630
+ void reloadGraphs();
1631
+ }, [reloadGraphs]);
1632
+ useEffect2(() => {
1633
+ const fetchLive = api.getLivePositions;
1634
+ if (!fetchLive) return;
1635
+ let active = true;
1636
+ async function poll() {
1637
+ try {
1638
+ const positions = await fetchLive();
1639
+ if (active) setLivePositions(positions);
1640
+ } catch {
1641
+ }
1642
+ }
1643
+ void poll();
1644
+ const timer = setInterval(() => void poll(), livePollIntervalMs);
1645
+ return () => {
1646
+ active = false;
1647
+ clearInterval(timer);
1648
+ };
1649
+ }, [api, livePollIntervalMs]);
1650
+ const primaryFlowKey = openFlowKeys[0] ?? rootFlowKey;
1651
+ const primaryGraph = workingGraphs[primaryFlowKey];
1652
+ useEffect2(() => {
1653
+ if (!graphs || hasAutoMerged) return;
1654
+ setOpenFlowKeys(mergedFlowKeysFrom(rootFlowKey, graphs));
1655
+ setHasAutoMerged(true);
1656
+ }, [graphs, hasAutoMerged, rootFlowKey]);
1657
+ useEffect2(() => {
1658
+ if (!graphs) return;
1659
+ setWorkingGraphs((prev) => {
1660
+ let changed = false;
1661
+ const next = { ...prev };
1662
+ for (const key of openFlowKeys) {
1663
+ if (!next[key] && graphs[key]) {
1664
+ next[key] = graphs[key];
1665
+ changed = true;
1666
+ }
1667
+ }
1668
+ return changed ? next : prev;
1669
+ });
1670
+ }, [graphs, openFlowKeys]);
1671
+ const isFlowDirty = useCallback(
1672
+ (key) => {
1673
+ const working = workingGraphs[key];
1674
+ const server = graphs?.[key];
1675
+ if (!working || !server) return false;
1676
+ return JSON.stringify(working) !== JSON.stringify(server);
1677
+ },
1678
+ [workingGraphs, graphs]
1679
+ );
1680
+ const issuesByFlow = useMemo2(() => {
1681
+ const map = {};
1682
+ for (const key of openFlowKeys) {
1683
+ const graph = workingGraphs[key];
1684
+ if (graph) map[key] = validateGraph(graph, labels.validation);
1685
+ }
1686
+ return map;
1687
+ }, [openFlowKeys, workingGraphs, labels]);
1688
+ const errorCount = Object.values(issuesByFlow).reduce(
1689
+ (sum, list) => sum + list.filter((issue) => issue.severity === "error").length,
1690
+ 0
1691
+ );
1692
+ const warningCount = Object.values(issuesByFlow).reduce(
1693
+ (sum, list) => sum + list.filter((issue) => issue.severity === "warning").length,
1694
+ 0
1695
+ );
1696
+ const dirtyKeys = openFlowKeys.filter(isFlowDirty);
1697
+ const isDirty = dirtyKeys.length > 0;
1698
+ const updateFlow = useCallback((flowKey, updater) => {
1699
+ setWorkingGraphs((prev) => {
1700
+ const graph = prev[flowKey];
1701
+ if (!graph) return prev;
1702
+ return { ...prev, [flowKey]: updater(graph) };
1703
+ });
1704
+ }, []);
1705
+ const focusFlow = useCallback(
1706
+ (key) => {
1707
+ const others = openFlowKeys.filter((each) => each !== key);
1708
+ if (others.some(isFlowDirty) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
1709
+ setOpenFlowKeys(graphs ? mergedFlowKeysFrom(key, graphs) : [key]);
1710
+ if (editingRef && editingRef.flowKey !== key) setEditingRef(null);
1711
+ },
1712
+ [openFlowKeys, isFlowDirty, editingRef, graphs, labels]
1713
+ );
1714
+ const closeFlow = useCallback(
1715
+ (key) => {
1716
+ if (isFlowDirty(key) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
1717
+ setOpenFlowKeys((prev) => prev.filter((each) => each !== key));
1718
+ setWorkingGraphs((prev) => {
1719
+ const { [key]: _removed, ...rest } = prev;
1720
+ return rest;
1721
+ });
1722
+ if (editingRef?.flowKey === key) setEditingRef(null);
1723
+ },
1724
+ [isFlowDirty, editingRef, labels]
1725
+ );
1726
+ const mergeFlow = useCallback((targetFlowKey) => {
1727
+ setOpenFlowKeys((prev) => prev.includes(targetFlowKey) ? prev : [...prev, targetFlowKey]);
1728
+ }, []);
1729
+ const isMerged = openFlowKeys.length > 1;
1730
+ const mergedPositions = useMemo2(
1731
+ () => isMerged ? computeMergedLayout({ openKeys: openFlowKeys, graphs: workingGraphs, primaryFlowKey }) : null,
1732
+ [isMerged, openFlowKeys, workingGraphs, primaryFlowKey]
1733
+ );
1734
+ const renderedPositionsRef = useRef2(/* @__PURE__ */ new Map());
1735
+ const derivedNodes = useMemo2(() => {
1736
+ const allNodes = [];
1737
+ for (const flowKey of openFlowKeys) {
1738
+ let resolvePosition2 = function(nodeId) {
1739
+ if (mergedPositions) {
1740
+ const nsId = namespaceNodeId(flowKey, nodeId);
1741
+ return renderedPositionsRef.current.get(nsId) ?? mergedPositions.get(nsId) ?? { x: 0, y: 0 };
1742
+ }
1743
+ return graph.nodes[nodeId]?.position ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
1744
+ };
1745
+ var resolvePosition = resolvePosition2;
1746
+ const graph = workingGraphs[flowKey];
1747
+ if (!graph) continue;
1748
+ const fallbackPositions = mergedPositions ? {} : computeAutoLayout(graph);
1749
+ const liveCounts = countLiveByNode({ flowKey, rootFlowKey, positions: livePositions });
1750
+ const flowIssues = issuesByFlow[flowKey] ?? [];
1751
+ const isPrimary = flowKey === primaryFlowKey;
1752
+ const connectedTargets = /* @__PURE__ */ new Set();
1753
+ for (const candidate of Object.values(graph.nodes)) {
1754
+ for (const { target } of targetsOf(candidate)) {
1755
+ if (!isCrossFlowTarget(target)) connectedTargets.add(target);
1756
+ }
1757
+ }
1758
+ for (const node of Object.values(graph.nodes)) {
1759
+ const position = resolvePosition2(node.id);
1760
+ allNodes.push({
1761
+ id: namespaceNodeId(flowKey, node.id),
1762
+ type: "flowNode",
1763
+ position,
1764
+ draggable: true,
1765
+ data: {
1766
+ node,
1767
+ liveCount: liveCounts[node.id] ?? 0,
1768
+ isStart: node.id === graph.startNodeId,
1769
+ isSelected: editingRef?.flowKey === flowKey && editingRef?.nodeId === node.id,
1770
+ isDetached: node.id !== graph.startNodeId && !connectedTargets.has(node.id),
1771
+ issues: flowIssues,
1772
+ labels,
1773
+ onSelect: (nodeId) => setEditingRef({ flowKey, nodeId })
1774
+ }
1775
+ });
1776
+ const crossFlowTargets = [...new Set(targetsOf(node).map((edge) => edge.target).filter(isCrossFlowTarget))];
1777
+ crossFlowTargets.forEach((target, index) => {
1778
+ const targetFlowKey = crossFlowKey(target);
1779
+ if (openFlowKeys.includes(targetFlowKey)) return;
1780
+ allNodes.push({
1781
+ id: namespaceNodeId(flowKey, portalNodeId(node.id, target)),
1782
+ type: "flowPortal",
1783
+ draggable: false,
1784
+ selectable: false,
1785
+ position: { x: position.x + 320, y: position.y + index * 70 },
1786
+ data: {
1787
+ label: graphs?.[targetFlowKey]?.label ?? targetFlowKey,
1788
+ onNavigate: () => mergeFlow(targetFlowKey)
1789
+ }
1790
+ });
1791
+ });
1792
+ }
1793
+ for (const chain of findCollectionChains(graph)) {
1794
+ const chainNodeIds = [...chain.nodeIds, chain.actionNodeId];
1795
+ const positions = chainNodeIds.map((id) => resolvePosition2(id));
1796
+ const minX = Math.min(...positions.map((point) => point.x));
1797
+ const maxX = Math.max(...positions.map((point) => point.x)) + NODE_CARD_WIDTH;
1798
+ const minY = Math.min(...positions.map((point) => point.y));
1799
+ const maxY = Math.max(...positions.map((point) => point.y)) + estimateNodeHeight(graph.nodes[chain.actionNodeId]);
1800
+ allNodes.push({
1801
+ id: namespaceNodeId(flowKey, `__chain__${chain.actionNodeId}`),
1802
+ type: "flowGroupFrame",
1803
+ draggable: false,
1804
+ selectable: false,
1805
+ zIndex: -1,
1806
+ position: { x: minX - CHAIN_FRAME_PADDING, y: minY - CHAIN_FRAME_PADDING - 24 },
1807
+ style: {
1808
+ width: maxX - minX + CHAIN_FRAME_PADDING * 2,
1809
+ height: maxY - minY + CHAIN_FRAME_PADDING * 2 + 24
1810
+ },
1811
+ data: {
1812
+ label: labels.collectionChain.feeds(nodeLabel(graph.nodes[chain.actionNodeId], labels))
1813
+ }
1814
+ });
1815
+ }
1816
+ if (!isPrimary) {
1817
+ const startPosition = resolvePosition2(graph.startNodeId);
1818
+ allNodes.push({
1819
+ id: namespaceNodeId(flowKey, "__group_header__"),
1820
+ type: "flowGroupHeader",
1821
+ draggable: false,
1822
+ selectable: false,
1823
+ position: { x: startPosition.x, y: startPosition.y - 60 },
1824
+ data: {
1825
+ label: graph.label,
1826
+ onFocus: () => focusFlow(flowKey),
1827
+ onClose: () => closeFlow(flowKey)
1828
+ }
1829
+ });
1830
+ }
1831
+ }
1832
+ return allNodes;
1833
+ }, [
1834
+ openFlowKeys,
1835
+ workingGraphs,
1836
+ mergedPositions,
1837
+ livePositions,
1838
+ issuesByFlow,
1839
+ primaryFlowKey,
1840
+ rootFlowKey,
1841
+ editingRef,
1842
+ graphs,
1843
+ mergeFlow,
1844
+ focusFlow,
1845
+ closeFlow,
1846
+ labels
1847
+ ]);
1848
+ const edges = useMemo2(
1849
+ () => buildFlowEdges({ openKeys: openFlowKeys, graphs: workingGraphs, rootFlowKey, livePositions }).map(styleEdge),
1850
+ [openFlowKeys, workingGraphs, rootFlowKey, livePositions]
1851
+ );
1852
+ useEffect2(() => {
1853
+ setRfNodes(derivedNodes);
1854
+ for (const node of derivedNodes) {
1855
+ if (node.type === "flowNode") renderedPositionsRef.current.set(node.id, node.position);
1856
+ }
1857
+ }, [derivedNodes]);
1858
+ useEffect2(() => {
1859
+ if (!pendingFocusNodeId || !flowInstance) return;
1860
+ const target = rfNodes.find((node) => node.id === pendingFocusNodeId);
1861
+ if (!target) return;
1862
+ flowInstance.setCenter(target.position.x + NODE_CARD_WIDTH / 2, target.position.y, { zoom: 1, duration: 400 });
1863
+ setPendingFocusNodeId(null);
1864
+ }, [pendingFocusNodeId, flowInstance, rfNodes]);
1865
+ const onNodesChange = useCallback((changes) => {
1866
+ setRfNodes((current) => applyNodeChanges(changes, current));
1867
+ }, []);
1868
+ const onNodeDragStop = useCallback(
1869
+ (_event, node) => {
1870
+ renderedPositionsRef.current.set(node.id, node.position);
1871
+ const { flowKey, nodeId } = parseNamespacedId(node.id);
1872
+ if (!openFlowKeys.includes(flowKey)) return;
1873
+ updateFlow(
1874
+ flowKey,
1875
+ (graph) => graph.nodes[nodeId] ? { ...graph, nodes: { ...graph.nodes, [nodeId]: { ...graph.nodes[nodeId], position: node.position } } } : graph
1876
+ );
1877
+ },
1878
+ [openFlowKeys, updateFlow]
1879
+ );
1880
+ const onConnect = useCallback(
1881
+ (connection) => {
1882
+ const resolved = resolveConnection({
1883
+ connection: { source: connection.source, target: connection.target, sourceHandle: connection.sourceHandle },
1884
+ graphs: workingGraphs
1885
+ });
1886
+ if (!resolved) return;
1887
+ updateFlow(resolved.flowKey, (graph) => {
1888
+ const node = graph.nodes[resolved.nodeId];
1889
+ if (!node) return graph;
1890
+ return { ...graph, nodes: { ...graph.nodes, [resolved.nodeId]: applyConnection(node, resolved) } };
1891
+ });
1892
+ },
1893
+ [workingGraphs, updateFlow]
1894
+ );
1895
+ function handleAddNode(spec) {
1896
+ if (!primaryGraph) return;
1897
+ const newNode = newNodeFromSpec(spec, new Set(Object.keys(primaryGraph.nodes)));
1898
+ const maxY = Math.max(0, ...Object.values(primaryGraph.nodes).map((node) => node.position?.y ?? 0));
1899
+ newNode.position = { x: 0, y: maxY + 170 };
1900
+ updateFlow(primaryFlowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [newNode.id]: newNode } }));
1901
+ setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
1902
+ setPendingFocusNodeId(namespaceNodeId(primaryFlowKey, newNode.id));
1903
+ }
1904
+ function handleNodePanelChange(updated) {
1905
+ if (!editingRef) return;
1906
+ updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [updated.id]: updated } }));
1907
+ setEditingRef(null);
1908
+ }
1909
+ function handleNodeDelete(nodeId) {
1910
+ if (!editingRef) return;
1911
+ updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: removeNodeAndCleanRefs(graph.nodes, nodeId) }));
1912
+ setEditingRef(null);
1913
+ }
1914
+ function handleOrganize() {
1915
+ if (!primaryGraph) return;
1916
+ renderedPositionsRef.current.clear();
1917
+ if (isMerged) {
1918
+ const positions2 = computeMergedLayout({ openKeys: openFlowKeys, graphs: workingGraphs, primaryFlowKey });
1919
+ for (const key of openFlowKeys) {
1920
+ updateFlow(key, (graph) => ({
1921
+ ...graph,
1922
+ nodes: Object.fromEntries(
1923
+ Object.entries(graph.nodes).map(([id, node]) => {
1924
+ const position = positions2.get(namespaceNodeId(key, id));
1925
+ return [id, position ? { ...node, position } : node];
1926
+ })
1927
+ )
1928
+ }));
1929
+ }
1930
+ return;
1931
+ }
1932
+ const positions = computeAutoLayout(primaryGraph);
1933
+ updateFlow(primaryFlowKey, (graph) => ({
1934
+ ...graph,
1935
+ nodes: Object.fromEntries(
1936
+ Object.entries(graph.nodes).map(([id, node]) => [id, { ...node, position: positions[id] ?? node.position }])
1937
+ )
1938
+ }));
1939
+ }
1940
+ function handleDiscardChanges() {
1941
+ if (!graphs || !isDirty) return;
1942
+ if (!window.confirm(labels.workspace.discardConfirm)) return;
1943
+ setWorkingGraphs((prev) => {
1944
+ const next = { ...prev };
1945
+ for (const key of dirtyKeys) {
1946
+ const published = graphs[key];
1947
+ if (published) next[key] = published;
1948
+ }
1949
+ return next;
1950
+ });
1951
+ renderedPositionsRef.current.clear();
1952
+ setEditingRef(null);
1953
+ }
1954
+ async function handlePublish() {
1955
+ if (dirtyKeys.length === 0 || errorCount > 0) return;
1956
+ setSaveState("saving");
1957
+ setSaveErrorMessage(void 0);
1958
+ try {
1959
+ await Promise.all(dirtyKeys.map((key) => api.saveGraph(key, workingGraphs[key])));
1960
+ await reloadGraphs();
1961
+ setSaveState("success");
1962
+ setTimeout(() => setSaveState("idle"), 3e3);
1963
+ } catch (error) {
1964
+ setSaveState("error");
1965
+ setSaveErrorMessage(extractErrorMessage(error));
1966
+ }
1967
+ }
1968
+ async function handleCreateFlow() {
1969
+ const createFlow = api.createFlow;
1970
+ if (!createFlow) return;
1971
+ setFlowMutationState({ pending: true });
1972
+ try {
1973
+ await createFlow({
1974
+ key: newFlow.key,
1975
+ label: newFlow.label,
1976
+ showInMenu: newFlow.showInMenu,
1977
+ ...newFlow.showInMenu ? { menuOptionLabel: newFlow.menuOptionLabel || newFlow.label } : {}
1978
+ });
1979
+ await reloadGraphs();
1980
+ setOpenFlowKeys([newFlow.key]);
1981
+ setShowCreateDialog(false);
1982
+ setNewFlow({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
1983
+ setFlowMutationState({ pending: false });
1984
+ } catch (error) {
1985
+ setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.createError });
1986
+ }
1987
+ }
1988
+ async function handleDeleteFlow() {
1989
+ const deleteFlow = api.deleteFlow;
1990
+ if (!deleteFlow || !primaryGraph) return;
1991
+ setFlowMutationState({ pending: true });
1992
+ try {
1993
+ await deleteFlow(primaryGraph.key);
1994
+ const reloaded = await reloadGraphs();
1995
+ setOpenFlowKeys(reloaded ? mergedFlowKeysFrom(rootFlowKey, reloaded) : [rootFlowKey]);
1996
+ setShowDeleteDialog(false);
1997
+ setFlowMutationState({ pending: false });
1998
+ } catch (error) {
1999
+ setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.deleteError });
2000
+ }
2001
+ }
2002
+ const editingGraph = editingRef ? workingGraphs[editingRef.flowKey] : null;
2003
+ const editingNode = editingRef && editingGraph ? editingGraph.nodes[editingRef.nodeId] : null;
2004
+ const otherFlows = useMemo2(
2005
+ () => Object.values(graphs ?? {}).filter((graph) => graph.key !== editingRef?.flowKey).map((graph) => ({ key: graph.key, label: graph.label })),
2006
+ [graphs, editingRef?.flowKey]
2007
+ );
2008
+ const keyIsValid = FLOW_KEY_PATTERN.test(newFlow.key);
2009
+ const canCreateFlow = Boolean(api.createFlow);
2010
+ const canDeleteFlow = Boolean(api.deleteFlow) && primaryFlowKey !== rootFlowKey;
2011
+ return /* @__PURE__ */ jsxs9("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
2012
+ /* @__PURE__ */ jsx10(TooltipLayer, {}),
2013
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
2014
+ /* @__PURE__ */ jsxs9("div", { children: [
2015
+ /* @__PURE__ */ jsx10("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
2016
+ /* @__PURE__ */ jsx10("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
2017
+ ] }),
2018
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-3", children: [
2019
+ saveState === "success" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
2020
+ saveState === "error" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
2021
+ /* @__PURE__ */ jsxs9(
2022
+ "button",
2023
+ {
2024
+ "data-cv-tooltip": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
2025
+ "aria-label": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
2026
+ type: "button",
2027
+ onClick: () => setViewMode((mode) => mode === "map" ? "detail" : "map"),
2028
+ className: OUTLINE_BUTTON,
2029
+ children: [
2030
+ viewMode === "map" ? /* @__PURE__ */ jsx10(Workflow, { size: 14, "aria-hidden": "true" }) : /* @__PURE__ */ jsx10(MapIcon, { size: 14, "aria-hidden": "true" }),
2031
+ viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap
2032
+ ]
2033
+ }
2034
+ ),
2035
+ viewMode === "detail" && /* @__PURE__ */ jsxs9(Fragment2, { children: [
2036
+ /* @__PURE__ */ jsxs9(
2037
+ "button",
2038
+ {
2039
+ type: "button",
2040
+ onClick: handleOrganize,
2041
+ className: OUTLINE_BUTTON,
2042
+ "data-cv-tooltip": labels.workspace.organizeTooltip,
2043
+ "aria-label": labels.workspace.organizeTooltip,
2044
+ disabled: !primaryGraph,
2045
+ children: [
2046
+ /* @__PURE__ */ jsx10(LayoutGrid, { size: 14, "aria-hidden": "true" }),
2047
+ " ",
2048
+ labels.workspace.organize
2049
+ ]
2050
+ }
2051
+ ),
2052
+ /* @__PURE__ */ jsxs9(
2053
+ "button",
2054
+ {
2055
+ type: "button",
2056
+ onClick: handleDiscardChanges,
2057
+ className: OUTLINE_BUTTON,
2058
+ "data-cv-tooltip": labels.workspace.discardTooltip,
2059
+ "aria-label": labels.workspace.discardTooltip,
2060
+ disabled: !isDirty || saveState === "saving",
2061
+ children: [
2062
+ /* @__PURE__ */ jsx10(Undo2, { size: 14, "aria-hidden": "true" }),
2063
+ " ",
2064
+ labels.workspace.discardChanges
2065
+ ]
2066
+ }
2067
+ ),
2068
+ /* @__PURE__ */ jsxs9(
2069
+ "button",
2070
+ {
2071
+ "data-cv-tooltip": labels.workspace.saveGraph,
2072
+ "aria-label": labels.workspace.saveGraph,
2073
+ type: "button",
2074
+ onClick: () => void handlePublish(),
2075
+ className: PRIMARY_BUTTON,
2076
+ disabled: !isDirty || errorCount > 0 || saveState === "saving",
2077
+ children: [
2078
+ /* @__PURE__ */ jsx10(Save2, { size: 14, "aria-hidden": "true" }),
2079
+ saveState === "saving" ? labels.workspace.saving : labels.workspace.saveGraph
2080
+ ]
2081
+ }
2082
+ )
2083
+ ] })
2084
+ ] })
2085
+ ] }),
2086
+ viewMode === "detail" && /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
2087
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 flex-wrap", children: [
2088
+ graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx10(
2089
+ "button",
2090
+ {
2091
+ "data-cv-tooltip": graph.label,
2092
+ "aria-label": graph.label,
2093
+ type: "button",
2094
+ onClick: () => focusFlow(graph.key),
2095
+ 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"}`,
2096
+ children: graph.label
2097
+ },
2098
+ graph.key
2099
+ )),
2100
+ canCreateFlow && /* @__PURE__ */ jsxs9(
2101
+ "button",
2102
+ {
2103
+ "data-cv-tooltip": labels.flowManager.newFlow,
2104
+ "aria-label": labels.flowManager.newFlow,
2105
+ type: "button",
2106
+ onClick: () => {
2107
+ setFlowMutationState({ pending: false });
2108
+ setShowCreateDialog(true);
2109
+ },
2110
+ 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",
2111
+ children: [
2112
+ /* @__PURE__ */ jsx10(Plus3, { size: 12, "aria-hidden": "true" }),
2113
+ " ",
2114
+ labels.flowManager.newFlow
2115
+ ]
2116
+ }
2117
+ )
2118
+ ] }),
2119
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
2120
+ primaryGraph && /* @__PURE__ */ jsx10(
2121
+ FlowPalette,
2122
+ {
2123
+ onAdd: handleAddNode,
2124
+ labels,
2125
+ ...actionOptions ? { actionOptions: [...actionOptions] } : {}
2126
+ }
2127
+ ),
2128
+ primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs9(
2129
+ "button",
2130
+ {
2131
+ "data-cv-tooltip": labels.flowManager.deleteFlow,
2132
+ "aria-label": labels.flowManager.deleteFlow,
2133
+ type: "button",
2134
+ onClick: () => {
2135
+ setFlowMutationState({ pending: false });
2136
+ setShowDeleteDialog(true);
2137
+ },
2138
+ 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",
2139
+ children: [
2140
+ /* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
2141
+ " ",
2142
+ labels.flowManager.deleteFlow
2143
+ ]
2144
+ }
2145
+ )
2146
+ ] })
2147
+ ] }),
2148
+ viewMode === "detail" && (errorCount > 0 || warningCount > 0) && /* @__PURE__ */ jsxs9("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: [
2149
+ /* @__PURE__ */ jsxs9("span", { className: "font-medium text-gray-600 dark:text-gray-300", children: [
2150
+ labels.validation.title,
2151
+ ":"
2152
+ ] }),
2153
+ errorCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-red-600 dark:text-red-400 font-medium", children: [
2154
+ /* @__PURE__ */ jsx10(AlertCircle3, { size: 13, "aria-hidden": "true" }),
2155
+ " ",
2156
+ labels.validation.errors(errorCount)
2157
+ ] }),
2158
+ warningCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [
2159
+ /* @__PURE__ */ jsx10(AlertTriangle3, { size: 13, "aria-hidden": "true" }),
2160
+ " ",
2161
+ labels.validation.warnings(warningCount)
2162
+ ] })
2163
+ ] }),
2164
+ /* @__PURE__ */ jsxs9("div", { className: "flex-1 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative", children: [
2165
+ loadState === "loading" && /* @__PURE__ */ jsx10("p", { className: "text-center text-gray-400 py-12", children: labels.workspace.loading }),
2166
+ loadState === "error" && /* @__PURE__ */ jsx10("p", { className: "text-center text-red-500 py-12", children: labels.workspace.loadError }),
2167
+ loadState === "ready" && graphs && viewMode === "map" && /* @__PURE__ */ jsx10(
2168
+ FlowMapCanvas,
2169
+ {
2170
+ graphs,
2171
+ rootKey: rootFlowKey,
2172
+ labels,
2173
+ onOpenFlow: (key) => {
2174
+ focusFlow(key);
2175
+ setViewMode("detail");
2176
+ }
2177
+ }
2178
+ ),
2179
+ loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs9(
2180
+ ReactFlow2,
2181
+ {
2182
+ nodes: rfNodes,
2183
+ edges,
2184
+ nodeTypes: RF_NODE_TYPES,
2185
+ onNodesChange,
2186
+ onNodeDragStop,
2187
+ onConnect,
2188
+ onInit: setFlowInstance,
2189
+ fitView: true,
2190
+ proOptions: { hideAttribution: true },
2191
+ colorMode: isDark ? "dark" : "light",
2192
+ children: [
2193
+ /* @__PURE__ */ jsx10(Background2, { color: isDark ? BACKGROUND_COLOR_DARK2 : BACKGROUND_COLOR_LIGHT2 }),
2194
+ /* @__PURE__ */ jsx10(Controls2, {}),
2195
+ /* @__PURE__ */ jsx10(MiniMap, { pannable: true, zoomable: true, className: "!bg-white dark:!bg-gray-800" })
2196
+ ]
2197
+ }
2198
+ )
2199
+ ] }),
2200
+ editingNode && editingGraph && /* @__PURE__ */ jsx10(
2201
+ FlowNodePanel,
2202
+ {
2203
+ graph: editingGraph,
2204
+ node: editingNode,
2205
+ issues: editingRef ? issuesByFlow[editingRef.flowKey] ?? [] : [],
2206
+ otherFlows,
2207
+ labels,
2208
+ onClose: () => setEditingRef(null),
2209
+ onChange: handleNodePanelChange,
2210
+ onDelete: handleNodeDelete,
2211
+ ...renderMediaPicker ? { renderMediaPicker } : {}
2212
+ },
2213
+ `${editingRef?.flowKey}:${editingRef?.nodeId}`
2214
+ ),
2215
+ showCreateDialog && canCreateFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.createTitle, onClose: () => setShowCreateDialog(false), children: [
2216
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-3", children: [
2217
+ /* @__PURE__ */ jsxs9("div", { children: [
2218
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.label }),
2219
+ /* @__PURE__ */ jsx10(
2220
+ "input",
2221
+ {
2222
+ value: newFlow.label,
2223
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, label: event.target.value })),
2224
+ className: `mt-1 ${DIALOG_INPUT}`
2225
+ }
2226
+ )
2227
+ ] }),
2228
+ /* @__PURE__ */ jsxs9("div", { children: [
2229
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.key }),
2230
+ /* @__PURE__ */ jsx10(
2231
+ "input",
2232
+ {
2233
+ value: newFlow.key,
2234
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, key: event.target.value.toLowerCase() })),
2235
+ className: `mt-1 ${DIALOG_INPUT}`
2236
+ }
2237
+ ),
2238
+ /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-gray-400 mt-1", children: newFlow.key && !keyIsValid ? labels.flowManager.keyInvalid : labels.flowManager.keyHint })
2239
+ ] }),
2240
+ /* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200", children: [
2241
+ /* @__PURE__ */ jsx10(
2242
+ "input",
2243
+ {
2244
+ type: "checkbox",
2245
+ checked: newFlow.showInMenu,
2246
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, showInMenu: event.target.checked }))
2247
+ }
2248
+ ),
2249
+ labels.flowManager.showInMenu
2250
+ ] }),
2251
+ newFlow.showInMenu && /* @__PURE__ */ jsxs9("div", { children: [
2252
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.menuOptionLabel }),
2253
+ /* @__PURE__ */ jsx10(
2254
+ "input",
2255
+ {
2256
+ value: newFlow.menuOptionLabel,
2257
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, menuOptionLabel: event.target.value })),
2258
+ placeholder: newFlow.label,
2259
+ className: `mt-1 ${DIALOG_INPUT}`
2260
+ }
2261
+ )
2262
+ ] }),
2263
+ flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600", children: flowMutationState.error })
2264
+ ] }),
2265
+ /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2266
+ /* @__PURE__ */ jsx10("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowCreateDialog(false), children: labels.nodePanel.cancel }),
2267
+ /* @__PURE__ */ jsx10(
2268
+ "button",
2269
+ {
2270
+ "data-cv-tooltip": labels.flowManager.create,
2271
+ "aria-label": labels.flowManager.create,
2272
+ type: "button",
2273
+ className: PRIMARY_BUTTON,
2274
+ onClick: () => void handleCreateFlow(),
2275
+ disabled: !keyIsValid || !newFlow.label || flowMutationState.pending,
2276
+ children: flowMutationState.pending ? labels.flowManager.creating : labels.flowManager.create
2277
+ }
2278
+ )
2279
+ ] })
2280
+ ] }),
2281
+ showDeleteDialog && canDeleteFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.deleteFlow, onClose: () => setShowDeleteDialog(false), children: [
2282
+ /* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
2283
+ flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
2284
+ /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2285
+ /* @__PURE__ */ jsx10("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowDeleteDialog(false), children: labels.nodePanel.cancel }),
2286
+ /* @__PURE__ */ jsxs9(
2287
+ "button",
2288
+ {
2289
+ "data-cv-tooltip": labels.flowManager.deleteFlow,
2290
+ "aria-label": labels.flowManager.deleteFlow,
2291
+ type: "button",
2292
+ 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",
2293
+ onClick: () => void handleDeleteFlow(),
2294
+ disabled: flowMutationState.pending,
2295
+ children: [
2296
+ /* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
2297
+ " ",
2298
+ labels.flowManager.deleteFlow
2299
+ ]
2300
+ }
2301
+ )
2302
+ ] })
1075
2303
  ] })
1076
2304
  ] });
1077
2305
  }
2306
+ function FlowDialog({
2307
+ title,
2308
+ onClose,
2309
+ children
2310
+ }) {
2311
+ return /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4", children: [
2312
+ /* @__PURE__ */ jsx10("div", { className: "absolute inset-0", onClick: onClose, "aria-hidden": "true" }),
2313
+ /* @__PURE__ */ jsxs9(
2314
+ "div",
2315
+ {
2316
+ role: "dialog",
2317
+ "aria-modal": "true",
2318
+ "aria-label": title,
2319
+ className: "relative w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-5 shadow-xl",
2320
+ children: [
2321
+ /* @__PURE__ */ jsx10("h3", { className: "text-base font-semibold text-gray-900 dark:text-gray-100 mb-3", children: title }),
2322
+ children
2323
+ ]
2324
+ }
2325
+ )
2326
+ ] });
2327
+ }
1078
2328
  export {
1079
2329
  BUILT_IN_ACTION_KINDS,
1080
2330
  CONDITION_OPERATORS,
@@ -1089,12 +2339,21 @@ export {
1089
2339
  FlowPalette,
1090
2340
  FlowPortalNode,
1091
2341
  FlowWhatsAppPreview,
2342
+ FlowsWorkspace,
2343
+ GROUP_HEADER_NODE_ID,
1092
2344
  NODE_CARD_WIDTH,
1093
2345
  WHATSAPP_LIMITS,
2346
+ applyConnection,
2347
+ buildFlowEdges,
2348
+ chainFrameBounds,
2349
+ chainFrameNodeId,
1094
2350
  computeAutoLayout,
1095
2351
  computeFlowMapLayout,
2352
+ computeMergedLayout,
2353
+ countLiveByNode,
1096
2354
  crossFlowKey,
1097
2355
  crossFlowTargetsOf,
2356
+ detachedNodeIds,
1098
2357
  estimateNodeHeight,
1099
2358
  findCollectionChains,
1100
2359
  flowGroupFrameNodeTypes,
@@ -1103,9 +2362,17 @@ export {
1103
2362
  flowNodeTypes,
1104
2363
  flowPortalNodeTypes,
1105
2364
  isCrossFlowTarget,
2365
+ isGraphDirty,
1106
2366
  mergeFlowEditorLabels,
2367
+ mergedFlowKeysFrom,
2368
+ namespaceNodeId,
2369
+ newNodeFromSpec,
1107
2370
  nodeLabel,
2371
+ parseNamespacedId,
2372
+ portalNodeId,
2373
+ removeNodeAndCleanRefs,
1108
2374
  rendersAsButtons,
2375
+ resolveConnection,
1109
2376
  slugifyNodeId,
1110
2377
  targetsOf,
1111
2378
  validateGraph