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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,17 @@
1
1
  import {
2
2
  TooltipLayer
3
- } from "../chunk-DXPSPUWF.js";
3
+ } from "../chunk-DKPXKQGC.js";
4
4
  import {
5
5
  parseWhatsAppFormatting,
6
6
  useIsDarkTheme
7
7
  } from "../chunk-WCBDXZ3X.js";
8
8
 
9
9
  // src/flows/FlowNodeCard.tsx
10
+ import { useState } from "react";
10
11
  import { Handle, Position } from "@xyflow/react";
11
12
  import {
12
13
  MessageCircleQuestion,
14
+ Plus,
13
15
  GitBranch,
14
16
  Zap,
15
17
  ListTree,
@@ -18,9 +20,254 @@ import {
18
20
  AlertCircle,
19
21
  Headset,
20
22
  Clock3,
21
- ShoppingBag
23
+ ShoppingBag,
24
+ RotateCcw
22
25
  } from "lucide-react";
26
+
27
+ // src/flows/flowGraph.ts
28
+ import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
29
+ var CONDITION_OPERATORS = [">", ">=", "<", "<=", "==", "!=", "contains"];
30
+ var BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND;
31
+ var PASS_THROUGH_ACTION_KINDS = ["send_media"];
32
+ var CROSS_FLOW_PREFIX = "flow:";
33
+ var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
34
+ var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
35
+ var NODE_CARD_WIDTH = 240;
36
+ function estimateNodeHeight(node) {
37
+ const HEADER_HEIGHT = 28;
38
+ const BODY_HEIGHT = 56;
39
+ const PADDING = 16;
40
+ const ROW_HEIGHT = 34;
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;
45
+ return HEADER_HEIGHT + BODY_HEIGHT + PADDING + rowCount * ROW_HEIGHT;
46
+ }
47
+ var WHATSAPP_LIMITS = {
48
+ MAX_BUTTONS: 3,
49
+ MAX_LIST_ROWS: 10,
50
+ BUTTON_TITLE_MAX: 20,
51
+ LIST_ROW_TITLE_MAX: 24,
52
+ BODY_MAX: 1024
53
+ };
54
+ function rendersAsButtons(options) {
55
+ return (options?.length ?? 0) <= WHATSAPP_LIMITS.MAX_BUTTONS;
56
+ }
57
+ function targetsOf(node) {
58
+ if (!node.next) return [];
59
+ if (typeof node.next === "string") return [{ target: node.next }];
60
+ return [
61
+ ...Object.entries(node.next.byAnswer).map(([optionId, target]) => ({ target, optionId })),
62
+ { target: node.next.default, isDefault: true }
63
+ ];
64
+ }
65
+ function validateGraph(graph, issueText) {
66
+ const issues = [];
67
+ const nodeIds = new Set(Object.keys(graph.nodes));
68
+ const isValidTarget = (target) => nodeIds.has(target) || isCrossFlowTarget(target);
69
+ if (!nodeIds.has(graph.startNodeId)) {
70
+ issues.push({ severity: "error", message: issueText.noStart });
71
+ }
72
+ for (const node of Object.values(graph.nodes)) {
73
+ for (const { target } of targetsOf(node)) {
74
+ if (!isValidTarget(target)) {
75
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.brokenRef(node.id, target) });
76
+ }
77
+ }
78
+ const isChoice = node.questionType === "choice" || node.type === "menu";
79
+ if (isChoice) {
80
+ const options = node.options ?? [];
81
+ if (options.length === 0) {
82
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.choiceWithoutOptions(node.id) });
83
+ }
84
+ const seen = /* @__PURE__ */ new Set();
85
+ for (const [optionId, label] of options) {
86
+ if (seen.has(optionId)) {
87
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.duplicatedOptionId(node.id, optionId) });
88
+ }
89
+ seen.add(optionId);
90
+ const byAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
91
+ if (!byAnswer[optionId]) {
92
+ issues.push({ severity: "warning", nodeId: node.id, message: issueText.optionWithoutTarget(node.id, label) });
93
+ }
94
+ const usesButtons = rendersAsButtons(options);
95
+ if (usesButtons && label.length > WHATSAPP_LIMITS.BUTTON_TITLE_MAX) {
96
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.buttonTitleTooLong(node.id, label) });
97
+ }
98
+ if (!usesButtons && label.length > WHATSAPP_LIMITS.LIST_ROW_TITLE_MAX) {
99
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.listTitleTooLong(node.id, label) });
100
+ }
101
+ }
102
+ if (options.length > WHATSAPP_LIMITS.MAX_LIST_ROWS) {
103
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.tooManyOptions(node.id, options.length) });
104
+ }
105
+ }
106
+ const bodyText = node.question ?? node.directMessage ?? "";
107
+ if (bodyText.length > WHATSAPP_LIMITS.BODY_MAX) {
108
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.bodyTooLong(node.id) });
109
+ }
110
+ if (node.type === "question" && !node.next) {
111
+ issues.push({ severity: "warning", nodeId: node.id, message: issueText.deadEndQuestion(node.id) });
112
+ }
113
+ if (node.type === "condition") {
114
+ if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) {
115
+ issues.push({ severity: "error", nodeId: node.id, message: issueText.conditionIncomplete(node.id) });
116
+ }
117
+ const byAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
118
+ if (!byAnswer.true)
119
+ issues.push({
120
+ severity: "warning",
121
+ nodeId: node.id,
122
+ message: issueText.conditionBranchMissing(node.id, "true")
123
+ });
124
+ if (!byAnswer.false)
125
+ issues.push({
126
+ severity: "warning",
127
+ nodeId: node.id,
128
+ message: issueText.conditionBranchMissing(node.id, "false")
129
+ });
130
+ }
131
+ }
132
+ for (const id of findUnreachable(graph)) {
133
+ issues.push({ severity: "warning", nodeId: id, message: issueText.unreachable(id) });
134
+ }
135
+ return issues;
136
+ }
137
+ function findUnreachable(graph) {
138
+ const reachable = /* @__PURE__ */ new Set();
139
+ const queue = [graph.startNodeId];
140
+ while (queue.length > 0) {
141
+ const id = queue.shift();
142
+ if (reachable.has(id) || !graph.nodes[id]) continue;
143
+ reachable.add(id);
144
+ for (const { target } of targetsOf(graph.nodes[id])) {
145
+ if (!isCrossFlowTarget(target)) queue.push(target);
146
+ }
147
+ }
148
+ return Object.keys(graph.nodes).filter((id) => !reachable.has(id));
149
+ }
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);
158
+ }
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 });
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);
173
+ const positions = {};
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;
178
+ }
179
+ return positions;
180
+ }
181
+ function crossFlowTargetsOf(graph) {
182
+ const keys = /* @__PURE__ */ new Set();
183
+ for (const node of Object.values(graph.nodes)) {
184
+ for (const { target } of targetsOf(node)) {
185
+ if (isCrossFlowTarget(target)) keys.add(crossFlowKey(target));
186
+ }
187
+ }
188
+ return [...keys];
189
+ }
190
+ function computeFlowMapLayout(graphs, rootKey) {
191
+ const H_GAP = 280;
192
+ const V_GAP = 170;
193
+ const rank = {};
194
+ const queue = graphs[rootKey] ? [rootKey] : Object.keys(graphs);
195
+ if (graphs[rootKey]) rank[rootKey] = 0;
196
+ while (queue.length > 0) {
197
+ const key = queue.shift();
198
+ const g = graphs[key];
199
+ if (!g) continue;
200
+ for (const target of crossFlowTargetsOf(g)) {
201
+ if (!graphs[target]) continue;
202
+ if (rank[target] === void 0) {
203
+ rank[target] = rank[key] + 1;
204
+ queue.push(target);
205
+ }
206
+ }
207
+ }
208
+ const maxRank = Math.max(0, ...Object.values(rank));
209
+ let strayRank = maxRank + 1;
210
+ for (const key of Object.keys(graphs)) {
211
+ if (rank[key] === void 0) rank[key] = strayRank++;
212
+ }
213
+ const layers = {};
214
+ for (const [key, r] of Object.entries(rank)) {
215
+ layers[r] = [...layers[r] ?? [], key];
216
+ }
217
+ const positions = {};
218
+ for (const [r, keys] of Object.entries(layers)) {
219
+ const width = (keys.length - 1) * H_GAP;
220
+ keys.forEach((key, index) => {
221
+ positions[key] = { x: index * H_GAP - width / 2, y: Number(r) * V_GAP };
222
+ });
223
+ }
224
+ return positions;
225
+ }
226
+ function findCollectionChains(graph) {
227
+ const actionIds = new Set(
228
+ Object.values(graph.nodes).filter((n) => n.type === "action").map((n) => n.id)
229
+ );
230
+ const memo = /* @__PURE__ */ new Map();
231
+ function reachableActions(id, stack) {
232
+ if (memo.has(id)) return memo.get(id);
233
+ if (stack.has(id)) return /* @__PURE__ */ new Set();
234
+ if (actionIds.has(id)) return /* @__PURE__ */ new Set([id]);
235
+ const node = graph.nodes[id];
236
+ if (!node) return /* @__PURE__ */ new Set();
237
+ const nextStack = new Set(stack);
238
+ nextStack.add(id);
239
+ const result = /* @__PURE__ */ new Set();
240
+ for (const { target } of targetsOf(node)) {
241
+ if (isCrossFlowTarget(target) || !graph.nodes[target]) continue;
242
+ for (const actionId of reachableActions(target, nextStack)) result.add(actionId);
243
+ }
244
+ memo.set(id, result);
245
+ return result;
246
+ }
247
+ const nodeIdsByAction = /* @__PURE__ */ new Map();
248
+ for (const node of Object.values(graph.nodes)) {
249
+ if (node.type !== "question") continue;
250
+ const reached = reachableActions(node.id, /* @__PURE__ */ new Set());
251
+ if (reached.size !== 1) continue;
252
+ const [actionNodeId] = [...reached];
253
+ nodeIdsByAction.set(actionNodeId, [...nodeIdsByAction.get(actionNodeId) ?? [], node.id]);
254
+ }
255
+ return [...nodeIdsByAction.entries()].filter(([, nodeIds]) => nodeIds.length >= 2).map(([actionNodeId, nodeIds]) => ({ actionNodeId, nodeIds }));
256
+ }
257
+ function slugifyNodeId(label, existing) {
258
+ const base = label.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 30) || "no";
259
+ let candidate = base;
260
+ let counter = 2;
261
+ while (existing.has(candidate)) {
262
+ candidate = `${base}_${counter}`;
263
+ counter++;
264
+ }
265
+ return candidate;
266
+ }
267
+
268
+ // src/flows/FlowNodeCard.tsx
23
269
  import { jsx, jsxs } from "react/jsx-runtime";
270
+ var HANDLE_SIZE_PX = 14;
24
271
  var NODE_TYPE_COLOR = {
25
272
  question: "border-blue-300 bg-blue-50 dark:bg-blue-950/40 dark:border-blue-800",
26
273
  entrada_choice: "border-purple-300 bg-purple-50 dark:bg-purple-950/40 dark:border-purple-800",
@@ -54,51 +301,124 @@ function nodeLabel(node, labels) {
54
301
  }
55
302
  return node.question || node.contextKey || node.id;
56
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
+ }
57
317
  function sourceRows(node, labels) {
58
- if (node.type === "action") return [];
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);
59
322
  if (node.type === "condition") {
60
323
  return [
61
- { id: "true", label: labels.nodePanel.conditionTrue, isDefault: false },
62
- { id: "false", label: labels.nodePanel.conditionFalse, isDefault: false }
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") }
63
326
  ];
64
327
  }
65
328
  const isChoice = node.type === "menu" || node.questionType === "choice";
66
- if (!isChoice) return [{ id: "next", label: labels.nodePanel.nextRowLabel, isDefault: false }];
329
+ if (!isChoice)
330
+ return [{ id: "next", label: labels.nodePanel.nextRowLabel, isDefault: false, isSelfLoop: loops.has("next") }];
67
331
  const options = node.options ?? [];
68
332
  return [
69
- ...options.map(([id, label]) => ({ id, label, isDefault: false })),
70
- { id: "__default", label: labels.edgeFallbackLabel, isDefault: true }
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") }
71
335
  ];
72
336
  }
73
- function SourceRow({ label, isDefault, handleId }) {
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: [
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
- ),
82
- /* @__PURE__ */ jsx(
83
- Handle,
84
- {
85
- type: "source",
86
- position: Position.Right,
87
- id: handleId,
88
- style: { position: "absolute", right: -7, top: "50%", transform: "translateY(-50%)" },
89
- className: isDefault ? "!bg-gray-400 dark:!bg-gray-500" : "!bg-purple-500"
90
- }
91
- )
92
- ] });
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
+ );
93
409
  }
94
410
  function FlowNodeCard({ data }) {
95
- const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect } = data;
411
+ const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect, onQuickAdd } = data;
96
412
  const label = nodeLabel(node, labels);
97
413
  const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
98
414
  const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
99
415
  const rows = sourceRows(node, labels);
100
- const hasError = issues.some((i) => i.severity === "error");
101
- const hasWarning = !hasError && issues.some((i) => i.severity === "warning");
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 ");
102
422
  return /* @__PURE__ */ jsxs(
103
423
  "div",
104
424
  {
@@ -106,7 +426,16 @@ function FlowNodeCard({ data }) {
106
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" : ""}`,
107
427
  onClick: () => onSelect(node.id),
108
428
  children: [
109
- /* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
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
+ ),
110
439
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
111
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: [
112
441
  /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
@@ -114,9 +443,25 @@ function FlowNodeCard({ data }) {
114
443
  labels.legend[node.type]
115
444
  ] }),
116
445
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
117
- hasError && /* @__PURE__ */ jsx(AlertCircle, { size: 13, className: "text-red-600 dark:text-red-400" }),
118
- hasWarning && /* @__PURE__ */ jsx(AlertTriangle, { size: 13, className: "text-amber-500 dark:text-amber-400" }),
119
- liveCount > 0 && /* @__PURE__ */ jsx(
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(
120
465
  "span",
121
466
  {
122
467
  "data-cv-tooltip": labels.liveCountTooltip(liveCount),
@@ -127,7 +472,19 @@ function FlowNodeCard({ data }) {
127
472
  ] })
128
473
  ] }),
129
474
  /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-900 dark:text-gray-100 mt-1 line-clamp-3", children: label }),
130
- 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)) })
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
+ )) })
131
488
  ]
132
489
  }
133
490
  );
@@ -219,6 +576,24 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
219
576
  media: "Arquivos enviados neste ponto",
220
577
  mediaUnavailable: "A biblioteca de arquivos n\xE3o est\xE1 dispon\xEDvel neste painel."
221
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
+ },
222
597
  palette: {
223
598
  title: "Adicionar ao fluxo",
224
599
  question: "Pergunta",
@@ -290,321 +665,73 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
290
665
  create: "Criar fluxo",
291
666
  creating: "Criando\u2026",
292
667
  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."
296
- }
297
- };
298
- function mergeFlowEditorLabels(override) {
299
- if (!override) return DEFAULT_FLOW_EDITOR_LABELS;
300
- return {
301
- ...DEFAULT_FLOW_EDITOR_LABELS,
302
- ...override,
303
- legend: { ...DEFAULT_FLOW_EDITOR_LABELS.legend, ...override.legend },
304
- actionKindLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.actionKindLabels, ...override.actionKindLabels },
305
- conditionOperatorLabels: {
306
- ...DEFAULT_FLOW_EDITOR_LABELS.conditionOperatorLabels,
307
- ...override.conditionOperatorLabels
308
- },
309
- questionTypeLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.questionTypeLabels, ...override.questionTypeLabels },
310
- nodePanel: { ...DEFAULT_FLOW_EDITOR_LABELS.nodePanel, ...override.nodePanel },
311
- palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
312
- flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
313
- flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
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 }
319
- };
320
- }
321
-
322
- // src/flows/FlowMapNode.tsx
323
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
324
- function FlowMapNode({ data }) {
325
- const { label, nodeCount, isRoot, labels = DEFAULT_FLOW_EDITOR_LABELS, onOpen } = data;
326
- 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: [
327
- /* @__PURE__ */ jsx2(Handle2, { type: "target", position: Position2.Top, className: "!bg-gray-400 dark:!bg-gray-500" }),
328
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
329
- /* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
330
- /* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
331
- isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", "data-cv-tooltip": labels.startNodeTooltip })
332
- ] }),
333
- /* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
334
- /* @__PURE__ */ jsxs2(
335
- "button",
336
- {
337
- "data-cv-tooltip": labels.flowMap.openFlow,
338
- "aria-label": labels.flowMap.openFlow,
339
- type: "button",
340
- onClick: onOpen,
341
- className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
342
- children: [
343
- /* @__PURE__ */ jsx2(Maximize2, { size: 11 }),
344
- " ",
345
- labels.flowMap.openFlow
346
- ]
347
- }
348
- ),
349
- /* @__PURE__ */ jsx2(Handle2, { type: "source", position: Position2.Bottom, className: "!bg-gray-400 dark:!bg-gray-500" })
350
- ] });
351
- }
352
- var flowMapNodeTypes = { flowMapNode: FlowMapNode };
353
-
354
- // src/flows/FlowMapCanvas.tsx
355
- import { useMemo } from "react";
356
- import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
357
- import "@xyflow/react/dist/style.css";
358
-
359
- // src/flows/flowGraph.ts
360
- import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
361
- var CONDITION_OPERATORS = [">", ">=", "<", "<=", "==", "!=", "contains"];
362
- var BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND;
363
- var CROSS_FLOW_PREFIX = "flow:";
364
- var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
365
- var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
366
- var NODE_CARD_WIDTH = 240;
367
- function estimateNodeHeight(node) {
368
- const HEADER_HEIGHT = 28;
369
- const BODY_HEIGHT = 56;
370
- const PADDING = 16;
371
- const ROW_HEIGHT = 34;
372
- const rowCount = node.type === "action" ? 0 : node.type === "condition" ? 2 : node.type === "menu" || node.questionType === "choice" ? (node.options?.length ?? 0) + 1 : 1;
373
- return HEADER_HEIGHT + BODY_HEIGHT + PADDING + rowCount * ROW_HEIGHT;
374
- }
375
- var WHATSAPP_LIMITS = {
376
- MAX_BUTTONS: 3,
377
- MAX_LIST_ROWS: 10,
378
- BUTTON_TITLE_MAX: 20,
379
- LIST_ROW_TITLE_MAX: 24,
380
- BODY_MAX: 1024
381
- };
382
- function rendersAsButtons(options) {
383
- return (options?.length ?? 0) <= WHATSAPP_LIMITS.MAX_BUTTONS;
384
- }
385
- function targetsOf(node) {
386
- if (!node.next) return [];
387
- if (typeof node.next === "string") return [{ target: node.next }];
388
- return [
389
- ...Object.entries(node.next.byAnswer).map(([optionId, target]) => ({ target, optionId })),
390
- { target: node.next.default, isDefault: true }
391
- ];
392
- }
393
- function validateGraph(graph, issueText) {
394
- const issues = [];
395
- const nodeIds = new Set(Object.keys(graph.nodes));
396
- const isValidTarget = (target) => nodeIds.has(target) || isCrossFlowTarget(target);
397
- if (!nodeIds.has(graph.startNodeId)) {
398
- issues.push({ severity: "error", message: issueText.noStart });
399
- }
400
- for (const node of Object.values(graph.nodes)) {
401
- for (const { target } of targetsOf(node)) {
402
- if (!isValidTarget(target)) {
403
- issues.push({ severity: "error", nodeId: node.id, message: issueText.brokenRef(node.id, target) });
404
- }
405
- }
406
- const isChoice = node.questionType === "choice" || node.type === "menu";
407
- if (isChoice) {
408
- const options = node.options ?? [];
409
- if (options.length === 0) {
410
- issues.push({ severity: "error", nodeId: node.id, message: issueText.choiceWithoutOptions(node.id) });
411
- }
412
- const seen = /* @__PURE__ */ new Set();
413
- for (const [optionId, label] of options) {
414
- if (seen.has(optionId)) {
415
- issues.push({ severity: "error", nodeId: node.id, message: issueText.duplicatedOptionId(node.id, optionId) });
416
- }
417
- seen.add(optionId);
418
- const byAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
419
- if (!byAnswer[optionId]) {
420
- issues.push({ severity: "warning", nodeId: node.id, message: issueText.optionWithoutTarget(node.id, label) });
421
- }
422
- const usesButtons = rendersAsButtons(options);
423
- if (usesButtons && label.length > WHATSAPP_LIMITS.BUTTON_TITLE_MAX) {
424
- issues.push({ severity: "error", nodeId: node.id, message: issueText.buttonTitleTooLong(node.id, label) });
425
- }
426
- if (!usesButtons && label.length > WHATSAPP_LIMITS.LIST_ROW_TITLE_MAX) {
427
- issues.push({ severity: "error", nodeId: node.id, message: issueText.listTitleTooLong(node.id, label) });
428
- }
429
- }
430
- if (options.length > WHATSAPP_LIMITS.MAX_LIST_ROWS) {
431
- issues.push({ severity: "error", nodeId: node.id, message: issueText.tooManyOptions(node.id, options.length) });
432
- }
433
- }
434
- const bodyText = node.question ?? node.directMessage ?? "";
435
- if (bodyText.length > WHATSAPP_LIMITS.BODY_MAX) {
436
- issues.push({ severity: "error", nodeId: node.id, message: issueText.bodyTooLong(node.id) });
437
- }
438
- if (node.type === "question" && !node.next) {
439
- issues.push({ severity: "warning", nodeId: node.id, message: issueText.deadEndQuestion(node.id) });
440
- }
441
- if (node.type === "condition") {
442
- if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) {
443
- issues.push({ severity: "error", nodeId: node.id, message: issueText.conditionIncomplete(node.id) });
444
- }
445
- const byAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
446
- if (!byAnswer.true)
447
- issues.push({
448
- severity: "warning",
449
- nodeId: node.id,
450
- message: issueText.conditionBranchMissing(node.id, "true")
451
- });
452
- if (!byAnswer.false)
453
- issues.push({
454
- severity: "warning",
455
- nodeId: node.id,
456
- message: issueText.conditionBranchMissing(node.id, "false")
457
- });
458
- }
459
- }
460
- for (const id of findUnreachable(graph)) {
461
- issues.push({ severity: "warning", nodeId: id, message: issueText.unreachable(id) });
462
- }
463
- return issues;
464
- }
465
- function findUnreachable(graph) {
466
- const reachable = /* @__PURE__ */ new Set();
467
- const queue = [graph.startNodeId];
468
- while (queue.length > 0) {
469
- const id = queue.shift();
470
- if (reachable.has(id) || !graph.nodes[id]) continue;
471
- reachable.add(id);
472
- for (const { target } of targetsOf(graph.nodes[id])) {
473
- if (!isCrossFlowTarget(target)) queue.push(target);
474
- }
475
- }
476
- return Object.keys(graph.nodes).filter((id) => !reachable.has(id));
477
- }
478
- function computeAutoLayout(graph) {
479
- const H_GAP = 300;
480
- const V_GAP = 90;
481
- const rank = {};
482
- const queue = [graph.startNodeId];
483
- rank[graph.startNodeId] = 0;
484
- while (queue.length > 0) {
485
- const id = queue.shift();
486
- const node = graph.nodes[id];
487
- if (!node) continue;
488
- for (const { target } of targetsOf(node)) {
489
- if (isCrossFlowTarget(target) || !graph.nodes[target]) continue;
490
- if (rank[target] === void 0) {
491
- rank[target] = rank[id] + 1;
492
- queue.push(target);
493
- }
494
- }
495
- }
496
- const maxRank = Math.max(0, ...Object.values(rank));
497
- let strayRank = maxRank + 1;
498
- for (const id of Object.keys(graph.nodes)) {
499
- if (rank[id] === void 0) rank[id] = strayRank++;
500
- }
501
- const layers = {};
502
- for (const [id, r] of Object.entries(rank)) {
503
- layers[r] = [...layers[r] ?? [], id];
504
- }
505
- const positions = {};
506
- const sortedRanks = Object.keys(layers).map(Number).sort((a, b) => a - b);
507
- let cumulativeY = 0;
508
- for (const r of sortedRanks) {
509
- const ids = layers[r];
510
- const width = (ids.length - 1) * H_GAP;
511
- let maxHeight = 0;
512
- ids.forEach((id, index) => {
513
- maxHeight = Math.max(maxHeight, estimateNodeHeight(graph.nodes[id]));
514
- positions[id] = { x: index * H_GAP - width / 2, y: cumulativeY };
515
- });
516
- cumulativeY += maxHeight + V_GAP;
517
- }
518
- return positions;
519
- }
520
- function crossFlowTargetsOf(graph) {
521
- const keys = /* @__PURE__ */ new Set();
522
- for (const node of Object.values(graph.nodes)) {
523
- for (const { target } of targetsOf(node)) {
524
- if (isCrossFlowTarget(target)) keys.add(crossFlowKey(target));
525
- }
526
- }
527
- return [...keys];
528
- }
529
- function computeFlowMapLayout(graphs, rootKey) {
530
- const H_GAP = 280;
531
- const V_GAP = 170;
532
- const rank = {};
533
- const queue = graphs[rootKey] ? [rootKey] : Object.keys(graphs);
534
- if (graphs[rootKey]) rank[rootKey] = 0;
535
- while (queue.length > 0) {
536
- const key = queue.shift();
537
- const g = graphs[key];
538
- if (!g) continue;
539
- for (const target of crossFlowTargetsOf(g)) {
540
- if (!graphs[target]) continue;
541
- if (rank[target] === void 0) {
542
- rank[target] = rank[key] + 1;
543
- queue.push(target);
544
- }
545
- }
546
- }
547
- const maxRank = Math.max(0, ...Object.values(rank));
548
- let strayRank = maxRank + 1;
549
- for (const key of Object.keys(graphs)) {
550
- if (rank[key] === void 0) rank[key] = strayRank++;
551
- }
552
- const layers = {};
553
- for (const [key, r] of Object.entries(rank)) {
554
- layers[r] = [...layers[r] ?? [], key];
555
- }
556
- const positions = {};
557
- for (const [r, keys] of Object.entries(layers)) {
558
- const width = (keys.length - 1) * H_GAP;
559
- keys.forEach((key, index) => {
560
- positions[key] = { x: index * H_GAP - width / 2, y: Number(r) * V_GAP };
561
- });
562
- }
563
- return positions;
564
- }
565
- function findCollectionChains(graph) {
566
- const actionIds = new Set(
567
- Object.values(graph.nodes).filter((n) => n.type === "action").map((n) => n.id)
568
- );
569
- const memo = /* @__PURE__ */ new Map();
570
- function reachableActions(id, stack) {
571
- if (memo.has(id)) return memo.get(id);
572
- if (stack.has(id)) return /* @__PURE__ */ new Set();
573
- if (actionIds.has(id)) return /* @__PURE__ */ new Set([id]);
574
- const node = graph.nodes[id];
575
- if (!node) return /* @__PURE__ */ new Set();
576
- const nextStack = new Set(stack);
577
- nextStack.add(id);
578
- const result = /* @__PURE__ */ new Set();
579
- for (const { target } of targetsOf(node)) {
580
- if (isCrossFlowTarget(target) || !graph.nodes[target]) continue;
581
- for (const actionId of reachableActions(target, nextStack)) result.add(actionId);
582
- }
583
- memo.set(id, result);
584
- return result;
585
- }
586
- const nodeIdsByAction = /* @__PURE__ */ new Map();
587
- for (const node of Object.values(graph.nodes)) {
588
- if (node.type !== "question") continue;
589
- const reached = reachableActions(node.id, /* @__PURE__ */ new Set());
590
- if (reached.size !== 1) continue;
591
- const [actionNodeId] = [...reached];
592
- nodeIdsByAction.set(actionNodeId, [...nodeIdsByAction.get(actionNodeId) ?? [], node.id]);
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."
593
671
  }
594
- return [...nodeIdsByAction.entries()].filter(([, nodeIds]) => nodeIds.length >= 2).map(([actionNodeId, nodeIds]) => ({ actionNodeId, nodeIds }));
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
+ };
595
697
  }
596
- function slugifyNodeId(label, existing) {
597
- const base = label.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 30) || "no";
598
- let candidate = base;
599
- let counter = 2;
600
- while (existing.has(candidate)) {
601
- candidate = `${base}_${counter}`;
602
- counter++;
603
- }
604
- return candidate;
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
+ ] });
605
728
  }
729
+ var flowMapNodeTypes = { flowMapNode: FlowMapNode };
606
730
 
607
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";
608
735
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
609
736
  var MAP_NODE_TYPES = { ...flowMapNodeTypes };
610
737
  var EDGE_COLOR = "#06b6d4";
@@ -731,23 +858,122 @@ function FlowPortalNode({ data }) {
731
858
  var flowPortalNodeTypes = { flowPortal: FlowPortalNode };
732
859
 
733
860
  // src/flows/FlowPalette.tsx
734
- import { useEffect, useRef, useState } from "react";
735
- import { Plus, MessageCircleQuestion as MessageCircleQuestion2, GitBranch as GitBranch3, Zap as Zap2, Diamond as Diamond2, ChevronRight } from "lucide-react";
736
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
861
+ import { useEffect, useRef, useState as useState2 } from "react";
862
+ import { Plus as Plus2, MessageCircleQuestion as MessageCircleQuestion2, GitBranch as GitBranch3, Zap as Zap2, Diamond as Diamond2, ChevronRight } from "lucide-react";
863
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
737
864
  var QUESTION_TYPES = ["text", "money", "date", "int", "cpf"];
738
- function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
739
- const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
865
+ function FlowPaletteMenu({ onSelect, labels, actionOptions }) {
740
866
  const resolvedActionOptions = actionOptions ?? [
741
867
  { actionKind: "handoff", label: labels.actionKindLabels.handoff ?? "Encaminhar para atendimento" }
742
868
  ];
743
- const [open, setOpen] = useState(false);
744
- const [submenu, setSubmenu] = useState(null);
869
+ const [submenu, setSubmenu] = useState2(null);
870
+ function select(spec) {
871
+ onSelect(spec);
872
+ setSubmenu(null);
873
+ }
874
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
875
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
876
+ /* @__PURE__ */ jsxs6(
877
+ "button",
878
+ {
879
+ "data-cv-tooltip": labels.palette.question,
880
+ "aria-label": labels.palette.question,
881
+ onMouseEnter: () => setSubmenu("question"),
882
+ onClick: () => setSubmenu(submenu === "question" ? null : "question"),
883
+ className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
884
+ children: [
885
+ /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
886
+ /* @__PURE__ */ jsx7(MessageCircleQuestion2, { size: 15, className: "text-blue-500" }),
887
+ " ",
888
+ labels.palette.question
889
+ ] }),
890
+ /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
891
+ ]
892
+ }
893
+ ),
894
+ submenu === "question" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: QUESTION_TYPES.map((qt) => /* @__PURE__ */ jsx7(
895
+ "button",
896
+ {
897
+ "data-cv-tooltip": labels.questionTypeLabels[qt],
898
+ "aria-label": labels.questionTypeLabels[qt],
899
+ onClick: () => select({ kind: "question", questionType: qt }),
900
+ className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
901
+ children: labels.questionTypeLabels[qt]
902
+ },
903
+ qt
904
+ )) })
905
+ ] }),
906
+ /* @__PURE__ */ jsxs6(
907
+ "button",
908
+ {
909
+ "data-cv-tooltip": labels.palette.decision,
910
+ "aria-label": labels.palette.decision,
911
+ onMouseEnter: () => setSubmenu(null),
912
+ onClick: () => select({ kind: "decision" }),
913
+ className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
914
+ children: [
915
+ /* @__PURE__ */ jsx7(GitBranch3, { size: 15, className: "text-purple-500" }),
916
+ " ",
917
+ labels.palette.decision
918
+ ]
919
+ }
920
+ ),
921
+ /* @__PURE__ */ jsxs6(
922
+ "button",
923
+ {
924
+ onMouseEnter: () => setSubmenu(null),
925
+ onClick: () => select({ kind: "condition" }),
926
+ className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
927
+ "data-cv-tooltip": labels.palette.conditionHint,
928
+ "aria-label": labels.palette.conditionHint,
929
+ children: [
930
+ /* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
931
+ " ",
932
+ labels.palette.condition
933
+ ]
934
+ }
935
+ ),
936
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
937
+ /* @__PURE__ */ jsxs6(
938
+ "button",
939
+ {
940
+ "data-cv-tooltip": labels.palette.action,
941
+ "aria-label": labels.palette.action,
942
+ onMouseEnter: () => setSubmenu("action"),
943
+ onClick: () => setSubmenu(submenu === "action" ? null : "action"),
944
+ className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
945
+ children: [
946
+ /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
947
+ /* @__PURE__ */ jsx7(Zap2, { size: 15, className: "text-orange-500" }),
948
+ " ",
949
+ labels.palette.action
950
+ ] }),
951
+ /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
952
+ ]
953
+ }
954
+ ),
955
+ submenu === "action" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: resolvedActionOptions.map((option) => /* @__PURE__ */ jsx7(
956
+ "button",
957
+ {
958
+ "data-cv-tooltip": option.label,
959
+ "aria-label": option.label,
960
+ onClick: () => select({ kind: "action", actionKind: option.actionKind }),
961
+ className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
962
+ children: option.label
963
+ },
964
+ option.actionKind
965
+ )) })
966
+ ] })
967
+ ] });
968
+ }
969
+ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
970
+ const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
971
+ const [open, setOpen] = useState2(false);
745
972
  const containerRef = useRef(null);
746
973
  useEffect(() => {
747
974
  function handleClickOutside(event) {
748
975
  if (containerRef.current && !containerRef.current.contains(event.target)) {
749
976
  setOpen(false);
750
- setSubmenu(null);
751
977
  }
752
978
  }
753
979
  document.addEventListener("mousedown", handleClickOutside);
@@ -756,7 +982,6 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
756
982
  function select(spec) {
757
983
  onAdd(spec);
758
984
  setOpen(false);
759
- setSubmenu(null);
760
985
  }
761
986
  return /* @__PURE__ */ jsxs6("div", { ref: containerRef, className: "relative", children: [
762
987
  /* @__PURE__ */ jsxs6(
@@ -767,155 +992,214 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
767
992
  onClick: () => setOpen((v) => !v),
768
993
  className: "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700",
769
994
  children: [
770
- /* @__PURE__ */ jsx7(Plus, { size: 14 }),
995
+ /* @__PURE__ */ jsx7(Plus2, { size: 14 }),
771
996
  " ",
772
997
  labels.palette.title
773
998
  ]
774
999
  }
775
1000
  ),
776
- 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: [
777
- /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
778
- /* @__PURE__ */ jsxs6(
779
- "button",
780
- {
781
- "data-cv-tooltip": labels.palette.question,
782
- "aria-label": labels.palette.question,
783
- onMouseEnter: () => setSubmenu("question"),
784
- onClick: () => setSubmenu(submenu === "question" ? null : "question"),
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",
786
- children: [
787
- /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
788
- /* @__PURE__ */ jsx7(MessageCircleQuestion2, { size: 15, className: "text-blue-500" }),
789
- " ",
790
- labels.palette.question
791
- ] }),
792
- /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
793
- ]
794
- }
795
- ),
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(
797
- "button",
798
- {
799
- "data-cv-tooltip": labels.questionTypeLabels[qt],
800
- "aria-label": labels.questionTypeLabels[qt],
801
- onClick: () => select({ kind: "question", questionType: qt }),
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",
803
- children: labels.questionTypeLabels[qt]
804
- },
805
- qt
806
- )) })
807
- ] }),
808
- /* @__PURE__ */ jsxs6(
809
- "button",
810
- {
811
- "data-cv-tooltip": labels.palette.decision,
812
- "aria-label": labels.palette.decision,
813
- onMouseEnter: () => setSubmenu(null),
814
- onClick: () => select({ kind: "decision" }),
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",
816
- children: [
817
- /* @__PURE__ */ jsx7(GitBranch3, { size: 15, className: "text-purple-500" }),
818
- " ",
819
- labels.palette.decision
820
- ]
821
- }
822
- ),
823
- /* @__PURE__ */ jsxs6(
824
- "button",
825
- {
826
- onMouseEnter: () => setSubmenu(null),
827
- onClick: () => select({ kind: "condition" }),
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",
829
- "data-cv-tooltip": labels.palette.conditionHint,
830
- "aria-label": labels.palette.conditionHint,
831
- children: [
832
- /* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
833
- " ",
834
- labels.palette.condition
835
- ]
836
- }
837
- ),
838
- /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
839
- /* @__PURE__ */ jsxs6(
840
- "button",
841
- {
842
- "data-cv-tooltip": labels.palette.action,
843
- "aria-label": labels.palette.action,
844
- onMouseEnter: () => setSubmenu("action"),
845
- onClick: () => setSubmenu(submenu === "action" ? null : "action"),
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",
847
- children: [
848
- /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
849
- /* @__PURE__ */ jsx7(Zap2, { size: 15, className: "text-orange-500" }),
850
- " ",
851
- labels.palette.action
852
- ] }),
853
- /* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
854
- ]
855
- }
856
- ),
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(
858
- "button",
859
- {
860
- "data-cv-tooltip": option.label,
861
- "aria-label": option.label,
862
- onClick: () => select({ kind: "action", actionKind: option.actionKind }),
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",
864
- children: option.label
865
- },
866
- option.actionKind
867
- )) })
1001
+ open && /* @__PURE__ */ jsx7("div", { className: "absolute left-0 top-full mt-1.5 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg z-50 py-1", children: /* @__PURE__ */ jsx7(
1002
+ FlowPaletteMenu,
1003
+ {
1004
+ onSelect: select,
1005
+ labels,
1006
+ ...actionOptions ? { actionOptions } : {}
1007
+ }
1008
+ ) })
1009
+ ] });
1010
+ }
1011
+
1012
+ // src/flows/FlowLegend.tsx
1013
+ import { useState as useState3 } from "react";
1014
+ import { ChevronDown, ChevronUp, RotateCcw as RotateCcw2 } from "lucide-react";
1015
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1016
+ function EdgeSample({ sample }) {
1017
+ return /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1018
+ /* @__PURE__ */ jsx8("svg", { width: "28", height: "12", viewBox: "0 0 28 12", "aria-hidden": "true", className: "shrink-0", children: /* @__PURE__ */ jsx8(
1019
+ "line",
1020
+ {
1021
+ x1: "2",
1022
+ y1: "6",
1023
+ x2: "26",
1024
+ y2: "6",
1025
+ stroke: sample.color,
1026
+ strokeWidth: "1.75",
1027
+ ...sample.dash ? { strokeDasharray: sample.dash } : {}
1028
+ }
1029
+ ) }),
1030
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: sample.label })
1031
+ ] });
1032
+ }
1033
+ function FlowLegend({ labels, edgeSamples, nodeSwatches }) {
1034
+ const [open, setOpen] = useState3(false);
1035
+ return /* @__PURE__ */ jsxs7("div", { className: "rounded-lg border border-gray-200 bg-white/95 shadow-sm backdrop-blur dark:border-gray-700 dark:bg-gray-800/95", children: [
1036
+ /* @__PURE__ */ jsxs7(
1037
+ "button",
1038
+ {
1039
+ type: "button",
1040
+ onClick: () => setOpen((current) => !current),
1041
+ "data-cv-tooltip": labels.legendPanel.title,
1042
+ "aria-label": labels.legendPanel.title,
1043
+ "aria-expanded": open,
1044
+ className: "flex w-full items-center justify-between gap-2 px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400",
1045
+ children: [
1046
+ labels.legendPanel.title,
1047
+ open ? /* @__PURE__ */ jsx8(ChevronDown, { size: 13 }) : /* @__PURE__ */ jsx8(ChevronUp, { size: 13 })
1048
+ ]
1049
+ }
1050
+ ),
1051
+ open && /* @__PURE__ */ jsxs7("div", { className: "max-h-72 w-64 overflow-y-auto border-t border-gray-100 px-2.5 py-2 dark:border-gray-700", children: [
1052
+ /* @__PURE__ */ jsx8("p", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500", children: labels.legendPanel.connections }),
1053
+ /* @__PURE__ */ jsx8("ul", { className: "space-y-1.5", children: edgeSamples.map((sample) => /* @__PURE__ */ jsx8(EdgeSample, { sample }, sample.label)) }),
1054
+ /* @__PURE__ */ jsx8("p", { className: "mb-1 mt-3 text-[10px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500", children: labels.legendPanel.nodes }),
1055
+ /* @__PURE__ */ jsxs7("ul", { className: "space-y-1.5", children: [
1056
+ nodeSwatches.map((swatch) => /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1057
+ /* @__PURE__ */ jsx8("span", { className: `h-3 w-5 shrink-0 rounded border-2 ${swatch.className}`, "aria-hidden": "true" }),
1058
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legend[swatch.type] })
1059
+ ] }, swatch.type)),
1060
+ /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1061
+ /* @__PURE__ */ jsx8(
1062
+ "span",
1063
+ {
1064
+ className: "h-3 w-5 shrink-0 rounded border-2 border-dashed border-amber-400",
1065
+ "aria-hidden": "true"
1066
+ }
1067
+ ),
1068
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legendPanel.detached })
1069
+ ] }),
1070
+ /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1071
+ /* @__PURE__ */ jsx8("span", { className: "flex h-3 w-5 shrink-0 items-center justify-center", "aria-hidden": "true", children: /* @__PURE__ */ jsx8("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }) }),
1072
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legendPanel.startNode })
1073
+ ] }),
1074
+ /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-2", children: [
1075
+ /* @__PURE__ */ jsx8("span", { className: "flex h-3 w-5 shrink-0 items-center justify-center text-gray-400", "aria-hidden": "true", children: /* @__PURE__ */ jsx8(RotateCcw2, { size: 12, strokeWidth: 2.5 }) }),
1076
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] leading-tight text-gray-600 dark:text-gray-300", children: labels.legendPanel.selfLoop })
1077
+ ] })
868
1078
  ] })
869
1079
  ] })
870
1080
  ] });
871
1081
  }
872
1082
 
1083
+ // src/flows/FlowConnectionEdge.tsx
1084
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
1085
+ import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@xyflow/react";
1086
+ import { X as X2 } from "lucide-react";
1087
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1088
+ var HIDE_DELAY_MS = 320;
1089
+ function FlowConnectionEdge({
1090
+ id,
1091
+ sourceX,
1092
+ sourceY,
1093
+ targetX,
1094
+ targetY,
1095
+ sourcePosition,
1096
+ targetPosition,
1097
+ markerEnd,
1098
+ style,
1099
+ data,
1100
+ interactionWidth
1101
+ }) {
1102
+ const [hovered, setHovered] = useState4(false);
1103
+ const hideTimer = useRef2(void 0);
1104
+ const { onDisconnect, disconnectLabel } = data ?? {};
1105
+ function show() {
1106
+ if (hideTimer.current) clearTimeout(hideTimer.current);
1107
+ setHovered(true);
1108
+ }
1109
+ function scheduleHide() {
1110
+ if (hideTimer.current) clearTimeout(hideTimer.current);
1111
+ hideTimer.current = setTimeout(() => setHovered(false), HIDE_DELAY_MS);
1112
+ }
1113
+ useEffect2(() => () => clearTimeout(hideTimer.current), []);
1114
+ const [path, labelX, labelY] = getBezierPath({
1115
+ sourceX,
1116
+ sourceY,
1117
+ sourcePosition,
1118
+ targetX,
1119
+ targetY,
1120
+ targetPosition
1121
+ });
1122
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
1123
+ /* @__PURE__ */ jsx9(BaseEdge, { id, path, markerEnd, style, interactionWidth: interactionWidth ?? 20 }),
1124
+ /* @__PURE__ */ jsx9(
1125
+ "path",
1126
+ {
1127
+ d: path,
1128
+ fill: "none",
1129
+ strokeWidth: 22,
1130
+ stroke: "transparent",
1131
+ className: "react-flow__edge-interaction",
1132
+ onMouseEnter: show,
1133
+ onMouseLeave: scheduleHide
1134
+ }
1135
+ ),
1136
+ onDisconnect && hovered && /* @__PURE__ */ jsx9(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx9(
1137
+ "button",
1138
+ {
1139
+ type: "button",
1140
+ "data-cv-tooltip": disconnectLabel,
1141
+ "aria-label": disconnectLabel,
1142
+ onMouseEnter: show,
1143
+ onMouseLeave: scheduleHide,
1144
+ onClick: (event) => {
1145
+ event.stopPropagation();
1146
+ onDisconnect();
1147
+ },
1148
+ style: { transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` },
1149
+ className: "nodrag nopan pointer-events-auto absolute flex h-5 w-5 items-center justify-center rounded-full border border-gray-300 bg-white text-gray-500 shadow-sm hover:border-red-400 hover:text-red-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400",
1150
+ children: /* @__PURE__ */ jsx9(X2, { size: 12, strokeWidth: 2.5 })
1151
+ }
1152
+ ) })
1153
+ ] });
1154
+ }
1155
+ var flowEdgeTypes = { flowConnection: FlowConnectionEdge };
1156
+
873
1157
  // src/flows/FlowNodePanel.tsx
874
- import { useState as useState2 } from "react";
875
- import { Plus as Plus2, Trash2, Save, X as X2, AlertTriangle as AlertTriangle2, AlertCircle as AlertCircle2 } from "lucide-react";
1158
+ import { useState as useState5 } from "react";
1159
+ import { Plus as Plus3, Trash2, Save, X as X3, AlertTriangle as AlertTriangle2, AlertCircle as AlertCircle2 } from "lucide-react";
876
1160
 
877
1161
  // src/flows/FlowWhatsAppPreview.tsx
878
1162
  import { List } from "lucide-react";
879
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1163
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
880
1164
  function FlowWhatsAppPreview({
881
1165
  body,
882
1166
  options,
883
1167
  labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel
884
1168
  }) {
885
1169
  if (!body && (!options || options.length === 0)) {
886
- return /* @__PURE__ */ jsx8("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
1170
+ return /* @__PURE__ */ jsx10("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
887
1171
  }
888
1172
  const usesButtons = rendersAsButtons(options);
889
1173
  const hasOptions = (options?.length ?? 0) > 0;
890
- return /* @__PURE__ */ jsxs7("div", { className: "rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5", children: [
891
- /* @__PURE__ */ jsxs7("div", { className: "max-w-[85%]", children: [
892
- /* @__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: [
893
- body ? parseWhatsAppFormatting(body) : /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyBody }),
894
- 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: [
895
- /* @__PURE__ */ jsx8(List, { size: 15 }),
1174
+ return /* @__PURE__ */ jsxs9("div", { className: "rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5", children: [
1175
+ /* @__PURE__ */ jsxs9("div", { className: "max-w-[85%]", children: [
1176
+ /* @__PURE__ */ jsxs9("div", { className: "rounded-lg rounded-tl-none bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm whitespace-pre-wrap break-words", children: [
1177
+ body ? parseWhatsAppFormatting(body) : /* @__PURE__ */ jsx10("span", { className: "italic text-gray-400", children: labels.previewEmptyBody }),
1178
+ hasOptions && !usesButtons && /* @__PURE__ */ jsx10("div", { className: "mt-2 -mx-3 -mb-2 border-t border-gray-100 dark:border-gray-600", children: /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-center gap-1.5 py-2 text-sm font-medium text-cyan-600 dark:text-cyan-400", children: [
1179
+ /* @__PURE__ */ jsx10(List, { size: 15 }),
896
1180
  " ",
897
1181
  labels.previewListButton
898
1182
  ] }) })
899
1183
  ] }),
900
- hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8(
1184
+ hasOptions && usesButtons && /* @__PURE__ */ jsx10("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx10(
901
1185
  "div",
902
1186
  {
903
1187
  className: "rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm",
904
- children: label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
1188
+ children: label || /* @__PURE__ */ jsx10("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
905
1189
  },
906
1190
  id
907
1191
  )) }),
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: [
909
- /* @__PURE__ */ jsx8("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
910
- label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
1192
+ hasOptions && !usesButtons && /* @__PURE__ */ jsx10("div", { className: "mt-1.5 rounded-lg bg-white dark:bg-gray-700 shadow-sm divide-y divide-gray-100 dark:divide-gray-600 overflow-hidden", children: options.slice(0, WHATSAPP_LIMITS.MAX_LIST_ROWS).map(([id, label]) => /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-3 py-1.5 text-sm text-gray-800 dark:text-gray-100", children: [
1193
+ /* @__PURE__ */ jsx10("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
1194
+ label || /* @__PURE__ */ jsx10("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
911
1195
  ] }, id)) })
912
1196
  ] }),
913
- hasOptions && /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-gray-500 dark:text-gray-400 px-1", children: usesButtons ? labels.previewModeButtons : labels.previewModeList })
1197
+ hasOptions && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-gray-500 dark:text-gray-400 px-1", children: usesButtons ? labels.previewModeButtons : labels.previewModeList })
914
1198
  ] });
915
1199
  }
916
1200
 
917
1201
  // src/flows/FlowNodePanel.tsx
918
- import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1202
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
919
1203
  var SELECT_CLASSNAME = "border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all";
920
1204
  var INPUT_CLASSNAME = "border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all";
921
1205
  function truncateLabel(value, max = 60) {
@@ -930,10 +1214,10 @@ function WhatsAppTextField({
930
1214
  placeholder,
931
1215
  labels
932
1216
  }) {
933
- return /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
934
- /* @__PURE__ */ jsxs8("div", { children: [
935
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: label }),
936
- /* @__PURE__ */ jsx9(
1217
+ return /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
1218
+ /* @__PURE__ */ jsxs10("div", { children: [
1219
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: label }),
1220
+ /* @__PURE__ */ jsx11(
937
1221
  "textarea",
938
1222
  {
939
1223
  value,
@@ -944,18 +1228,18 @@ function WhatsAppTextField({
944
1228
  }
945
1229
  )
946
1230
  ] }),
947
- /* @__PURE__ */ jsxs8("div", { children: [
948
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.preview }),
949
- /* @__PURE__ */ jsx9("div", { className: "mt-1", children: /* @__PURE__ */ jsx9(FlowWhatsAppPreview, { body: value, options, labels: labels.nodePanel }) })
1231
+ /* @__PURE__ */ jsxs10("div", { children: [
1232
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.preview }),
1233
+ /* @__PURE__ */ jsx11("div", { className: "mt-1", children: /* @__PURE__ */ jsx11(FlowWhatsAppPreview, { body: value, options, labels: labels.nodePanel }) })
950
1234
  ] })
951
1235
  ] });
952
1236
  }
953
1237
  function IssueRow({ issue }) {
954
1238
  const Icon = issue.severity === "error" ? AlertCircle2 : AlertTriangle2;
955
1239
  const color = issue.severity === "error" ? "text-red-600 dark:text-red-400" : "text-amber-600 dark:text-amber-400";
956
- return /* @__PURE__ */ jsxs8("div", { className: `flex items-start gap-1.5 text-xs ${color}`, children: [
957
- /* @__PURE__ */ jsx9(Icon, { size: 13, className: "mt-0.5 shrink-0" }),
958
- /* @__PURE__ */ jsx9("span", { children: issue.message })
1240
+ return /* @__PURE__ */ jsxs10("div", { className: `flex items-start gap-1.5 text-xs ${color}`, children: [
1241
+ /* @__PURE__ */ jsx11(Icon, { size: 13, className: "mt-0.5 shrink-0" }),
1242
+ /* @__PURE__ */ jsx11("span", { children: issue.message })
959
1243
  ] });
960
1244
  }
961
1245
  function FlowNodePanel({
@@ -970,11 +1254,12 @@ function FlowNodePanel({
970
1254
  renderMediaPicker
971
1255
  }) {
972
1256
  const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
973
- const [draft, setDraft] = useState2(node);
1257
+ const [draft, setDraft] = useState5(node);
974
1258
  const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id);
975
1259
  const isFixedLogic = draft.type === "entrada_choice";
976
1260
  const isAction = draft.type === "action";
977
1261
  const isSendMedia = isAction && draft.actionKind === BUILT_IN_ACTION_KINDS.SEND_MEDIA;
1262
+ const isPassThroughAction = isAction && Boolean(draft.actionKind && PASS_THROUGH_ACTION_KINDS.includes(draft.actionKind));
978
1263
  const isCondition = draft.type === "condition";
979
1264
  const isStart = graph.startNodeId === node.id;
980
1265
  const nodeIssues = issues.filter((i) => i.nodeId === node.id);
@@ -1018,24 +1303,24 @@ function FlowNodePanel({
1018
1303
  setDraft((prev) => ({ ...prev, options: (prev.options ?? []).filter((_, i) => i !== index) }));
1019
1304
  }
1020
1305
  function nextNodeOptions() {
1021
- return /* @__PURE__ */ jsxs8(Fragment, { children: [
1022
- otherNodeIds.map((id) => /* @__PURE__ */ jsx9("option", { value: id, children: truncateLabel(nodeLabel(graph.nodes[id], labels)) }, id)),
1023
- 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)) })
1306
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1307
+ otherNodeIds.map((id) => /* @__PURE__ */ jsx11("option", { value: id, children: truncateLabel(nodeLabel(graph.nodes[id], labels)) }, id)),
1308
+ otherFlows.length > 0 && /* @__PURE__ */ jsx11("optgroup", { label: labels.nodePanel.otherFlowsGroup, children: otherFlows.map((flow) => /* @__PURE__ */ jsx11("option", { value: `${CROSS_FLOW_PREFIX}${flow.key}`, children: flow.label }, flow.key)) })
1024
1309
  ] });
1025
1310
  }
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: [
1027
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
1028
- /* @__PURE__ */ jsx9("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
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 }) })
1311
+ return /* @__PURE__ */ jsxs10("div", { className: "fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col", children: [
1312
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
1313
+ /* @__PURE__ */ jsx11("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
1314
+ /* @__PURE__ */ jsx11("button", { "data-cv-tooltip": labels.nodePanel.close, "aria-label": labels.nodePanel.close, onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx11(X3, { size: 18 }) })
1030
1315
  ] }),
1031
- /* @__PURE__ */ jsxs8("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
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)) }),
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 }),
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 }),
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(
1316
+ /* @__PURE__ */ jsxs10("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
1317
+ nodeIssues.length > 0 && /* @__PURE__ */ jsx11("div", { className: "rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-2.5 space-y-1.5", children: nodeIssues.map((issue, i) => /* @__PURE__ */ jsx11(IssueRow, { issue }, i)) }),
1318
+ isFixedLogic && /* @__PURE__ */ jsx11("p", { className: "text-xs text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30 rounded-lg p-2", children: labels.nodePanel.fixedLogicNotice }),
1319
+ isAction && /* @__PURE__ */ jsx11("p", { className: "text-xs text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/30 rounded-lg p-2", children: labels.nodePanel.actionNotice }),
1320
+ isCondition && /* @__PURE__ */ jsx11("p", { className: "text-xs text-cyan-700 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/30 rounded-lg p-2", children: labels.nodePanel.conditionNotice }),
1321
+ /* @__PURE__ */ jsxs10("div", { children: [
1322
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.nodeName }),
1323
+ /* @__PURE__ */ jsx11(
1039
1324
  "input",
1040
1325
  {
1041
1326
  value: draft.label ?? "",
@@ -1044,11 +1329,11 @@ function FlowNodePanel({
1044
1329
  className: `w-full mt-1 ${INPUT_CLASSNAME}`
1045
1330
  }
1046
1331
  ),
1047
- /* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mt-1", children: labels.nodePanel.nodeNameHint })
1332
+ /* @__PURE__ */ jsx11("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mt-1", children: labels.nodePanel.nodeNameHint })
1048
1333
  ] }),
1049
- draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
1050
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
1051
- /* @__PURE__ */ jsx9(
1334
+ draft.contextKey && /* @__PURE__ */ jsxs10("div", { children: [
1335
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
1336
+ /* @__PURE__ */ jsx11(
1052
1337
  "input",
1053
1338
  {
1054
1339
  value: draft.contextKey,
@@ -1057,19 +1342,19 @@ function FlowNodePanel({
1057
1342
  }
1058
1343
  )
1059
1344
  ] }),
1060
- !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs8("div", { children: [
1061
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
1062
- /* @__PURE__ */ jsx9(
1345
+ !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs10("div", { children: [
1346
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
1347
+ /* @__PURE__ */ jsx11(
1063
1348
  "select",
1064
1349
  {
1065
1350
  value: draft.questionType ?? "text",
1066
1351
  onChange: (e) => setDraft((prev) => ({ ...prev, questionType: e.target.value })),
1067
1352
  className: `w-full mt-1 ${SELECT_CLASSNAME}`,
1068
- children: Object.entries(labels.questionTypeLabels).map(([key, label]) => /* @__PURE__ */ jsx9("option", { value: key, children: label }, key))
1353
+ children: Object.entries(labels.questionTypeLabels).map(([key, label]) => /* @__PURE__ */ jsx11("option", { value: key, children: label }, key))
1069
1354
  }
1070
1355
  )
1071
1356
  ] }),
1072
- !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsx9(
1357
+ !isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsx11(
1073
1358
  WhatsAppTextField,
1074
1359
  {
1075
1360
  label: labels.nodePanel.question,
@@ -1079,10 +1364,10 @@ function FlowNodePanel({
1079
1364
  labels
1080
1365
  }
1081
1366
  ),
1082
- isCondition && /* @__PURE__ */ jsxs8("div", { className: "space-y-3", children: [
1083
- /* @__PURE__ */ jsxs8("div", { children: [
1084
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionVariable }),
1085
- /* @__PURE__ */ jsx9(
1367
+ isCondition && /* @__PURE__ */ jsxs10("div", { className: "space-y-3", children: [
1368
+ /* @__PURE__ */ jsxs10("div", { children: [
1369
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionVariable }),
1370
+ /* @__PURE__ */ jsx11(
1086
1371
  "input",
1087
1372
  {
1088
1373
  value: draft.conditionContextKey ?? "",
@@ -1091,11 +1376,11 @@ function FlowNodePanel({
1091
1376
  className: `w-full mt-1 ${INPUT_CLASSNAME}`
1092
1377
  }
1093
1378
  ),
1094
- /* @__PURE__ */ jsx9("datalist", { id: "condition-context-keys", children: knownContextKeys.map((key) => /* @__PURE__ */ jsx9("option", { value: key }, key)) })
1379
+ /* @__PURE__ */ jsx11("datalist", { id: "condition-context-keys", children: knownContextKeys.map((key) => /* @__PURE__ */ jsx11("option", { value: key }, key)) })
1095
1380
  ] }),
1096
- /* @__PURE__ */ jsxs8("div", { children: [
1097
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionOperator }),
1098
- /* @__PURE__ */ jsx9(
1381
+ /* @__PURE__ */ jsxs10("div", { children: [
1382
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionOperator }),
1383
+ /* @__PURE__ */ jsx11(
1099
1384
  "select",
1100
1385
  {
1101
1386
  value: draft.conditionOperator ?? ">",
@@ -1104,13 +1389,13 @@ function FlowNodePanel({
1104
1389
  conditionOperator: e.target.value
1105
1390
  })),
1106
1391
  className: `w-full mt-1 ${SELECT_CLASSNAME}`,
1107
- children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx9("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
1392
+ children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx11("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
1108
1393
  }
1109
1394
  )
1110
1395
  ] }),
1111
- /* @__PURE__ */ jsxs8("div", { children: [
1112
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionValue }),
1113
- /* @__PURE__ */ jsx9(
1396
+ /* @__PURE__ */ jsxs10("div", { children: [
1397
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionValue }),
1398
+ /* @__PURE__ */ jsx11(
1114
1399
  "input",
1115
1400
  {
1116
1401
  value: draft.conditionValue ?? "",
@@ -1120,7 +1405,7 @@ function FlowNodePanel({
1120
1405
  )
1121
1406
  ] })
1122
1407
  ] }),
1123
- isAction && draft.actionKind === "send_product_list" && /* @__PURE__ */ jsx9(
1408
+ isAction && draft.actionKind === "send_product_list" && /* @__PURE__ */ jsx11(
1124
1409
  WhatsAppTextField,
1125
1410
  {
1126
1411
  label: labels.nodePanel.fallbackMessage,
@@ -1130,11 +1415,11 @@ function FlowNodePanel({
1130
1415
  labels
1131
1416
  }
1132
1417
  ),
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 }) })
1418
+ isSendMedia && /* @__PURE__ */ jsxs10("div", { children: [
1419
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.media }),
1420
+ /* @__PURE__ */ jsx11("div", { className: "mt-1", children: renderMediaPicker?.(node, graph) ?? /* @__PURE__ */ jsx11("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
1136
1421
  ] }),
1137
- isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx9(
1422
+ isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx11(
1138
1423
  WhatsAppTextField,
1139
1424
  {
1140
1425
  label: labels.nodePanel.directMessage,
@@ -1144,17 +1429,17 @@ function FlowNodePanel({
1144
1429
  labels
1145
1430
  }
1146
1431
  ),
1147
- (draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs8("div", { children: [
1148
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between mb-1.5", children: [
1149
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
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: [
1151
- /* @__PURE__ */ jsx9(Plus2, { size: 12 }),
1432
+ (draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs10("div", { children: [
1433
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between mb-1.5", children: [
1434
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
1435
+ /* @__PURE__ */ jsxs10("button", { "data-cv-tooltip": labels.nodePanel.addOption, "aria-label": labels.nodePanel.addOption, onClick: addOption, className: "text-xs text-blue-600 hover:underline flex items-center gap-1", children: [
1436
+ /* @__PURE__ */ jsx11(Plus3, { size: 12 }),
1152
1437
  " ",
1153
1438
  labels.nodePanel.addOption
1154
1439
  ] })
1155
1440
  ] }),
1156
- /* @__PURE__ */ jsx9("div", { className: "space-y-2", children: (draft.options ?? []).map(([id, label], i) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1157
- /* @__PURE__ */ jsx9(
1441
+ /* @__PURE__ */ jsx11("div", { className: "space-y-2", children: (draft.options ?? []).map(([id, label], i) => /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
1442
+ /* @__PURE__ */ jsx11(
1158
1443
  "input",
1159
1444
  {
1160
1445
  value: id,
@@ -1163,7 +1448,7 @@ function FlowNodePanel({
1163
1448
  className: `w-16 ${INPUT_CLASSNAME}`
1164
1449
  }
1165
1450
  ),
1166
- /* @__PURE__ */ jsx9(
1451
+ /* @__PURE__ */ jsx11(
1167
1452
  "input",
1168
1453
  {
1169
1454
  value: label,
@@ -1172,49 +1457,49 @@ function FlowNodePanel({
1172
1457
  className: `flex-1 ${INPUT_CLASSNAME}`
1173
1458
  }
1174
1459
  ),
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 }) })
1460
+ /* @__PURE__ */ jsx11("button", { "data-cv-tooltip": labels.nodePanel.removeOption, "aria-label": labels.nodePanel.removeOption, onClick: () => removeOption(i), className: "text-gray-400 hover:text-red-600", children: /* @__PURE__ */ jsx11(Trash2, { size: 14 }) })
1176
1461
  ] }, i)) })
1177
1462
  ] }),
1178
- !isAction && /* @__PURE__ */ jsxs8("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
1179
- /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.next }),
1180
- /* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mb-1", children: labels.nodePanel.nextHint }),
1181
- typeof draft.next !== "object" && !isCondition ? /* @__PURE__ */ jsxs8(
1463
+ (!isAction || isPassThroughAction) && /* @__PURE__ */ jsxs10("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
1464
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.next }),
1465
+ /* @__PURE__ */ jsx11("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mb-1", children: labels.nodePanel.nextHint }),
1466
+ typeof draft.next !== "object" && !isCondition ? /* @__PURE__ */ jsxs10(
1182
1467
  "select",
1183
1468
  {
1184
1469
  value: typeof draft.next === "string" ? draft.next : "",
1185
1470
  onChange: (e) => updateNextString(e.target.value),
1186
1471
  className: `w-full mt-1 ${SELECT_CLASSNAME}`,
1187
1472
  children: [
1188
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
1473
+ /* @__PURE__ */ jsx11("option", { value: "", children: "\u2014" }),
1189
1474
  nextNodeOptions()
1190
1475
  ]
1191
1476
  }
1192
- ) : /* @__PURE__ */ jsxs8("div", { className: "space-y-2 mt-1", children: [
1193
- conditionAnswerIds.map((id) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1194
- /* @__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) }),
1195
- /* @__PURE__ */ jsxs8(
1477
+ ) : /* @__PURE__ */ jsxs10("div", { className: "space-y-2 mt-1", children: [
1478
+ conditionAnswerIds.map((id) => /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
1479
+ /* @__PURE__ */ jsx11("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? id === "true" ? labels.nodePanel.conditionTrue : labels.nodePanel.conditionFalse : labels.nodePanel.nextByAnswer(id) }),
1480
+ /* @__PURE__ */ jsxs10(
1196
1481
  "select",
1197
1482
  {
1198
1483
  value: draft.next && typeof draft.next === "object" ? draft.next.byAnswer[id] ?? "" : "",
1199
1484
  onChange: (e) => updateNextByAnswer(id, e.target.value),
1200
1485
  className: `flex-1 ${SELECT_CLASSNAME}`,
1201
1486
  children: [
1202
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
1487
+ /* @__PURE__ */ jsx11("option", { value: "", children: "\u2014" }),
1203
1488
  nextNodeOptions()
1204
1489
  ]
1205
1490
  }
1206
1491
  )
1207
1492
  ] }, id)),
1208
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1209
- /* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault }),
1210
- /* @__PURE__ */ jsxs8(
1493
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
1494
+ /* @__PURE__ */ jsx11("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault }),
1495
+ /* @__PURE__ */ jsxs10(
1211
1496
  "select",
1212
1497
  {
1213
1498
  value: draft.next && typeof draft.next === "object" ? draft.next.default : "",
1214
1499
  onChange: (e) => updateNextDefault(e.target.value),
1215
1500
  className: `flex-1 ${SELECT_CLASSNAME}`,
1216
1501
  children: [
1217
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
1502
+ /* @__PURE__ */ jsx11("option", { value: "", children: "\u2014" }),
1218
1503
  nextNodeOptions()
1219
1504
  ]
1220
1505
  }
@@ -1223,8 +1508,8 @@ function FlowNodePanel({
1223
1508
  ] })
1224
1509
  ] })
1225
1510
  ] }),
1226
- /* @__PURE__ */ jsxs8("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
1227
- /* @__PURE__ */ jsxs8(
1511
+ /* @__PURE__ */ jsxs10("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
1512
+ /* @__PURE__ */ jsxs10(
1228
1513
  "button",
1229
1514
  {
1230
1515
  "data-cv-tooltip": labels.nodePanel.save,
@@ -1232,13 +1517,13 @@ function FlowNodePanel({
1232
1517
  onClick: () => onChange(draft),
1233
1518
  className: "flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
1234
1519
  children: [
1235
- /* @__PURE__ */ jsx9(Save, { size: 14 }),
1520
+ /* @__PURE__ */ jsx11(Save, { size: 14 }),
1236
1521
  " ",
1237
1522
  labels.nodePanel.save
1238
1523
  ]
1239
1524
  }
1240
1525
  ),
1241
- !isStart && /* @__PURE__ */ jsx9(
1526
+ !isStart && /* @__PURE__ */ jsx11(
1242
1527
  "button",
1243
1528
  {
1244
1529
  onClick: () => {
@@ -1247,26 +1532,27 @@ function FlowNodePanel({
1247
1532
  "data-cv-tooltip": labels.nodePanel.delete,
1248
1533
  "aria-label": labels.nodePanel.delete,
1249
1534
  className: "px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg",
1250
- children: /* @__PURE__ */ jsx9(Trash2, { size: 14 })
1535
+ children: /* @__PURE__ */ jsx11(Trash2, { size: 14 })
1251
1536
  }
1252
1537
  ),
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 })
1538
+ /* @__PURE__ */ jsx11("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
1254
1539
  ] })
1255
1540
  ] });
1256
1541
  }
1257
1542
 
1258
1543
  // src/flows/FlowsWorkspace.tsx
1259
- import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
1544
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState6 } from "react";
1260
1545
  import {
1261
1546
  ReactFlow as ReactFlow2,
1262
1547
  Background as Background2,
1263
1548
  Controls as Controls2,
1264
1549
  MiniMap,
1265
1550
  MarkerType as MarkerType2,
1551
+ Panel,
1266
1552
  applyNodeChanges
1267
1553
  } from "@xyflow/react";
1268
1554
  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";
1555
+ import { Plus as Plus4, Trash2 as Trash22, LayoutGrid, AlertTriangle as AlertTriangle3, AlertCircle as AlertCircle3, Save as Save2, Undo2, Map as MapIcon, Workflow } from "lucide-react";
1270
1556
 
1271
1557
  // src/flows/flowEditorOps.ts
1272
1558
  var NAMESPACE_SEPARATOR = "::";
@@ -1346,6 +1632,21 @@ function isGraphDirty(working, published) {
1346
1632
  if (!working || !published) return false;
1347
1633
  return JSON.stringify(working) !== JSON.stringify(published);
1348
1634
  }
1635
+ function clearConnection(node, handle) {
1636
+ if (handle === "next" || typeof node.next === "string" || node.next === void 0) {
1637
+ return { ...node, next: "" };
1638
+ }
1639
+ if (handle === "__default") {
1640
+ return { ...node, next: { byAnswer: node.next.byAnswer ?? {}, default: "" } };
1641
+ }
1642
+ return {
1643
+ ...node,
1644
+ next: {
1645
+ byAnswer: { ...node.next.byAnswer ?? {}, [handle]: "" },
1646
+ default: node.next.default ?? ""
1647
+ }
1648
+ };
1649
+ }
1349
1650
 
1350
1651
  // src/flows/flowCanvasModel.ts
1351
1652
  var COLUMN_GAP = 360;
@@ -1387,7 +1688,10 @@ function computeMergedLayout(params) {
1387
1688
  nodeById.set(namespaceNodeId(key, node.id), node);
1388
1689
  }
1389
1690
  }
1390
- function forwardEdges(flowKey, node) {
1691
+ function forwardEdges(id) {
1692
+ const node = nodeById.get(id);
1693
+ if (!node) return [];
1694
+ const flowKey = parseNamespacedId(id).flowKey;
1391
1695
  const result = [];
1392
1696
  for (const { target } of targetsOf(node)) {
1393
1697
  if (isCrossFlowTarget(target)) {
@@ -1400,41 +1704,17 @@ function computeMergedLayout(params) {
1400
1704
  }
1401
1705
  return result;
1402
1706
  }
1403
- const rank = /* @__PURE__ */ new Map();
1404
1707
  const primaryGraph = graphs[primaryFlowKey];
1405
- const rootId = primaryGraph ? namespaceNodeId(primaryFlowKey, primaryGraph.startNodeId) : void 0;
1406
- if (rootId && nodeById.has(rootId)) {
1407
- rank.set(rootId, 0);
1408
- const queue = [rootId];
1409
- while (queue.length > 0) {
1410
- const id = queue.shift();
1411
- const node = nodeById.get(id);
1412
- if (!node) continue;
1413
- for (const nextId of forwardEdges(parseNamespacedId(id).flowKey, node)) {
1414
- if (!rank.has(nextId)) {
1415
- rank.set(nextId, rank.get(id) + 1);
1416
- queue.push(nextId);
1417
- }
1418
- }
1419
- }
1420
- }
1421
- const strayRank = Math.max(0, ...rank.values()) + 1;
1422
- for (const id of nodeById.keys()) {
1423
- if (!rank.has(id)) rank.set(id, strayRank);
1424
- }
1425
- const layers = /* @__PURE__ */ new Map();
1426
- for (const [id, value] of rank) {
1427
- const layer = layers.get(value);
1428
- if (layer) layer.push(id);
1429
- else layers.set(value, [id]);
1430
- }
1708
+ const placed = cascadeOrder({
1709
+ rootId: primaryGraph ? namespaceNodeId(primaryFlowKey, primaryGraph.startNodeId) : "",
1710
+ allIds: [...nodeById.keys()],
1711
+ forwardEdges
1712
+ });
1431
1713
  const positions = /* @__PURE__ */ new Map();
1432
- for (const value of [...layers.keys()].sort((a, b) => a - b)) {
1433
- let cursorY = 0;
1434
- for (const id of layers.get(value)) {
1435
- positions.set(id, { x: value * COLUMN_GAP, y: cursorY });
1436
- cursorY += estimateNodeHeight(nodeById.get(id)) + ROW_GAP;
1437
- }
1714
+ let cursorY = 0;
1715
+ for (const [id, { depth }] of [...placed.entries()].sort((a, b) => a[1].order - b[1].order)) {
1716
+ positions.set(id, { x: depth * COLUMN_GAP, y: cursorY });
1717
+ cursorY += estimateNodeHeight(nodeById.get(id)) + ROW_GAP;
1438
1718
  }
1439
1719
  return positions;
1440
1720
  }
@@ -1465,6 +1745,7 @@ function buildFlowEdges(params) {
1465
1745
  }
1466
1746
  const source = namespaceNodeId(flowKey, nodeId);
1467
1747
  const edgeTarget = namespaceNodeId(targetFlowKey, targetNodeId);
1748
+ if (source === edgeTarget) continue;
1468
1749
  if (isDefault) {
1469
1750
  edges.push({
1470
1751
  id: `${source}->${edgeTarget}-default`,
@@ -1550,16 +1831,42 @@ function newNodeFromSpec(spec, existingIds) {
1550
1831
  const id = slugifyNodeId("nova_acao", existingIds);
1551
1832
  return { id, type: "action", actionKind: spec.actionKind };
1552
1833
  }
1834
+ var FREE_SLOT_STEP = 120;
1835
+ function findFreeSlot(params) {
1836
+ const step = params.step ?? FREE_SLOT_STEP;
1837
+ const isOccupied = (candidate) => params.taken.some(
1838
+ (each) => Math.abs(each.x - candidate.x) < NODE_CARD_WIDTH && Math.abs(each.y - candidate.y) < step
1839
+ );
1840
+ let slot = params.desired;
1841
+ for (let attempt = 0; attempt <= params.taken.length && isOccupied(slot); attempt += 1) {
1842
+ slot = { x: slot.x, y: slot.y + step };
1843
+ }
1844
+ return slot;
1845
+ }
1553
1846
 
1554
1847
  // src/flows/FlowsWorkspace.tsx
1555
- import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1848
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1556
1849
  var RF_NODE_TYPES = {
1557
1850
  ...flowNodeTypes,
1558
1851
  ...flowPortalNodeTypes,
1559
1852
  ...flowGroupHeaderNodeTypes,
1560
1853
  ...flowGroupFrameNodeTypes
1561
1854
  };
1855
+ function legendEdgeSamples(labels) {
1856
+ return [
1857
+ { color: EDGE_COLOR_LINEAR, label: labels.legendPanel.linear },
1858
+ { color: EDGE_COLOR_BRANCH, label: labels.legendPanel.branch },
1859
+ { color: EDGE_COLOR_FALLBACK, dash: "5 4", label: labels.legendPanel.fallback },
1860
+ { color: EDGE_COLOR_CROSS_FLOW, dash: "3 3", label: labels.legendPanel.crossFlow },
1861
+ { color: EDGE_COLOR_LIVE, label: labels.legendPanel.live }
1862
+ ];
1863
+ }
1864
+ var LEGEND_NODE_SWATCHES = Object.keys(NODE_TYPE_COLOR).map((type) => ({
1865
+ type,
1866
+ className: NODE_TYPE_COLOR[type]
1867
+ }));
1562
1868
  var CHAIN_FRAME_PADDING = 36;
1869
+ var QUICK_ADD_COLUMN_GAP = 320;
1563
1870
  var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
1564
1871
  var EDGE_COLOR_LINEAR = "#94a3b8";
1565
1872
  var EDGE_COLOR_BRANCH = "#8b5cf6";
@@ -1568,7 +1875,7 @@ var EDGE_COLOR_LIVE = "#3b82f6";
1568
1875
  var EDGE_COLOR_CROSS_FLOW = "#06b6d4";
1569
1876
  var BACKGROUND_COLOR_LIGHT2 = "#cbd5e1";
1570
1877
  var BACKGROUND_COLOR_DARK2 = "#334155";
1571
- function styleEdge(spec) {
1878
+ function styleEdge(spec, params) {
1572
1879
  const color = spec.crossFlow ? EDGE_COLOR_CROSS_FLOW : spec.kind === "fallback" ? EDGE_COLOR_FALLBACK : spec.live ? EDGE_COLOR_LIVE : spec.kind === "branch" ? EDGE_COLOR_BRANCH : EDGE_COLOR_LINEAR;
1573
1880
  const baseWidth = spec.kind === "branch" ? 1.75 : 1.5;
1574
1881
  const dash = spec.crossFlow ? "3 3" : spec.kind === "fallback" ? "5 4" : void 0;
@@ -1578,7 +1885,12 @@ function styleEdge(spec) {
1578
1885
  source: spec.source,
1579
1886
  target: spec.target,
1580
1887
  ...spec.sourceHandle === void 0 ? {} : { sourceHandle: spec.sourceHandle },
1581
- type: "bezier",
1888
+ type: "flowConnection",
1889
+ reconnectable: "target",
1890
+ data: {
1891
+ disconnectLabel: params.disconnectLabel,
1892
+ onDisconnect: () => params.onDisconnect(spec)
1893
+ },
1582
1894
  animated: spec.live,
1583
1895
  style: {
1584
1896
  stroke: color,
@@ -1607,23 +1919,24 @@ function FlowsWorkspace({
1607
1919
  }) {
1608
1920
  const labels = useMemo2(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride]);
1609
1921
  const isDark = useIsDarkTheme();
1610
- const [graphs, setGraphs] = useState3(void 0);
1611
- const [loadState, setLoadState] = useState3("loading");
1612
- const [livePositions, setLivePositions] = useState3(void 0);
1613
- const [viewMode, setViewMode] = useState3("detail");
1614
- const [openFlowKeys, setOpenFlowKeys] = useState3([rootFlowKey]);
1615
- const [hasAutoMerged, setHasAutoMerged] = useState3(false);
1616
- const [workingGraphs, setWorkingGraphs] = useState3({});
1617
- const [editingRef, setEditingRef] = useState3(null);
1618
- const [saveState, setSaveState] = useState3("idle");
1619
- const [saveErrorMessage, setSaveErrorMessage] = useState3(void 0);
1620
- const [showCreateDialog, setShowCreateDialog] = useState3(false);
1621
- const [showDeleteDialog, setShowDeleteDialog] = useState3(false);
1622
- const [newFlow, setNewFlow] = useState3({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
1623
- const [flowMutationState, setFlowMutationState] = useState3({ pending: false });
1624
- const [rfNodes, setRfNodes] = useState3([]);
1625
- const [flowInstance, setFlowInstance] = useState3(null);
1626
- const [pendingFocusNodeId, setPendingFocusNodeId] = useState3(null);
1922
+ const [graphs, setGraphs] = useState6(void 0);
1923
+ const [loadState, setLoadState] = useState6("loading");
1924
+ const [livePositions, setLivePositions] = useState6(void 0);
1925
+ const [viewMode, setViewMode] = useState6("detail");
1926
+ const [openFlowKeys, setOpenFlowKeys] = useState6([rootFlowKey]);
1927
+ const [hasAutoMerged, setHasAutoMerged] = useState6(false);
1928
+ const [workingGraphs, setWorkingGraphs] = useState6({});
1929
+ const [editingRef, setEditingRef] = useState6(null);
1930
+ const [saveState, setSaveState] = useState6("idle");
1931
+ const [saveErrorMessage, setSaveErrorMessage] = useState6(void 0);
1932
+ const [showCreateDialog, setShowCreateDialog] = useState6(false);
1933
+ const [showDeleteDialog, setShowDeleteDialog] = useState6(false);
1934
+ const [quickAddFrom, setQuickAddFrom] = useState6(null);
1935
+ const [newFlow, setNewFlow] = useState6({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
1936
+ const [flowMutationState, setFlowMutationState] = useState6({ pending: false });
1937
+ const [rfNodes, setRfNodes] = useState6([]);
1938
+ const [flowInstance, setFlowInstance] = useState6(null);
1939
+ const [pendingFocusNodeId, setPendingFocusNodeId] = useState6(null);
1627
1940
  const reloadGraphs = useCallback(async () => {
1628
1941
  try {
1629
1942
  const loaded = await api.getGraphs();
@@ -1635,10 +1948,10 @@ function FlowsWorkspace({
1635
1948
  return void 0;
1636
1949
  }
1637
1950
  }, [api]);
1638
- useEffect2(() => {
1951
+ useEffect3(() => {
1639
1952
  void reloadGraphs();
1640
1953
  }, [reloadGraphs]);
1641
- useEffect2(() => {
1954
+ useEffect3(() => {
1642
1955
  const fetchLive = api.getLivePositions;
1643
1956
  if (!fetchLive) return;
1644
1957
  let active = true;
@@ -1658,12 +1971,12 @@ function FlowsWorkspace({
1658
1971
  }, [api, livePollIntervalMs]);
1659
1972
  const primaryFlowKey = openFlowKeys[0] ?? rootFlowKey;
1660
1973
  const primaryGraph = workingGraphs[primaryFlowKey];
1661
- useEffect2(() => {
1974
+ useEffect3(() => {
1662
1975
  if (!graphs || hasAutoMerged) return;
1663
1976
  setOpenFlowKeys(mergedFlowKeysFrom(rootFlowKey, graphs));
1664
1977
  setHasAutoMerged(true);
1665
1978
  }, [graphs, hasAutoMerged, rootFlowKey]);
1666
- useEffect2(() => {
1979
+ useEffect3(() => {
1667
1980
  if (!graphs) return;
1668
1981
  setWorkingGraphs((prev) => {
1669
1982
  let changed = false;
@@ -1740,16 +2053,17 @@ function FlowsWorkspace({
1740
2053
  () => isMerged ? computeMergedLayout({ openKeys: openFlowKeys, graphs: workingGraphs, primaryFlowKey }) : null,
1741
2054
  [isMerged, openFlowKeys, workingGraphs, primaryFlowKey]
1742
2055
  );
1743
- const renderedPositionsRef = useRef2(/* @__PURE__ */ new Map());
2056
+ const renderedPositionsRef = useRef3(/* @__PURE__ */ new Map());
1744
2057
  const derivedNodes = useMemo2(() => {
1745
2058
  const allNodes = [];
1746
2059
  for (const flowKey of openFlowKeys) {
1747
2060
  let resolvePosition2 = function(nodeId) {
1748
2061
  if (mergedPositions) {
1749
- const nsId = namespaceNodeId(flowKey, nodeId);
1750
- return renderedPositionsRef.current.get(nsId) ?? mergedPositions.get(nsId) ?? { x: 0, y: 0 };
2062
+ const nsId2 = namespaceNodeId(flowKey, nodeId);
2063
+ return renderedPositionsRef.current.get(nsId2) ?? mergedPositions.get(nsId2) ?? { x: 0, y: 0 };
1751
2064
  }
1752
- return graph.nodes[nodeId]?.position ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
2065
+ const nsId = namespaceNodeId(flowKey, nodeId);
2066
+ return graph.nodes[nodeId]?.position ?? renderedPositionsRef.current.get(nsId) ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
1753
2067
  };
1754
2068
  var resolvePosition = resolvePosition2;
1755
2069
  const graph = workingGraphs[flowKey];
@@ -1779,7 +2093,8 @@ function FlowsWorkspace({
1779
2093
  isDetached: node.id !== graph.startNodeId && !connectedTargets.has(node.id),
1780
2094
  issues: flowIssues,
1781
2095
  labels,
1782
- onSelect: (nodeId) => setEditingRef({ flowKey, nodeId })
2096
+ onSelect: (nodeId) => setEditingRef({ flowKey, nodeId }),
2097
+ onQuickAdd: ({ nodeId, handle, anchor }) => setQuickAddFrom({ flowKey, nodeId, handle, anchor })
1783
2098
  }
1784
2099
  });
1785
2100
  const crossFlowTargets = [...new Set(targetsOf(node).map((edge) => edge.target).filter(isCrossFlowTarget))];
@@ -1854,17 +2169,53 @@ function FlowsWorkspace({
1854
2169
  closeFlow,
1855
2170
  labels
1856
2171
  ]);
2172
+ const disconnectEdge = useCallback(
2173
+ (spec) => {
2174
+ const { flowKey, nodeId } = parseNamespacedId(spec.source);
2175
+ updateFlow(flowKey, (graph) => {
2176
+ const node = graph.nodes[nodeId];
2177
+ if (!node) return graph;
2178
+ return { ...graph, nodes: { ...graph.nodes, [nodeId]: clearConnection(node, spec.sourceHandle ?? "next") } };
2179
+ });
2180
+ },
2181
+ [updateFlow]
2182
+ );
1857
2183
  const edges = useMemo2(
1858
- () => buildFlowEdges({ openKeys: openFlowKeys, graphs: workingGraphs, rootFlowKey, livePositions }).map(styleEdge),
1859
- [openFlowKeys, workingGraphs, rootFlowKey, livePositions]
2184
+ () => buildFlowEdges({ openKeys: openFlowKeys, graphs: workingGraphs, rootFlowKey, livePositions }).map(
2185
+ (spec) => (
2186
+ // Salto entre fluxos desenhado como portal não se desliga daqui: quem manda nele é o `next`
2187
+ // do nó de origem, e o portal é só a caixa que representa o fluxo alvo ausente.
2188
+ styleEdge(spec, { disconnectLabel: labels.quickAdd.disconnect, onDisconnect: disconnectEdge })
2189
+ )
2190
+ ),
2191
+ [openFlowKeys, workingGraphs, rootFlowKey, livePositions, labels, disconnectEdge]
2192
+ );
2193
+ const onReconnect = useCallback(
2194
+ (oldEdge, connection) => {
2195
+ const resolved = resolveConnection({
2196
+ connection: {
2197
+ source: connection.source,
2198
+ target: connection.target,
2199
+ sourceHandle: connection.sourceHandle ?? oldEdge.sourceHandle
2200
+ },
2201
+ graphs: workingGraphs
2202
+ });
2203
+ if (!resolved) return;
2204
+ updateFlow(resolved.flowKey, (graph) => {
2205
+ const node = graph.nodes[resolved.nodeId];
2206
+ if (!node) return graph;
2207
+ return { ...graph, nodes: { ...graph.nodes, [resolved.nodeId]: applyConnection(node, resolved) } };
2208
+ });
2209
+ },
2210
+ [workingGraphs, updateFlow]
1860
2211
  );
1861
- useEffect2(() => {
2212
+ useEffect3(() => {
1862
2213
  setRfNodes(derivedNodes);
1863
2214
  for (const node of derivedNodes) {
1864
2215
  if (node.type === "flowNode") renderedPositionsRef.current.set(node.id, node.position);
1865
2216
  }
1866
2217
  }, [derivedNodes]);
1867
- useEffect2(() => {
2218
+ useEffect3(() => {
1868
2219
  if (!pendingFocusNodeId || !flowInstance) return;
1869
2220
  const target = rfNodes.find((node) => node.id === pendingFocusNodeId);
1870
2221
  if (!target) return;
@@ -1910,6 +2261,37 @@ function FlowsWorkspace({
1910
2261
  setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
1911
2262
  setPendingFocusNodeId(namespaceNodeId(primaryFlowKey, newNode.id));
1912
2263
  }
2264
+ function handleQuickAdd(spec) {
2265
+ const origin = quickAddFrom;
2266
+ setQuickAddFrom(null);
2267
+ if (!origin) return;
2268
+ const originGraph = workingGraphs[origin.flowKey];
2269
+ if (!originGraph) return;
2270
+ const newNode = newNodeFromSpec(spec, new Set(Object.keys(originGraph.nodes)));
2271
+ const originPosition = renderedPositionsRef.current.get(namespaceNodeId(origin.flowKey, origin.nodeId)) ?? originGraph.nodes[origin.nodeId]?.position ?? { x: 0, y: 0 };
2272
+ const taken = openFlowKeys.flatMap(
2273
+ (key) => Object.entries(workingGraphs[key]?.nodes ?? {}).map(
2274
+ ([id, node]) => renderedPositionsRef.current.get(namespaceNodeId(key, id)) ?? node.position ?? { x: 0, y: 0 }
2275
+ )
2276
+ );
2277
+ newNode.position = findFreeSlot({
2278
+ desired: { x: originPosition.x + QUICK_ADD_COLUMN_GAP, y: originPosition.y },
2279
+ taken
2280
+ });
2281
+ renderedPositionsRef.current.set(namespaceNodeId(origin.flowKey, newNode.id), newNode.position);
2282
+ updateFlow(origin.flowKey, (graph) => {
2283
+ const sourceNode = graph.nodes[origin.nodeId];
2284
+ if (!sourceNode) return graph;
2285
+ const connected = applyConnection(sourceNode, {
2286
+ flowKey: origin.flowKey,
2287
+ nodeId: origin.nodeId,
2288
+ handle: origin.handle,
2289
+ targetValue: newNode.id
2290
+ });
2291
+ return { ...graph, nodes: { ...graph.nodes, [origin.nodeId]: connected, [newNode.id]: newNode } };
2292
+ });
2293
+ setEditingRef({ flowKey: origin.flowKey, nodeId: newNode.id });
2294
+ }
1913
2295
  function handleNodePanelChange(updated) {
1914
2296
  if (!editingRef) return;
1915
2297
  updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [updated.id]: updated } }));
@@ -2017,17 +2399,17 @@ function FlowsWorkspace({
2017
2399
  const keyIsValid = FLOW_KEY_PATTERN.test(newFlow.key);
2018
2400
  const canCreateFlow = Boolean(api.createFlow);
2019
2401
  const canDeleteFlow = Boolean(api.deleteFlow) && primaryFlowKey !== rootFlowKey;
2020
- return /* @__PURE__ */ jsxs9("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
2021
- /* @__PURE__ */ jsx10(TooltipLayer, {}),
2022
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
2023
- showHeader && /* @__PURE__ */ jsxs9("div", { className: "min-w-0", children: [
2024
- /* @__PURE__ */ jsx10("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
2025
- /* @__PURE__ */ jsx10("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
2402
+ return /* @__PURE__ */ jsxs11("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
2403
+ /* @__PURE__ */ jsx12(TooltipLayer, {}),
2404
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
2405
+ showHeader && /* @__PURE__ */ jsxs11("div", { className: "min-w-0", children: [
2406
+ /* @__PURE__ */ jsx12("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
2407
+ /* @__PURE__ */ jsx12("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
2026
2408
  ] }),
2027
- /* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center justify-end gap-3 ml-auto", children: [
2028
- saveState === "success" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
2029
- saveState === "error" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
2030
- /* @__PURE__ */ jsxs9(
2409
+ /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap items-center justify-end gap-3 ml-auto", children: [
2410
+ saveState === "success" && /* @__PURE__ */ jsx12("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
2411
+ saveState === "error" && /* @__PURE__ */ jsx12("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
2412
+ /* @__PURE__ */ jsxs11(
2031
2413
  "button",
2032
2414
  {
2033
2415
  "data-cv-tooltip": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
@@ -2036,13 +2418,13 @@ function FlowsWorkspace({
2036
2418
  onClick: () => setViewMode((mode) => mode === "map" ? "detail" : "map"),
2037
2419
  className: OUTLINE_BUTTON,
2038
2420
  children: [
2039
- viewMode === "map" ? /* @__PURE__ */ jsx10(Workflow, { size: 14, "aria-hidden": "true" }) : /* @__PURE__ */ jsx10(MapIcon, { size: 14, "aria-hidden": "true" }),
2421
+ viewMode === "map" ? /* @__PURE__ */ jsx12(Workflow, { size: 14, "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(MapIcon, { size: 14, "aria-hidden": "true" }),
2040
2422
  viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap
2041
2423
  ]
2042
2424
  }
2043
2425
  ),
2044
- viewMode === "detail" && /* @__PURE__ */ jsxs9(Fragment2, { children: [
2045
- /* @__PURE__ */ jsxs9(
2426
+ viewMode === "detail" && /* @__PURE__ */ jsxs11(Fragment4, { children: [
2427
+ /* @__PURE__ */ jsxs11(
2046
2428
  "button",
2047
2429
  {
2048
2430
  type: "button",
@@ -2052,13 +2434,13 @@ function FlowsWorkspace({
2052
2434
  "aria-label": labels.workspace.organizeTooltip,
2053
2435
  disabled: !primaryGraph,
2054
2436
  children: [
2055
- /* @__PURE__ */ jsx10(LayoutGrid, { size: 14, "aria-hidden": "true" }),
2437
+ /* @__PURE__ */ jsx12(LayoutGrid, { size: 14, "aria-hidden": "true" }),
2056
2438
  " ",
2057
2439
  labels.workspace.organize
2058
2440
  ]
2059
2441
  }
2060
2442
  ),
2061
- /* @__PURE__ */ jsxs9(
2443
+ /* @__PURE__ */ jsxs11(
2062
2444
  "button",
2063
2445
  {
2064
2446
  type: "button",
@@ -2068,13 +2450,13 @@ function FlowsWorkspace({
2068
2450
  "aria-label": labels.workspace.discardTooltip,
2069
2451
  disabled: !isDirty || saveState === "saving",
2070
2452
  children: [
2071
- /* @__PURE__ */ jsx10(Undo2, { size: 14, "aria-hidden": "true" }),
2453
+ /* @__PURE__ */ jsx12(Undo2, { size: 14, "aria-hidden": "true" }),
2072
2454
  " ",
2073
2455
  labels.workspace.discardChanges
2074
2456
  ]
2075
2457
  }
2076
2458
  ),
2077
- /* @__PURE__ */ jsxs9(
2459
+ /* @__PURE__ */ jsxs11(
2078
2460
  "button",
2079
2461
  {
2080
2462
  "data-cv-tooltip": labels.workspace.saveGraph,
@@ -2084,7 +2466,7 @@ function FlowsWorkspace({
2084
2466
  className: PRIMARY_BUTTON,
2085
2467
  disabled: !isDirty || errorCount > 0 || saveState === "saving",
2086
2468
  children: [
2087
- /* @__PURE__ */ jsx10(Save2, { size: 14, "aria-hidden": "true" }),
2469
+ /* @__PURE__ */ jsx12(Save2, { size: 14, "aria-hidden": "true" }),
2088
2470
  saveState === "saving" ? labels.workspace.saving : labels.workspace.saveGraph
2089
2471
  ]
2090
2472
  }
@@ -2092,9 +2474,9 @@ function FlowsWorkspace({
2092
2474
  ] })
2093
2475
  ] })
2094
2476
  ] }),
2095
- viewMode === "detail" && /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
2096
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 flex-wrap", children: [
2097
- graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx10(
2477
+ viewMode === "detail" && /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
2478
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 flex-wrap", children: [
2479
+ graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx12(
2098
2480
  "button",
2099
2481
  {
2100
2482
  "data-cv-tooltip": graph.label,
@@ -2106,7 +2488,7 @@ function FlowsWorkspace({
2106
2488
  },
2107
2489
  graph.key
2108
2490
  )),
2109
- canCreateFlow && /* @__PURE__ */ jsxs9(
2491
+ canCreateFlow && /* @__PURE__ */ jsxs11(
2110
2492
  "button",
2111
2493
  {
2112
2494
  "data-cv-tooltip": labels.flowManager.newFlow,
@@ -2118,15 +2500,15 @@ function FlowsWorkspace({
2118
2500
  },
2119
2501
  className: "inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium border border-dashed border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:border-blue-400 hover:text-blue-600",
2120
2502
  children: [
2121
- /* @__PURE__ */ jsx10(Plus3, { size: 12, "aria-hidden": "true" }),
2503
+ /* @__PURE__ */ jsx12(Plus4, { size: 12, "aria-hidden": "true" }),
2122
2504
  " ",
2123
2505
  labels.flowManager.newFlow
2124
2506
  ]
2125
2507
  }
2126
2508
  )
2127
2509
  ] }),
2128
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
2129
- primaryGraph && /* @__PURE__ */ jsx10(
2510
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
2511
+ primaryGraph && /* @__PURE__ */ jsx12(
2130
2512
  FlowPalette,
2131
2513
  {
2132
2514
  onAdd: handleAddNode,
@@ -2134,7 +2516,7 @@ function FlowsWorkspace({
2134
2516
  ...actionOptions ? { actionOptions: [...actionOptions] } : {}
2135
2517
  }
2136
2518
  ),
2137
- primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs9(
2519
+ primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs11(
2138
2520
  "button",
2139
2521
  {
2140
2522
  "data-cv-tooltip": labels.flowManager.deleteFlow,
@@ -2146,7 +2528,7 @@ function FlowsWorkspace({
2146
2528
  },
2147
2529
  className: "inline-flex items-center gap-1.5 rounded-lg border border-red-200 dark:border-red-900 px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30",
2148
2530
  children: [
2149
- /* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
2531
+ /* @__PURE__ */ jsx12(Trash22, { size: 13, "aria-hidden": "true" }),
2150
2532
  " ",
2151
2533
  labels.flowManager.deleteFlow
2152
2534
  ]
@@ -2154,26 +2536,26 @@ function FlowsWorkspace({
2154
2536
  )
2155
2537
  ] })
2156
2538
  ] }),
2157
- 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: [
2158
- /* @__PURE__ */ jsxs9("span", { className: "font-medium text-gray-600 dark:text-gray-300", children: [
2539
+ viewMode === "detail" && (errorCount > 0 || warningCount > 0) && /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/60 px-3 py-2 text-xs", children: [
2540
+ /* @__PURE__ */ jsxs11("span", { className: "font-medium text-gray-600 dark:text-gray-300", children: [
2159
2541
  labels.validation.title,
2160
2542
  ":"
2161
2543
  ] }),
2162
- errorCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-red-600 dark:text-red-400 font-medium", children: [
2163
- /* @__PURE__ */ jsx10(AlertCircle3, { size: 13, "aria-hidden": "true" }),
2544
+ errorCount > 0 && /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-1 text-red-600 dark:text-red-400 font-medium", children: [
2545
+ /* @__PURE__ */ jsx12(AlertCircle3, { size: 13, "aria-hidden": "true" }),
2164
2546
  " ",
2165
2547
  labels.validation.errors(errorCount)
2166
2548
  ] }),
2167
- warningCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [
2168
- /* @__PURE__ */ jsx10(AlertTriangle3, { size: 13, "aria-hidden": "true" }),
2549
+ warningCount > 0 && /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [
2550
+ /* @__PURE__ */ jsx12(AlertTriangle3, { size: 13, "aria-hidden": "true" }),
2169
2551
  " ",
2170
2552
  labels.validation.warnings(warningCount)
2171
2553
  ] })
2172
2554
  ] }),
2173
- /* @__PURE__ */ jsxs9("div", { className: "flex-1 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative", children: [
2174
- loadState === "loading" && /* @__PURE__ */ jsx10("p", { className: "text-center text-gray-400 py-12", children: labels.workspace.loading }),
2175
- loadState === "error" && /* @__PURE__ */ jsx10("p", { className: "text-center text-red-500 py-12", children: labels.workspace.loadError }),
2176
- loadState === "ready" && graphs && viewMode === "map" && /* @__PURE__ */ jsx10(
2555
+ /* @__PURE__ */ jsxs11("div", { className: "flex-1 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative", children: [
2556
+ loadState === "loading" && /* @__PURE__ */ jsx12("p", { className: "text-center text-gray-400 py-12", children: labels.workspace.loading }),
2557
+ loadState === "error" && /* @__PURE__ */ jsx12("p", { className: "text-center text-red-500 py-12", children: labels.workspace.loadError }),
2558
+ loadState === "ready" && graphs && viewMode === "map" && /* @__PURE__ */ jsx12(
2177
2559
  FlowMapCanvas,
2178
2560
  {
2179
2561
  graphs,
@@ -2185,28 +2567,59 @@ function FlowsWorkspace({
2185
2567
  }
2186
2568
  }
2187
2569
  ),
2188
- loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs9(
2570
+ loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs11(
2189
2571
  ReactFlow2,
2190
2572
  {
2191
2573
  nodes: rfNodes,
2192
2574
  edges,
2193
2575
  nodeTypes: RF_NODE_TYPES,
2576
+ edgeTypes: flowEdgeTypes,
2194
2577
  onNodesChange,
2195
2578
  onNodeDragStop,
2196
2579
  onConnect,
2580
+ onReconnect,
2197
2581
  onInit: setFlowInstance,
2198
2582
  fitView: true,
2199
2583
  proOptions: { hideAttribution: true },
2200
2584
  colorMode: isDark ? "dark" : "light",
2201
2585
  children: [
2202
- /* @__PURE__ */ jsx10(Background2, { color: isDark ? BACKGROUND_COLOR_DARK2 : BACKGROUND_COLOR_LIGHT2 }),
2203
- /* @__PURE__ */ jsx10(Controls2, {}),
2204
- /* @__PURE__ */ jsx10(MiniMap, { pannable: true, zoomable: true, className: "!bg-white dark:!bg-gray-800" })
2586
+ /* @__PURE__ */ jsx12(Background2, { color: isDark ? BACKGROUND_COLOR_DARK2 : BACKGROUND_COLOR_LIGHT2 }),
2587
+ /* @__PURE__ */ jsx12(Controls2, {}),
2588
+ /* @__PURE__ */ jsx12(Panel, { position: "top-right", children: /* @__PURE__ */ jsx12(
2589
+ FlowLegend,
2590
+ {
2591
+ labels,
2592
+ edgeSamples: legendEdgeSamples(labels),
2593
+ nodeSwatches: LEGEND_NODE_SWATCHES
2594
+ }
2595
+ ) }),
2596
+ /* @__PURE__ */ jsx12(MiniMap, { pannable: true, zoomable: true, className: "!bg-white dark:!bg-gray-800" })
2597
+ ]
2598
+ }
2599
+ )
2600
+ ] }),
2601
+ quickAddFrom && /* @__PURE__ */ jsxs11(Fragment4, { children: [
2602
+ /* @__PURE__ */ jsx12("div", { className: "fixed inset-0 z-40", onClick: () => setQuickAddFrom(null) }),
2603
+ /* @__PURE__ */ jsxs11(
2604
+ "div",
2605
+ {
2606
+ className: "fixed z-50 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1",
2607
+ style: { left: quickAddFrom.anchor.x + 12, top: quickAddFrom.anchor.y },
2608
+ children: [
2609
+ /* @__PURE__ */ jsx12("p", { className: "px-3 py-1.5 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500", children: labels.quickAdd.title }),
2610
+ /* @__PURE__ */ jsx12(
2611
+ FlowPaletteMenu,
2612
+ {
2613
+ onSelect: handleQuickAdd,
2614
+ labels,
2615
+ ...actionOptions ? { actionOptions: [...actionOptions] } : {}
2616
+ }
2617
+ )
2205
2618
  ]
2206
2619
  }
2207
2620
  )
2208
2621
  ] }),
2209
- editingNode && editingGraph && /* @__PURE__ */ jsx10(
2622
+ editingNode && editingGraph && /* @__PURE__ */ jsx12(
2210
2623
  FlowNodePanel,
2211
2624
  {
2212
2625
  graph: editingGraph,
@@ -2221,11 +2634,11 @@ function FlowsWorkspace({
2221
2634
  },
2222
2635
  `${editingRef?.flowKey}:${editingRef?.nodeId}`
2223
2636
  ),
2224
- showCreateDialog && canCreateFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.createTitle, onClose: () => setShowCreateDialog(false), children: [
2225
- /* @__PURE__ */ jsxs9("div", { className: "space-y-3", children: [
2226
- /* @__PURE__ */ jsxs9("div", { children: [
2227
- /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.label }),
2228
- /* @__PURE__ */ jsx10(
2637
+ showCreateDialog && canCreateFlow && /* @__PURE__ */ jsxs11(FlowDialog, { title: labels.flowManager.createTitle, onClose: () => setShowCreateDialog(false), children: [
2638
+ /* @__PURE__ */ jsxs11("div", { className: "space-y-3", children: [
2639
+ /* @__PURE__ */ jsxs11("div", { children: [
2640
+ /* @__PURE__ */ jsx12("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.label }),
2641
+ /* @__PURE__ */ jsx12(
2229
2642
  "input",
2230
2643
  {
2231
2644
  value: newFlow.label,
@@ -2234,9 +2647,9 @@ function FlowsWorkspace({
2234
2647
  }
2235
2648
  )
2236
2649
  ] }),
2237
- /* @__PURE__ */ jsxs9("div", { children: [
2238
- /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.key }),
2239
- /* @__PURE__ */ jsx10(
2650
+ /* @__PURE__ */ jsxs11("div", { children: [
2651
+ /* @__PURE__ */ jsx12("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.key }),
2652
+ /* @__PURE__ */ jsx12(
2240
2653
  "input",
2241
2654
  {
2242
2655
  value: newFlow.key,
@@ -2244,10 +2657,10 @@ function FlowsWorkspace({
2244
2657
  className: `mt-1 ${DIALOG_INPUT}`
2245
2658
  }
2246
2659
  ),
2247
- /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-gray-400 mt-1", children: newFlow.key && !keyIsValid ? labels.flowManager.keyInvalid : labels.flowManager.keyHint })
2660
+ /* @__PURE__ */ jsx12("p", { className: "text-[11px] text-gray-400 mt-1", children: newFlow.key && !keyIsValid ? labels.flowManager.keyInvalid : labels.flowManager.keyHint })
2248
2661
  ] }),
2249
- /* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200", children: [
2250
- /* @__PURE__ */ jsx10(
2662
+ /* @__PURE__ */ jsxs11("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200", children: [
2663
+ /* @__PURE__ */ jsx12(
2251
2664
  "input",
2252
2665
  {
2253
2666
  type: "checkbox",
@@ -2257,9 +2670,9 @@ function FlowsWorkspace({
2257
2670
  ),
2258
2671
  labels.flowManager.showInMenu
2259
2672
  ] }),
2260
- newFlow.showInMenu && /* @__PURE__ */ jsxs9("div", { children: [
2261
- /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.menuOptionLabel }),
2262
- /* @__PURE__ */ jsx10(
2673
+ newFlow.showInMenu && /* @__PURE__ */ jsxs11("div", { children: [
2674
+ /* @__PURE__ */ jsx12("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.menuOptionLabel }),
2675
+ /* @__PURE__ */ jsx12(
2263
2676
  "input",
2264
2677
  {
2265
2678
  value: newFlow.menuOptionLabel,
@@ -2269,11 +2682,11 @@ function FlowsWorkspace({
2269
2682
  }
2270
2683
  )
2271
2684
  ] }),
2272
- flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600", children: flowMutationState.error })
2685
+ flowMutationState.error && /* @__PURE__ */ jsx12("p", { className: "text-xs text-red-600", children: flowMutationState.error })
2273
2686
  ] }),
2274
- /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2275
- /* @__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 }),
2276
- /* @__PURE__ */ jsx10(
2687
+ /* @__PURE__ */ jsxs11("div", { className: "flex justify-end gap-2 mt-4", children: [
2688
+ /* @__PURE__ */ jsx12("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowCreateDialog(false), children: labels.nodePanel.cancel }),
2689
+ /* @__PURE__ */ jsx12(
2277
2690
  "button",
2278
2691
  {
2279
2692
  "data-cv-tooltip": labels.flowManager.create,
@@ -2287,12 +2700,12 @@ function FlowsWorkspace({
2287
2700
  )
2288
2701
  ] })
2289
2702
  ] }),
2290
- showDeleteDialog && canDeleteFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.deleteFlow, onClose: () => setShowDeleteDialog(false), children: [
2291
- /* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
2292
- flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
2293
- /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2294
- /* @__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 }),
2295
- /* @__PURE__ */ jsxs9(
2703
+ showDeleteDialog && canDeleteFlow && /* @__PURE__ */ jsxs11(FlowDialog, { title: labels.flowManager.deleteFlow, onClose: () => setShowDeleteDialog(false), children: [
2704
+ /* @__PURE__ */ jsx12("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
2705
+ flowMutationState.error && /* @__PURE__ */ jsx12("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
2706
+ /* @__PURE__ */ jsxs11("div", { className: "flex justify-end gap-2 mt-4", children: [
2707
+ /* @__PURE__ */ jsx12("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowDeleteDialog(false), children: labels.nodePanel.cancel }),
2708
+ /* @__PURE__ */ jsxs11(
2296
2709
  "button",
2297
2710
  {
2298
2711
  "data-cv-tooltip": labels.flowManager.deleteFlow,
@@ -2302,7 +2715,7 @@ function FlowsWorkspace({
2302
2715
  onClick: () => void handleDeleteFlow(),
2303
2716
  disabled: flowMutationState.pending,
2304
2717
  children: [
2305
- /* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
2718
+ /* @__PURE__ */ jsx12(Trash22, { size: 13, "aria-hidden": "true" }),
2306
2719
  " ",
2307
2720
  labels.flowManager.deleteFlow
2308
2721
  ]
@@ -2317,9 +2730,9 @@ function FlowDialog({
2317
2730
  onClose,
2318
2731
  children
2319
2732
  }) {
2320
- return /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4", children: [
2321
- /* @__PURE__ */ jsx10("div", { className: "absolute inset-0", onClick: onClose, "aria-hidden": "true" }),
2322
- /* @__PURE__ */ jsxs9(
2733
+ return /* @__PURE__ */ jsxs11("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4", children: [
2734
+ /* @__PURE__ */ jsx12("div", { className: "absolute inset-0", onClick: onClose, "aria-hidden": "true" }),
2735
+ /* @__PURE__ */ jsxs11(
2323
2736
  "div",
2324
2737
  {
2325
2738
  role: "dialog",
@@ -2327,7 +2740,7 @@ function FlowDialog({
2327
2740
  "aria-label": title,
2328
2741
  className: "relative w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-5 shadow-xl",
2329
2742
  children: [
2330
- /* @__PURE__ */ jsx10("h3", { className: "text-base font-semibold text-gray-900 dark:text-gray-100 mb-3", children: title }),
2743
+ /* @__PURE__ */ jsx12("h3", { className: "text-base font-semibold text-gray-900 dark:text-gray-100 mb-3", children: title }),
2331
2744
  children
2332
2745
  ]
2333
2746
  }
@@ -2339,18 +2752,22 @@ export {
2339
2752
  CONDITION_OPERATORS,
2340
2753
  CROSS_FLOW_PREFIX,
2341
2754
  DEFAULT_FLOW_EDITOR_LABELS,
2755
+ FlowConnectionEdge,
2342
2756
  FlowGroupFrame,
2343
2757
  FlowGroupHeader,
2758
+ FlowLegend,
2344
2759
  FlowMapCanvas,
2345
2760
  FlowMapNode,
2346
2761
  FlowNodeCard,
2347
2762
  FlowNodePanel,
2348
2763
  FlowPalette,
2764
+ FlowPaletteMenu,
2349
2765
  FlowPortalNode,
2350
2766
  FlowWhatsAppPreview,
2351
2767
  FlowsWorkspace,
2352
2768
  GROUP_HEADER_NODE_ID,
2353
2769
  NODE_CARD_WIDTH,
2770
+ PASS_THROUGH_ACTION_KINDS,
2354
2771
  WHATSAPP_LIMITS,
2355
2772
  applyConnection,
2356
2773
  buildFlowEdges,
@@ -2365,6 +2782,7 @@ export {
2365
2782
  detachedNodeIds,
2366
2783
  estimateNodeHeight,
2367
2784
  findCollectionChains,
2785
+ flowEdgeTypes,
2368
2786
  flowGroupFrameNodeTypes,
2369
2787
  flowGroupHeaderNodeTypes,
2370
2788
  flowMapNodeTypes,