@adatechnology/conversations-ui 0.1.0-rc.23 → 0.1.0-rc.25

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.
@@ -39,6 +39,7 @@ var DEFAULT_ACTION_KIND_ICON = {
39
39
  };
40
40
  function nodeLabel(node, labels) {
41
41
  if (!node) return "\u2014";
42
+ if (node.label) return node.label;
42
43
  if (node.type === "action") {
43
44
  const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
44
45
  return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
@@ -88,7 +89,7 @@ function SourceRow({ label, isDefault, handleId }) {
88
89
  ] });
89
90
  }
90
91
  function FlowNodeCard({ data }) {
91
- const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } = data;
92
+ const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect } = data;
92
93
  const label = nodeLabel(node, labels);
93
94
  const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
94
95
  const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
@@ -98,8 +99,8 @@ function FlowNodeCard({ data }) {
98
99
  return /* @__PURE__ */ jsxs(
99
100
  "div",
100
101
  {
101
- title: label,
102
- 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" : ""}`,
102
+ title: isDetached ? labels.detachedNodeTooltip : label,
103
+ 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" : ""}`,
103
104
  onClick: () => onSelect(node.id),
104
105
  children: [
105
106
  /* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
@@ -144,6 +145,7 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
144
145
  condition: "Condi\xE7\xE3o"
145
146
  },
146
147
  startNodeTooltip: "In\xEDcio do fluxo",
148
+ detachedNodeTooltip: "Sem liga\xE7\xE3o de entrada \u2014 o bot n\xE3o chega neste n\xF3. Puxe um fio de outro card at\xE9 ele.",
147
149
  liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
148
150
  edgeFallbackLabel: "outro",
149
151
  actionKindLabels: {
@@ -172,6 +174,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
172
174
  nodePanel: {
173
175
  title: "Editar n\xF3",
174
176
  contextKey: "Chave (contexto)",
177
+ nodeName: "Nome do n\xF3 (opcional)",
178
+ nodeNamePlaceholder: "Ex.: Enviar tabela de pre\xE7os",
179
+ nodeNameHint: "S\xF3 aparece no editor \u2014 o cliente n\xE3o v\xEA.",
175
180
  questionType: "Tipo de resposta",
176
181
  question: "Texto da pergunta",
177
182
  options: "Op\xE7\xF5es (choice)",
@@ -219,7 +224,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
219
224
  },
220
225
  flowMap: {
221
226
  nodeCount: (count) => `${count} n\xF3(s)`,
222
- openFlow: "Abrir fluxo"
227
+ openFlow: "Abrir fluxo",
228
+ toggleToMap: "Mapa de fluxos",
229
+ toggleToDetail: "Voltar ao editor"
223
230
  },
224
231
  flowGroup: {
225
232
  focus: "Focar neste fluxo",
@@ -228,6 +235,59 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
228
235
  crossFlowPortal: {
229
236
  tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
230
237
  goesTo: (label) => `\u21AA Vai para: ${label}`
238
+ },
239
+ collectionChain: {
240
+ feeds: (label) => `Alimenta: ${label}`
241
+ },
242
+ validation: {
243
+ title: "Antes de publicar",
244
+ errors: (count) => `${count} erro(s) \u2014 corrija antes de salvar`,
245
+ warnings: (count) => `${count} aviso(s)`,
246
+ noStart: "O fluxo precisa de um n\xF3 inicial v\xE1lido.",
247
+ brokenRef: (from, to) => `N\xF3 "${from}": liga\xE7\xE3o aponta para "${to}", que n\xE3o existe.`,
248
+ choiceWithoutOptions: (id) => `N\xF3 "${id}": escolha sem nenhuma op\xE7\xE3o.`,
249
+ duplicatedOptionId: (id, optionId) => `N\xF3 "${id}": valor de op\xE7\xE3o "${optionId}" duplicado.`,
250
+ optionWithoutTarget: (id, label) => `N\xF3 "${id}": op\xE7\xE3o "${label}" n\xE3o tem destino definido.`,
251
+ tooManyOptions: (id, count) => `N\xF3 "${id}": ${count} op\xE7\xF5es \u2014 o WhatsApp aceita no m\xE1ximo 10 em lista.`,
252
+ buttonTitleTooLong: (id, label) => `N\xF3 "${id}": bot\xE3o "${label}" passa de 20 caracteres.`,
253
+ listTitleTooLong: (id, label) => `N\xF3 "${id}": item de lista "${label}" passa de 24 caracteres.`,
254
+ bodyTooLong: (id) => `N\xF3 "${id}": texto passa de 1024 caracteres.`,
255
+ unreachable: (id) => `N\xF3 "${id}" \xE9 inalcan\xE7\xE1vel a partir do in\xEDcio do fluxo.`,
256
+ deadEndQuestion: (id) => `N\xF3 "${id}": pergunta sem pr\xF3ximo passo definido.`,
257
+ conditionIncomplete: (id) => `N\xF3 "${id}": condi\xE7\xE3o incompleta \u2014 defina vari\xE1vel, operador e valor.`,
258
+ conditionBranchMissing: (id, branch) => `N\xF3 "${id}": ramo "${branch === "true" ? "Verdadeiro" : "Falso"}" sem destino definido.`
259
+ },
260
+ workspace: {
261
+ title: "Fluxos do Bot",
262
+ subtitle: "Blueprint visual dos fluxos de conversa, sincronizado com o que est\xE1 em produ\xE7\xE3o.",
263
+ loading: "Carregando fluxos\u2026",
264
+ loadError: "N\xE3o foi poss\xEDvel carregar os fluxos.",
265
+ saveGraph: "Publicar altera\xE7\xF5es",
266
+ saving: "Publicando\u2026",
267
+ saveSuccess: "Fluxo publicado! O bot j\xE1 est\xE1 usando a vers\xE3o nova.",
268
+ saveError: "N\xE3o foi poss\xEDvel salvar \u2014 verifique se todos os destinos apontam para n\xF3s existentes.",
269
+ organize: "Organizar",
270
+ organizeTooltip: "Reorganiza os n\xF3s automaticamente e salva as novas posi\xE7\xF5es",
271
+ discardChanges: "Desfazer altera\xE7\xF5es",
272
+ discardTooltip: "Devolve os fluxos abertos ao que est\xE1 publicado, descartando o que n\xE3o foi salvo",
273
+ discardConfirm: "Descartar todas as altera\xE7\xF5es n\xE3o publicadas e voltar ao fluxo que est\xE1 no ar?",
274
+ unsavedChangesConfirm: "Voc\xEA tem altera\xE7\xF5es n\xE3o publicadas neste fluxo. Trocar de fluxo agora descarta essas edi\xE7\xF5es. Continuar?"
275
+ },
276
+ flowManager: {
277
+ newFlow: "Novo fluxo",
278
+ createTitle: "Criar novo fluxo",
279
+ key: "Identificador \xFAnico",
280
+ keyHint: "letras min\xFAsculas, n\xFAmeros e _ (ex.: promocoes_semana)",
281
+ keyInvalid: "Use apenas letras min\xFAsculas, n\xFAmeros e _ (2 a 40 caracteres)",
282
+ label: "Nome exibido",
283
+ showInMenu: "Exibir como op\xE7\xE3o no menu principal do bot",
284
+ menuOptionLabel: "Texto da op\xE7\xE3o no menu",
285
+ create: "Criar fluxo",
286
+ creating: "Criando\u2026",
287
+ deleteFlow: "Excluir fluxo",
288
+ deleteConfirm: (label) => `Excluir o fluxo "${label}"? Esta a\xE7\xE3o n\xE3o pode ser desfeita.`,
289
+ createError: "N\xE3o foi poss\xEDvel criar o fluxo.",
290
+ deleteError: "N\xE3o foi poss\xEDvel excluir o fluxo."
231
291
  }
232
292
  };
233
293
  function mergeFlowEditorLabels(override) {
@@ -246,7 +306,11 @@ function mergeFlowEditorLabels(override) {
246
306
  palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
247
307
  flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
248
308
  flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
249
- crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal }
309
+ crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal },
310
+ collectionChain: { ...DEFAULT_FLOW_EDITOR_LABELS.collectionChain, ...override.collectionChain },
311
+ validation: { ...DEFAULT_FLOW_EDITOR_LABELS.validation, ...override.validation },
312
+ workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, ...override.workspace },
313
+ flowManager: { ...DEFAULT_FLOW_EDITOR_LABELS.flowManager, ...override.flowManager }
250
314
  };
251
315
  }
252
316
 
@@ -946,6 +1010,19 @@ function FlowNodePanel({
946
1010
  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 }),
947
1011
  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 }),
948
1012
  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 }),
1013
+ /* @__PURE__ */ jsxs8("div", { children: [
1014
+ /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.nodeName }),
1015
+ /* @__PURE__ */ jsx9(
1016
+ "input",
1017
+ {
1018
+ value: draft.label ?? "",
1019
+ placeholder: labels.nodePanel.nodeNamePlaceholder,
1020
+ onChange: (event) => setDraft((prev) => ({ ...prev, label: event.target.value || void 0 })),
1021
+ className: `w-full mt-1 ${INPUT_CLASSNAME}`
1022
+ }
1023
+ ),
1024
+ /* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mt-1", children: labels.nodePanel.nodeNameHint })
1025
+ ] }),
949
1026
  draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
950
1027
  /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
951
1028
  /* @__PURE__ */ jsx9(
@@ -1032,7 +1109,7 @@ function FlowNodePanel({
1032
1109
  ),
1033
1110
  isSendMedia && /* @__PURE__ */ jsxs8("div", { children: [
1034
1111
  /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.media }),
1035
- /* @__PURE__ */ jsx9("div", { className: "mt-1", children: renderMediaPicker?.(node) ?? /* @__PURE__ */ jsx9("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
1112
+ /* @__PURE__ */ jsx9("div", { className: "mt-1", children: renderMediaPicker?.(node, graph) ?? /* @__PURE__ */ jsx9("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
1036
1113
  ] }),
1037
1114
  isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx9(
1038
1115
  WhatsAppTextField,
@@ -1151,6 +1228,1007 @@ function FlowNodePanel({
1151
1228
  ] })
1152
1229
  ] });
1153
1230
  }
1231
+
1232
+ // src/flows/FlowsWorkspace.tsx
1233
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
1234
+ import {
1235
+ ReactFlow as ReactFlow2,
1236
+ Background as Background2,
1237
+ Controls as Controls2,
1238
+ MiniMap,
1239
+ MarkerType as MarkerType2,
1240
+ applyNodeChanges
1241
+ } from "@xyflow/react";
1242
+ import "@xyflow/react/dist/style.css";
1243
+ 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";
1244
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1245
+ var RF_NODE_TYPES = {
1246
+ ...flowNodeTypes,
1247
+ ...flowPortalNodeTypes,
1248
+ ...flowGroupHeaderNodeTypes,
1249
+ ...flowGroupFrameNodeTypes
1250
+ };
1251
+ var CHAIN_FRAME_PADDING = 36;
1252
+ var COLUMN_GAP = 360;
1253
+ var ROW_GAP = 40;
1254
+ var NS_SEP = "::";
1255
+ var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
1256
+ var EDGE_COLOR_LINEAR = "#94a3b8";
1257
+ var EDGE_COLOR_BRANCH = "#8b5cf6";
1258
+ var EDGE_COLOR_FALLBACK = "#cbd5e1";
1259
+ var EDGE_COLOR_LIVE = "#3b82f6";
1260
+ var EDGE_COLOR_CROSS_FLOW = "#06b6d4";
1261
+ var BACKGROUND_COLOR_LIGHT2 = "#cbd5e1";
1262
+ var BACKGROUND_COLOR_DARK2 = "#334155";
1263
+ function portalNodeId(sourceId, target) {
1264
+ return `__portal__${sourceId}__${target}`;
1265
+ }
1266
+ function ns(flowKey, nodeId) {
1267
+ return `${flowKey}${NS_SEP}${nodeId}`;
1268
+ }
1269
+ function parseNs(id) {
1270
+ const index = id.indexOf(NS_SEP);
1271
+ return index === -1 ? { flowKey: "", nodeId: id } : { flowKey: id.slice(0, index), nodeId: id.slice(index + NS_SEP.length) };
1272
+ }
1273
+ function autoMergeAll(rootKey, graphsSource) {
1274
+ const visited = /* @__PURE__ */ new Set();
1275
+ const queue = [rootKey];
1276
+ while (queue.length > 0) {
1277
+ const key = queue.shift();
1278
+ if (visited.has(key) || !graphsSource[key]) continue;
1279
+ visited.add(key);
1280
+ for (const target of crossFlowTargetsOf(graphsSource[key])) {
1281
+ if (!visited.has(target) && graphsSource[target]) queue.push(target);
1282
+ }
1283
+ }
1284
+ return [...visited].map((key) => ({ key, offset: { x: 0, y: 0 } }));
1285
+ }
1286
+ function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1287
+ const openKeys = new Set(openFlows.map((flow) => flow.key));
1288
+ const nodeByNsId = /* @__PURE__ */ new Map();
1289
+ for (const { key } of openFlows) {
1290
+ const graph = workingGraphs[key];
1291
+ if (!graph) continue;
1292
+ for (const node of Object.values(graph.nodes)) nodeByNsId.set(ns(key, node.id), node);
1293
+ }
1294
+ function forwardEdges(flowKey, node) {
1295
+ const result = [];
1296
+ for (const { target } of targetsOf(node)) {
1297
+ if (isCrossFlowTarget(target)) {
1298
+ const targetFlowKey = crossFlowKey(target);
1299
+ const targetGraph = openKeys.has(targetFlowKey) ? workingGraphs[targetFlowKey] : void 0;
1300
+ if (targetGraph) result.push(ns(targetFlowKey, targetGraph.startNodeId));
1301
+ } else if (workingGraphs[flowKey]?.nodes[target]) {
1302
+ result.push(ns(flowKey, target));
1303
+ }
1304
+ }
1305
+ return result;
1306
+ }
1307
+ const rank = /* @__PURE__ */ new Map();
1308
+ const primaryGraph = workingGraphs[primaryFlowKey];
1309
+ const rootId = primaryGraph ? ns(primaryFlowKey, primaryGraph.startNodeId) : void 0;
1310
+ if (rootId && nodeByNsId.has(rootId)) {
1311
+ rank.set(rootId, 0);
1312
+ const queue = [rootId];
1313
+ while (queue.length > 0) {
1314
+ const id = queue.shift();
1315
+ const node = nodeByNsId.get(id);
1316
+ if (!node) continue;
1317
+ for (const nextId of forwardEdges(parseNs(id).flowKey, node)) {
1318
+ if (!rank.has(nextId)) {
1319
+ rank.set(nextId, rank.get(id) + 1);
1320
+ queue.push(nextId);
1321
+ }
1322
+ }
1323
+ }
1324
+ }
1325
+ const strayRank = Math.max(0, ...rank.values()) + 1;
1326
+ for (const nsId of nodeByNsId.keys()) {
1327
+ if (!rank.has(nsId)) rank.set(nsId, strayRank);
1328
+ }
1329
+ const layers = /* @__PURE__ */ new Map();
1330
+ for (const [nsId, nodeRank] of rank) {
1331
+ if (!layers.has(nodeRank)) layers.set(nodeRank, []);
1332
+ layers.get(nodeRank).push(nsId);
1333
+ }
1334
+ const positions = /* @__PURE__ */ new Map();
1335
+ for (const nodeRank of [...layers.keys()].sort((a, b) => a - b)) {
1336
+ let cursorY = 0;
1337
+ for (const nsId of layers.get(nodeRank)) {
1338
+ positions.set(nsId, { x: nodeRank * COLUMN_GAP, y: cursorY });
1339
+ cursorY += estimateNodeHeight(nodeByNsId.get(nsId)) + ROW_GAP;
1340
+ }
1341
+ }
1342
+ return positions;
1343
+ }
1344
+ function computeLiveCounts(flowKey, rootFlowKey, livePositions) {
1345
+ const counts = {};
1346
+ for (const position of livePositions ?? []) {
1347
+ if (position.flow !== flowKey && flowKey !== rootFlowKey) continue;
1348
+ const nodeId = flowKey === rootFlowKey ? position.menuNodeId : position.nodeId;
1349
+ if (!nodeId) continue;
1350
+ counts[nodeId] = (counts[nodeId] ?? 0) + 1;
1351
+ }
1352
+ return counts;
1353
+ }
1354
+ function buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions) {
1355
+ const openKeys = new Set(openFlows.map((flow) => flow.key));
1356
+ const edges = [];
1357
+ for (const { key: flowKey } of openFlows) {
1358
+ const graph = workingGraphs[flowKey];
1359
+ if (!graph) continue;
1360
+ const liveCounts = computeLiveCounts(flowKey, rootFlowKey, livePositions);
1361
+ for (const [id, node] of Object.entries(graph.nodes)) {
1362
+ const isLive = (liveCounts[id] ?? 0) > 0;
1363
+ for (const { target, optionId, isDefault } of targetsOf(node)) {
1364
+ const crossFlow = isCrossFlowTarget(target);
1365
+ let targetFlowKey = flowKey;
1366
+ let rawTargetId = target;
1367
+ if (crossFlow) {
1368
+ const wantedKey = crossFlowKey(target);
1369
+ const targetGraph = openKeys.has(wantedKey) ? workingGraphs[wantedKey] : void 0;
1370
+ if (targetGraph) {
1371
+ targetFlowKey = wantedKey;
1372
+ rawTargetId = targetGraph.startNodeId;
1373
+ } else {
1374
+ rawTargetId = portalNodeId(id, target);
1375
+ }
1376
+ }
1377
+ const source = ns(flowKey, id);
1378
+ const edgeTarget = ns(targetFlowKey, rawTargetId);
1379
+ if (optionId === void 0 && !isDefault) {
1380
+ const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : isLive ? EDGE_COLOR_LIVE : EDGE_COLOR_LINEAR;
1381
+ edges.push({
1382
+ id: `${source}->${edgeTarget}`,
1383
+ source,
1384
+ target: edgeTarget,
1385
+ type: "bezier",
1386
+ animated: isLive,
1387
+ style: crossFlow ? { stroke: color, strokeWidth: 1.5, strokeDasharray: "3 3" } : { stroke: color, strokeWidth: isLive ? 2.5 : 1.5 },
1388
+ markerEnd: { type: MarkerType2.ArrowClosed, color, width: 18, height: 18 }
1389
+ });
1390
+ } else if (isDefault) {
1391
+ const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : EDGE_COLOR_FALLBACK;
1392
+ edges.push({
1393
+ id: `${source}->${edgeTarget}-default`,
1394
+ source,
1395
+ sourceHandle: "__default",
1396
+ target: edgeTarget,
1397
+ type: "bezier",
1398
+ style: { stroke: color, strokeWidth: 1.5, strokeDasharray: "5 4" },
1399
+ markerEnd: { type: MarkerType2.ArrowClosed, color, width: 16, height: 16 }
1400
+ });
1401
+ } else {
1402
+ const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : isLive ? EDGE_COLOR_LIVE : EDGE_COLOR_BRANCH;
1403
+ edges.push({
1404
+ id: `${source}->${edgeTarget}-${optionId}`,
1405
+ source,
1406
+ sourceHandle: optionId,
1407
+ target: edgeTarget,
1408
+ type: "bezier",
1409
+ animated: isLive,
1410
+ style: crossFlow ? { stroke: color, strokeWidth: 1.75, strokeDasharray: "3 3" } : { stroke: color, strokeWidth: isLive ? 2.5 : 1.75 },
1411
+ markerEnd: { type: MarkerType2.ArrowClosed, color, width: 18, height: 18 }
1412
+ });
1413
+ }
1414
+ }
1415
+ }
1416
+ }
1417
+ return edges;
1418
+ }
1419
+ function newNodeFromSpec(spec, existingIds) {
1420
+ if (spec.kind === "question") {
1421
+ const id2 = slugifyNodeId("nova_pergunta", existingIds);
1422
+ return { id: id2, type: "question", questionType: spec.questionType, contextKey: id2, question: "" };
1423
+ }
1424
+ if (spec.kind === "decision") {
1425
+ const id2 = slugifyNodeId("nova_decisao", existingIds);
1426
+ return {
1427
+ id: id2,
1428
+ type: "question",
1429
+ questionType: "choice",
1430
+ contextKey: id2,
1431
+ question: "",
1432
+ options: [
1433
+ ["1", "Op\xE7\xE3o 1"],
1434
+ ["2", "Op\xE7\xE3o 2"]
1435
+ ]
1436
+ };
1437
+ }
1438
+ if (spec.kind === "condition") {
1439
+ const id2 = slugifyNodeId("nova_condicao", existingIds);
1440
+ return { id: id2, type: "condition", conditionOperator: ">" };
1441
+ }
1442
+ const id = slugifyNodeId("nova_acao", existingIds);
1443
+ return { id, type: "action", actionKind: spec.actionKind };
1444
+ }
1445
+ function removeNodeAndCleanRefs(nodes, nodeId) {
1446
+ const { [nodeId]: _removed, ...rest } = nodes;
1447
+ return Object.fromEntries(
1448
+ Object.entries(rest).map(([id, node]) => {
1449
+ if (!node.next) return [id, node];
1450
+ if (typeof node.next === "string") {
1451
+ return [id, node.next === nodeId ? { ...node, next: void 0 } : node];
1452
+ }
1453
+ return [
1454
+ id,
1455
+ {
1456
+ ...node,
1457
+ next: {
1458
+ byAnswer: Object.fromEntries(
1459
+ Object.entries(node.next.byAnswer).map(([key, value]) => [key, value === nodeId ? "" : value])
1460
+ ),
1461
+ default: node.next.default === nodeId ? "" : node.next.default
1462
+ }
1463
+ }
1464
+ ];
1465
+ })
1466
+ );
1467
+ }
1468
+ function extractErrorMessage(error) {
1469
+ if (error instanceof Error) return error.message;
1470
+ return void 0;
1471
+ }
1472
+ var OUTLINE_BUTTON = "inline-flex items-center gap-1.5 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300 hover:border-blue-300 disabled:opacity-40 disabled:cursor-not-allowed";
1473
+ var PRIMARY_BUTTON = "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed";
1474
+ var DIALOG_INPUT = "w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400";
1475
+ function FlowsWorkspace({
1476
+ api,
1477
+ rootFlowKey = "menu",
1478
+ labels: labelsOverride,
1479
+ actionOptions,
1480
+ renderMediaPicker,
1481
+ livePollIntervalMs = 5e3,
1482
+ className
1483
+ }) {
1484
+ const labels = useMemo2(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride]);
1485
+ const isDark = useIsDarkTheme();
1486
+ const [graphs, setGraphs] = useState3(void 0);
1487
+ const [loadState, setLoadState] = useState3("loading");
1488
+ const [livePositions, setLivePositions] = useState3(void 0);
1489
+ const [viewMode, setViewMode] = useState3("detail");
1490
+ const [openFlows, setOpenFlows] = useState3([{ key: rootFlowKey, offset: { x: 0, y: 0 } }]);
1491
+ const [hasAutoMerged, setHasAutoMerged] = useState3(false);
1492
+ const [workingGraphs, setWorkingGraphs] = useState3({});
1493
+ const [editingRef, setEditingRef] = useState3(null);
1494
+ const [saveState, setSaveState] = useState3("idle");
1495
+ const [saveErrorMessage, setSaveErrorMessage] = useState3(void 0);
1496
+ const [showCreateDialog, setShowCreateDialog] = useState3(false);
1497
+ const [showDeleteDialog, setShowDeleteDialog] = useState3(false);
1498
+ const [newFlow, setNewFlow] = useState3({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
1499
+ const [flowMutationState, setFlowMutationState] = useState3({ pending: false });
1500
+ const [rfNodes, setRfNodes] = useState3([]);
1501
+ const [flowInstance, setFlowInstance] = useState3(null);
1502
+ const [pendingFocusNodeId, setPendingFocusNodeId] = useState3(null);
1503
+ const reloadGraphs = useCallback(async () => {
1504
+ try {
1505
+ const loaded = await api.getGraphs();
1506
+ setGraphs(loaded);
1507
+ setLoadState("ready");
1508
+ return loaded;
1509
+ } catch {
1510
+ setLoadState("error");
1511
+ return void 0;
1512
+ }
1513
+ }, [api]);
1514
+ useEffect2(() => {
1515
+ void reloadGraphs();
1516
+ }, [reloadGraphs]);
1517
+ useEffect2(() => {
1518
+ const fetchLive = api.getLivePositions;
1519
+ if (!fetchLive) return;
1520
+ let active = true;
1521
+ async function poll() {
1522
+ try {
1523
+ const positions = await fetchLive();
1524
+ if (active) setLivePositions(positions);
1525
+ } catch {
1526
+ }
1527
+ }
1528
+ void poll();
1529
+ const timer = setInterval(() => void poll(), livePollIntervalMs);
1530
+ return () => {
1531
+ active = false;
1532
+ clearInterval(timer);
1533
+ };
1534
+ }, [api, livePollIntervalMs]);
1535
+ const primaryFlowKey = openFlows[0]?.key ?? rootFlowKey;
1536
+ const primaryGraph = workingGraphs[primaryFlowKey];
1537
+ useEffect2(() => {
1538
+ if (!graphs || hasAutoMerged) return;
1539
+ setOpenFlows(autoMergeAll(rootFlowKey, graphs));
1540
+ setHasAutoMerged(true);
1541
+ }, [graphs, hasAutoMerged, rootFlowKey]);
1542
+ useEffect2(() => {
1543
+ if (!graphs) return;
1544
+ setWorkingGraphs((prev) => {
1545
+ let changed = false;
1546
+ const next = { ...prev };
1547
+ for (const { key } of openFlows) {
1548
+ if (!next[key] && graphs[key]) {
1549
+ next[key] = graphs[key];
1550
+ changed = true;
1551
+ }
1552
+ }
1553
+ return changed ? next : prev;
1554
+ });
1555
+ }, [graphs, openFlows]);
1556
+ const isFlowDirty = useCallback(
1557
+ (key) => {
1558
+ const working = workingGraphs[key];
1559
+ const server = graphs?.[key];
1560
+ if (!working || !server) return false;
1561
+ return JSON.stringify(working) !== JSON.stringify(server);
1562
+ },
1563
+ [workingGraphs, graphs]
1564
+ );
1565
+ const issuesByFlow = useMemo2(() => {
1566
+ const map = {};
1567
+ for (const { key } of openFlows) {
1568
+ const graph = workingGraphs[key];
1569
+ if (graph) map[key] = validateGraph(graph, labels.validation);
1570
+ }
1571
+ return map;
1572
+ }, [openFlows, workingGraphs, labels]);
1573
+ const errorCount = Object.values(issuesByFlow).reduce(
1574
+ (sum, list) => sum + list.filter((issue) => issue.severity === "error").length,
1575
+ 0
1576
+ );
1577
+ const warningCount = Object.values(issuesByFlow).reduce(
1578
+ (sum, list) => sum + list.filter((issue) => issue.severity === "warning").length,
1579
+ 0
1580
+ );
1581
+ const dirtyKeys = openFlows.map((flow) => flow.key).filter(isFlowDirty);
1582
+ const isDirty = dirtyKeys.length > 0;
1583
+ const updateFlow = useCallback((flowKey, updater) => {
1584
+ setWorkingGraphs((prev) => {
1585
+ const graph = prev[flowKey];
1586
+ if (!graph) return prev;
1587
+ return { ...prev, [flowKey]: updater(graph) };
1588
+ });
1589
+ }, []);
1590
+ const focusFlow = useCallback(
1591
+ (key) => {
1592
+ const others = openFlows.filter((flow) => flow.key !== key);
1593
+ if (others.some((flow) => isFlowDirty(flow.key)) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
1594
+ setOpenFlows(graphs ? autoMergeAll(key, graphs) : [{ key, offset: { x: 0, y: 0 } }]);
1595
+ if (editingRef && editingRef.flowKey !== key) setEditingRef(null);
1596
+ },
1597
+ [openFlows, isFlowDirty, editingRef, graphs, labels]
1598
+ );
1599
+ const closeFlow = useCallback(
1600
+ (key) => {
1601
+ if (isFlowDirty(key) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
1602
+ setOpenFlows((prev) => prev.filter((flow) => flow.key !== key));
1603
+ setWorkingGraphs((prev) => {
1604
+ const { [key]: _removed, ...rest } = prev;
1605
+ return rest;
1606
+ });
1607
+ if (editingRef?.flowKey === key) setEditingRef(null);
1608
+ },
1609
+ [isFlowDirty, editingRef, labels]
1610
+ );
1611
+ const mergeFlow = useCallback(
1612
+ (targetFlowKey, source) => {
1613
+ setOpenFlows((prev) => {
1614
+ if (prev.some((flow) => flow.key === targetFlowKey)) return prev;
1615
+ const sourceOpen = prev.find((flow) => flow.key === source.flowKey);
1616
+ const sourceGraph = workingGraphs[source.flowKey];
1617
+ const sourceLocalPosition = sourceGraph?.nodes[source.nodeId]?.position ?? (sourceGraph ? computeAutoLayout(sourceGraph)[source.nodeId] : void 0) ?? { x: 0, y: 0 };
1618
+ const baseOffset = sourceOpen?.offset ?? { x: 0, y: 0 };
1619
+ return [
1620
+ ...prev,
1621
+ {
1622
+ key: targetFlowKey,
1623
+ offset: {
1624
+ x: baseOffset.x + sourceLocalPosition.x + 400,
1625
+ y: baseOffset.y + sourceLocalPosition.y
1626
+ }
1627
+ }
1628
+ ];
1629
+ });
1630
+ },
1631
+ [workingGraphs]
1632
+ );
1633
+ const isMerged = openFlows.length > 1;
1634
+ const mergedPositions = useMemo2(
1635
+ () => isMerged ? computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) : null,
1636
+ [isMerged, openFlows, workingGraphs, primaryFlowKey]
1637
+ );
1638
+ const renderedPositionsRef = useRef2(/* @__PURE__ */ new Map());
1639
+ const derivedNodes = useMemo2(() => {
1640
+ const allNodes = [];
1641
+ for (const { key: flowKey, offset } of openFlows) {
1642
+ let resolvePosition2 = function(nodeId) {
1643
+ if (mergedPositions) {
1644
+ const nsId = ns(flowKey, nodeId);
1645
+ return renderedPositionsRef.current.get(nsId) ?? mergedPositions.get(nsId) ?? { x: 0, y: 0 };
1646
+ }
1647
+ const local = graph.nodes[nodeId]?.position ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
1648
+ return { x: local.x + offset.x, y: local.y + offset.y };
1649
+ };
1650
+ var resolvePosition = resolvePosition2;
1651
+ const graph = workingGraphs[flowKey];
1652
+ if (!graph) continue;
1653
+ const fallbackPositions = mergedPositions ? {} : computeAutoLayout(graph);
1654
+ const liveCounts = computeLiveCounts(flowKey, rootFlowKey, livePositions);
1655
+ const flowIssues = issuesByFlow[flowKey] ?? [];
1656
+ const isPrimary = flowKey === primaryFlowKey;
1657
+ const connectedTargets = /* @__PURE__ */ new Set();
1658
+ for (const candidate of Object.values(graph.nodes)) {
1659
+ for (const { target } of targetsOf(candidate)) {
1660
+ if (!isCrossFlowTarget(target)) connectedTargets.add(target);
1661
+ }
1662
+ }
1663
+ for (const node of Object.values(graph.nodes)) {
1664
+ const position = resolvePosition2(node.id);
1665
+ allNodes.push({
1666
+ id: ns(flowKey, node.id),
1667
+ type: "flowNode",
1668
+ position,
1669
+ draggable: true,
1670
+ data: {
1671
+ node,
1672
+ liveCount: liveCounts[node.id] ?? 0,
1673
+ isStart: node.id === graph.startNodeId,
1674
+ isSelected: editingRef?.flowKey === flowKey && editingRef?.nodeId === node.id,
1675
+ isDetached: node.id !== graph.startNodeId && !connectedTargets.has(node.id),
1676
+ issues: flowIssues,
1677
+ labels,
1678
+ onSelect: (nodeId) => setEditingRef({ flowKey, nodeId })
1679
+ }
1680
+ });
1681
+ const crossFlowTargets = [...new Set(targetsOf(node).map((edge) => edge.target).filter(isCrossFlowTarget))];
1682
+ crossFlowTargets.forEach((target, index) => {
1683
+ const targetFlowKey = crossFlowKey(target);
1684
+ if (openFlows.some((flow) => flow.key === targetFlowKey)) return;
1685
+ allNodes.push({
1686
+ id: ns(flowKey, portalNodeId(node.id, target)),
1687
+ type: "flowPortal",
1688
+ draggable: false,
1689
+ selectable: false,
1690
+ position: { x: position.x + 320, y: position.y + index * 70 },
1691
+ data: {
1692
+ label: graphs?.[targetFlowKey]?.label ?? targetFlowKey,
1693
+ onNavigate: () => mergeFlow(targetFlowKey, { flowKey, nodeId: node.id })
1694
+ }
1695
+ });
1696
+ });
1697
+ }
1698
+ for (const chain of findCollectionChains(graph)) {
1699
+ const chainNodeIds = [...chain.nodeIds, chain.actionNodeId];
1700
+ const positions = chainNodeIds.map((id) => resolvePosition2(id));
1701
+ const minX = Math.min(...positions.map((point) => point.x));
1702
+ const maxX = Math.max(...positions.map((point) => point.x)) + NODE_CARD_WIDTH;
1703
+ const minY = Math.min(...positions.map((point) => point.y));
1704
+ const maxY = Math.max(...positions.map((point) => point.y)) + estimateNodeHeight(graph.nodes[chain.actionNodeId]);
1705
+ allNodes.push({
1706
+ id: ns(flowKey, `__chain__${chain.actionNodeId}`),
1707
+ type: "flowGroupFrame",
1708
+ draggable: false,
1709
+ selectable: false,
1710
+ zIndex: -1,
1711
+ position: { x: minX - CHAIN_FRAME_PADDING, y: minY - CHAIN_FRAME_PADDING - 24 },
1712
+ style: {
1713
+ width: maxX - minX + CHAIN_FRAME_PADDING * 2,
1714
+ height: maxY - minY + CHAIN_FRAME_PADDING * 2 + 24
1715
+ },
1716
+ data: {
1717
+ label: labels.collectionChain.feeds(nodeLabel(graph.nodes[chain.actionNodeId], labels))
1718
+ }
1719
+ });
1720
+ }
1721
+ if (!isPrimary) {
1722
+ const startPosition = resolvePosition2(graph.startNodeId);
1723
+ allNodes.push({
1724
+ id: ns(flowKey, "__group_header__"),
1725
+ type: "flowGroupHeader",
1726
+ draggable: false,
1727
+ selectable: false,
1728
+ position: { x: startPosition.x, y: startPosition.y - 60 },
1729
+ data: {
1730
+ label: graph.label,
1731
+ onFocus: () => focusFlow(flowKey),
1732
+ onClose: () => closeFlow(flowKey)
1733
+ }
1734
+ });
1735
+ }
1736
+ }
1737
+ return allNodes;
1738
+ }, [
1739
+ openFlows,
1740
+ workingGraphs,
1741
+ mergedPositions,
1742
+ livePositions,
1743
+ issuesByFlow,
1744
+ primaryFlowKey,
1745
+ rootFlowKey,
1746
+ editingRef,
1747
+ graphs,
1748
+ mergeFlow,
1749
+ focusFlow,
1750
+ closeFlow,
1751
+ labels
1752
+ ]);
1753
+ const edges = useMemo2(
1754
+ () => buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions),
1755
+ [openFlows, workingGraphs, rootFlowKey, livePositions]
1756
+ );
1757
+ useEffect2(() => {
1758
+ setRfNodes(derivedNodes);
1759
+ for (const node of derivedNodes) {
1760
+ if (node.type === "flowNode") renderedPositionsRef.current.set(node.id, node.position);
1761
+ }
1762
+ }, [derivedNodes]);
1763
+ useEffect2(() => {
1764
+ if (!pendingFocusNodeId || !flowInstance) return;
1765
+ const target = rfNodes.find((node) => node.id === pendingFocusNodeId);
1766
+ if (!target) return;
1767
+ flowInstance.setCenter(target.position.x + NODE_CARD_WIDTH / 2, target.position.y, { zoom: 1, duration: 400 });
1768
+ setPendingFocusNodeId(null);
1769
+ }, [pendingFocusNodeId, flowInstance, rfNodes]);
1770
+ const onNodesChange = useCallback((changes) => {
1771
+ setRfNodes((current) => applyNodeChanges(changes, current));
1772
+ }, []);
1773
+ const onNodeDragStop = useCallback(
1774
+ (_event, node) => {
1775
+ renderedPositionsRef.current.set(node.id, node.position);
1776
+ const { flowKey, nodeId } = parseNs(node.id);
1777
+ const openFlow = openFlows.find((flow) => flow.key === flowKey);
1778
+ if (!openFlow) return;
1779
+ const localPosition = { x: node.position.x - openFlow.offset.x, y: node.position.y - openFlow.offset.y };
1780
+ updateFlow(
1781
+ flowKey,
1782
+ (graph) => graph.nodes[nodeId] ? { ...graph, nodes: { ...graph.nodes, [nodeId]: { ...graph.nodes[nodeId], position: localPosition } } } : graph
1783
+ );
1784
+ },
1785
+ [openFlows, updateFlow]
1786
+ );
1787
+ const onConnect = useCallback(
1788
+ (connection) => {
1789
+ const { source, sourceHandle, target } = connection;
1790
+ if (!source || !target || source === target) return;
1791
+ const sourceRef = parseNs(source);
1792
+ const targetRef = parseNs(target);
1793
+ let targetValue;
1794
+ if (targetRef.flowKey === sourceRef.flowKey) {
1795
+ targetValue = targetRef.nodeId;
1796
+ } else {
1797
+ const targetGraph = workingGraphs[targetRef.flowKey];
1798
+ if (!targetGraph || targetGraph.startNodeId !== targetRef.nodeId) return;
1799
+ targetValue = `${CROSS_FLOW_PREFIX}${targetRef.flowKey}`;
1800
+ }
1801
+ updateFlow(sourceRef.flowKey, (graph) => {
1802
+ const node = graph.nodes[sourceRef.nodeId];
1803
+ if (!node) return graph;
1804
+ const handle = sourceHandle ?? "next";
1805
+ const currentByAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
1806
+ const currentDefault = typeof node.next === "object" && node.next ? node.next.default : "";
1807
+ const updatedNode = handle === "next" ? { ...node, next: targetValue } : handle === "__default" ? { ...node, next: { byAnswer: currentByAnswer, default: targetValue } } : { ...node, next: { byAnswer: { ...currentByAnswer, [handle]: targetValue }, default: currentDefault } };
1808
+ return { ...graph, nodes: { ...graph.nodes, [sourceRef.nodeId]: updatedNode } };
1809
+ });
1810
+ },
1811
+ [workingGraphs, updateFlow]
1812
+ );
1813
+ function handleAddNode(spec) {
1814
+ if (!primaryGraph) return;
1815
+ const newNode = newNodeFromSpec(spec, new Set(Object.keys(primaryGraph.nodes)));
1816
+ const maxY = Math.max(0, ...Object.values(primaryGraph.nodes).map((node) => node.position?.y ?? 0));
1817
+ newNode.position = { x: 0, y: maxY + 170 };
1818
+ updateFlow(primaryFlowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [newNode.id]: newNode } }));
1819
+ setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
1820
+ setPendingFocusNodeId(ns(primaryFlowKey, newNode.id));
1821
+ }
1822
+ function handleNodePanelChange(updated) {
1823
+ if (!editingRef) return;
1824
+ updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [updated.id]: updated } }));
1825
+ setEditingRef(null);
1826
+ }
1827
+ function handleNodeDelete(nodeId) {
1828
+ if (!editingRef) return;
1829
+ updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: removeNodeAndCleanRefs(graph.nodes, nodeId) }));
1830
+ setEditingRef(null);
1831
+ }
1832
+ function handleOrganize() {
1833
+ if (!primaryGraph) return;
1834
+ renderedPositionsRef.current.clear();
1835
+ if (isMerged) {
1836
+ const positions2 = computeMergedLayout(openFlows, workingGraphs, primaryFlowKey);
1837
+ for (const { key, offset } of openFlows) {
1838
+ updateFlow(key, (graph) => ({
1839
+ ...graph,
1840
+ nodes: Object.fromEntries(
1841
+ Object.entries(graph.nodes).map(([id, node]) => {
1842
+ const position = positions2.get(ns(key, id));
1843
+ return [
1844
+ id,
1845
+ position ? { ...node, position: { x: position.x - offset.x, y: position.y - offset.y } } : node
1846
+ ];
1847
+ })
1848
+ )
1849
+ }));
1850
+ }
1851
+ return;
1852
+ }
1853
+ const positions = computeAutoLayout(primaryGraph);
1854
+ updateFlow(primaryFlowKey, (graph) => ({
1855
+ ...graph,
1856
+ nodes: Object.fromEntries(
1857
+ Object.entries(graph.nodes).map(([id, node]) => [id, { ...node, position: positions[id] ?? node.position }])
1858
+ )
1859
+ }));
1860
+ }
1861
+ function handleDiscardChanges() {
1862
+ if (!graphs || !isDirty) return;
1863
+ if (!window.confirm(labels.workspace.discardConfirm)) return;
1864
+ setWorkingGraphs((prev) => {
1865
+ const next = { ...prev };
1866
+ for (const key of dirtyKeys) {
1867
+ const published = graphs[key];
1868
+ if (published) next[key] = published;
1869
+ }
1870
+ return next;
1871
+ });
1872
+ renderedPositionsRef.current.clear();
1873
+ setEditingRef(null);
1874
+ }
1875
+ async function handlePublish() {
1876
+ if (dirtyKeys.length === 0 || errorCount > 0) return;
1877
+ setSaveState("saving");
1878
+ setSaveErrorMessage(void 0);
1879
+ try {
1880
+ await Promise.all(dirtyKeys.map((key) => api.saveGraph(key, workingGraphs[key])));
1881
+ await reloadGraphs();
1882
+ setSaveState("success");
1883
+ setTimeout(() => setSaveState("idle"), 3e3);
1884
+ } catch (error) {
1885
+ setSaveState("error");
1886
+ setSaveErrorMessage(extractErrorMessage(error));
1887
+ }
1888
+ }
1889
+ async function handleCreateFlow() {
1890
+ const createFlow = api.createFlow;
1891
+ if (!createFlow) return;
1892
+ setFlowMutationState({ pending: true });
1893
+ try {
1894
+ await createFlow({
1895
+ key: newFlow.key,
1896
+ label: newFlow.label,
1897
+ showInMenu: newFlow.showInMenu,
1898
+ ...newFlow.showInMenu ? { menuOptionLabel: newFlow.menuOptionLabel || newFlow.label } : {}
1899
+ });
1900
+ await reloadGraphs();
1901
+ setOpenFlows([{ key: newFlow.key, offset: { x: 0, y: 0 } }]);
1902
+ setShowCreateDialog(false);
1903
+ setNewFlow({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
1904
+ setFlowMutationState({ pending: false });
1905
+ } catch (error) {
1906
+ setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.createError });
1907
+ }
1908
+ }
1909
+ async function handleDeleteFlow() {
1910
+ const deleteFlow = api.deleteFlow;
1911
+ if (!deleteFlow || !primaryGraph) return;
1912
+ setFlowMutationState({ pending: true });
1913
+ try {
1914
+ await deleteFlow(primaryGraph.key);
1915
+ const reloaded = await reloadGraphs();
1916
+ setOpenFlows(reloaded ? autoMergeAll(rootFlowKey, reloaded) : [{ key: rootFlowKey, offset: { x: 0, y: 0 } }]);
1917
+ setShowDeleteDialog(false);
1918
+ setFlowMutationState({ pending: false });
1919
+ } catch (error) {
1920
+ setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.deleteError });
1921
+ }
1922
+ }
1923
+ const editingGraph = editingRef ? workingGraphs[editingRef.flowKey] : null;
1924
+ const editingNode = editingRef && editingGraph ? editingGraph.nodes[editingRef.nodeId] : null;
1925
+ const otherFlows = useMemo2(
1926
+ () => Object.values(graphs ?? {}).filter((graph) => graph.key !== editingRef?.flowKey).map((graph) => ({ key: graph.key, label: graph.label })),
1927
+ [graphs, editingRef?.flowKey]
1928
+ );
1929
+ const keyIsValid = FLOW_KEY_PATTERN.test(newFlow.key);
1930
+ const canCreateFlow = Boolean(api.createFlow);
1931
+ const canDeleteFlow = Boolean(api.deleteFlow) && primaryFlowKey !== rootFlowKey;
1932
+ return /* @__PURE__ */ jsxs9("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
1933
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
1934
+ /* @__PURE__ */ jsxs9("div", { children: [
1935
+ /* @__PURE__ */ jsx10("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
1936
+ /* @__PURE__ */ jsx10("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
1937
+ ] }),
1938
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-3", children: [
1939
+ saveState === "success" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
1940
+ saveState === "error" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
1941
+ /* @__PURE__ */ jsxs9(
1942
+ "button",
1943
+ {
1944
+ type: "button",
1945
+ onClick: () => setViewMode((mode) => mode === "map" ? "detail" : "map"),
1946
+ className: OUTLINE_BUTTON,
1947
+ children: [
1948
+ viewMode === "map" ? /* @__PURE__ */ jsx10(Workflow, { size: 14, "aria-hidden": "true" }) : /* @__PURE__ */ jsx10(MapIcon, { size: 14, "aria-hidden": "true" }),
1949
+ viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap
1950
+ ]
1951
+ }
1952
+ ),
1953
+ viewMode === "detail" && /* @__PURE__ */ jsxs9(Fragment2, { children: [
1954
+ /* @__PURE__ */ jsxs9(
1955
+ "button",
1956
+ {
1957
+ type: "button",
1958
+ onClick: handleOrganize,
1959
+ className: OUTLINE_BUTTON,
1960
+ title: labels.workspace.organizeTooltip,
1961
+ disabled: !primaryGraph,
1962
+ children: [
1963
+ /* @__PURE__ */ jsx10(LayoutGrid, { size: 14, "aria-hidden": "true" }),
1964
+ " ",
1965
+ labels.workspace.organize
1966
+ ]
1967
+ }
1968
+ ),
1969
+ /* @__PURE__ */ jsxs9(
1970
+ "button",
1971
+ {
1972
+ type: "button",
1973
+ onClick: handleDiscardChanges,
1974
+ className: OUTLINE_BUTTON,
1975
+ title: labels.workspace.discardTooltip,
1976
+ disabled: !isDirty || saveState === "saving",
1977
+ children: [
1978
+ /* @__PURE__ */ jsx10(Undo2, { size: 14, "aria-hidden": "true" }),
1979
+ " ",
1980
+ labels.workspace.discardChanges
1981
+ ]
1982
+ }
1983
+ ),
1984
+ /* @__PURE__ */ jsxs9(
1985
+ "button",
1986
+ {
1987
+ type: "button",
1988
+ onClick: () => void handlePublish(),
1989
+ className: PRIMARY_BUTTON,
1990
+ disabled: !isDirty || errorCount > 0 || saveState === "saving",
1991
+ children: [
1992
+ /* @__PURE__ */ jsx10(Save2, { size: 14, "aria-hidden": "true" }),
1993
+ saveState === "saving" ? labels.workspace.saving : labels.workspace.saveGraph
1994
+ ]
1995
+ }
1996
+ )
1997
+ ] })
1998
+ ] })
1999
+ ] }),
2000
+ viewMode === "detail" && /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
2001
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 flex-wrap", children: [
2002
+ graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx10(
2003
+ "button",
2004
+ {
2005
+ type: "button",
2006
+ onClick: () => focusFlow(graph.key),
2007
+ className: `px-3 py-1.5 rounded-full text-xs font-medium border transition-colors ${primaryFlowKey === graph.key ? "bg-blue-600 text-white border-blue-600" : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-700 hover:border-blue-300"}`,
2008
+ children: graph.label
2009
+ },
2010
+ graph.key
2011
+ )),
2012
+ canCreateFlow && /* @__PURE__ */ jsxs9(
2013
+ "button",
2014
+ {
2015
+ type: "button",
2016
+ onClick: () => {
2017
+ setFlowMutationState({ pending: false });
2018
+ setShowCreateDialog(true);
2019
+ },
2020
+ 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",
2021
+ children: [
2022
+ /* @__PURE__ */ jsx10(Plus3, { size: 12, "aria-hidden": "true" }),
2023
+ " ",
2024
+ labels.flowManager.newFlow
2025
+ ]
2026
+ }
2027
+ )
2028
+ ] }),
2029
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
2030
+ primaryGraph && /* @__PURE__ */ jsx10(
2031
+ FlowPalette,
2032
+ {
2033
+ onAdd: handleAddNode,
2034
+ labels,
2035
+ ...actionOptions ? { actionOptions: [...actionOptions] } : {}
2036
+ }
2037
+ ),
2038
+ primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs9(
2039
+ "button",
2040
+ {
2041
+ type: "button",
2042
+ onClick: () => {
2043
+ setFlowMutationState({ pending: false });
2044
+ setShowDeleteDialog(true);
2045
+ },
2046
+ 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",
2047
+ children: [
2048
+ /* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
2049
+ " ",
2050
+ labels.flowManager.deleteFlow
2051
+ ]
2052
+ }
2053
+ )
2054
+ ] })
2055
+ ] }),
2056
+ 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: [
2057
+ /* @__PURE__ */ jsxs9("span", { className: "font-medium text-gray-600 dark:text-gray-300", children: [
2058
+ labels.validation.title,
2059
+ ":"
2060
+ ] }),
2061
+ errorCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-red-600 dark:text-red-400 font-medium", children: [
2062
+ /* @__PURE__ */ jsx10(AlertCircle3, { size: 13, "aria-hidden": "true" }),
2063
+ " ",
2064
+ labels.validation.errors(errorCount)
2065
+ ] }),
2066
+ warningCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [
2067
+ /* @__PURE__ */ jsx10(AlertTriangle3, { size: 13, "aria-hidden": "true" }),
2068
+ " ",
2069
+ labels.validation.warnings(warningCount)
2070
+ ] })
2071
+ ] }),
2072
+ /* @__PURE__ */ jsxs9("div", { className: "flex-1 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative", children: [
2073
+ loadState === "loading" && /* @__PURE__ */ jsx10("p", { className: "text-center text-gray-400 py-12", children: labels.workspace.loading }),
2074
+ loadState === "error" && /* @__PURE__ */ jsx10("p", { className: "text-center text-red-500 py-12", children: labels.workspace.loadError }),
2075
+ loadState === "ready" && graphs && viewMode === "map" && /* @__PURE__ */ jsx10(
2076
+ FlowMapCanvas,
2077
+ {
2078
+ graphs,
2079
+ rootKey: rootFlowKey,
2080
+ labels,
2081
+ onOpenFlow: (key) => {
2082
+ focusFlow(key);
2083
+ setViewMode("detail");
2084
+ }
2085
+ }
2086
+ ),
2087
+ loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs9(
2088
+ ReactFlow2,
2089
+ {
2090
+ nodes: rfNodes,
2091
+ edges,
2092
+ nodeTypes: RF_NODE_TYPES,
2093
+ onNodesChange,
2094
+ onNodeDragStop,
2095
+ onConnect,
2096
+ onInit: setFlowInstance,
2097
+ fitView: true,
2098
+ proOptions: { hideAttribution: true },
2099
+ colorMode: isDark ? "dark" : "light",
2100
+ children: [
2101
+ /* @__PURE__ */ jsx10(Background2, { color: isDark ? BACKGROUND_COLOR_DARK2 : BACKGROUND_COLOR_LIGHT2 }),
2102
+ /* @__PURE__ */ jsx10(Controls2, {}),
2103
+ /* @__PURE__ */ jsx10(MiniMap, { pannable: true, zoomable: true, className: "!bg-white dark:!bg-gray-800" })
2104
+ ]
2105
+ }
2106
+ )
2107
+ ] }),
2108
+ editingNode && editingGraph && /* @__PURE__ */ jsx10(
2109
+ FlowNodePanel,
2110
+ {
2111
+ graph: editingGraph,
2112
+ node: editingNode,
2113
+ issues: editingRef ? issuesByFlow[editingRef.flowKey] ?? [] : [],
2114
+ otherFlows,
2115
+ labels,
2116
+ onClose: () => setEditingRef(null),
2117
+ onChange: handleNodePanelChange,
2118
+ onDelete: handleNodeDelete,
2119
+ ...renderMediaPicker ? { renderMediaPicker } : {}
2120
+ },
2121
+ `${editingRef?.flowKey}:${editingRef?.nodeId}`
2122
+ ),
2123
+ showCreateDialog && canCreateFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.createTitle, onClose: () => setShowCreateDialog(false), children: [
2124
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-3", children: [
2125
+ /* @__PURE__ */ jsxs9("div", { children: [
2126
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.label }),
2127
+ /* @__PURE__ */ jsx10(
2128
+ "input",
2129
+ {
2130
+ value: newFlow.label,
2131
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, label: event.target.value })),
2132
+ className: `mt-1 ${DIALOG_INPUT}`
2133
+ }
2134
+ )
2135
+ ] }),
2136
+ /* @__PURE__ */ jsxs9("div", { children: [
2137
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.key }),
2138
+ /* @__PURE__ */ jsx10(
2139
+ "input",
2140
+ {
2141
+ value: newFlow.key,
2142
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, key: event.target.value.toLowerCase() })),
2143
+ className: `mt-1 ${DIALOG_INPUT}`
2144
+ }
2145
+ ),
2146
+ /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-gray-400 mt-1", children: newFlow.key && !keyIsValid ? labels.flowManager.keyInvalid : labels.flowManager.keyHint })
2147
+ ] }),
2148
+ /* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200", children: [
2149
+ /* @__PURE__ */ jsx10(
2150
+ "input",
2151
+ {
2152
+ type: "checkbox",
2153
+ checked: newFlow.showInMenu,
2154
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, showInMenu: event.target.checked }))
2155
+ }
2156
+ ),
2157
+ labels.flowManager.showInMenu
2158
+ ] }),
2159
+ newFlow.showInMenu && /* @__PURE__ */ jsxs9("div", { children: [
2160
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.menuOptionLabel }),
2161
+ /* @__PURE__ */ jsx10(
2162
+ "input",
2163
+ {
2164
+ value: newFlow.menuOptionLabel,
2165
+ onChange: (event) => setNewFlow((prev) => ({ ...prev, menuOptionLabel: event.target.value })),
2166
+ placeholder: newFlow.label,
2167
+ className: `mt-1 ${DIALOG_INPUT}`
2168
+ }
2169
+ )
2170
+ ] }),
2171
+ flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600", children: flowMutationState.error })
2172
+ ] }),
2173
+ /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2174
+ /* @__PURE__ */ jsx10("button", { type: "button", className: OUTLINE_BUTTON, onClick: () => setShowCreateDialog(false), children: labels.nodePanel.cancel }),
2175
+ /* @__PURE__ */ jsx10(
2176
+ "button",
2177
+ {
2178
+ type: "button",
2179
+ className: PRIMARY_BUTTON,
2180
+ onClick: () => void handleCreateFlow(),
2181
+ disabled: !keyIsValid || !newFlow.label || flowMutationState.pending,
2182
+ children: flowMutationState.pending ? labels.flowManager.creating : labels.flowManager.create
2183
+ }
2184
+ )
2185
+ ] })
2186
+ ] }),
2187
+ showDeleteDialog && canDeleteFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.deleteFlow, onClose: () => setShowDeleteDialog(false), children: [
2188
+ /* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
2189
+ flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
2190
+ /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2191
+ /* @__PURE__ */ jsx10("button", { type: "button", className: OUTLINE_BUTTON, onClick: () => setShowDeleteDialog(false), children: labels.nodePanel.cancel }),
2192
+ /* @__PURE__ */ jsxs9(
2193
+ "button",
2194
+ {
2195
+ type: "button",
2196
+ className: "inline-flex items-center gap-1.5 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 disabled:opacity-40",
2197
+ onClick: () => void handleDeleteFlow(),
2198
+ disabled: flowMutationState.pending,
2199
+ children: [
2200
+ /* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
2201
+ " ",
2202
+ labels.flowManager.deleteFlow
2203
+ ]
2204
+ }
2205
+ )
2206
+ ] })
2207
+ ] })
2208
+ ] });
2209
+ }
2210
+ function FlowDialog({
2211
+ title,
2212
+ onClose,
2213
+ children
2214
+ }) {
2215
+ return /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4", children: [
2216
+ /* @__PURE__ */ jsx10("div", { className: "absolute inset-0", onClick: onClose, "aria-hidden": "true" }),
2217
+ /* @__PURE__ */ jsxs9(
2218
+ "div",
2219
+ {
2220
+ role: "dialog",
2221
+ "aria-modal": "true",
2222
+ "aria-label": title,
2223
+ className: "relative w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-5 shadow-xl",
2224
+ children: [
2225
+ /* @__PURE__ */ jsx10("h3", { className: "text-base font-semibold text-gray-900 dark:text-gray-100 mb-3", children: title }),
2226
+ children
2227
+ ]
2228
+ }
2229
+ )
2230
+ ] });
2231
+ }
1154
2232
  export {
1155
2233
  BUILT_IN_ACTION_KINDS,
1156
2234
  CONDITION_OPERATORS,
@@ -1165,6 +2243,7 @@ export {
1165
2243
  FlowPalette,
1166
2244
  FlowPortalNode,
1167
2245
  FlowWhatsAppPreview,
2246
+ FlowsWorkspace,
1168
2247
  NODE_CARD_WIDTH,
1169
2248
  WHATSAPP_LIMITS,
1170
2249
  computeAutoLayout,