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