@adatechnology/conversations-ui 0.0.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/channel/index.d.ts +2 -0
- package/dist/channel/index.js +0 -0
- package/dist/chunk-ZDURDZTM.js +199 -0
- package/dist/flows/index.d.ts +246 -0
- package/dist/flows/index.js +1110 -0
- package/dist/index.d.ts +542 -0
- package/dist/index.js +2077 -0
- package/dist/styles.css +11 -0
- package/dist/styles.d.ts +2 -0
- package/package.json +52 -0
- package/src/AudioPlayer.tsx +103 -0
- package/src/Avatar.tsx +63 -0
- package/src/ConversationListItem.tsx +147 -0
- package/src/ConversationLocalesProvider.tsx +76 -0
- package/src/DateDivider.tsx +32 -0
- package/src/EmojiPicker.tsx +77 -0
- package/src/FileIcon.tsx +33 -0
- package/src/Lightbox.tsx +17 -0
- package/src/MediaRenderer.tsx +158 -0
- package/src/MessageBubble.tsx +129 -0
- package/src/MessageComposer.tsx +211 -0
- package/src/MessageTail.tsx +18 -0
- package/src/MessageText.tsx +42 -0
- package/src/MessageTimestamp.tsx +22 -0
- package/src/SimpleEmojiPicker.tsx +72 -0
- package/src/StatusTicks.tsx +39 -0
- package/src/Toast.tsx +140 -0
- package/src/Wallpaper.tsx +13 -0
- package/src/WhatsAppMessageEditor.tsx +142 -0
- package/src/channel/index.ts +1 -0
- package/src/conversations/index.ts +1 -0
- package/src/flows/FlowGroupFrame.tsx +21 -0
- package/src/flows/FlowGroupHeader.tsx +31 -0
- package/src/flows/FlowMapCanvas.tsx +76 -0
- package/src/flows/FlowMapNode.tsx +39 -0
- package/src/flows/FlowNodeCard.tsx +150 -0
- package/src/flows/FlowNodePanel.tsx +356 -0
- package/src/flows/FlowPalette.tsx +137 -0
- package/src/flows/FlowPortalNode.tsx +30 -0
- package/src/flows/FlowWhatsAppPreview.tsx +67 -0
- package/src/flows/flowGraph.ts +391 -0
- package/src/flows/index.ts +55 -0
- package/src/flows/labels.ts +187 -0
- package/src/hooks/useAsyncResource.ts +38 -0
- package/src/hooks/useConversationContext.ts +23 -0
- package/src/hooks/useConversationDocuments.ts +33 -0
- package/src/hooks/useConversationList.ts +32 -0
- package/src/hooks/useConversationMessages.ts +64 -0
- package/src/hooks/useConversationRealtime.ts +50 -0
- package/src/index.ts +87 -0
- package/src/lib/format.ts +32 -0
- package/src/lib/phone.ts +26 -0
- package/src/lib/whatsapp-formatting.tsx +215 -0
- package/src/providers/ConversationsProvider.tsx +29 -0
- package/src/providers/types.ts +54 -0
- package/src/settings/TopicsForm.tsx +109 -0
- package/src/settings/WelcomeFarewellForm.tsx +118 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +309 -0
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +264 -0
- package/src/styles.css +21 -0
- package/src/theme.ts +30 -0
- package/src/types.ts +44 -0
- package/src/useDarkMode.ts +48 -0
- package/src/useWaitingNotifications.ts +64 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseWhatsAppFormatting,
|
|
3
|
+
useDarkMode
|
|
4
|
+
} from "../chunk-ZDURDZTM.js";
|
|
5
|
+
|
|
6
|
+
// src/flows/FlowNodeCard.tsx
|
|
7
|
+
import { Handle, Position } from "@xyflow/react";
|
|
8
|
+
import { MessageCircleQuestion, GitBranch, Zap, ListTree, Diamond, AlertTriangle, AlertCircle, Headset, Clock3, ShoppingBag } from "lucide-react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
var NODE_TYPE_COLOR = {
|
|
11
|
+
question: "border-blue-300 bg-blue-50 dark:bg-blue-950/40 dark:border-blue-800",
|
|
12
|
+
entrada_choice: "border-purple-300 bg-purple-50 dark:bg-purple-950/40 dark:border-purple-800",
|
|
13
|
+
action: "border-orange-300 bg-orange-50 dark:bg-orange-950/40 dark:border-orange-800",
|
|
14
|
+
menu: "border-emerald-300 bg-emerald-50 dark:bg-emerald-950/40 dark:border-emerald-800",
|
|
15
|
+
condition: "border-cyan-300 bg-cyan-50 dark:bg-cyan-950/40 dark:border-cyan-800"
|
|
16
|
+
};
|
|
17
|
+
var NODE_TYPE_ICON = {
|
|
18
|
+
question: MessageCircleQuestion,
|
|
19
|
+
entrada_choice: ListTree,
|
|
20
|
+
action: Zap,
|
|
21
|
+
menu: GitBranch,
|
|
22
|
+
condition: Diamond
|
|
23
|
+
};
|
|
24
|
+
var DEFAULT_ACTION_KIND_ICON = {
|
|
25
|
+
handoff: Headset,
|
|
26
|
+
rate_limited_handoff: Clock3,
|
|
27
|
+
send_product_list: ShoppingBag
|
|
28
|
+
};
|
|
29
|
+
function nodeLabel(node, labels) {
|
|
30
|
+
if (!node) return "\u2014";
|
|
31
|
+
if (node.type === "action") {
|
|
32
|
+
const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
|
|
33
|
+
return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
|
|
34
|
+
}
|
|
35
|
+
if (node.type === "condition") {
|
|
36
|
+
if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return node.id;
|
|
37
|
+
const operatorLabel = labels.conditionOperatorLabels[node.conditionOperator] ?? node.conditionOperator;
|
|
38
|
+
return `${node.conditionContextKey} ${operatorLabel} ${node.conditionValue}`;
|
|
39
|
+
}
|
|
40
|
+
return node.question || node.contextKey || node.id;
|
|
41
|
+
}
|
|
42
|
+
function sourceRows(node, labels) {
|
|
43
|
+
if (node.type === "action") return [];
|
|
44
|
+
if (node.type === "condition") {
|
|
45
|
+
return [
|
|
46
|
+
{ id: "true", label: labels.nodePanel.conditionTrue, isDefault: false },
|
|
47
|
+
{ id: "false", label: labels.nodePanel.conditionFalse, isDefault: false }
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
const isChoice = node.type === "menu" || node.questionType === "choice";
|
|
51
|
+
if (!isChoice) return [{ id: "next", label: labels.nodePanel.nextRowLabel, isDefault: false }];
|
|
52
|
+
const options = node.options ?? [];
|
|
53
|
+
return [
|
|
54
|
+
...options.map(([id, label]) => ({ id, label, isDefault: false })),
|
|
55
|
+
{ id: "__default", label: labels.edgeFallbackLabel, isDefault: true }
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
function SourceRow({ label, isDefault, handleId }) {
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-600 bg-white/70 dark:bg-gray-900/40 px-2 py-1 pr-3", children: [
|
|
60
|
+
/* @__PURE__ */ jsx("span", { className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`, children: label }),
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
Handle,
|
|
63
|
+
{
|
|
64
|
+
type: "source",
|
|
65
|
+
position: Position.Right,
|
|
66
|
+
id: handleId,
|
|
67
|
+
style: { position: "absolute", right: -7, top: "50%", transform: "translateY(-50%)" },
|
|
68
|
+
className: isDefault ? "!bg-gray-400 dark:!bg-gray-500" : "!bg-purple-500"
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
] });
|
|
72
|
+
}
|
|
73
|
+
function FlowNodeCard({ data }) {
|
|
74
|
+
const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } = data;
|
|
75
|
+
const label = nodeLabel(node, labels);
|
|
76
|
+
const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
|
|
77
|
+
const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
|
|
78
|
+
const rows = sourceRows(node, labels);
|
|
79
|
+
const hasError = issues.some((i) => i.severity === "error");
|
|
80
|
+
const hasWarning = !hasError && issues.some((i) => i.severity === "warning");
|
|
81
|
+
return /* @__PURE__ */ jsxs(
|
|
82
|
+
"div",
|
|
83
|
+
{
|
|
84
|
+
title: label,
|
|
85
|
+
className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""}`,
|
|
86
|
+
onClick: () => onSelect(node.id),
|
|
87
|
+
children: [
|
|
88
|
+
/* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
|
|
89
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
90
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs", children: [
|
|
91
|
+
/* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
|
|
92
|
+
isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", title: labels.startNodeTooltip }),
|
|
93
|
+
labels.legend[node.type]
|
|
94
|
+
] }),
|
|
95
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
96
|
+
hasError && /* @__PURE__ */ jsx(AlertCircle, { size: 13, className: "text-red-600 dark:text-red-400" }),
|
|
97
|
+
hasWarning && /* @__PURE__ */ jsx(AlertTriangle, { size: 13, className: "text-amber-500 dark:text-amber-400" }),
|
|
98
|
+
liveCount > 0 && /* @__PURE__ */ jsx(
|
|
99
|
+
"span",
|
|
100
|
+
{
|
|
101
|
+
title: labels.liveCountTooltip(liveCount),
|
|
102
|
+
className: "font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs",
|
|
103
|
+
children: liveCount
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
] })
|
|
107
|
+
] }),
|
|
108
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-900 dark:text-gray-100 mt-1 line-clamp-3", children: label }),
|
|
109
|
+
rows.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-2 space-y-1", children: rows.map((row) => /* @__PURE__ */ jsx(SourceRow, { label: row.label, isDefault: row.isDefault, handleId: row.id }, row.id)) })
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
var flowNodeTypes = { flowNode: FlowNodeCard };
|
|
115
|
+
|
|
116
|
+
// src/flows/FlowMapNode.tsx
|
|
117
|
+
import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
|
|
118
|
+
import { GitBranch as GitBranch2, Maximize2 } from "lucide-react";
|
|
119
|
+
|
|
120
|
+
// src/flows/labels.ts
|
|
121
|
+
var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
122
|
+
legend: {
|
|
123
|
+
question: "Pergunta",
|
|
124
|
+
entrada_choice: "Escolha calculada",
|
|
125
|
+
action: "A\xE7\xE3o (simula\xE7\xE3o/atendimento)",
|
|
126
|
+
menu: "Menu",
|
|
127
|
+
condition: "Condi\xE7\xE3o"
|
|
128
|
+
},
|
|
129
|
+
startNodeTooltip: "In\xEDcio do fluxo",
|
|
130
|
+
liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
|
|
131
|
+
edgeFallbackLabel: "outro",
|
|
132
|
+
actionKindLabels: {
|
|
133
|
+
handoff: "Encaminhar para atendimento",
|
|
134
|
+
rate_limited_handoff: "Encaminhar (limite de simula\xE7\xF5es atingido)",
|
|
135
|
+
send_product_list: "Enviar cat\xE1logo de produtos"
|
|
136
|
+
},
|
|
137
|
+
conditionOperatorLabels: {
|
|
138
|
+
">": "maior que",
|
|
139
|
+
">=": "maior ou igual a",
|
|
140
|
+
"<": "menor que",
|
|
141
|
+
"<=": "menor ou igual a",
|
|
142
|
+
"==": "igual a",
|
|
143
|
+
"!=": "diferente de",
|
|
144
|
+
contains: "cont\xE9m"
|
|
145
|
+
},
|
|
146
|
+
questionTypeLabels: {
|
|
147
|
+
text: "Texto livre",
|
|
148
|
+
money: "Valor em R$",
|
|
149
|
+
date: "Data",
|
|
150
|
+
int: "N\xFAmero inteiro",
|
|
151
|
+
cpf: "CPF",
|
|
152
|
+
choice: "Escolha (bot\xF5es/lista)"
|
|
153
|
+
},
|
|
154
|
+
nodePanel: {
|
|
155
|
+
title: "Editar n\xF3",
|
|
156
|
+
contextKey: "Chave (contexto)",
|
|
157
|
+
questionType: "Tipo de resposta",
|
|
158
|
+
question: "Texto da pergunta",
|
|
159
|
+
options: "Op\xE7\xF5es (choice)",
|
|
160
|
+
addOption: "Adicionar op\xE7\xE3o",
|
|
161
|
+
optionId: "Valor",
|
|
162
|
+
optionLabel: "Texto exibido",
|
|
163
|
+
next: "Pr\xF3ximo n\xF3",
|
|
164
|
+
nextHint: "Tamb\xE9m \xE9 poss\xEDvel arrastar um fio no canvas para conectar.",
|
|
165
|
+
nextRowLabel: "Pr\xF3ximo",
|
|
166
|
+
otherFlowsGroup: "Outros fluxos (salto)",
|
|
167
|
+
nextByAnswer: (id) => `Se responder "${id}" \u2192`,
|
|
168
|
+
nextDefault: "Caso contr\xE1rio \u2192",
|
|
169
|
+
save: "Salvar altera\xE7\xF5es",
|
|
170
|
+
cancel: "Cancelar",
|
|
171
|
+
fixedLogicNotice: "Este n\xF3 tem l\xF3gica fixa (valida\xE7\xF5es/c\xE1lculos do sistema) \u2014 s\xF3 o texto e o destino s\xE3o edit\xE1veis.",
|
|
172
|
+
actionNotice: "N\xF3 de a\xE7\xE3o \u2014 dispara a a\xE7\xE3o registrada ou encaminha para atendimento humano.",
|
|
173
|
+
preview: "Como o cliente v\xEA no WhatsApp",
|
|
174
|
+
previewPlaceholder: "Pr\xE9-visualiza\xE7\xE3o\u2026",
|
|
175
|
+
previewEmptyBody: "(escreva o texto da mensagem)",
|
|
176
|
+
previewEmptyOption: "(sem texto)",
|
|
177
|
+
previewListButton: "Ver op\xE7\xF5es",
|
|
178
|
+
previewModeButtons: "Enviado como bot\xF5es (at\xE9 3 op\xE7\xF5es)",
|
|
179
|
+
previewModeList: "Enviado como lista (4+ op\xE7\xF5es)",
|
|
180
|
+
delete: "Excluir n\xF3",
|
|
181
|
+
deleteConfirm: "Excluir este n\xF3? Liga\xE7\xF5es que apontam para ele ficar\xE3o quebradas.",
|
|
182
|
+
directMessage: "Mensagem (opcional)",
|
|
183
|
+
fallbackMessage: "Mensagem de fallback (quando o cat\xE1logo n\xE3o est\xE1 dispon\xEDvel)",
|
|
184
|
+
conditionNotice: "N\xF3 de condi\xE7\xE3o \u2014 n\xE3o pergunta nada ao cliente, s\xF3 decide automaticamente entre Verdadeiro/Falso.",
|
|
185
|
+
conditionVariable: "Vari\xE1vel (chave j\xE1 coletada por uma pergunta anterior)",
|
|
186
|
+
conditionOperator: "Operador",
|
|
187
|
+
conditionValue: "Valor de compara\xE7\xE3o",
|
|
188
|
+
conditionTrue: "Se verdadeiro \u2192",
|
|
189
|
+
conditionFalse: "Se falso \u2192",
|
|
190
|
+
conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192"
|
|
191
|
+
},
|
|
192
|
+
palette: {
|
|
193
|
+
title: "Adicionar ao fluxo",
|
|
194
|
+
question: "Pergunta",
|
|
195
|
+
decision: "Decis\xE3o",
|
|
196
|
+
condition: "Condi\xE7\xE3o",
|
|
197
|
+
conditionHint: "Compara uma vari\xE1vel j\xE1 coletada e segue automaticamente, sem perguntar nada",
|
|
198
|
+
action: "A\xE7\xE3o"
|
|
199
|
+
},
|
|
200
|
+
flowMap: {
|
|
201
|
+
nodeCount: (count) => `${count} n\xF3(s)`,
|
|
202
|
+
openFlow: "Abrir fluxo"
|
|
203
|
+
},
|
|
204
|
+
flowGroup: {
|
|
205
|
+
focus: "Focar neste fluxo",
|
|
206
|
+
close: "Fechar"
|
|
207
|
+
},
|
|
208
|
+
crossFlowPortal: {
|
|
209
|
+
tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
|
|
210
|
+
goesTo: (label) => `\u21AA Vai para: ${label}`
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
function mergeFlowEditorLabels(override) {
|
|
214
|
+
if (!override) return DEFAULT_FLOW_EDITOR_LABELS;
|
|
215
|
+
return {
|
|
216
|
+
...DEFAULT_FLOW_EDITOR_LABELS,
|
|
217
|
+
...override,
|
|
218
|
+
legend: { ...DEFAULT_FLOW_EDITOR_LABELS.legend, ...override.legend },
|
|
219
|
+
actionKindLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.actionKindLabels, ...override.actionKindLabels },
|
|
220
|
+
conditionOperatorLabels: {
|
|
221
|
+
...DEFAULT_FLOW_EDITOR_LABELS.conditionOperatorLabels,
|
|
222
|
+
...override.conditionOperatorLabels
|
|
223
|
+
},
|
|
224
|
+
questionTypeLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.questionTypeLabels, ...override.questionTypeLabels },
|
|
225
|
+
nodePanel: { ...DEFAULT_FLOW_EDITOR_LABELS.nodePanel, ...override.nodePanel },
|
|
226
|
+
palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
|
|
227
|
+
flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
|
|
228
|
+
flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
|
|
229
|
+
crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal }
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/flows/FlowMapNode.tsx
|
|
234
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
235
|
+
function FlowMapNode({ data }) {
|
|
236
|
+
const { label, nodeCount, isRoot, labels = DEFAULT_FLOW_EDITOR_LABELS, onOpen } = data;
|
|
237
|
+
return /* @__PURE__ */ jsxs2("div", { className: "relative rounded-xl border-2 border-emerald-300 dark:border-emerald-700 bg-emerald-50 dark:bg-emerald-950/40 px-4 py-3 w-56 shadow-sm", children: [
|
|
238
|
+
/* @__PURE__ */ jsx2(Handle2, { type: "target", position: Position2.Top, className: "!bg-gray-400 dark:!bg-gray-500" }),
|
|
239
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
|
|
240
|
+
/* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
|
|
241
|
+
/* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
|
|
242
|
+
isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", title: labels.startNodeTooltip })
|
|
243
|
+
] }),
|
|
244
|
+
/* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
|
|
245
|
+
/* @__PURE__ */ jsxs2(
|
|
246
|
+
"button",
|
|
247
|
+
{
|
|
248
|
+
type: "button",
|
|
249
|
+
onClick: onOpen,
|
|
250
|
+
className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
|
|
251
|
+
children: [
|
|
252
|
+
/* @__PURE__ */ jsx2(Maximize2, { size: 11 }),
|
|
253
|
+
" ",
|
|
254
|
+
labels.flowMap.openFlow
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
),
|
|
258
|
+
/* @__PURE__ */ jsx2(Handle2, { type: "source", position: Position2.Bottom, className: "!bg-gray-400 dark:!bg-gray-500" })
|
|
259
|
+
] });
|
|
260
|
+
}
|
|
261
|
+
var flowMapNodeTypes = { flowMapNode: FlowMapNode };
|
|
262
|
+
|
|
263
|
+
// src/flows/FlowMapCanvas.tsx
|
|
264
|
+
import { useMemo } from "react";
|
|
265
|
+
import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
|
|
266
|
+
import "@xyflow/react/dist/style.css";
|
|
267
|
+
|
|
268
|
+
// src/flows/flowGraph.ts
|
|
269
|
+
var CONDITION_OPERATORS = [">", ">=", "<", "<=", "==", "!=", "contains"];
|
|
270
|
+
var BUILT_IN_ACTION_KINDS = {
|
|
271
|
+
HANDOFF: "handoff",
|
|
272
|
+
RATE_LIMITED_HANDOFF: "rate_limited_handoff",
|
|
273
|
+
SEND_PRODUCT_LIST: "send_product_list"
|
|
274
|
+
};
|
|
275
|
+
var CROSS_FLOW_PREFIX = "flow:";
|
|
276
|
+
var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
|
|
277
|
+
var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
|
|
278
|
+
var NODE_CARD_WIDTH = 240;
|
|
279
|
+
function estimateNodeHeight(node) {
|
|
280
|
+
const HEADER_HEIGHT = 28;
|
|
281
|
+
const BODY_HEIGHT = 56;
|
|
282
|
+
const PADDING = 16;
|
|
283
|
+
const ROW_HEIGHT = 34;
|
|
284
|
+
const rowCount = node.type === "action" ? 0 : node.type === "condition" ? 2 : node.type === "menu" || node.questionType === "choice" ? (node.options?.length ?? 0) + 1 : 1;
|
|
285
|
+
return HEADER_HEIGHT + BODY_HEIGHT + PADDING + rowCount * ROW_HEIGHT;
|
|
286
|
+
}
|
|
287
|
+
var WHATSAPP_LIMITS = {
|
|
288
|
+
MAX_BUTTONS: 3,
|
|
289
|
+
MAX_LIST_ROWS: 10,
|
|
290
|
+
BUTTON_TITLE_MAX: 20,
|
|
291
|
+
LIST_ROW_TITLE_MAX: 24,
|
|
292
|
+
BODY_MAX: 1024
|
|
293
|
+
};
|
|
294
|
+
function rendersAsButtons(options) {
|
|
295
|
+
return (options?.length ?? 0) <= WHATSAPP_LIMITS.MAX_BUTTONS;
|
|
296
|
+
}
|
|
297
|
+
function targetsOf(node) {
|
|
298
|
+
if (!node.next) return [];
|
|
299
|
+
if (typeof node.next === "string") return [{ target: node.next }];
|
|
300
|
+
return [
|
|
301
|
+
...Object.entries(node.next.byAnswer).map(([optionId, target]) => ({ target, optionId })),
|
|
302
|
+
{ target: node.next.default, isDefault: true }
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
function validateGraph(graph, issueText) {
|
|
306
|
+
const issues = [];
|
|
307
|
+
const nodeIds = new Set(Object.keys(graph.nodes));
|
|
308
|
+
const isValidTarget = (target) => nodeIds.has(target) || isCrossFlowTarget(target);
|
|
309
|
+
if (!nodeIds.has(graph.startNodeId)) {
|
|
310
|
+
issues.push({ severity: "error", message: issueText.noStart });
|
|
311
|
+
}
|
|
312
|
+
for (const node of Object.values(graph.nodes)) {
|
|
313
|
+
for (const { target } of targetsOf(node)) {
|
|
314
|
+
if (!isValidTarget(target)) {
|
|
315
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.brokenRef(node.id, target) });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const isChoice = node.questionType === "choice" || node.type === "menu";
|
|
319
|
+
if (isChoice) {
|
|
320
|
+
const options = node.options ?? [];
|
|
321
|
+
if (options.length === 0) {
|
|
322
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.choiceWithoutOptions(node.id) });
|
|
323
|
+
}
|
|
324
|
+
const seen = /* @__PURE__ */ new Set();
|
|
325
|
+
for (const [optionId, label] of options) {
|
|
326
|
+
if (seen.has(optionId)) {
|
|
327
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.duplicatedOptionId(node.id, optionId) });
|
|
328
|
+
}
|
|
329
|
+
seen.add(optionId);
|
|
330
|
+
const byAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
|
|
331
|
+
if (!byAnswer[optionId]) {
|
|
332
|
+
issues.push({ severity: "warning", nodeId: node.id, message: issueText.optionWithoutTarget(node.id, label) });
|
|
333
|
+
}
|
|
334
|
+
const usesButtons = rendersAsButtons(options);
|
|
335
|
+
if (usesButtons && label.length > WHATSAPP_LIMITS.BUTTON_TITLE_MAX) {
|
|
336
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.buttonTitleTooLong(node.id, label) });
|
|
337
|
+
}
|
|
338
|
+
if (!usesButtons && label.length > WHATSAPP_LIMITS.LIST_ROW_TITLE_MAX) {
|
|
339
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.listTitleTooLong(node.id, label) });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (options.length > WHATSAPP_LIMITS.MAX_LIST_ROWS) {
|
|
343
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.tooManyOptions(node.id, options.length) });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const bodyText = node.question ?? node.directMessage ?? "";
|
|
347
|
+
if (bodyText.length > WHATSAPP_LIMITS.BODY_MAX) {
|
|
348
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.bodyTooLong(node.id) });
|
|
349
|
+
}
|
|
350
|
+
if (node.type === "question" && !node.next) {
|
|
351
|
+
issues.push({ severity: "warning", nodeId: node.id, message: issueText.deadEndQuestion(node.id) });
|
|
352
|
+
}
|
|
353
|
+
if (node.type === "condition") {
|
|
354
|
+
if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) {
|
|
355
|
+
issues.push({ severity: "error", nodeId: node.id, message: issueText.conditionIncomplete(node.id) });
|
|
356
|
+
}
|
|
357
|
+
const byAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
|
|
358
|
+
if (!byAnswer.true)
|
|
359
|
+
issues.push({
|
|
360
|
+
severity: "warning",
|
|
361
|
+
nodeId: node.id,
|
|
362
|
+
message: issueText.conditionBranchMissing(node.id, "true")
|
|
363
|
+
});
|
|
364
|
+
if (!byAnswer.false)
|
|
365
|
+
issues.push({
|
|
366
|
+
severity: "warning",
|
|
367
|
+
nodeId: node.id,
|
|
368
|
+
message: issueText.conditionBranchMissing(node.id, "false")
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
for (const id of findUnreachable(graph)) {
|
|
373
|
+
issues.push({ severity: "warning", nodeId: id, message: issueText.unreachable(id) });
|
|
374
|
+
}
|
|
375
|
+
return issues;
|
|
376
|
+
}
|
|
377
|
+
function findUnreachable(graph) {
|
|
378
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
379
|
+
const queue = [graph.startNodeId];
|
|
380
|
+
while (queue.length > 0) {
|
|
381
|
+
const id = queue.shift();
|
|
382
|
+
if (reachable.has(id) || !graph.nodes[id]) continue;
|
|
383
|
+
reachable.add(id);
|
|
384
|
+
for (const { target } of targetsOf(graph.nodes[id])) {
|
|
385
|
+
if (!isCrossFlowTarget(target)) queue.push(target);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return Object.keys(graph.nodes).filter((id) => !reachable.has(id));
|
|
389
|
+
}
|
|
390
|
+
function computeAutoLayout(graph) {
|
|
391
|
+
const H_GAP = 300;
|
|
392
|
+
const V_GAP = 90;
|
|
393
|
+
const rank = {};
|
|
394
|
+
const queue = [graph.startNodeId];
|
|
395
|
+
rank[graph.startNodeId] = 0;
|
|
396
|
+
while (queue.length > 0) {
|
|
397
|
+
const id = queue.shift();
|
|
398
|
+
const node = graph.nodes[id];
|
|
399
|
+
if (!node) continue;
|
|
400
|
+
for (const { target } of targetsOf(node)) {
|
|
401
|
+
if (isCrossFlowTarget(target) || !graph.nodes[target]) continue;
|
|
402
|
+
if (rank[target] === void 0) {
|
|
403
|
+
rank[target] = rank[id] + 1;
|
|
404
|
+
queue.push(target);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const maxRank = Math.max(0, ...Object.values(rank));
|
|
409
|
+
let strayRank = maxRank + 1;
|
|
410
|
+
for (const id of Object.keys(graph.nodes)) {
|
|
411
|
+
if (rank[id] === void 0) rank[id] = strayRank++;
|
|
412
|
+
}
|
|
413
|
+
const layers = {};
|
|
414
|
+
for (const [id, r] of Object.entries(rank)) {
|
|
415
|
+
layers[r] = [...layers[r] ?? [], id];
|
|
416
|
+
}
|
|
417
|
+
const positions = {};
|
|
418
|
+
const sortedRanks = Object.keys(layers).map(Number).sort((a, b) => a - b);
|
|
419
|
+
let cumulativeY = 0;
|
|
420
|
+
for (const r of sortedRanks) {
|
|
421
|
+
const ids = layers[r];
|
|
422
|
+
const width = (ids.length - 1) * H_GAP;
|
|
423
|
+
let maxHeight = 0;
|
|
424
|
+
ids.forEach((id, index) => {
|
|
425
|
+
maxHeight = Math.max(maxHeight, estimateNodeHeight(graph.nodes[id]));
|
|
426
|
+
positions[id] = { x: index * H_GAP - width / 2, y: cumulativeY };
|
|
427
|
+
});
|
|
428
|
+
cumulativeY += maxHeight + V_GAP;
|
|
429
|
+
}
|
|
430
|
+
return positions;
|
|
431
|
+
}
|
|
432
|
+
function crossFlowTargetsOf(graph) {
|
|
433
|
+
const keys = /* @__PURE__ */ new Set();
|
|
434
|
+
for (const node of Object.values(graph.nodes)) {
|
|
435
|
+
for (const { target } of targetsOf(node)) {
|
|
436
|
+
if (isCrossFlowTarget(target)) keys.add(crossFlowKey(target));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return [...keys];
|
|
440
|
+
}
|
|
441
|
+
function computeFlowMapLayout(graphs, rootKey) {
|
|
442
|
+
const H_GAP = 280;
|
|
443
|
+
const V_GAP = 170;
|
|
444
|
+
const rank = {};
|
|
445
|
+
const queue = graphs[rootKey] ? [rootKey] : Object.keys(graphs);
|
|
446
|
+
if (graphs[rootKey]) rank[rootKey] = 0;
|
|
447
|
+
while (queue.length > 0) {
|
|
448
|
+
const key = queue.shift();
|
|
449
|
+
const g = graphs[key];
|
|
450
|
+
if (!g) continue;
|
|
451
|
+
for (const target of crossFlowTargetsOf(g)) {
|
|
452
|
+
if (!graphs[target]) continue;
|
|
453
|
+
if (rank[target] === void 0) {
|
|
454
|
+
rank[target] = rank[key] + 1;
|
|
455
|
+
queue.push(target);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const maxRank = Math.max(0, ...Object.values(rank));
|
|
460
|
+
let strayRank = maxRank + 1;
|
|
461
|
+
for (const key of Object.keys(graphs)) {
|
|
462
|
+
if (rank[key] === void 0) rank[key] = strayRank++;
|
|
463
|
+
}
|
|
464
|
+
const layers = {};
|
|
465
|
+
for (const [key, r] of Object.entries(rank)) {
|
|
466
|
+
layers[r] = [...layers[r] ?? [], key];
|
|
467
|
+
}
|
|
468
|
+
const positions = {};
|
|
469
|
+
for (const [r, keys] of Object.entries(layers)) {
|
|
470
|
+
const width = (keys.length - 1) * H_GAP;
|
|
471
|
+
keys.forEach((key, index) => {
|
|
472
|
+
positions[key] = { x: index * H_GAP - width / 2, y: Number(r) * V_GAP };
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return positions;
|
|
476
|
+
}
|
|
477
|
+
function findCollectionChains(graph) {
|
|
478
|
+
const actionIds = new Set(
|
|
479
|
+
Object.values(graph.nodes).filter((n) => n.type === "action").map((n) => n.id)
|
|
480
|
+
);
|
|
481
|
+
const memo = /* @__PURE__ */ new Map();
|
|
482
|
+
function reachableActions(id, stack) {
|
|
483
|
+
if (memo.has(id)) return memo.get(id);
|
|
484
|
+
if (stack.has(id)) return /* @__PURE__ */ new Set();
|
|
485
|
+
if (actionIds.has(id)) return /* @__PURE__ */ new Set([id]);
|
|
486
|
+
const node = graph.nodes[id];
|
|
487
|
+
if (!node) return /* @__PURE__ */ new Set();
|
|
488
|
+
const nextStack = new Set(stack);
|
|
489
|
+
nextStack.add(id);
|
|
490
|
+
const result = /* @__PURE__ */ new Set();
|
|
491
|
+
for (const { target } of targetsOf(node)) {
|
|
492
|
+
if (isCrossFlowTarget(target) || !graph.nodes[target]) continue;
|
|
493
|
+
for (const actionId of reachableActions(target, nextStack)) result.add(actionId);
|
|
494
|
+
}
|
|
495
|
+
memo.set(id, result);
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
const nodeIdsByAction = /* @__PURE__ */ new Map();
|
|
499
|
+
for (const node of Object.values(graph.nodes)) {
|
|
500
|
+
if (node.type !== "question") continue;
|
|
501
|
+
const reached = reachableActions(node.id, /* @__PURE__ */ new Set());
|
|
502
|
+
if (reached.size !== 1) continue;
|
|
503
|
+
const [actionNodeId] = [...reached];
|
|
504
|
+
nodeIdsByAction.set(actionNodeId, [...nodeIdsByAction.get(actionNodeId) ?? [], node.id]);
|
|
505
|
+
}
|
|
506
|
+
return [...nodeIdsByAction.entries()].filter(([, nodeIds]) => nodeIds.length >= 2).map(([actionNodeId, nodeIds]) => ({ actionNodeId, nodeIds }));
|
|
507
|
+
}
|
|
508
|
+
function slugifyNodeId(label, existing) {
|
|
509
|
+
const base = label.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 30) || "no";
|
|
510
|
+
let candidate = base;
|
|
511
|
+
let counter = 2;
|
|
512
|
+
while (existing.has(candidate)) {
|
|
513
|
+
candidate = `${base}_${counter}`;
|
|
514
|
+
counter++;
|
|
515
|
+
}
|
|
516
|
+
return candidate;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/flows/FlowMapCanvas.tsx
|
|
520
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
521
|
+
var MAP_NODE_TYPES = { ...flowMapNodeTypes };
|
|
522
|
+
var EDGE_COLOR = "#06b6d4";
|
|
523
|
+
var BACKGROUND_COLOR_LIGHT = "#cbd5e1";
|
|
524
|
+
var BACKGROUND_COLOR_DARK = "#334155";
|
|
525
|
+
function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverride }) {
|
|
526
|
+
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
|
|
527
|
+
const { isDark } = useDarkMode();
|
|
528
|
+
const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey]);
|
|
529
|
+
const nodes = useMemo(
|
|
530
|
+
() => Object.values(graphs).map((g) => ({
|
|
531
|
+
id: g.key,
|
|
532
|
+
type: "flowMapNode",
|
|
533
|
+
position: positions[g.key] ?? { x: 0, y: 0 },
|
|
534
|
+
data: {
|
|
535
|
+
label: g.label,
|
|
536
|
+
nodeCount: Object.keys(g.nodes).length,
|
|
537
|
+
isRoot: g.key === rootKey,
|
|
538
|
+
labels,
|
|
539
|
+
onOpen: () => onOpenFlow(g.key)
|
|
540
|
+
}
|
|
541
|
+
})),
|
|
542
|
+
[graphs, positions, rootKey, onOpenFlow, labels]
|
|
543
|
+
);
|
|
544
|
+
const edges = useMemo(() => {
|
|
545
|
+
const list = [];
|
|
546
|
+
for (const g of Object.values(graphs)) {
|
|
547
|
+
for (const targetKey of crossFlowTargetsOf(g)) {
|
|
548
|
+
if (!graphs[targetKey]) continue;
|
|
549
|
+
list.push({
|
|
550
|
+
id: `${g.key}->${targetKey}`,
|
|
551
|
+
source: g.key,
|
|
552
|
+
target: targetKey,
|
|
553
|
+
type: "default",
|
|
554
|
+
style: { stroke: EDGE_COLOR, strokeWidth: 1.75 },
|
|
555
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: EDGE_COLOR, width: 18, height: 18 }
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return list;
|
|
560
|
+
}, [graphs]);
|
|
561
|
+
return /* @__PURE__ */ jsxs3(
|
|
562
|
+
ReactFlow,
|
|
563
|
+
{
|
|
564
|
+
nodes,
|
|
565
|
+
edges,
|
|
566
|
+
nodeTypes: MAP_NODE_TYPES,
|
|
567
|
+
fitView: true,
|
|
568
|
+
proOptions: { hideAttribution: true },
|
|
569
|
+
colorMode: isDark ? "dark" : "light",
|
|
570
|
+
children: [
|
|
571
|
+
/* @__PURE__ */ jsx3(Background, { color: isDark ? BACKGROUND_COLOR_DARK : BACKGROUND_COLOR_LIGHT }),
|
|
572
|
+
/* @__PURE__ */ jsx3(Controls, {})
|
|
573
|
+
]
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/flows/FlowGroupFrame.tsx
|
|
579
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
580
|
+
function FlowGroupFrame({ data }) {
|
|
581
|
+
const { label } = data;
|
|
582
|
+
return /* @__PURE__ */ jsx4("div", { className: "relative w-full h-full rounded-xl border-2 border-dashed border-orange-300/70 dark:border-orange-700/50 bg-orange-50/40 dark:bg-orange-950/10", children: /* @__PURE__ */ jsx4("span", { className: "absolute -top-6 left-1 text-[11px] font-semibold text-orange-600 dark:text-orange-400 uppercase tracking-wide whitespace-nowrap", children: label }) });
|
|
583
|
+
}
|
|
584
|
+
var flowGroupFrameNodeTypes = { flowGroupFrame: FlowGroupFrame };
|
|
585
|
+
|
|
586
|
+
// src/flows/FlowGroupHeader.tsx
|
|
587
|
+
import { Maximize2 as Maximize22, X } from "lucide-react";
|
|
588
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
589
|
+
function FlowGroupHeader({ data }) {
|
|
590
|
+
const { label, labels = DEFAULT_FLOW_EDITOR_LABELS, onFocus, onClose } = data;
|
|
591
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 rounded-full border border-cyan-300 dark:border-cyan-700 bg-cyan-50 dark:bg-cyan-950/50 px-3 py-1 text-xs font-medium text-cyan-800 dark:text-cyan-200 shadow-sm whitespace-nowrap", children: [
|
|
592
|
+
/* @__PURE__ */ jsx5("span", { children: label }),
|
|
593
|
+
/* @__PURE__ */ jsx5("button", { type: "button", onClick: onFocus, title: labels.flowGroup.focus, className: "hover:text-blue-600 dark:hover:text-blue-400", children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 }) }),
|
|
594
|
+
/* @__PURE__ */ jsx5("button", { type: "button", onClick: onClose, title: labels.flowGroup.close, className: "hover:text-red-600 dark:hover:text-red-400", children: /* @__PURE__ */ jsx5(X, { size: 12 }) })
|
|
595
|
+
] });
|
|
596
|
+
}
|
|
597
|
+
var flowGroupHeaderNodeTypes = { flowGroupHeader: FlowGroupHeader };
|
|
598
|
+
|
|
599
|
+
// src/flows/FlowPortalNode.tsx
|
|
600
|
+
import { Handle as Handle3, Position as Position3 } from "@xyflow/react";
|
|
601
|
+
import { ArrowUpRight } from "lucide-react";
|
|
602
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
603
|
+
function FlowPortalNode({ data }) {
|
|
604
|
+
const { label, labels = DEFAULT_FLOW_EDITOR_LABELS, onNavigate } = data;
|
|
605
|
+
return /* @__PURE__ */ jsxs5(
|
|
606
|
+
"button",
|
|
607
|
+
{
|
|
608
|
+
type: "button",
|
|
609
|
+
title: labels.crossFlowPortal.tooltip,
|
|
610
|
+
onClick: onNavigate,
|
|
611
|
+
className: "relative flex items-center gap-1.5 rounded-full border-2 border-dashed border-cyan-400 dark:border-cyan-600 bg-cyan-50 dark:bg-cyan-950/40 px-3 py-1.5 text-xs font-medium text-cyan-700 dark:text-cyan-300 hover:bg-cyan-100 dark:hover:bg-cyan-950/70 transition-colors cursor-pointer",
|
|
612
|
+
children: [
|
|
613
|
+
/* @__PURE__ */ jsx6(Handle3, { type: "target", position: Position3.Top, id: "target", className: "!bg-cyan-400 dark:!bg-cyan-600" }),
|
|
614
|
+
/* @__PURE__ */ jsx6(ArrowUpRight, { size: 12, strokeWidth: 2.5 }),
|
|
615
|
+
labels.crossFlowPortal.goesTo(label)
|
|
616
|
+
]
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
var flowPortalNodeTypes = { flowPortal: FlowPortalNode };
|
|
621
|
+
|
|
622
|
+
// src/flows/FlowPalette.tsx
|
|
623
|
+
import { useEffect, useRef, useState } from "react";
|
|
624
|
+
import { Plus, MessageCircleQuestion as MessageCircleQuestion2, GitBranch as GitBranch3, Zap as Zap2, Diamond as Diamond2, ChevronRight } from "lucide-react";
|
|
625
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
626
|
+
var QUESTION_TYPES = ["text", "money", "date", "int", "cpf"];
|
|
627
|
+
function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
628
|
+
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
|
|
629
|
+
const resolvedActionOptions = actionOptions ?? [
|
|
630
|
+
{ actionKind: "handoff", label: labels.actionKindLabels.handoff ?? "Encaminhar para atendimento" }
|
|
631
|
+
];
|
|
632
|
+
const [open, setOpen] = useState(false);
|
|
633
|
+
const [submenu, setSubmenu] = useState(null);
|
|
634
|
+
const containerRef = useRef(null);
|
|
635
|
+
useEffect(() => {
|
|
636
|
+
function handleClickOutside(event) {
|
|
637
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
638
|
+
setOpen(false);
|
|
639
|
+
setSubmenu(null);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
643
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
644
|
+
}, []);
|
|
645
|
+
function select(spec) {
|
|
646
|
+
onAdd(spec);
|
|
647
|
+
setOpen(false);
|
|
648
|
+
setSubmenu(null);
|
|
649
|
+
}
|
|
650
|
+
return /* @__PURE__ */ jsxs6("div", { ref: containerRef, className: "relative", children: [
|
|
651
|
+
/* @__PURE__ */ jsxs6(
|
|
652
|
+
"button",
|
|
653
|
+
{
|
|
654
|
+
onClick: () => setOpen((v) => !v),
|
|
655
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700",
|
|
656
|
+
children: [
|
|
657
|
+
/* @__PURE__ */ jsx7(Plus, { size: 14 }),
|
|
658
|
+
" ",
|
|
659
|
+
labels.palette.title
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
),
|
|
663
|
+
open && /* @__PURE__ */ jsxs6("div", { className: "absolute left-0 top-full mt-1.5 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg z-50 py-1", children: [
|
|
664
|
+
/* @__PURE__ */ jsxs6("div", { className: "relative", children: [
|
|
665
|
+
/* @__PURE__ */ jsxs6(
|
|
666
|
+
"button",
|
|
667
|
+
{
|
|
668
|
+
onMouseEnter: () => setSubmenu("question"),
|
|
669
|
+
onClick: () => setSubmenu(submenu === "question" ? null : "question"),
|
|
670
|
+
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",
|
|
671
|
+
children: [
|
|
672
|
+
/* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
|
|
673
|
+
/* @__PURE__ */ jsx7(MessageCircleQuestion2, { size: 15, className: "text-blue-500" }),
|
|
674
|
+
" ",
|
|
675
|
+
labels.palette.question
|
|
676
|
+
] }),
|
|
677
|
+
/* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
|
|
678
|
+
]
|
|
679
|
+
}
|
|
680
|
+
),
|
|
681
|
+
submenu === "question" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: QUESTION_TYPES.map((qt) => /* @__PURE__ */ jsx7(
|
|
682
|
+
"button",
|
|
683
|
+
{
|
|
684
|
+
onClick: () => select({ kind: "question", questionType: qt }),
|
|
685
|
+
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",
|
|
686
|
+
children: labels.questionTypeLabels[qt]
|
|
687
|
+
},
|
|
688
|
+
qt
|
|
689
|
+
)) })
|
|
690
|
+
] }),
|
|
691
|
+
/* @__PURE__ */ jsxs6(
|
|
692
|
+
"button",
|
|
693
|
+
{
|
|
694
|
+
onMouseEnter: () => setSubmenu(null),
|
|
695
|
+
onClick: () => select({ kind: "decision" }),
|
|
696
|
+
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",
|
|
697
|
+
children: [
|
|
698
|
+
/* @__PURE__ */ jsx7(GitBranch3, { size: 15, className: "text-purple-500" }),
|
|
699
|
+
" ",
|
|
700
|
+
labels.palette.decision
|
|
701
|
+
]
|
|
702
|
+
}
|
|
703
|
+
),
|
|
704
|
+
/* @__PURE__ */ jsxs6(
|
|
705
|
+
"button",
|
|
706
|
+
{
|
|
707
|
+
onMouseEnter: () => setSubmenu(null),
|
|
708
|
+
onClick: () => select({ kind: "condition" }),
|
|
709
|
+
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",
|
|
710
|
+
title: labels.palette.conditionHint,
|
|
711
|
+
children: [
|
|
712
|
+
/* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
|
|
713
|
+
" ",
|
|
714
|
+
labels.palette.condition
|
|
715
|
+
]
|
|
716
|
+
}
|
|
717
|
+
),
|
|
718
|
+
/* @__PURE__ */ jsxs6("div", { className: "relative", children: [
|
|
719
|
+
/* @__PURE__ */ jsxs6(
|
|
720
|
+
"button",
|
|
721
|
+
{
|
|
722
|
+
onMouseEnter: () => setSubmenu("action"),
|
|
723
|
+
onClick: () => setSubmenu(submenu === "action" ? null : "action"),
|
|
724
|
+
className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
|
|
725
|
+
children: [
|
|
726
|
+
/* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
|
|
727
|
+
/* @__PURE__ */ jsx7(Zap2, { size: 15, className: "text-orange-500" }),
|
|
728
|
+
" ",
|
|
729
|
+
labels.palette.action
|
|
730
|
+
] }),
|
|
731
|
+
/* @__PURE__ */ jsx7(ChevronRight, { size: 13, className: "text-gray-400" })
|
|
732
|
+
]
|
|
733
|
+
}
|
|
734
|
+
),
|
|
735
|
+
submenu === "action" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: resolvedActionOptions.map((option) => /* @__PURE__ */ jsx7(
|
|
736
|
+
"button",
|
|
737
|
+
{
|
|
738
|
+
onClick: () => select({ kind: "action", actionKind: option.actionKind }),
|
|
739
|
+
className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
|
|
740
|
+
children: option.label
|
|
741
|
+
},
|
|
742
|
+
option.actionKind
|
|
743
|
+
)) })
|
|
744
|
+
] })
|
|
745
|
+
] })
|
|
746
|
+
] });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/flows/FlowNodePanel.tsx
|
|
750
|
+
import { useState as useState2 } from "react";
|
|
751
|
+
import { Plus as Plus2, Trash2, Save, X as X2, AlertTriangle as AlertTriangle2, AlertCircle as AlertCircle2 } from "lucide-react";
|
|
752
|
+
|
|
753
|
+
// src/flows/FlowWhatsAppPreview.tsx
|
|
754
|
+
import { List } from "lucide-react";
|
|
755
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
756
|
+
function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel }) {
|
|
757
|
+
if (!body && (!options || options.length === 0)) {
|
|
758
|
+
return /* @__PURE__ */ jsx8("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
|
|
759
|
+
}
|
|
760
|
+
const usesButtons = rendersAsButtons(options);
|
|
761
|
+
const hasOptions = (options?.length ?? 0) > 0;
|
|
762
|
+
return /* @__PURE__ */ jsxs7("div", { className: "rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5", children: [
|
|
763
|
+
/* @__PURE__ */ jsxs7("div", { className: "max-w-[85%]", children: [
|
|
764
|
+
/* @__PURE__ */ jsxs7("div", { className: "rounded-lg rounded-tl-none bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm whitespace-pre-wrap break-words", children: [
|
|
765
|
+
body ? parseWhatsAppFormatting(body) : /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyBody }),
|
|
766
|
+
hasOptions && !usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-2 -mx-3 -mb-2 border-t border-gray-100 dark:border-gray-600", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-center gap-1.5 py-2 text-sm font-medium text-cyan-600 dark:text-cyan-400", children: [
|
|
767
|
+
/* @__PURE__ */ jsx8(List, { size: 15 }),
|
|
768
|
+
" ",
|
|
769
|
+
labels.previewListButton
|
|
770
|
+
] }) })
|
|
771
|
+
] }),
|
|
772
|
+
hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8("div", { className: "rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm", children: label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption }) }, id)) }),
|
|
773
|
+
hasOptions && !usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1.5 rounded-lg bg-white dark:bg-gray-700 shadow-sm divide-y divide-gray-100 dark:divide-gray-600 overflow-hidden", children: options.slice(0, WHATSAPP_LIMITS.MAX_LIST_ROWS).map(([id, label]) => /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 px-3 py-1.5 text-sm text-gray-800 dark:text-gray-100", children: [
|
|
774
|
+
/* @__PURE__ */ jsx8("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
|
|
775
|
+
label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
|
|
776
|
+
] }, id)) })
|
|
777
|
+
] }),
|
|
778
|
+
hasOptions && /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-gray-500 dark:text-gray-400 px-1", children: usesButtons ? labels.previewModeButtons : labels.previewModeList })
|
|
779
|
+
] });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/flows/FlowNodePanel.tsx
|
|
783
|
+
import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
784
|
+
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";
|
|
785
|
+
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";
|
|
786
|
+
function truncateLabel(value, max = 60) {
|
|
787
|
+
if (!value) return "\u2014";
|
|
788
|
+
return value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
|
|
789
|
+
}
|
|
790
|
+
function WhatsAppTextField({
|
|
791
|
+
label,
|
|
792
|
+
value,
|
|
793
|
+
onValueChange,
|
|
794
|
+
options,
|
|
795
|
+
placeholder,
|
|
796
|
+
labels
|
|
797
|
+
}) {
|
|
798
|
+
return /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
|
|
799
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
800
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: label }),
|
|
801
|
+
/* @__PURE__ */ jsx9(
|
|
802
|
+
"textarea",
|
|
803
|
+
{
|
|
804
|
+
value,
|
|
805
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
806
|
+
rows: 3,
|
|
807
|
+
placeholder,
|
|
808
|
+
className: `w-full mt-1 ${INPUT_CLASSNAME}`
|
|
809
|
+
}
|
|
810
|
+
)
|
|
811
|
+
] }),
|
|
812
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
813
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.preview }),
|
|
814
|
+
/* @__PURE__ */ jsx9("div", { className: "mt-1", children: /* @__PURE__ */ jsx9(FlowWhatsAppPreview, { body: value, options, labels: labels.nodePanel }) })
|
|
815
|
+
] })
|
|
816
|
+
] });
|
|
817
|
+
}
|
|
818
|
+
function IssueRow({ issue }) {
|
|
819
|
+
const Icon = issue.severity === "error" ? AlertCircle2 : AlertTriangle2;
|
|
820
|
+
const color = issue.severity === "error" ? "text-red-600 dark:text-red-400" : "text-amber-600 dark:text-amber-400";
|
|
821
|
+
return /* @__PURE__ */ jsxs8("div", { className: `flex items-start gap-1.5 text-xs ${color}`, children: [
|
|
822
|
+
/* @__PURE__ */ jsx9(Icon, { size: 13, className: "mt-0.5 shrink-0" }),
|
|
823
|
+
/* @__PURE__ */ jsx9("span", { children: issue.message })
|
|
824
|
+
] });
|
|
825
|
+
}
|
|
826
|
+
function FlowNodePanel({
|
|
827
|
+
graph,
|
|
828
|
+
node,
|
|
829
|
+
issues,
|
|
830
|
+
otherFlows,
|
|
831
|
+
onClose,
|
|
832
|
+
onChange,
|
|
833
|
+
onDelete,
|
|
834
|
+
labels: labelsOverride
|
|
835
|
+
}) {
|
|
836
|
+
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
|
|
837
|
+
const [draft, setDraft] = useState2(node);
|
|
838
|
+
const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id);
|
|
839
|
+
const isFixedLogic = draft.type === "entrada_choice";
|
|
840
|
+
const isAction = draft.type === "action";
|
|
841
|
+
const isCondition = draft.type === "condition";
|
|
842
|
+
const isStart = graph.startNodeId === node.id;
|
|
843
|
+
const nodeIssues = issues.filter((i) => i.nodeId === node.id);
|
|
844
|
+
const knownContextKeys = [...new Set(Object.values(graph.nodes).map((n) => n.contextKey).filter((key) => !!key))];
|
|
845
|
+
const conditionAnswerIds = isCondition ? ["true", "false"] : (draft.options ?? []).map(([id]) => id);
|
|
846
|
+
function updateNextString(value) {
|
|
847
|
+
setDraft((prev) => ({ ...prev, next: value }));
|
|
848
|
+
}
|
|
849
|
+
function updateNextByAnswer(answerId, value) {
|
|
850
|
+
setDraft((prev) => {
|
|
851
|
+
const current = typeof prev.next === "object" && prev.next ? prev.next : { byAnswer: {}, default: otherNodeIds[0] ?? "" };
|
|
852
|
+
return { ...prev, next: { ...current, byAnswer: { ...current.byAnswer, [answerId]: value } } };
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
function updateNextDefault(value) {
|
|
856
|
+
setDraft((prev) => {
|
|
857
|
+
const current = typeof prev.next === "object" && prev.next ? prev.next : { byAnswer: {}, default: value };
|
|
858
|
+
return { ...prev, next: { ...current, default: value } };
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
function updateOption(index, field, value) {
|
|
862
|
+
setDraft((prev) => {
|
|
863
|
+
const opts = [...prev.options ?? []];
|
|
864
|
+
const opt = [...opts[index]];
|
|
865
|
+
opt[field] = value;
|
|
866
|
+
opts[index] = opt;
|
|
867
|
+
return { ...prev, options: opts };
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
function addOption() {
|
|
871
|
+
setDraft((prev) => ({ ...prev, options: [...prev.options ?? [], [String((prev.options?.length ?? 0) + 1), "Nova op\xE7\xE3o"]] }));
|
|
872
|
+
}
|
|
873
|
+
function removeOption(index) {
|
|
874
|
+
setDraft((prev) => ({ ...prev, options: (prev.options ?? []).filter((_, i) => i !== index) }));
|
|
875
|
+
}
|
|
876
|
+
function nextNodeOptions() {
|
|
877
|
+
return /* @__PURE__ */ jsxs8(Fragment, { children: [
|
|
878
|
+
otherNodeIds.map((id) => /* @__PURE__ */ jsx9("option", { value: id, children: truncateLabel(nodeLabel(graph.nodes[id], labels)) }, id)),
|
|
879
|
+
otherFlows.length > 0 && /* @__PURE__ */ jsx9("optgroup", { label: labels.nodePanel.otherFlowsGroup, children: otherFlows.map((flow) => /* @__PURE__ */ jsx9("option", { value: `${CROSS_FLOW_PREFIX}${flow.key}`, children: flow.label }, flow.key)) })
|
|
880
|
+
] });
|
|
881
|
+
}
|
|
882
|
+
return /* @__PURE__ */ jsxs8("div", { className: "fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col", children: [
|
|
883
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
|
|
884
|
+
/* @__PURE__ */ jsx9("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
|
|
885
|
+
/* @__PURE__ */ jsx9("button", { onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx9(X2, { size: 18 }) })
|
|
886
|
+
] }),
|
|
887
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
|
|
888
|
+
nodeIssues.length > 0 && /* @__PURE__ */ jsx9("div", { className: "rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-2.5 space-y-1.5", children: nodeIssues.map((issue, i) => /* @__PURE__ */ jsx9(IssueRow, { issue }, i)) }),
|
|
889
|
+
isFixedLogic && /* @__PURE__ */ jsx9("p", { className: "text-xs text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30 rounded-lg p-2", children: labels.nodePanel.fixedLogicNotice }),
|
|
890
|
+
isAction && /* @__PURE__ */ jsx9("p", { className: "text-xs text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/30 rounded-lg p-2", children: labels.nodePanel.actionNotice }),
|
|
891
|
+
isCondition && /* @__PURE__ */ jsx9("p", { className: "text-xs text-cyan-700 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/30 rounded-lg p-2", children: labels.nodePanel.conditionNotice }),
|
|
892
|
+
draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
|
|
893
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
|
|
894
|
+
/* @__PURE__ */ jsx9("input", { value: draft.contextKey, disabled: true, className: "w-full mt-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-3 py-2.5 text-sm text-gray-500" })
|
|
895
|
+
] }),
|
|
896
|
+
!isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs8("div", { children: [
|
|
897
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
|
|
898
|
+
/* @__PURE__ */ jsx9(
|
|
899
|
+
"select",
|
|
900
|
+
{
|
|
901
|
+
value: draft.questionType ?? "text",
|
|
902
|
+
onChange: (e) => setDraft((prev) => ({ ...prev, questionType: e.target.value })),
|
|
903
|
+
className: `w-full mt-1 ${SELECT_CLASSNAME}`,
|
|
904
|
+
children: Object.entries(labels.questionTypeLabels).map(([key, label]) => /* @__PURE__ */ jsx9("option", { value: key, children: label }, key))
|
|
905
|
+
}
|
|
906
|
+
)
|
|
907
|
+
] }),
|
|
908
|
+
!isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsx9(
|
|
909
|
+
WhatsAppTextField,
|
|
910
|
+
{
|
|
911
|
+
label: labels.nodePanel.question,
|
|
912
|
+
value: draft.question ?? "",
|
|
913
|
+
options: draft.questionType === "choice" ? draft.options : void 0,
|
|
914
|
+
onValueChange: (value) => setDraft((prev) => ({ ...prev, question: value })),
|
|
915
|
+
labels
|
|
916
|
+
}
|
|
917
|
+
),
|
|
918
|
+
isCondition && /* @__PURE__ */ jsxs8("div", { className: "space-y-3", children: [
|
|
919
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
920
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionVariable }),
|
|
921
|
+
/* @__PURE__ */ jsx9(
|
|
922
|
+
"input",
|
|
923
|
+
{
|
|
924
|
+
value: draft.conditionContextKey ?? "",
|
|
925
|
+
onChange: (e) => setDraft((prev) => ({ ...prev, conditionContextKey: e.target.value })),
|
|
926
|
+
list: "condition-context-keys",
|
|
927
|
+
className: `w-full mt-1 ${INPUT_CLASSNAME}`
|
|
928
|
+
}
|
|
929
|
+
),
|
|
930
|
+
/* @__PURE__ */ jsx9("datalist", { id: "condition-context-keys", children: knownContextKeys.map((key) => /* @__PURE__ */ jsx9("option", { value: key }, key)) })
|
|
931
|
+
] }),
|
|
932
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
933
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionOperator }),
|
|
934
|
+
/* @__PURE__ */ jsx9(
|
|
935
|
+
"select",
|
|
936
|
+
{
|
|
937
|
+
value: draft.conditionOperator ?? ">",
|
|
938
|
+
onChange: (e) => setDraft((prev) => ({ ...prev, conditionOperator: e.target.value })),
|
|
939
|
+
className: `w-full mt-1 ${SELECT_CLASSNAME}`,
|
|
940
|
+
children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx9("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
|
|
941
|
+
}
|
|
942
|
+
)
|
|
943
|
+
] }),
|
|
944
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
945
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.conditionValue }),
|
|
946
|
+
/* @__PURE__ */ jsx9(
|
|
947
|
+
"input",
|
|
948
|
+
{
|
|
949
|
+
value: draft.conditionValue ?? "",
|
|
950
|
+
onChange: (e) => setDraft((prev) => ({ ...prev, conditionValue: e.target.value })),
|
|
951
|
+
className: `w-full mt-1 ${INPUT_CLASSNAME}`
|
|
952
|
+
}
|
|
953
|
+
)
|
|
954
|
+
] })
|
|
955
|
+
] }),
|
|
956
|
+
isAction && draft.actionKind === "send_product_list" && /* @__PURE__ */ jsx9(
|
|
957
|
+
WhatsAppTextField,
|
|
958
|
+
{
|
|
959
|
+
label: labels.nodePanel.fallbackMessage,
|
|
960
|
+
value: draft.fallbackMessage ?? "",
|
|
961
|
+
onValueChange: (value) => setDraft((prev) => ({ ...prev, fallbackMessage: value })),
|
|
962
|
+
placeholder: "(usa a mensagem padr\xE3o de fallback se vazio)",
|
|
963
|
+
labels
|
|
964
|
+
}
|
|
965
|
+
),
|
|
966
|
+
isAction && draft.actionKind !== "send_product_list" && /* @__PURE__ */ jsx9(
|
|
967
|
+
WhatsAppTextField,
|
|
968
|
+
{
|
|
969
|
+
label: labels.nodePanel.directMessage,
|
|
970
|
+
value: draft.directMessage ?? "",
|
|
971
|
+
onValueChange: (value) => setDraft((prev) => ({ ...prev, directMessage: value })),
|
|
972
|
+
placeholder: "(usa a mensagem padr\xE3o de encaminhamento se vazio)",
|
|
973
|
+
labels
|
|
974
|
+
}
|
|
975
|
+
),
|
|
976
|
+
(draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs8("div", { children: [
|
|
977
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between mb-1.5", children: [
|
|
978
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
|
|
979
|
+
/* @__PURE__ */ jsxs8("button", { onClick: addOption, className: "text-xs text-blue-600 hover:underline flex items-center gap-1", children: [
|
|
980
|
+
/* @__PURE__ */ jsx9(Plus2, { size: 12 }),
|
|
981
|
+
" ",
|
|
982
|
+
labels.nodePanel.addOption
|
|
983
|
+
] })
|
|
984
|
+
] }),
|
|
985
|
+
/* @__PURE__ */ jsx9("div", { className: "space-y-2", children: (draft.options ?? []).map(([id, label], i) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
|
|
986
|
+
/* @__PURE__ */ jsx9(
|
|
987
|
+
"input",
|
|
988
|
+
{
|
|
989
|
+
value: id,
|
|
990
|
+
onChange: (e) => updateOption(i, 0, e.target.value),
|
|
991
|
+
placeholder: labels.nodePanel.optionId,
|
|
992
|
+
className: `w-16 ${INPUT_CLASSNAME}`
|
|
993
|
+
}
|
|
994
|
+
),
|
|
995
|
+
/* @__PURE__ */ jsx9(
|
|
996
|
+
"input",
|
|
997
|
+
{
|
|
998
|
+
value: label,
|
|
999
|
+
onChange: (e) => updateOption(i, 1, e.target.value),
|
|
1000
|
+
placeholder: labels.nodePanel.optionLabel,
|
|
1001
|
+
className: `flex-1 ${INPUT_CLASSNAME}`
|
|
1002
|
+
}
|
|
1003
|
+
),
|
|
1004
|
+
/* @__PURE__ */ jsx9("button", { onClick: () => removeOption(i), className: "text-gray-400 hover:text-red-600", children: /* @__PURE__ */ jsx9(Trash2, { size: 14 }) })
|
|
1005
|
+
] }, i)) })
|
|
1006
|
+
] }),
|
|
1007
|
+
!isAction && /* @__PURE__ */ jsxs8("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
|
|
1008
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.next }),
|
|
1009
|
+
/* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mb-1", children: labels.nodePanel.nextHint }),
|
|
1010
|
+
typeof draft.next !== "object" && !isCondition ? /* @__PURE__ */ jsxs8(
|
|
1011
|
+
"select",
|
|
1012
|
+
{
|
|
1013
|
+
value: typeof draft.next === "string" ? draft.next : "",
|
|
1014
|
+
onChange: (e) => updateNextString(e.target.value),
|
|
1015
|
+
className: `w-full mt-1 ${SELECT_CLASSNAME}`,
|
|
1016
|
+
children: [
|
|
1017
|
+
/* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
|
|
1018
|
+
nextNodeOptions()
|
|
1019
|
+
]
|
|
1020
|
+
}
|
|
1021
|
+
) : /* @__PURE__ */ jsxs8("div", { className: "space-y-2 mt-1", children: [
|
|
1022
|
+
conditionAnswerIds.map((id) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
|
|
1023
|
+
/* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? id === "true" ? labels.nodePanel.conditionTrue : labels.nodePanel.conditionFalse : labels.nodePanel.nextByAnswer(id) }),
|
|
1024
|
+
/* @__PURE__ */ jsxs8(
|
|
1025
|
+
"select",
|
|
1026
|
+
{
|
|
1027
|
+
value: draft.next && typeof draft.next === "object" ? draft.next.byAnswer[id] ?? "" : "",
|
|
1028
|
+
onChange: (e) => updateNextByAnswer(id, e.target.value),
|
|
1029
|
+
className: `flex-1 ${SELECT_CLASSNAME}`,
|
|
1030
|
+
children: [
|
|
1031
|
+
/* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
|
|
1032
|
+
nextNodeOptions()
|
|
1033
|
+
]
|
|
1034
|
+
}
|
|
1035
|
+
)
|
|
1036
|
+
] }, id)),
|
|
1037
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
|
|
1038
|
+
/* @__PURE__ */ jsx9("span", { className: "text-xs text-gray-500 w-32 shrink-0", children: isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault }),
|
|
1039
|
+
/* @__PURE__ */ jsxs8(
|
|
1040
|
+
"select",
|
|
1041
|
+
{
|
|
1042
|
+
value: draft.next && typeof draft.next === "object" ? draft.next.default : "",
|
|
1043
|
+
onChange: (e) => updateNextDefault(e.target.value),
|
|
1044
|
+
className: `flex-1 ${SELECT_CLASSNAME}`,
|
|
1045
|
+
children: [
|
|
1046
|
+
/* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
|
|
1047
|
+
nextNodeOptions()
|
|
1048
|
+
]
|
|
1049
|
+
}
|
|
1050
|
+
)
|
|
1051
|
+
] })
|
|
1052
|
+
] })
|
|
1053
|
+
] })
|
|
1054
|
+
] }),
|
|
1055
|
+
/* @__PURE__ */ jsxs8("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
|
|
1056
|
+
/* @__PURE__ */ jsxs8("button", { onClick: () => onChange(draft), className: "flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700", children: [
|
|
1057
|
+
/* @__PURE__ */ jsx9(Save, { size: 14 }),
|
|
1058
|
+
" ",
|
|
1059
|
+
labels.nodePanel.save
|
|
1060
|
+
] }),
|
|
1061
|
+
!isStart && /* @__PURE__ */ jsx9(
|
|
1062
|
+
"button",
|
|
1063
|
+
{
|
|
1064
|
+
onClick: () => {
|
|
1065
|
+
if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id);
|
|
1066
|
+
},
|
|
1067
|
+
title: labels.nodePanel.delete,
|
|
1068
|
+
className: "px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg",
|
|
1069
|
+
children: /* @__PURE__ */ jsx9(Trash2, { size: 14 })
|
|
1070
|
+
}
|
|
1071
|
+
),
|
|
1072
|
+
/* @__PURE__ */ jsx9("button", { onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
|
|
1073
|
+
] })
|
|
1074
|
+
] });
|
|
1075
|
+
}
|
|
1076
|
+
export {
|
|
1077
|
+
BUILT_IN_ACTION_KINDS,
|
|
1078
|
+
CONDITION_OPERATORS,
|
|
1079
|
+
CROSS_FLOW_PREFIX,
|
|
1080
|
+
DEFAULT_FLOW_EDITOR_LABELS,
|
|
1081
|
+
FlowGroupFrame,
|
|
1082
|
+
FlowGroupHeader,
|
|
1083
|
+
FlowMapCanvas,
|
|
1084
|
+
FlowMapNode,
|
|
1085
|
+
FlowNodeCard,
|
|
1086
|
+
FlowNodePanel,
|
|
1087
|
+
FlowPalette,
|
|
1088
|
+
FlowPortalNode,
|
|
1089
|
+
FlowWhatsAppPreview,
|
|
1090
|
+
NODE_CARD_WIDTH,
|
|
1091
|
+
WHATSAPP_LIMITS,
|
|
1092
|
+
computeAutoLayout,
|
|
1093
|
+
computeFlowMapLayout,
|
|
1094
|
+
crossFlowKey,
|
|
1095
|
+
crossFlowTargetsOf,
|
|
1096
|
+
estimateNodeHeight,
|
|
1097
|
+
findCollectionChains,
|
|
1098
|
+
flowGroupFrameNodeTypes,
|
|
1099
|
+
flowGroupHeaderNodeTypes,
|
|
1100
|
+
flowMapNodeTypes,
|
|
1101
|
+
flowNodeTypes,
|
|
1102
|
+
flowPortalNodeTypes,
|
|
1103
|
+
isCrossFlowTarget,
|
|
1104
|
+
mergeFlowEditorLabels,
|
|
1105
|
+
nodeLabel,
|
|
1106
|
+
rendersAsButtons,
|
|
1107
|
+
slugifyNodeId,
|
|
1108
|
+
targetsOf,
|
|
1109
|
+
validateGraph
|
|
1110
|
+
};
|