@adatechnology/conversations-ui 0.1.0-rc.18 → 0.1.0-rc.19
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/flows/index.d.ts +14 -2
- package/dist/flows/index.js +114 -36
- package/package.json +2 -2
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +3 -1
- package/src/flows/FlowNodeCard.tsx +22 -4
- package/src/flows/FlowNodePanel.tsx +132 -35
- package/src/flows/FlowPalette.tsx +6 -2
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/flowGraph.ts +5 -5
- package/src/flows/labels.ts +5 -0
package/dist/flows/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
import { NodeProps } from '@xyflow/react';
|
|
3
4
|
import { LucideIcon } from 'lucide-react';
|
|
4
5
|
import { FlowConditionOperator, FlowGraphData, FlowNodeData, FlowNodeType, FlowQuestionType, FlowActionKind } from '@adatechnology/meta-whatsapp-contracts';
|
|
@@ -9,6 +10,7 @@ declare const BUILT_IN_ACTION_KINDS: {
|
|
|
9
10
|
readonly HANDOFF: "handoff";
|
|
10
11
|
readonly RATE_LIMITED_HANDOFF: "rate_limited_handoff";
|
|
11
12
|
readonly SEND_PRODUCT_LIST: "send_product_list";
|
|
13
|
+
readonly SEND_MEDIA: "send_media";
|
|
12
14
|
};
|
|
13
15
|
declare const CROSS_FLOW_PREFIX = "flow:";
|
|
14
16
|
declare const isCrossFlowTarget: (target: string) => boolean;
|
|
@@ -109,6 +111,8 @@ interface FlowEditorLabels {
|
|
|
109
111
|
conditionTrue: string;
|
|
110
112
|
conditionFalse: string;
|
|
111
113
|
conditionVariableMissing: string;
|
|
114
|
+
media: string;
|
|
115
|
+
mediaUnavailable: string;
|
|
112
116
|
};
|
|
113
117
|
palette: {
|
|
114
118
|
title: string;
|
|
@@ -233,14 +237,22 @@ interface FlowNodePanelProps {
|
|
|
233
237
|
onChange: (updated: FlowNodeData) => void;
|
|
234
238
|
onDelete: (nodeId: string) => void;
|
|
235
239
|
labels?: Partial<FlowEditorLabels>;
|
|
240
|
+
/**
|
|
241
|
+
* Seletor de arquivos do nó `send_media`, renderizado no lugar da mensagem direta.
|
|
242
|
+
*
|
|
243
|
+
* Slot, e não uma lista de arquivos por prop, porque a biblioteca é do host: upload, permissão e
|
|
244
|
+
* URL assinada são dele, e o painel não tem como buscar nada. Ausente, o nó continua editável —
|
|
245
|
+
* só não dá para anexar por aqui.
|
|
246
|
+
*/
|
|
247
|
+
renderMediaPicker?: (node: FlowNodeData) => ReactNode;
|
|
236
248
|
}
|
|
237
|
-
declare function FlowNodePanel({ graph, node, issues, otherFlows, onClose, onChange, onDelete, labels: labelsOverride, }: FlowNodePanelProps): react.JSX.Element;
|
|
249
|
+
declare function FlowNodePanel({ graph, node, issues, otherFlows, onClose, onChange, onDelete, labels: labelsOverride, renderMediaPicker, }: FlowNodePanelProps): react.JSX.Element;
|
|
238
250
|
|
|
239
251
|
interface FlowWhatsAppPreviewProps {
|
|
240
252
|
body: string;
|
|
241
253
|
options?: [string, string][];
|
|
242
254
|
labels?: FlowEditorLabels['nodePanel'];
|
|
243
255
|
}
|
|
244
|
-
declare function FlowWhatsAppPreview({ body, options, labels }: FlowWhatsAppPreviewProps): react.JSX.Element;
|
|
256
|
+
declare function FlowWhatsAppPreview({ body, options, labels, }: FlowWhatsAppPreviewProps): react.JSX.Element;
|
|
245
257
|
|
|
246
258
|
export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, DEFAULT_FLOW_EDITOR_LABELS, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, WHATSAPP_LIMITS, computeAutoLayout, computeFlowMapLayout, crossFlowKey, crossFlowTargetsOf, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, mergeFlowEditorLabels, nodeLabel, rendersAsButtons, slugifyNodeId, targetsOf, validateGraph };
|
package/dist/flows/index.js
CHANGED
|
@@ -5,7 +5,18 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/flows/FlowNodeCard.tsx
|
|
7
7
|
import { Handle, Position } from "@xyflow/react";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
MessageCircleQuestion,
|
|
10
|
+
GitBranch,
|
|
11
|
+
Zap,
|
|
12
|
+
ListTree,
|
|
13
|
+
Diamond,
|
|
14
|
+
AlertTriangle,
|
|
15
|
+
AlertCircle,
|
|
16
|
+
Headset,
|
|
17
|
+
Clock3,
|
|
18
|
+
ShoppingBag
|
|
19
|
+
} from "lucide-react";
|
|
9
20
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
21
|
var NODE_TYPE_COLOR = {
|
|
11
22
|
question: "border-blue-300 bg-blue-50 dark:bg-blue-950/40 dark:border-blue-800",
|
|
@@ -57,7 +68,13 @@ function sourceRows(node, labels) {
|
|
|
57
68
|
}
|
|
58
69
|
function SourceRow({ label, isDefault, handleId }) {
|
|
59
70
|
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(
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
"span",
|
|
73
|
+
{
|
|
74
|
+
className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`,
|
|
75
|
+
children: label
|
|
76
|
+
}
|
|
77
|
+
),
|
|
61
78
|
/* @__PURE__ */ jsx(
|
|
62
79
|
Handle,
|
|
63
80
|
{
|
|
@@ -132,7 +149,8 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
132
149
|
actionKindLabels: {
|
|
133
150
|
handoff: "Encaminhar para atendimento",
|
|
134
151
|
rate_limited_handoff: "Encaminhar (limite de simula\xE7\xF5es atingido)",
|
|
135
|
-
send_product_list: "Enviar cat\xE1logo de produtos"
|
|
152
|
+
send_product_list: "Enviar cat\xE1logo de produtos",
|
|
153
|
+
send_media: "Enviar arquivos da biblioteca"
|
|
136
154
|
},
|
|
137
155
|
conditionOperatorLabels: {
|
|
138
156
|
">": "maior que",
|
|
@@ -187,7 +205,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
187
205
|
conditionValue: "Valor de compara\xE7\xE3o",
|
|
188
206
|
conditionTrue: "Se verdadeiro \u2192",
|
|
189
207
|
conditionFalse: "Se falso \u2192",
|
|
190
|
-
conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192"
|
|
208
|
+
conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192",
|
|
209
|
+
media: "Arquivos enviados neste ponto",
|
|
210
|
+
mediaUnavailable: "A biblioteca de arquivos n\xE3o est\xE1 dispon\xEDvel neste painel."
|
|
191
211
|
},
|
|
192
212
|
palette: {
|
|
193
213
|
title: "Adicionar ao fluxo",
|
|
@@ -266,12 +286,9 @@ import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
|
|
|
266
286
|
import "@xyflow/react/dist/style.css";
|
|
267
287
|
|
|
268
288
|
// src/flows/flowGraph.ts
|
|
289
|
+
import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
|
|
269
290
|
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
|
-
};
|
|
291
|
+
var BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND;
|
|
275
292
|
var CROSS_FLOW_PREFIX = "flow:";
|
|
276
293
|
var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
|
|
277
294
|
var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
|
|
@@ -527,18 +544,20 @@ function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverride })
|
|
|
527
544
|
const isDark = useIsDarkTheme();
|
|
528
545
|
const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey]);
|
|
529
546
|
const nodes = useMemo(
|
|
530
|
-
() => Object.values(graphs).map(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
547
|
+
() => Object.values(graphs).map(
|
|
548
|
+
(g) => ({
|
|
549
|
+
id: g.key,
|
|
550
|
+
type: "flowMapNode",
|
|
551
|
+
position: positions[g.key] ?? { x: 0, y: 0 },
|
|
552
|
+
data: {
|
|
553
|
+
label: g.label,
|
|
554
|
+
nodeCount: Object.keys(g.nodes).length,
|
|
555
|
+
isRoot: g.key === rootKey,
|
|
556
|
+
labels,
|
|
557
|
+
onOpen: () => onOpenFlow(g.key)
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
),
|
|
542
561
|
[graphs, positions, rootKey, onOpenFlow, labels]
|
|
543
562
|
);
|
|
544
563
|
const edges = useMemo(() => {
|
|
@@ -590,8 +609,26 @@ function FlowGroupHeader({ data }) {
|
|
|
590
609
|
const { label, labels = DEFAULT_FLOW_EDITOR_LABELS, onFocus, onClose } = data;
|
|
591
610
|
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
611
|
/* @__PURE__ */ jsx5("span", { children: label }),
|
|
593
|
-
/* @__PURE__ */ jsx5(
|
|
594
|
-
|
|
612
|
+
/* @__PURE__ */ jsx5(
|
|
613
|
+
"button",
|
|
614
|
+
{
|
|
615
|
+
type: "button",
|
|
616
|
+
onClick: onFocus,
|
|
617
|
+
title: labels.flowGroup.focus,
|
|
618
|
+
className: "hover:text-blue-600 dark:hover:text-blue-400",
|
|
619
|
+
children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 })
|
|
620
|
+
}
|
|
621
|
+
),
|
|
622
|
+
/* @__PURE__ */ jsx5(
|
|
623
|
+
"button",
|
|
624
|
+
{
|
|
625
|
+
type: "button",
|
|
626
|
+
onClick: onClose,
|
|
627
|
+
title: labels.flowGroup.close,
|
|
628
|
+
className: "hover:text-red-600 dark:hover:text-red-400",
|
|
629
|
+
children: /* @__PURE__ */ jsx5(X, { size: 12 })
|
|
630
|
+
}
|
|
631
|
+
)
|
|
595
632
|
] });
|
|
596
633
|
}
|
|
597
634
|
var flowGroupHeaderNodeTypes = { flowGroupHeader: FlowGroupHeader };
|
|
@@ -753,7 +790,11 @@ import { Plus as Plus2, Trash2, Save, X as X2, AlertTriangle as AlertTriangle2,
|
|
|
753
790
|
// src/flows/FlowWhatsAppPreview.tsx
|
|
754
791
|
import { List } from "lucide-react";
|
|
755
792
|
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
756
|
-
function FlowWhatsAppPreview({
|
|
793
|
+
function FlowWhatsAppPreview({
|
|
794
|
+
body,
|
|
795
|
+
options,
|
|
796
|
+
labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel
|
|
797
|
+
}) {
|
|
757
798
|
if (!body && (!options || options.length === 0)) {
|
|
758
799
|
return /* @__PURE__ */ jsx8("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
|
|
759
800
|
}
|
|
@@ -769,7 +810,14 @@ function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITOR_LABEL
|
|
|
769
810
|
labels.previewListButton
|
|
770
811
|
] }) })
|
|
771
812
|
] }),
|
|
772
|
-
hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8(
|
|
813
|
+
hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8(
|
|
814
|
+
"div",
|
|
815
|
+
{
|
|
816
|
+
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",
|
|
817
|
+
children: label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
|
|
818
|
+
},
|
|
819
|
+
id
|
|
820
|
+
)) }),
|
|
773
821
|
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
822
|
/* @__PURE__ */ jsx8("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
|
|
775
823
|
label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
|
|
@@ -831,17 +879,23 @@ function FlowNodePanel({
|
|
|
831
879
|
onClose,
|
|
832
880
|
onChange,
|
|
833
881
|
onDelete,
|
|
834
|
-
labels: labelsOverride
|
|
882
|
+
labels: labelsOverride,
|
|
883
|
+
renderMediaPicker
|
|
835
884
|
}) {
|
|
836
885
|
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
|
|
837
886
|
const [draft, setDraft] = useState2(node);
|
|
838
887
|
const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id);
|
|
839
888
|
const isFixedLogic = draft.type === "entrada_choice";
|
|
840
889
|
const isAction = draft.type === "action";
|
|
890
|
+
const isSendMedia = isAction && draft.actionKind === BUILT_IN_ACTION_KINDS.SEND_MEDIA;
|
|
841
891
|
const isCondition = draft.type === "condition";
|
|
842
892
|
const isStart = graph.startNodeId === node.id;
|
|
843
893
|
const nodeIssues = issues.filter((i) => i.nodeId === node.id);
|
|
844
|
-
const knownContextKeys = [
|
|
894
|
+
const knownContextKeys = [
|
|
895
|
+
...new Set(
|
|
896
|
+
Object.values(graph.nodes).map((n) => n.contextKey).filter((key) => !!key)
|
|
897
|
+
)
|
|
898
|
+
];
|
|
845
899
|
const conditionAnswerIds = isCondition ? ["true", "false"] : (draft.options ?? []).map(([id]) => id);
|
|
846
900
|
function updateNextString(value) {
|
|
847
901
|
setDraft((prev) => ({ ...prev, next: value }));
|
|
@@ -868,7 +922,10 @@ function FlowNodePanel({
|
|
|
868
922
|
});
|
|
869
923
|
}
|
|
870
924
|
function addOption() {
|
|
871
|
-
setDraft((prev) => ({
|
|
925
|
+
setDraft((prev) => ({
|
|
926
|
+
...prev,
|
|
927
|
+
options: [...prev.options ?? [], [String((prev.options?.length ?? 0) + 1), "Nova op\xE7\xE3o"]]
|
|
928
|
+
}));
|
|
872
929
|
}
|
|
873
930
|
function removeOption(index) {
|
|
874
931
|
setDraft((prev) => ({ ...prev, options: (prev.options ?? []).filter((_, i) => i !== index) }));
|
|
@@ -891,7 +948,14 @@ function FlowNodePanel({
|
|
|
891
948
|
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
949
|
draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
|
|
893
950
|
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
|
|
894
|
-
/* @__PURE__ */ jsx9(
|
|
951
|
+
/* @__PURE__ */ jsx9(
|
|
952
|
+
"input",
|
|
953
|
+
{
|
|
954
|
+
value: draft.contextKey,
|
|
955
|
+
disabled: true,
|
|
956
|
+
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"
|
|
957
|
+
}
|
|
958
|
+
)
|
|
895
959
|
] }),
|
|
896
960
|
!isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs8("div", { children: [
|
|
897
961
|
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
|
|
@@ -935,7 +999,10 @@ function FlowNodePanel({
|
|
|
935
999
|
"select",
|
|
936
1000
|
{
|
|
937
1001
|
value: draft.conditionOperator ?? ">",
|
|
938
|
-
onChange: (e) => setDraft((prev) => ({
|
|
1002
|
+
onChange: (e) => setDraft((prev) => ({
|
|
1003
|
+
...prev,
|
|
1004
|
+
conditionOperator: e.target.value
|
|
1005
|
+
})),
|
|
939
1006
|
className: `w-full mt-1 ${SELECT_CLASSNAME}`,
|
|
940
1007
|
children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx9("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
|
|
941
1008
|
}
|
|
@@ -963,7 +1030,11 @@ function FlowNodePanel({
|
|
|
963
1030
|
labels
|
|
964
1031
|
}
|
|
965
1032
|
),
|
|
966
|
-
|
|
1033
|
+
isSendMedia && /* @__PURE__ */ jsxs8("div", { children: [
|
|
1034
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.media }),
|
|
1035
|
+
/* @__PURE__ */ jsx9("div", { className: "mt-1", children: renderMediaPicker?.(node) ?? /* @__PURE__ */ jsx9("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
|
|
1036
|
+
] }),
|
|
1037
|
+
isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx9(
|
|
967
1038
|
WhatsAppTextField,
|
|
968
1039
|
{
|
|
969
1040
|
label: labels.nodePanel.directMessage,
|
|
@@ -1053,11 +1124,18 @@ function FlowNodePanel({
|
|
|
1053
1124
|
] })
|
|
1054
1125
|
] }),
|
|
1055
1126
|
/* @__PURE__ */ jsxs8("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
|
|
1056
|
-
/* @__PURE__ */ jsxs8(
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1127
|
+
/* @__PURE__ */ jsxs8(
|
|
1128
|
+
"button",
|
|
1129
|
+
{
|
|
1130
|
+
onClick: () => onChange(draft),
|
|
1131
|
+
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",
|
|
1132
|
+
children: [
|
|
1133
|
+
/* @__PURE__ */ jsx9(Save, { size: 14 }),
|
|
1134
|
+
" ",
|
|
1135
|
+
labels.nodePanel.save
|
|
1136
|
+
]
|
|
1137
|
+
}
|
|
1138
|
+
),
|
|
1061
1139
|
!isStart && /* @__PURE__ */ jsx9(
|
|
1062
1140
|
"button",
|
|
1063
1141
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.19",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"lucide-react": "^1.21.0",
|
|
33
33
|
"tailwind-merge": "^3.6.0",
|
|
34
|
-
"@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.
|
|
34
|
+
"@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.7"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": "^18 || ^19",
|
|
@@ -18,10 +18,20 @@ export function FlowGroupHeader({ data }: NodeProps) {
|
|
|
18
18
|
return (
|
|
19
19
|
<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">
|
|
20
20
|
<span>{label}</span>
|
|
21
|
-
<button
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onFocus}
|
|
24
|
+
title={labels.flowGroup.focus}
|
|
25
|
+
className="hover:text-blue-600 dark:hover:text-blue-400"
|
|
26
|
+
>
|
|
22
27
|
<Maximize2 size={12} />
|
|
23
28
|
</button>
|
|
24
|
-
<button
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={onClose}
|
|
32
|
+
title={labels.flowGroup.close}
|
|
33
|
+
className="hover:text-red-600 dark:hover:text-red-400"
|
|
34
|
+
>
|
|
25
35
|
<X size={12} />
|
|
26
36
|
</button>
|
|
27
37
|
</div>
|
|
@@ -27,18 +27,21 @@ export function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverr
|
|
|
27
27
|
const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey])
|
|
28
28
|
|
|
29
29
|
const nodes = useMemo<Node[]>(
|
|
30
|
-
() =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
() =>
|
|
31
|
+
Object.values(graphs).map(
|
|
32
|
+
(g): Node => ({
|
|
33
|
+
id: g.key,
|
|
34
|
+
type: 'flowMapNode',
|
|
35
|
+
position: positions[g.key] ?? { x: 0, y: 0 },
|
|
36
|
+
data: {
|
|
37
|
+
label: g.label,
|
|
38
|
+
nodeCount: Object.keys(g.nodes).length,
|
|
39
|
+
isRoot: g.key === rootKey,
|
|
40
|
+
labels,
|
|
41
|
+
onOpen: () => onOpenFlow(g.key),
|
|
42
|
+
} satisfies FlowMapNodeData,
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
42
45
|
[graphs, positions, rootKey, onOpenFlow, labels],
|
|
43
46
|
)
|
|
44
47
|
|
|
@@ -21,7 +21,9 @@ export function FlowMapNode({ data }: NodeProps) {
|
|
|
21
21
|
<div className="flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300">
|
|
22
22
|
<GitBranch size={14} />
|
|
23
23
|
<span className="text-sm font-semibold truncate">{label}</span>
|
|
24
|
-
{isRoot &&
|
|
24
|
+
{isRoot && (
|
|
25
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0" title={labels.startNodeTooltip} />
|
|
26
|
+
)}
|
|
25
27
|
</div>
|
|
26
28
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{labels.flowMap.nodeCount(nodeCount)}</p>
|
|
27
29
|
<button
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { Handle, Position, type NodeProps } from '@xyflow/react'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
MessageCircleQuestion,
|
|
4
|
+
GitBranch,
|
|
5
|
+
Zap,
|
|
6
|
+
ListTree,
|
|
7
|
+
Diamond,
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
AlertCircle,
|
|
10
|
+
Headset,
|
|
11
|
+
Clock3,
|
|
12
|
+
ShoppingBag,
|
|
13
|
+
type LucideIcon,
|
|
14
|
+
} from 'lucide-react'
|
|
3
15
|
import type { FlowEditorLabels } from './labels'
|
|
4
16
|
import type { FlowNodeData, GraphIssue } from './flowGraph'
|
|
5
17
|
|
|
@@ -83,7 +95,9 @@ function sourceRows(node: FlowNodeData, labels: FlowEditorLabels): { id: string;
|
|
|
83
95
|
function SourceRow({ label, isDefault, handleId }: { label: string; isDefault: boolean; handleId: string }) {
|
|
84
96
|
return (
|
|
85
97
|
<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">
|
|
86
|
-
<span
|
|
98
|
+
<span
|
|
99
|
+
className={`text-xs truncate flex-1 ${isDefault ? 'italic text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-200'}`}
|
|
100
|
+
>
|
|
87
101
|
{label}
|
|
88
102
|
</span>
|
|
89
103
|
<Handle
|
|
@@ -98,10 +112,14 @@ function SourceRow({ label, isDefault, handleId }: { label: string; isDefault: b
|
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
export function FlowNodeCard({ data }: NodeProps) {
|
|
101
|
-
const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } =
|
|
115
|
+
const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } =
|
|
116
|
+
data as unknown as FlowNodeCardData
|
|
102
117
|
const label = nodeLabel(node, labels)
|
|
103
118
|
const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons }
|
|
104
|
-
const Icon =
|
|
119
|
+
const Icon =
|
|
120
|
+
node.type === 'action' && node.actionKind
|
|
121
|
+
? (iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type])
|
|
122
|
+
: NODE_TYPE_ICON[node.type]
|
|
105
123
|
const rows = sourceRows(node, labels)
|
|
106
124
|
const hasError = issues.some((i) => i.severity === 'error')
|
|
107
125
|
const hasWarning = !hasError && issues.some((i) => i.severity === 'warning')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
2
|
import { Plus, Trash2, Save, X, AlertTriangle, AlertCircle } from 'lucide-react'
|
|
3
3
|
import { FlowWhatsAppPreview } from './FlowWhatsAppPreview'
|
|
4
4
|
import { nodeLabel } from './FlowNodeCard'
|
|
5
|
-
import { CROSS_FLOW_PREFIX, CONDITION_OPERATORS } from './flowGraph'
|
|
5
|
+
import { CROSS_FLOW_PREFIX, CONDITION_OPERATORS, BUILT_IN_ACTION_KINDS } from './flowGraph'
|
|
6
6
|
import { DEFAULT_FLOW_EDITOR_LABELS, type FlowEditorLabels } from './labels'
|
|
7
7
|
import type { FlowGraphData, FlowNodeData, GraphIssue } from './flowGraph'
|
|
8
8
|
|
|
@@ -73,6 +73,14 @@ export interface FlowNodePanelProps {
|
|
|
73
73
|
onChange: (updated: FlowNodeData) => void
|
|
74
74
|
onDelete: (nodeId: string) => void
|
|
75
75
|
labels?: Partial<FlowEditorLabels>
|
|
76
|
+
/**
|
|
77
|
+
* Seletor de arquivos do nó `send_media`, renderizado no lugar da mensagem direta.
|
|
78
|
+
*
|
|
79
|
+
* Slot, e não uma lista de arquivos por prop, porque a biblioteca é do host: upload, permissão e
|
|
80
|
+
* URL assinada são dele, e o painel não tem como buscar nada. Ausente, o nó continua editável —
|
|
81
|
+
* só não dá para anexar por aqui.
|
|
82
|
+
*/
|
|
83
|
+
renderMediaPicker?: (node: FlowNodeData) => ReactNode
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
// Paridade com financiamento-imobiliario-bot/apps/web/src/components/flows/FlowNodePanel.tsx —
|
|
@@ -86,18 +94,26 @@ export function FlowNodePanel({
|
|
|
86
94
|
onChange,
|
|
87
95
|
onDelete,
|
|
88
96
|
labels: labelsOverride,
|
|
97
|
+
renderMediaPicker,
|
|
89
98
|
}: FlowNodePanelProps) {
|
|
90
99
|
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride }
|
|
91
100
|
const [draft, setDraft] = useState<FlowNodeData>(node)
|
|
92
101
|
const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id)
|
|
93
102
|
const isFixedLogic = draft.type === 'entrada_choice'
|
|
94
103
|
const isAction = draft.type === 'action'
|
|
104
|
+
const isSendMedia = isAction && draft.actionKind === BUILT_IN_ACTION_KINDS.SEND_MEDIA
|
|
95
105
|
const isCondition = draft.type === 'condition'
|
|
96
106
|
const isStart = graph.startNodeId === node.id
|
|
97
107
|
const nodeIssues = issues.filter((i) => i.nodeId === node.id)
|
|
98
108
|
// Chaves já usadas por perguntas deste fluxo — sugestão pro campo de variável da condição,
|
|
99
109
|
// sem travar em texto livre (a variável pode ter vindo de outro fluxo ou de um cálculo derivado).
|
|
100
|
-
const knownContextKeys = [
|
|
110
|
+
const knownContextKeys = [
|
|
111
|
+
...new Set(
|
|
112
|
+
Object.values(graph.nodes)
|
|
113
|
+
.map((n) => n.contextKey)
|
|
114
|
+
.filter((key): key is string => !!key),
|
|
115
|
+
),
|
|
116
|
+
]
|
|
101
117
|
const conditionAnswerIds = isCondition ? ['true', 'false'] : (draft.options ?? []).map(([id]) => id)
|
|
102
118
|
|
|
103
119
|
function updateNextString(value: string) {
|
|
@@ -106,7 +122,8 @@ export function FlowNodePanel({
|
|
|
106
122
|
|
|
107
123
|
function updateNextByAnswer(answerId: string, value: string) {
|
|
108
124
|
setDraft((prev) => {
|
|
109
|
-
const current =
|
|
125
|
+
const current =
|
|
126
|
+
typeof prev.next === 'object' && prev.next ? prev.next : { byAnswer: {}, default: otherNodeIds[0] ?? '' }
|
|
110
127
|
return { ...prev, next: { ...current, byAnswer: { ...current.byAnswer, [answerId]: value } } }
|
|
111
128
|
})
|
|
112
129
|
}
|
|
@@ -129,7 +146,10 @@ export function FlowNodePanel({
|
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
function addOption() {
|
|
132
|
-
setDraft((prev) => ({
|
|
149
|
+
setDraft((prev) => ({
|
|
150
|
+
...prev,
|
|
151
|
+
options: [...(prev.options ?? []), [String((prev.options?.length ?? 0) + 1), 'Nova opção']],
|
|
152
|
+
}))
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
function removeOption(index: number) {
|
|
@@ -140,12 +160,16 @@ export function FlowNodePanel({
|
|
|
140
160
|
return (
|
|
141
161
|
<>
|
|
142
162
|
{otherNodeIds.map((id) => (
|
|
143
|
-
<option key={id} value={id}>
|
|
163
|
+
<option key={id} value={id}>
|
|
164
|
+
{truncateLabel(nodeLabel(graph.nodes[id], labels))}
|
|
165
|
+
</option>
|
|
144
166
|
))}
|
|
145
167
|
{otherFlows.length > 0 && (
|
|
146
168
|
<optgroup label={labels.nodePanel.otherFlowsGroup}>
|
|
147
169
|
{otherFlows.map((flow) => (
|
|
148
|
-
<option key={flow.key} value={`${CROSS_FLOW_PREFIX}${flow.key}`}>
|
|
170
|
+
<option key={flow.key} value={`${CROSS_FLOW_PREFIX}${flow.key}`}>
|
|
171
|
+
{flow.label}
|
|
172
|
+
</option>
|
|
149
173
|
))}
|
|
150
174
|
</optgroup>
|
|
151
175
|
)}
|
|
@@ -157,13 +181,17 @@ export function FlowNodePanel({
|
|
|
157
181
|
<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">
|
|
158
182
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700">
|
|
159
183
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{labels.nodePanel.title}</h3>
|
|
160
|
-
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"
|
|
184
|
+
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
185
|
+
<X size={18} />
|
|
186
|
+
</button>
|
|
161
187
|
</div>
|
|
162
188
|
|
|
163
189
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
164
190
|
{nodeIssues.length > 0 && (
|
|
165
191
|
<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">
|
|
166
|
-
{nodeIssues.map((issue, i) =>
|
|
192
|
+
{nodeIssues.map((issue, i) => (
|
|
193
|
+
<IssueRow key={i} issue={issue} />
|
|
194
|
+
))}
|
|
167
195
|
</div>
|
|
168
196
|
)}
|
|
169
197
|
|
|
@@ -185,21 +213,33 @@ export function FlowNodePanel({
|
|
|
185
213
|
|
|
186
214
|
{draft.contextKey && (
|
|
187
215
|
<div>
|
|
188
|
-
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
189
|
-
|
|
216
|
+
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
217
|
+
{labels.nodePanel.contextKey}
|
|
218
|
+
</label>
|
|
219
|
+
<input
|
|
220
|
+
value={draft.contextKey}
|
|
221
|
+
disabled
|
|
222
|
+
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"
|
|
223
|
+
/>
|
|
190
224
|
</div>
|
|
191
225
|
)}
|
|
192
226
|
|
|
193
227
|
{!isFixedLogic && !isAction && !isCondition && (
|
|
194
228
|
<div>
|
|
195
|
-
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
229
|
+
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
230
|
+
{labels.nodePanel.questionType}
|
|
231
|
+
</label>
|
|
196
232
|
<select
|
|
197
233
|
value={draft.questionType ?? 'text'}
|
|
198
|
-
onChange={(e) =>
|
|
234
|
+
onChange={(e) =>
|
|
235
|
+
setDraft((prev) => ({ ...prev, questionType: e.target.value as FlowNodeData['questionType'] }))
|
|
236
|
+
}
|
|
199
237
|
className={`w-full mt-1 ${SELECT_CLASSNAME}`}
|
|
200
238
|
>
|
|
201
239
|
{Object.entries(labels.questionTypeLabels).map(([key, label]) => (
|
|
202
|
-
<option key={key} value={key}>
|
|
240
|
+
<option key={key} value={key}>
|
|
241
|
+
{label}
|
|
242
|
+
</option>
|
|
203
243
|
))}
|
|
204
244
|
</select>
|
|
205
245
|
</div>
|
|
@@ -218,7 +258,9 @@ export function FlowNodePanel({
|
|
|
218
258
|
{isCondition && (
|
|
219
259
|
<div className="space-y-3">
|
|
220
260
|
<div>
|
|
221
|
-
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
261
|
+
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
262
|
+
{labels.nodePanel.conditionVariable}
|
|
263
|
+
</label>
|
|
222
264
|
<input
|
|
223
265
|
value={draft.conditionContextKey ?? ''}
|
|
224
266
|
onChange={(e) => setDraft((prev) => ({ ...prev, conditionContextKey: e.target.value }))}
|
|
@@ -226,23 +268,36 @@ export function FlowNodePanel({
|
|
|
226
268
|
className={`w-full mt-1 ${INPUT_CLASSNAME}`}
|
|
227
269
|
/>
|
|
228
270
|
<datalist id="condition-context-keys">
|
|
229
|
-
{knownContextKeys.map((key) =>
|
|
271
|
+
{knownContextKeys.map((key) => (
|
|
272
|
+
<option key={key} value={key} />
|
|
273
|
+
))}
|
|
230
274
|
</datalist>
|
|
231
275
|
</div>
|
|
232
276
|
<div>
|
|
233
|
-
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
277
|
+
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
278
|
+
{labels.nodePanel.conditionOperator}
|
|
279
|
+
</label>
|
|
234
280
|
<select
|
|
235
281
|
value={draft.conditionOperator ?? '>'}
|
|
236
|
-
onChange={(e) =>
|
|
282
|
+
onChange={(e) =>
|
|
283
|
+
setDraft((prev) => ({
|
|
284
|
+
...prev,
|
|
285
|
+
conditionOperator: e.target.value as FlowNodeData['conditionOperator'],
|
|
286
|
+
}))
|
|
287
|
+
}
|
|
237
288
|
className={`w-full mt-1 ${SELECT_CLASSNAME}`}
|
|
238
289
|
>
|
|
239
290
|
{CONDITION_OPERATORS.map((operator) => (
|
|
240
|
-
<option key={operator} value={operator}>
|
|
291
|
+
<option key={operator} value={operator}>
|
|
292
|
+
{labels.conditionOperatorLabels[operator] ?? operator}
|
|
293
|
+
</option>
|
|
241
294
|
))}
|
|
242
295
|
</select>
|
|
243
296
|
</div>
|
|
244
297
|
<div>
|
|
245
|
-
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
298
|
+
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
299
|
+
{labels.nodePanel.conditionValue}
|
|
300
|
+
</label>
|
|
246
301
|
<input
|
|
247
302
|
value={draft.conditionValue ?? ''}
|
|
248
303
|
onChange={(e) => setDraft((prev) => ({ ...prev, conditionValue: e.target.value }))}
|
|
@@ -262,7 +317,21 @@ export function FlowNodePanel({
|
|
|
262
317
|
/>
|
|
263
318
|
)}
|
|
264
319
|
|
|
265
|
-
{
|
|
320
|
+
{/* Os arquivos moram na biblioteca, não no grafo — trocar o material não repassa pelo editor. */}
|
|
321
|
+
{isSendMedia && (
|
|
322
|
+
<div>
|
|
323
|
+
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.media}</label>
|
|
324
|
+
<div className="mt-1">
|
|
325
|
+
{renderMediaPicker?.(node) ?? (
|
|
326
|
+
<p className="text-xs text-gray-400">{labels.nodePanel.mediaUnavailable}</p>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{/* `send_media` fica de fora: o handler do módulo só envia os anexos, então um campo de
|
|
333
|
+
mensagem aqui seria texto que o cliente nunca recebe. A legenda é por arquivo. */}
|
|
334
|
+
{isAction && draft.actionKind !== 'send_product_list' && !isSendMedia && (
|
|
266
335
|
<WhatsAppTextField
|
|
267
336
|
label={labels.nodePanel.directMessage}
|
|
268
337
|
value={draft.directMessage ?? ''}
|
|
@@ -283,11 +352,21 @@ export function FlowNodePanel({
|
|
|
283
352
|
<div className="space-y-2">
|
|
284
353
|
{(draft.options ?? []).map(([id, label], i) => (
|
|
285
354
|
<div key={i} className="flex items-center gap-2">
|
|
286
|
-
<input
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
355
|
+
<input
|
|
356
|
+
value={id}
|
|
357
|
+
onChange={(e) => updateOption(i, 0, e.target.value)}
|
|
358
|
+
placeholder={labels.nodePanel.optionId}
|
|
359
|
+
className={`w-16 ${INPUT_CLASSNAME}`}
|
|
360
|
+
/>
|
|
361
|
+
<input
|
|
362
|
+
value={label}
|
|
363
|
+
onChange={(e) => updateOption(i, 1, e.target.value)}
|
|
364
|
+
placeholder={labels.nodePanel.optionLabel}
|
|
365
|
+
className={`flex-1 ${INPUT_CLASSNAME}`}
|
|
366
|
+
/>
|
|
367
|
+
<button onClick={() => removeOption(i)} className="text-gray-400 hover:text-red-600">
|
|
368
|
+
<Trash2 size={14} />
|
|
369
|
+
</button>
|
|
291
370
|
</div>
|
|
292
371
|
))}
|
|
293
372
|
</div>
|
|
@@ -299,8 +378,11 @@ export function FlowNodePanel({
|
|
|
299
378
|
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.next}</label>
|
|
300
379
|
<p className="text-[11px] text-gray-400 dark:text-gray-500 mb-1">{labels.nodePanel.nextHint}</p>
|
|
301
380
|
{typeof draft.next !== 'object' && !isCondition ? (
|
|
302
|
-
<select
|
|
303
|
-
|
|
381
|
+
<select
|
|
382
|
+
value={typeof draft.next === 'string' ? draft.next : ''}
|
|
383
|
+
onChange={(e) => updateNextString(e.target.value)}
|
|
384
|
+
className={`w-full mt-1 ${SELECT_CLASSNAME}`}
|
|
385
|
+
>
|
|
304
386
|
<option value="">—</option>
|
|
305
387
|
{nextNodeOptions()}
|
|
306
388
|
</select>
|
|
@@ -309,10 +391,17 @@ export function FlowNodePanel({
|
|
|
309
391
|
{conditionAnswerIds.map((id) => (
|
|
310
392
|
<div key={id} className="flex items-center gap-2">
|
|
311
393
|
<span className="text-xs text-gray-500 w-32 shrink-0">
|
|
312
|
-
{isCondition
|
|
394
|
+
{isCondition
|
|
395
|
+
? id === 'true'
|
|
396
|
+
? labels.nodePanel.conditionTrue
|
|
397
|
+
: labels.nodePanel.conditionFalse
|
|
398
|
+
: labels.nodePanel.nextByAnswer(id)}
|
|
313
399
|
</span>
|
|
314
|
-
<select
|
|
315
|
-
|
|
400
|
+
<select
|
|
401
|
+
value={draft.next && typeof draft.next === 'object' ? (draft.next.byAnswer[id] ?? '') : ''}
|
|
402
|
+
onChange={(e) => updateNextByAnswer(id, e.target.value)}
|
|
403
|
+
className={`flex-1 ${SELECT_CLASSNAME}`}
|
|
404
|
+
>
|
|
316
405
|
<option value="">—</option>
|
|
317
406
|
{nextNodeOptions()}
|
|
318
407
|
</select>
|
|
@@ -322,8 +411,11 @@ export function FlowNodePanel({
|
|
|
322
411
|
<span className="text-xs text-gray-500 w-32 shrink-0">
|
|
323
412
|
{isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault}
|
|
324
413
|
</span>
|
|
325
|
-
<select
|
|
326
|
-
|
|
414
|
+
<select
|
|
415
|
+
value={draft.next && typeof draft.next === 'object' ? draft.next.default : ''}
|
|
416
|
+
onChange={(e) => updateNextDefault(e.target.value)}
|
|
417
|
+
className={`flex-1 ${SELECT_CLASSNAME}`}
|
|
418
|
+
>
|
|
327
419
|
<option value="">—</option>
|
|
328
420
|
{nextNodeOptions()}
|
|
329
421
|
</select>
|
|
@@ -335,12 +427,17 @@ export function FlowNodePanel({
|
|
|
335
427
|
</div>
|
|
336
428
|
|
|
337
429
|
<div className="p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2">
|
|
338
|
-
<button
|
|
430
|
+
<button
|
|
431
|
+
onClick={() => onChange(draft)}
|
|
432
|
+
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"
|
|
433
|
+
>
|
|
339
434
|
<Save size={14} /> {labels.nodePanel.save}
|
|
340
435
|
</button>
|
|
341
436
|
{!isStart && (
|
|
342
437
|
<button
|
|
343
|
-
onClick={() => {
|
|
438
|
+
onClick={() => {
|
|
439
|
+
if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id)
|
|
440
|
+
}}
|
|
344
441
|
title={labels.nodePanel.delete}
|
|
345
442
|
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"
|
|
346
443
|
>
|
|
@@ -72,7 +72,9 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
72
72
|
onClick={() => setSubmenu(submenu === 'question' ? null : 'question')}
|
|
73
73
|
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"
|
|
74
74
|
>
|
|
75
|
-
<span className="flex items-center gap-2"
|
|
75
|
+
<span className="flex items-center gap-2">
|
|
76
|
+
<MessageCircleQuestion size={15} className="text-blue-500" /> {labels.palette.question}
|
|
77
|
+
</span>
|
|
76
78
|
<ChevronRight size={13} className="text-gray-400" />
|
|
77
79
|
</button>
|
|
78
80
|
{submenu === 'question' && (
|
|
@@ -113,7 +115,9 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
113
115
|
onClick={() => setSubmenu(submenu === 'action' ? null : 'action')}
|
|
114
116
|
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"
|
|
115
117
|
>
|
|
116
|
-
<span className="flex items-center gap-2"
|
|
118
|
+
<span className="flex items-center gap-2">
|
|
119
|
+
<Zap size={15} className="text-orange-500" /> {labels.palette.action}
|
|
120
|
+
</span>
|
|
117
121
|
<ChevronRight size={13} className="text-gray-400" />
|
|
118
122
|
</button>
|
|
119
123
|
{submenu === 'action' && (
|
|
@@ -13,7 +13,11 @@ export interface FlowWhatsAppPreviewProps {
|
|
|
13
13
|
// opções, botões (≤3) ou lista (4+) — para o editor mostrar exatamente o que o cliente verá.
|
|
14
14
|
// Consome a mesma bolha/formatação do pacote (parseWhatsAppFormatting, T6.5) — T7.3 elimina a
|
|
15
15
|
// duplicação de estilo que existia entre este preview e o MessageBubble do bot.
|
|
16
|
-
export function FlowWhatsAppPreview({
|
|
16
|
+
export function FlowWhatsAppPreview({
|
|
17
|
+
body,
|
|
18
|
+
options,
|
|
19
|
+
labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel,
|
|
20
|
+
}: FlowWhatsAppPreviewProps) {
|
|
17
21
|
if (!body && (!options || options.length === 0)) {
|
|
18
22
|
return <p className="text-xs text-gray-400 dark:text-gray-500 italic px-1">{labels.previewPlaceholder}</p>
|
|
19
23
|
}
|
|
@@ -25,7 +29,11 @@ export function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITO
|
|
|
25
29
|
<div className="rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5">
|
|
26
30
|
<div className="max-w-[85%]">
|
|
27
31
|
<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">
|
|
28
|
-
{body ?
|
|
32
|
+
{body ? (
|
|
33
|
+
parseWhatsAppFormatting(body)
|
|
34
|
+
) : (
|
|
35
|
+
<span className="italic text-gray-400">{labels.previewEmptyBody}</span>
|
|
36
|
+
)}
|
|
29
37
|
{hasOptions && !usesButtons && (
|
|
30
38
|
<div className="mt-2 -mx-3 -mb-2 border-t border-gray-100 dark:border-gray-600">
|
|
31
39
|
<div className="flex items-center justify-center gap-1.5 py-2 text-sm font-medium text-cyan-600 dark:text-cyan-400">
|
|
@@ -38,7 +46,10 @@ export function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITO
|
|
|
38
46
|
{hasOptions && usesButtons && (
|
|
39
47
|
<div className="mt-1 space-y-1">
|
|
40
48
|
{options!.map(([id, label]) => (
|
|
41
|
-
<div
|
|
49
|
+
<div
|
|
50
|
+
key={id}
|
|
51
|
+
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"
|
|
52
|
+
>
|
|
42
53
|
{label || <span className="italic text-gray-400">{labels.previewEmptyOption}</span>}
|
|
43
54
|
</div>
|
|
44
55
|
))}
|
package/src/flows/flowGraph.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
FlowNodeData,
|
|
14
14
|
FlowGraphData,
|
|
15
15
|
} from '@adatechnology/meta-whatsapp-contracts'
|
|
16
|
+
import { FLOW_ACTION_KIND } from '@adatechnology/meta-whatsapp-contracts'
|
|
16
17
|
|
|
17
18
|
export type {
|
|
18
19
|
FlowNodeType,
|
|
@@ -28,11 +29,10 @@ export const CONDITION_OPERATORS: FlowConditionOperator[] = ['>', '>=', '<', '<=
|
|
|
28
29
|
|
|
29
30
|
// Kinds de ação genéricos que o pacote conhece de fábrica — o host pode registrar quaisquer
|
|
30
31
|
// outros via `actionKindLabels`/`actionKinds` nos componentes (ver FlowPalette, labels.ts).
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} as const
|
|
32
|
+
// Reexporta o vocabulário do contrato em vez de redeclarar os literais: o editor oferece na
|
|
33
|
+
// paleta exatamente os `actionKind` que o backend sabe interpretar, e duas listas separadas
|
|
34
|
+
// divergiriam em silêncio (um nó publicável que nenhum handler atende).
|
|
35
|
+
export const BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND
|
|
36
36
|
|
|
37
37
|
// Destinos "flow:<key>" são saltos para outro fluxo, resolvidos pelo motor do host. Duplicado
|
|
38
38
|
// (em vez de importado do contracts) de propósito: são três linhas triviais e importá-las como
|
package/src/flows/labels.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface FlowEditorLabels {
|
|
|
47
47
|
conditionTrue: string
|
|
48
48
|
conditionFalse: string
|
|
49
49
|
conditionVariableMissing: string
|
|
50
|
+
media: string
|
|
51
|
+
mediaUnavailable: string
|
|
50
52
|
}
|
|
51
53
|
palette: {
|
|
52
54
|
title: string
|
|
@@ -87,6 +89,7 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
87
89
|
handoff: 'Encaminhar para atendimento',
|
|
88
90
|
rate_limited_handoff: 'Encaminhar (limite de simulações atingido)',
|
|
89
91
|
send_product_list: 'Enviar catálogo de produtos',
|
|
92
|
+
send_media: 'Enviar arquivos da biblioteca',
|
|
90
93
|
},
|
|
91
94
|
conditionOperatorLabels: {
|
|
92
95
|
'>': 'maior que',
|
|
@@ -143,6 +146,8 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
143
146
|
conditionTrue: 'Se verdadeiro →',
|
|
144
147
|
conditionFalse: 'Se falso →',
|
|
145
148
|
conditionVariableMissing: 'Se a variável ainda não foi coletada →',
|
|
149
|
+
media: 'Arquivos enviados neste ponto',
|
|
150
|
+
mediaUnavailable: 'A biblioteca de arquivos não está disponível neste painel.',
|
|
146
151
|
},
|
|
147
152
|
palette: {
|
|
148
153
|
title: 'Adicionar ao fluxo',
|