@adatechnology/conversations-ui 0.1.0-rc.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
- package/dist/chunk-BJNRLLDO.js +2708 -0
- package/dist/chunk-DKPXKQGC.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +422 -5
- package/dist/flows/index.js +2502 -678
- package/dist/index.d.ts +921 -17
- package/dist/index.js +3677 -678
- 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.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- 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/chunk-73MW5HNT.js +0 -1717
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-B5C1DLu1.d.ts +0 -365
- package/src/preview/AudioRecorderButton.tsx +0 -117
package/dist/flows/index.js
CHANGED
|
@@ -1,279 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "../chunk-
|
|
2
|
+
TooltipLayer
|
|
3
|
+
} from "../chunk-DKPXKQGC.js";
|
|
4
4
|
import {
|
|
5
|
-
parseWhatsAppFormatting
|
|
6
|
-
|
|
5
|
+
parseWhatsAppFormatting,
|
|
6
|
+
useIsDarkTheme
|
|
7
|
+
} from "../chunk-WCBDXZ3X.js";
|
|
7
8
|
|
|
8
9
|
// src/flows/FlowNodeCard.tsx
|
|
10
|
+
import { useState } from "react";
|
|
9
11
|
import { Handle, Position } from "@xyflow/react";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
condition: Diamond
|
|
25
|
-
};
|
|
26
|
-
var DEFAULT_ACTION_KIND_ICON = {
|
|
27
|
-
handoff: Headset,
|
|
28
|
-
rate_limited_handoff: Clock3,
|
|
29
|
-
send_product_list: ShoppingBag
|
|
30
|
-
};
|
|
31
|
-
function nodeLabel(node, labels) {
|
|
32
|
-
if (!node) return "\u2014";
|
|
33
|
-
if (node.type === "action") {
|
|
34
|
-
const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
|
|
35
|
-
return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
|
|
36
|
-
}
|
|
37
|
-
if (node.type === "condition") {
|
|
38
|
-
if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return node.id;
|
|
39
|
-
const operatorLabel = labels.conditionOperatorLabels[node.conditionOperator] ?? node.conditionOperator;
|
|
40
|
-
return `${node.conditionContextKey} ${operatorLabel} ${node.conditionValue}`;
|
|
41
|
-
}
|
|
42
|
-
return node.question || node.contextKey || node.id;
|
|
43
|
-
}
|
|
44
|
-
function sourceRows(node, labels) {
|
|
45
|
-
if (node.type === "action") return [];
|
|
46
|
-
if (node.type === "condition") {
|
|
47
|
-
return [
|
|
48
|
-
{ id: "true", label: labels.nodePanel.conditionTrue, isDefault: false },
|
|
49
|
-
{ id: "false", label: labels.nodePanel.conditionFalse, isDefault: false }
|
|
50
|
-
];
|
|
51
|
-
}
|
|
52
|
-
const isChoice = node.type === "menu" || node.questionType === "choice";
|
|
53
|
-
if (!isChoice) return [{ id: "next", label: labels.nodePanel.nextRowLabel, isDefault: false }];
|
|
54
|
-
const options = node.options ?? [];
|
|
55
|
-
return [
|
|
56
|
-
...options.map(([id, label]) => ({ id, label, isDefault: false })),
|
|
57
|
-
{ id: "__default", label: labels.edgeFallbackLabel, isDefault: true }
|
|
58
|
-
];
|
|
59
|
-
}
|
|
60
|
-
function SourceRow({ label, isDefault, handleId }) {
|
|
61
|
-
return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-600 bg-white/70 dark:bg-gray-900/40 px-2 py-1 pr-3", children: [
|
|
62
|
-
/* @__PURE__ */ jsx("span", { className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`, children: label }),
|
|
63
|
-
/* @__PURE__ */ jsx(
|
|
64
|
-
Handle,
|
|
65
|
-
{
|
|
66
|
-
type: "source",
|
|
67
|
-
position: Position.Right,
|
|
68
|
-
id: handleId,
|
|
69
|
-
style: { position: "absolute", right: -7, top: "50%", transform: "translateY(-50%)" },
|
|
70
|
-
className: isDefault ? "!bg-gray-400 dark:!bg-gray-500" : "!bg-purple-500"
|
|
71
|
-
}
|
|
72
|
-
)
|
|
73
|
-
] });
|
|
74
|
-
}
|
|
75
|
-
function FlowNodeCard({ data }) {
|
|
76
|
-
const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } = data;
|
|
77
|
-
const label = nodeLabel(node, labels);
|
|
78
|
-
const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
|
|
79
|
-
const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
|
|
80
|
-
const rows = sourceRows(node, labels);
|
|
81
|
-
const hasError = issues.some((i) => i.severity === "error");
|
|
82
|
-
const hasWarning = !hasError && issues.some((i) => i.severity === "warning");
|
|
83
|
-
return /* @__PURE__ */ jsxs(
|
|
84
|
-
"div",
|
|
85
|
-
{
|
|
86
|
-
title: label,
|
|
87
|
-
className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""}`,
|
|
88
|
-
onClick: () => onSelect(node.id),
|
|
89
|
-
children: [
|
|
90
|
-
/* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
|
|
91
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
92
|
-
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs", children: [
|
|
93
|
-
/* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
|
|
94
|
-
isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", title: labels.startNodeTooltip }),
|
|
95
|
-
labels.legend[node.type]
|
|
96
|
-
] }),
|
|
97
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
98
|
-
hasError && /* @__PURE__ */ jsx(AlertCircle, { size: 13, className: "text-red-600 dark:text-red-400" }),
|
|
99
|
-
hasWarning && /* @__PURE__ */ jsx(AlertTriangle, { size: 13, className: "text-amber-500 dark:text-amber-400" }),
|
|
100
|
-
liveCount > 0 && /* @__PURE__ */ jsx(
|
|
101
|
-
"span",
|
|
102
|
-
{
|
|
103
|
-
title: labels.liveCountTooltip(liveCount),
|
|
104
|
-
className: "font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs",
|
|
105
|
-
children: liveCount
|
|
106
|
-
}
|
|
107
|
-
)
|
|
108
|
-
] })
|
|
109
|
-
] }),
|
|
110
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-900 dark:text-gray-100 mt-1 line-clamp-3", children: label }),
|
|
111
|
-
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)) })
|
|
112
|
-
]
|
|
113
|
-
}
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
var flowNodeTypes = { flowNode: FlowNodeCard };
|
|
117
|
-
|
|
118
|
-
// src/flows/FlowMapNode.tsx
|
|
119
|
-
import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
|
|
120
|
-
import { GitBranch as GitBranch2, Maximize2 } from "lucide-react";
|
|
121
|
-
|
|
122
|
-
// src/flows/labels.ts
|
|
123
|
-
var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
124
|
-
legend: {
|
|
125
|
-
question: "Pergunta",
|
|
126
|
-
entrada_choice: "Escolha calculada",
|
|
127
|
-
action: "A\xE7\xE3o (simula\xE7\xE3o/atendimento)",
|
|
128
|
-
menu: "Menu",
|
|
129
|
-
condition: "Condi\xE7\xE3o"
|
|
130
|
-
},
|
|
131
|
-
startNodeTooltip: "In\xEDcio do fluxo",
|
|
132
|
-
liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
|
|
133
|
-
edgeFallbackLabel: "outro",
|
|
134
|
-
actionKindLabels: {
|
|
135
|
-
handoff: "Encaminhar para atendimento",
|
|
136
|
-
rate_limited_handoff: "Encaminhar (limite de simula\xE7\xF5es atingido)",
|
|
137
|
-
send_product_list: "Enviar cat\xE1logo de produtos"
|
|
138
|
-
},
|
|
139
|
-
conditionOperatorLabels: {
|
|
140
|
-
">": "maior que",
|
|
141
|
-
">=": "maior ou igual a",
|
|
142
|
-
"<": "menor que",
|
|
143
|
-
"<=": "menor ou igual a",
|
|
144
|
-
"==": "igual a",
|
|
145
|
-
"!=": "diferente de",
|
|
146
|
-
contains: "cont\xE9m"
|
|
147
|
-
},
|
|
148
|
-
questionTypeLabels: {
|
|
149
|
-
text: "Texto livre",
|
|
150
|
-
money: "Valor em R$",
|
|
151
|
-
date: "Data",
|
|
152
|
-
int: "N\xFAmero inteiro",
|
|
153
|
-
cpf: "CPF",
|
|
154
|
-
choice: "Escolha (bot\xF5es/lista)"
|
|
155
|
-
},
|
|
156
|
-
nodePanel: {
|
|
157
|
-
title: "Editar n\xF3",
|
|
158
|
-
contextKey: "Chave (contexto)",
|
|
159
|
-
questionType: "Tipo de resposta",
|
|
160
|
-
question: "Texto da pergunta",
|
|
161
|
-
options: "Op\xE7\xF5es (choice)",
|
|
162
|
-
addOption: "Adicionar op\xE7\xE3o",
|
|
163
|
-
optionId: "Valor",
|
|
164
|
-
optionLabel: "Texto exibido",
|
|
165
|
-
next: "Pr\xF3ximo n\xF3",
|
|
166
|
-
nextHint: "Tamb\xE9m \xE9 poss\xEDvel arrastar um fio no canvas para conectar.",
|
|
167
|
-
nextRowLabel: "Pr\xF3ximo",
|
|
168
|
-
otherFlowsGroup: "Outros fluxos (salto)",
|
|
169
|
-
nextByAnswer: (id) => `Se responder "${id}" \u2192`,
|
|
170
|
-
nextDefault: "Caso contr\xE1rio \u2192",
|
|
171
|
-
save: "Salvar altera\xE7\xF5es",
|
|
172
|
-
cancel: "Cancelar",
|
|
173
|
-
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.",
|
|
174
|
-
actionNotice: "N\xF3 de a\xE7\xE3o \u2014 dispara a a\xE7\xE3o registrada ou encaminha para atendimento humano.",
|
|
175
|
-
preview: "Como o cliente v\xEA no WhatsApp",
|
|
176
|
-
previewPlaceholder: "Pr\xE9-visualiza\xE7\xE3o\u2026",
|
|
177
|
-
previewEmptyBody: "(escreva o texto da mensagem)",
|
|
178
|
-
previewEmptyOption: "(sem texto)",
|
|
179
|
-
previewListButton: "Ver op\xE7\xF5es",
|
|
180
|
-
previewModeButtons: "Enviado como bot\xF5es (at\xE9 3 op\xE7\xF5es)",
|
|
181
|
-
previewModeList: "Enviado como lista (4+ op\xE7\xF5es)",
|
|
182
|
-
delete: "Excluir n\xF3",
|
|
183
|
-
deleteConfirm: "Excluir este n\xF3? Liga\xE7\xF5es que apontam para ele ficar\xE3o quebradas.",
|
|
184
|
-
directMessage: "Mensagem (opcional)",
|
|
185
|
-
fallbackMessage: "Mensagem de fallback (quando o cat\xE1logo n\xE3o est\xE1 dispon\xEDvel)",
|
|
186
|
-
conditionNotice: "N\xF3 de condi\xE7\xE3o \u2014 n\xE3o pergunta nada ao cliente, s\xF3 decide automaticamente entre Verdadeiro/Falso.",
|
|
187
|
-
conditionVariable: "Vari\xE1vel (chave j\xE1 coletada por uma pergunta anterior)",
|
|
188
|
-
conditionOperator: "Operador",
|
|
189
|
-
conditionValue: "Valor de compara\xE7\xE3o",
|
|
190
|
-
conditionTrue: "Se verdadeiro \u2192",
|
|
191
|
-
conditionFalse: "Se falso \u2192",
|
|
192
|
-
conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192"
|
|
193
|
-
},
|
|
194
|
-
palette: {
|
|
195
|
-
title: "Adicionar ao fluxo",
|
|
196
|
-
question: "Pergunta",
|
|
197
|
-
decision: "Decis\xE3o",
|
|
198
|
-
condition: "Condi\xE7\xE3o",
|
|
199
|
-
conditionHint: "Compara uma vari\xE1vel j\xE1 coletada e segue automaticamente, sem perguntar nada",
|
|
200
|
-
action: "A\xE7\xE3o"
|
|
201
|
-
},
|
|
202
|
-
flowMap: {
|
|
203
|
-
nodeCount: (count) => `${count} n\xF3(s)`,
|
|
204
|
-
openFlow: "Abrir fluxo"
|
|
205
|
-
},
|
|
206
|
-
flowGroup: {
|
|
207
|
-
focus: "Focar neste fluxo",
|
|
208
|
-
close: "Fechar"
|
|
209
|
-
},
|
|
210
|
-
crossFlowPortal: {
|
|
211
|
-
tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
|
|
212
|
-
goesTo: (label) => `\u21AA Vai para: ${label}`
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
function mergeFlowEditorLabels(override) {
|
|
216
|
-
if (!override) return DEFAULT_FLOW_EDITOR_LABELS;
|
|
217
|
-
return {
|
|
218
|
-
...DEFAULT_FLOW_EDITOR_LABELS,
|
|
219
|
-
...override,
|
|
220
|
-
legend: { ...DEFAULT_FLOW_EDITOR_LABELS.legend, ...override.legend },
|
|
221
|
-
actionKindLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.actionKindLabels, ...override.actionKindLabels },
|
|
222
|
-
conditionOperatorLabels: {
|
|
223
|
-
...DEFAULT_FLOW_EDITOR_LABELS.conditionOperatorLabels,
|
|
224
|
-
...override.conditionOperatorLabels
|
|
225
|
-
},
|
|
226
|
-
questionTypeLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.questionTypeLabels, ...override.questionTypeLabels },
|
|
227
|
-
nodePanel: { ...DEFAULT_FLOW_EDITOR_LABELS.nodePanel, ...override.nodePanel },
|
|
228
|
-
palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
|
|
229
|
-
flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
|
|
230
|
-
flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
|
|
231
|
-
crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal }
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// src/flows/FlowMapNode.tsx
|
|
236
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
237
|
-
function FlowMapNode({ data }) {
|
|
238
|
-
const { label, nodeCount, isRoot, labels = DEFAULT_FLOW_EDITOR_LABELS, onOpen } = data;
|
|
239
|
-
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: [
|
|
240
|
-
/* @__PURE__ */ jsx2(Handle2, { type: "target", position: Position2.Top, className: "!bg-gray-400 dark:!bg-gray-500" }),
|
|
241
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
|
|
242
|
-
/* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
|
|
243
|
-
/* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
|
|
244
|
-
isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", title: labels.startNodeTooltip })
|
|
245
|
-
] }),
|
|
246
|
-
/* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
|
|
247
|
-
/* @__PURE__ */ jsxs2(
|
|
248
|
-
"button",
|
|
249
|
-
{
|
|
250
|
-
type: "button",
|
|
251
|
-
onClick: onOpen,
|
|
252
|
-
className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
|
|
253
|
-
children: [
|
|
254
|
-
/* @__PURE__ */ jsx2(Maximize2, { size: 11 }),
|
|
255
|
-
" ",
|
|
256
|
-
labels.flowMap.openFlow
|
|
257
|
-
]
|
|
258
|
-
}
|
|
259
|
-
),
|
|
260
|
-
/* @__PURE__ */ jsx2(Handle2, { type: "source", position: Position2.Bottom, className: "!bg-gray-400 dark:!bg-gray-500" })
|
|
261
|
-
] });
|
|
262
|
-
}
|
|
263
|
-
var flowMapNodeTypes = { flowMapNode: FlowMapNode };
|
|
264
|
-
|
|
265
|
-
// src/flows/FlowMapCanvas.tsx
|
|
266
|
-
import { useMemo } from "react";
|
|
267
|
-
import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
|
|
268
|
-
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";
|
|
269
26
|
|
|
270
27
|
// src/flows/flowGraph.ts
|
|
28
|
+
import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
|
|
271
29
|
var CONDITION_OPERATORS = [">", ">=", "<", "<=", "==", "!=", "contains"];
|
|
272
|
-
var BUILT_IN_ACTION_KINDS =
|
|
273
|
-
|
|
274
|
-
RATE_LIMITED_HANDOFF: "rate_limited_handoff",
|
|
275
|
-
SEND_PRODUCT_LIST: "send_product_list"
|
|
276
|
-
};
|
|
30
|
+
var BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND;
|
|
31
|
+
var PASS_THROUGH_ACTION_KINDS = ["send_media"];
|
|
277
32
|
var CROSS_FLOW_PREFIX = "flow:";
|
|
278
33
|
var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
|
|
279
34
|
var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
|
|
@@ -283,7 +38,10 @@ function estimateNodeHeight(node) {
|
|
|
283
38
|
const BODY_HEIGHT = 56;
|
|
284
39
|
const PADDING = 16;
|
|
285
40
|
const ROW_HEIGHT = 34;
|
|
286
|
-
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;
|
|
287
45
|
return HEADER_HEIGHT + BODY_HEIGHT + PADDING + rowCount * ROW_HEIGHT;
|
|
288
46
|
}
|
|
289
47
|
var WHATSAPP_LIMITS = {
|
|
@@ -389,45 +147,34 @@ function findUnreachable(graph) {
|
|
|
389
147
|
}
|
|
390
148
|
return Object.keys(graph.nodes).filter((id) => !reachable.has(id));
|
|
391
149
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
const node = graph.nodes[id];
|
|
401
|
-
if (!node) continue;
|
|
402
|
-
for (const { target } of targetsOf(node)) {
|
|
403
|
-
if (isCrossFlowTarget(target) || !graph.nodes[target]) continue;
|
|
404
|
-
if (rank[target] === void 0) {
|
|
405
|
-
rank[target] = rank[id] + 1;
|
|
406
|
-
queue.push(target);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
const maxRank = Math.max(0, ...Object.values(rank));
|
|
411
|
-
let strayRank = maxRank + 1;
|
|
412
|
-
for (const id of Object.keys(graph.nodes)) {
|
|
413
|
-
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);
|
|
414
158
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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 });
|
|
418
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);
|
|
419
173
|
const positions = {};
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const width = (ids.length - 1) * H_GAP;
|
|
425
|
-
let maxHeight = 0;
|
|
426
|
-
ids.forEach((id, index) => {
|
|
427
|
-
maxHeight = Math.max(maxHeight, estimateNodeHeight(graph.nodes[id]));
|
|
428
|
-
positions[id] = { x: index * H_GAP - width / 2, y: cumulativeY };
|
|
429
|
-
});
|
|
430
|
-
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;
|
|
431
178
|
}
|
|
432
179
|
return positions;
|
|
433
180
|
}
|
|
@@ -518,29 +265,497 @@ function slugifyNodeId(label, existing) {
|
|
|
518
265
|
return candidate;
|
|
519
266
|
}
|
|
520
267
|
|
|
521
|
-
// src/flows/
|
|
522
|
-
import { jsx
|
|
523
|
-
var
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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 }) {
|
|
528
741
|
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
|
|
529
742
|
const isDark = useIsDarkTheme();
|
|
530
743
|
const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey]);
|
|
531
744
|
const nodes = useMemo(
|
|
532
|
-
() => Object.values(graphs).map(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
+
),
|
|
544
759
|
[graphs, positions, rootKey, onOpenFlow, labels]
|
|
545
760
|
);
|
|
546
761
|
const edges = useMemo(() => {
|
|
@@ -592,8 +807,28 @@ function FlowGroupHeader({ data }) {
|
|
|
592
807
|
const { label, labels = DEFAULT_FLOW_EDITOR_LABELS, onFocus, onClose } = data;
|
|
593
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: [
|
|
594
809
|
/* @__PURE__ */ jsx5("span", { children: label }),
|
|
595
|
-
/* @__PURE__ */ jsx5(
|
|
596
|
-
|
|
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
|
+
)
|
|
597
832
|
] });
|
|
598
833
|
}
|
|
599
834
|
var flowGroupHeaderNodeTypes = { flowGroupHeader: FlowGroupHeader };
|
|
@@ -608,7 +843,8 @@ function FlowPortalNode({ data }) {
|
|
|
608
843
|
"button",
|
|
609
844
|
{
|
|
610
845
|
type: "button",
|
|
611
|
-
|
|
846
|
+
"data-cv-tooltip": labels.crossFlowPortal.tooltip,
|
|
847
|
+
"aria-label": labels.crossFlowPortal.tooltip,
|
|
612
848
|
onClick: onNavigate,
|
|
613
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",
|
|
614
850
|
children: [
|
|
@@ -622,23 +858,206 @@ function FlowPortalNode({ data }) {
|
|
|
622
858
|
var flowPortalNodeTypes = { flowPortal: FlowPortalNode };
|
|
623
859
|
|
|
624
860
|
// src/flows/FlowPalette.tsx
|
|
625
|
-
import { useEffect, useRef, useState } from "react";
|
|
626
|
-
import { Plus, MessageCircleQuestion as MessageCircleQuestion2, GitBranch as GitBranch3, Zap as Zap2, Diamond as Diamond2, ChevronRight } from "lucide-react";
|
|
627
|
-
|
|
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";
|
|
628
898
|
var QUESTION_TYPES = ["text", "money", "date", "int", "cpf"];
|
|
629
|
-
function
|
|
630
|
-
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
|
|
899
|
+
function FlowPaletteMenu({ onSelect, labels, actionOptions }) {
|
|
631
900
|
const resolvedActionOptions = actionOptions ?? [
|
|
632
901
|
{ actionKind: "handoff", label: labels.actionKindLabels.handoff ?? "Encaminhar para atendimento" }
|
|
633
902
|
];
|
|
634
|
-
const [
|
|
635
|
-
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);
|
|
636
1056
|
const containerRef = useRef(null);
|
|
637
1057
|
useEffect(() => {
|
|
638
1058
|
function handleClickOutside(event) {
|
|
639
1059
|
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
640
1060
|
setOpen(false);
|
|
641
|
-
setSubmenu(null);
|
|
642
1061
|
}
|
|
643
1062
|
}
|
|
644
1063
|
document.addEventListener("mousedown", handleClickOutside);
|
|
@@ -647,465 +1066,1870 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
647
1066
|
function select(spec) {
|
|
648
1067
|
onAdd(spec);
|
|
649
1068
|
setOpen(false);
|
|
650
|
-
setSubmenu(null);
|
|
651
1069
|
}
|
|
652
1070
|
return /* @__PURE__ */ jsxs6("div", { ref: containerRef, className: "relative", children: [
|
|
653
1071
|
/* @__PURE__ */ jsxs6(
|
|
654
1072
|
"button",
|
|
655
1073
|
{
|
|
1074
|
+
"data-cv-tooltip": labels.palette.title,
|
|
1075
|
+
"aria-label": labels.palette.title,
|
|
656
1076
|
onClick: () => setOpen((v) => !v),
|
|
657
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",
|
|
658
1078
|
children: [
|
|
659
|
-
/* @__PURE__ */ jsx7(
|
|
1079
|
+
/* @__PURE__ */ jsx7(Plus2, { size: 14 }),
|
|
660
1080
|
" ",
|
|
661
1081
|
labels.palette.title
|
|
662
1082
|
]
|
|
663
1083
|
}
|
|
664
1084
|
),
|
|
665
|
-
open && /* @__PURE__ */
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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",
|
|
669
1552
|
{
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
className:
|
|
1553
|
+
value: typeof draft.next === "string" ? draft.next : "",
|
|
1554
|
+
onChange: (e) => updateNextString(e.target.value),
|
|
1555
|
+
className: `w-full mt-1 ${SELECT_CLASSNAME}`,
|
|
673
1556
|
children: [
|
|
674
|
-
/* @__PURE__ */
|
|
675
|
-
|
|
676
|
-
" ",
|
|
677
|
-
labels.palette.question
|
|
678
|
-
] }),
|
|
679
|
-
/* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
|
|
1557
|
+
/* @__PURE__ */ jsx11("option", { value: "", children: "\u2014" }),
|
|
1558
|
+
nextNodeOptions()
|
|
680
1559
|
]
|
|
681
1560
|
}
|
|
682
|
-
),
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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(
|
|
694
1597
|
"button",
|
|
695
1598
|
{
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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",
|
|
699
1603
|
children: [
|
|
700
|
-
/* @__PURE__ */
|
|
1604
|
+
/* @__PURE__ */ jsx11(Save, { size: 14 }),
|
|
701
1605
|
" ",
|
|
702
|
-
labels.
|
|
1606
|
+
labels.nodePanel.save
|
|
703
1607
|
]
|
|
704
1608
|
}
|
|
705
1609
|
),
|
|
706
|
-
/* @__PURE__ */
|
|
1610
|
+
!isStart && /* @__PURE__ */ jsx11(
|
|
707
1611
|
"button",
|
|
708
1612
|
{
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
labels.palette.condition
|
|
717
|
-
]
|
|
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 })
|
|
718
1620
|
}
|
|
719
1621
|
),
|
|
720
|
-
/* @__PURE__ */
|
|
721
|
-
/* @__PURE__ */ jsxs6(
|
|
722
|
-
"button",
|
|
723
|
-
{
|
|
724
|
-
onMouseEnter: () => setSubmenu("action"),
|
|
725
|
-
onClick: () => setSubmenu(submenu === "action" ? null : "action"),
|
|
726
|
-
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",
|
|
727
|
-
children: [
|
|
728
|
-
/* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
|
|
729
|
-
/* @__PURE__ */ jsx7(Zap2, { size: 15, className: "text-orange-500" }),
|
|
730
|
-
" ",
|
|
731
|
-
labels.palette.action
|
|
732
|
-
] }),
|
|
733
|
-
/* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
|
|
734
|
-
]
|
|
735
|
-
}
|
|
736
|
-
),
|
|
737
|
-
submenu === "action" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: resolvedActionOptions.map((option) => /* @__PURE__ */ jsx7(
|
|
738
|
-
"button",
|
|
739
|
-
{
|
|
740
|
-
onClick: () => select({ kind: "action", actionKind: option.actionKind }),
|
|
741
|
-
className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
|
|
742
|
-
children: option.label
|
|
743
|
-
},
|
|
744
|
-
option.actionKind
|
|
745
|
-
)) })
|
|
746
|
-
] })
|
|
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 })
|
|
747
1623
|
] })
|
|
748
1624
|
] });
|
|
749
1625
|
}
|
|
750
1626
|
|
|
751
|
-
// src/flows/
|
|
752
|
-
import { useState as
|
|
753
|
-
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";
|
|
754
1640
|
|
|
755
|
-
// src/flows/
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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) };
|
|
761
1656
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
+
};
|
|
782
1733
|
}
|
|
783
1734
|
|
|
784
|
-
// src/flows/
|
|
785
|
-
|
|
786
|
-
var
|
|
787
|
-
var
|
|
788
|
-
function
|
|
789
|
-
|
|
790
|
-
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";
|
|
791
1741
|
}
|
|
792
|
-
function
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
799
2007
|
}) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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);
|
|
811
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 })
|
|
812
2281
|
)
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
|
855
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));
|
|
856
2392
|
}
|
|
857
|
-
function
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
|
861
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 });
|
|
862
2423
|
}
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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;
|
|
870
2470
|
});
|
|
2471
|
+
renderedPositionsRef.current.clear();
|
|
2472
|
+
setEditingRef(null);
|
|
871
2473
|
}
|
|
872
|
-
function
|
|
873
|
-
|
|
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
|
+
}
|
|
874
2487
|
}
|
|
875
|
-
function
|
|
876
|
-
|
|
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
|
+
}
|
|
877
2507
|
}
|
|
878
|
-
function
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
+
}
|
|
883
2521
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
]
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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 })
|
|
897
2537
|
] }),
|
|
898
|
-
|
|
899
|
-
/* @__PURE__ */
|
|
900
|
-
/* @__PURE__ */
|
|
901
|
-
|
|
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",
|
|
902
2543
|
{
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
]
|
|
907
2553
|
}
|
|
908
|
-
)
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
{
|
|
913
|
-
label: labels.nodePanel.question,
|
|
914
|
-
value: draft.question ?? "",
|
|
915
|
-
options: draft.questionType === "choice" ? draft.options : void 0,
|
|
916
|
-
onValueChange: (value) => setDraft((prev) => ({ ...prev, question: value })),
|
|
917
|
-
labels
|
|
918
|
-
}
|
|
919
|
-
),
|
|
920
|
-
isCondition && /* @__PURE__ */ jsxs8("div", { className: "space-y-3", children: [
|
|
921
|
-
/* @__PURE__ */ jsxs8("div", { children: [
|
|
922
|
-
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionVariable }),
|
|
923
|
-
/* @__PURE__ */ jsx9(
|
|
924
|
-
"input",
|
|
2554
|
+
),
|
|
2555
|
+
viewMode === "detail" && /* @__PURE__ */ jsxs11(Fragment4, { children: [
|
|
2556
|
+
/* @__PURE__ */ jsxs11(
|
|
2557
|
+
"button",
|
|
925
2558
|
{
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
+
]
|
|
930
2570
|
}
|
|
931
2571
|
),
|
|
932
|
-
/* @__PURE__ */
|
|
933
|
-
|
|
934
|
-
/* @__PURE__ */ jsxs8("div", { children: [
|
|
935
|
-
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionOperator }),
|
|
936
|
-
/* @__PURE__ */ jsx9(
|
|
937
|
-
"select",
|
|
2572
|
+
/* @__PURE__ */ jsxs11(
|
|
2573
|
+
"button",
|
|
938
2574
|
{
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
className:
|
|
942
|
-
|
|
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
|
+
]
|
|
943
2586
|
}
|
|
944
|
-
)
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionValue }),
|
|
948
|
-
/* @__PURE__ */ jsx9(
|
|
949
|
-
"input",
|
|
2587
|
+
),
|
|
2588
|
+
/* @__PURE__ */ jsxs11(
|
|
2589
|
+
"button",
|
|
950
2590
|
{
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
+
]
|
|
954
2601
|
}
|
|
955
2602
|
)
|
|
956
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
|
+
)
|
|
957
2638
|
] }),
|
|
958
|
-
|
|
959
|
-
|
|
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,
|
|
960
2689
|
{
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
2690
|
+
graphs,
|
|
2691
|
+
rootKey: rootFlowKey,
|
|
2692
|
+
labels,
|
|
2693
|
+
onOpenFlow: (key) => {
|
|
2694
|
+
focusFlow(key);
|
|
2695
|
+
setViewMode("detail");
|
|
2696
|
+
}
|
|
966
2697
|
}
|
|
967
2698
|
),
|
|
968
|
-
|
|
969
|
-
|
|
2699
|
+
loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs11(
|
|
2700
|
+
ReactFlow2,
|
|
970
2701
|
{
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
+
]
|
|
976
2727
|
}
|
|
977
|
-
)
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
+
)
|
|
986
2779
|
] }),
|
|
987
|
-
/* @__PURE__ */
|
|
988
|
-
/* @__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(
|
|
989
2783
|
"input",
|
|
990
2784
|
{
|
|
991
|
-
value:
|
|
992
|
-
onChange: (
|
|
993
|
-
|
|
994
|
-
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}`
|
|
995
2788
|
}
|
|
996
2789
|
),
|
|
997
|
-
/* @__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(
|
|
998
2794
|
"input",
|
|
999
2795
|
{
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
className: `flex-1 ${INPUT_CLASSNAME}`
|
|
2796
|
+
type: "checkbox",
|
|
2797
|
+
checked: newFlow.showInMenu,
|
|
2798
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, showInMenu: event.target.checked }))
|
|
1004
2799
|
}
|
|
1005
2800
|
),
|
|
1006
|
-
|
|
1007
|
-
] },
|
|
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 })
|
|
1008
2816
|
] }),
|
|
1009
|
-
|
|
1010
|
-
/* @__PURE__ */
|
|
1011
|
-
/* @__PURE__ */
|
|
1012
|
-
|
|
1013
|
-
"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",
|
|
1014
2821
|
{
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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,
|
|
1018
2847
|
children: [
|
|
1019
|
-
/* @__PURE__ */
|
|
1020
|
-
|
|
2848
|
+
/* @__PURE__ */ jsx12(Trash22, { size: 13, "aria-hidden": "true" }),
|
|
2849
|
+
" ",
|
|
2850
|
+
labels.flowManager.deleteFlow
|
|
1021
2851
|
]
|
|
1022
2852
|
}
|
|
1023
|
-
)
|
|
1024
|
-
conditionAnswerIds.map((id) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
|
|
1025
|
-
/* @__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) }),
|
|
1026
|
-
/* @__PURE__ */ jsxs8(
|
|
1027
|
-
"select",
|
|
1028
|
-
{
|
|
1029
|
-
value: draft.next && typeof draft.next === "object" ? draft.next.byAnswer[id] ?? "" : "",
|
|
1030
|
-
onChange: (e) => updateNextByAnswer(id, e.target.value),
|
|
1031
|
-
className: `flex-1 ${SELECT_CLASSNAME}`,
|
|
1032
|
-
children: [
|
|
1033
|
-
/* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
|
|
1034
|
-
nextNodeOptions()
|
|
1035
|
-
]
|
|
1036
|
-
}
|
|
1037
|
-
)
|
|
1038
|
-
] }, id)),
|
|
1039
|
-
/* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
|
|
1040
|
-
/* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault }),
|
|
1041
|
-
/* @__PURE__ */ jsxs8(
|
|
1042
|
-
"select",
|
|
1043
|
-
{
|
|
1044
|
-
value: draft.next && typeof draft.next === "object" ? draft.next.default : "",
|
|
1045
|
-
onChange: (e) => updateNextDefault(e.target.value),
|
|
1046
|
-
className: `flex-1 ${SELECT_CLASSNAME}`,
|
|
1047
|
-
children: [
|
|
1048
|
-
/* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
|
|
1049
|
-
nextNodeOptions()
|
|
1050
|
-
]
|
|
1051
|
-
}
|
|
1052
|
-
)
|
|
1053
|
-
] })
|
|
1054
|
-
] })
|
|
2853
|
+
)
|
|
1055
2854
|
] })
|
|
1056
|
-
] }),
|
|
1057
|
-
/* @__PURE__ */ jsxs8("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
|
|
1058
|
-
/* @__PURE__ */ jsxs8("button", { onClick: () => onChange(draft), className: "flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700", children: [
|
|
1059
|
-
/* @__PURE__ */ jsx9(Save, { size: 14 }),
|
|
1060
|
-
" ",
|
|
1061
|
-
labels.nodePanel.save
|
|
1062
|
-
] }),
|
|
1063
|
-
!isStart && /* @__PURE__ */ jsx9(
|
|
1064
|
-
"button",
|
|
1065
|
-
{
|
|
1066
|
-
onClick: () => {
|
|
1067
|
-
if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id);
|
|
1068
|
-
},
|
|
1069
|
-
title: labels.nodePanel.delete,
|
|
1070
|
-
className: "px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg",
|
|
1071
|
-
children: /* @__PURE__ */ jsx9(Trash2, { size: 14 })
|
|
1072
|
-
}
|
|
1073
|
-
),
|
|
1074
|
-
/* @__PURE__ */ jsx9("button", { onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
|
|
1075
2855
|
] })
|
|
1076
2856
|
] });
|
|
1077
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
|
+
}
|
|
1078
2880
|
export {
|
|
1079
2881
|
BUILT_IN_ACTION_KINDS,
|
|
1080
2882
|
CONDITION_OPERATORS,
|
|
1081
2883
|
CROSS_FLOW_PREFIX,
|
|
1082
2884
|
DEFAULT_FLOW_EDITOR_LABELS,
|
|
2885
|
+
FlowConnectionEdge,
|
|
1083
2886
|
FlowGroupFrame,
|
|
1084
2887
|
FlowGroupHeader,
|
|
2888
|
+
FlowLegend,
|
|
1085
2889
|
FlowMapCanvas,
|
|
1086
2890
|
FlowMapNode,
|
|
1087
2891
|
FlowNodeCard,
|
|
1088
2892
|
FlowNodePanel,
|
|
1089
2893
|
FlowPalette,
|
|
2894
|
+
FlowPaletteMenu,
|
|
1090
2895
|
FlowPortalNode,
|
|
1091
2896
|
FlowWhatsAppPreview,
|
|
2897
|
+
FlowsWorkspace,
|
|
2898
|
+
GROUP_HEADER_NODE_ID,
|
|
1092
2899
|
NODE_CARD_WIDTH,
|
|
2900
|
+
PASS_THROUGH_ACTION_KINDS,
|
|
1093
2901
|
WHATSAPP_LIMITS,
|
|
2902
|
+
applyConnection,
|
|
2903
|
+
buildFlowEdges,
|
|
2904
|
+
chainFrameBounds,
|
|
2905
|
+
chainFrameNodeId,
|
|
1094
2906
|
computeAutoLayout,
|
|
1095
2907
|
computeFlowMapLayout,
|
|
2908
|
+
computeMergedLayout,
|
|
2909
|
+
countLiveByNode,
|
|
1096
2910
|
crossFlowKey,
|
|
1097
2911
|
crossFlowTargetsOf,
|
|
2912
|
+
detachedNodeIds,
|
|
1098
2913
|
estimateNodeHeight,
|
|
1099
2914
|
findCollectionChains,
|
|
2915
|
+
flowEdgeTypes,
|
|
1100
2916
|
flowGroupFrameNodeTypes,
|
|
1101
2917
|
flowGroupHeaderNodeTypes,
|
|
1102
2918
|
flowMapNodeTypes,
|
|
1103
2919
|
flowNodeTypes,
|
|
1104
2920
|
flowPortalNodeTypes,
|
|
1105
2921
|
isCrossFlowTarget,
|
|
2922
|
+
isGraphDirty,
|
|
1106
2923
|
mergeFlowEditorLabels,
|
|
2924
|
+
mergedFlowKeysFrom,
|
|
2925
|
+
namespaceNodeId,
|
|
2926
|
+
newNodeFromSpec,
|
|
1107
2927
|
nodeLabel,
|
|
2928
|
+
parseNamespacedId,
|
|
2929
|
+
portalNodeId,
|
|
2930
|
+
removeNodeAndCleanRefs,
|
|
1108
2931
|
rendersAsButtons,
|
|
2932
|
+
resolveConnection,
|
|
1109
2933
|
slugifyNodeId,
|
|
1110
2934
|
targetsOf,
|
|
1111
2935
|
validateGraph
|