import { useEffect, useMemo, useState } from 'react'; import { DataPanel } from './DataPanel'; import { createBotFlowNode, deleteBotFlowNode, getBotFlow, listBotFlowVersions, publishBotFlow, updateBotFlowNode, } from '../services/knowledgeService'; const fieldStyle = { width: '100%', border: '1px solid var(--color-border)', borderRadius: 14, padding: '0.78rem 0.9rem', background: '#fff', color: 'var(--color-text)', fontWeight: 600, }; const primaryButton = { border: 'none', borderRadius: 14, padding: '0.78rem 1rem', background: 'var(--color-primary)', color: '#fff', fontWeight: 800, }; const ghostButton = { border: '1px solid var(--color-border)', borderRadius: 14, padding: '0.72rem 0.9rem', background: '#fff', color: 'var(--color-text)', fontWeight: 800, }; const emptyDraft = { nodeType: 'question', title: '', messageText: '', keywords: '', fallbackMessage: '', fallbackAttempts: 2, fallbackAreaId: '', areaId: '', }; const closeDefaultMessage = 'Perfeito, vou encerrar por aqui. Se precisar de algo mais, é só chamar novamente.'; function nodeTypeLabel(type) { if (type === 'greeting') return 'Saudação'; if (type === 'agent') return 'Enviar para agente'; if (type === 'close') return 'Encerrar pelo bot'; return 'Pergunta'; } function splitKeywords(value) { return String(value || '') .split(',') .map((keyword) => keyword.trim()) .filter(Boolean); } function collectPublishWarnings(node, warnings = []) { if (!node) return warnings; const children = node.children || []; const isTerminal = node.node_type === 'agent' || node.node_type === 'close'; if (!isTerminal && children.length === 0) { warnings.push(`"${node.title}" precisa ter pelo menos um filho.`); } if (node.node_type === 'agent' && !node.area_id) { warnings.push(`"${node.title}" precisa de uma especialidade.`); } children.forEach((child) => collectPublishWarnings(child, warnings)); return warnings; } function getFlowNodeWidth(level) { return level >= 2 ? 260 : 300; } function getFlowChildGap(level) { return level >= 2 ? 56 : 40; } function getFlowSubtreeWidth(node, level = 0) { if (!node) return getFlowNodeWidth(level); const children = node.children || []; const nodeWidth = getFlowNodeWidth(level); const horizontalPadding = level >= 2 ? 56 : 72; if (!children.length) { return nodeWidth + horizontalPadding; } const gap = getFlowChildGap(level); const childrenWidth = children.reduce((total, child) => total + getFlowSubtreeWidth(child, level + 1), 0) + Math.max(0, children.length - 1) * gap; return Math.max(nodeWidth + horizontalPadding, childrenWidth); } function WhatsAppPreview({ message }) { return (
Preview WhatsApp
{message || 'Digite a mensagem para visualizar aqui.'}
); } function FlowNode({ node, areasById, onAdd, onEdit, onDelete, level = 0, parentTitle = '' }) { const keywords = splitKeywords(node.keywords); const isRoot = node.node_type === 'greeting'; const isAgent = node.node_type === 'agent'; const isClose = node.node_type === 'close'; const isDeep = level >= 2; const nodeWidth = getFlowNodeWidth(level); const visibleKeywordLimit = isDeep ? 4 : 8; const childGap = getFlowChildGap(level); const subtreeWidth = getFlowSubtreeWidth(node, level); const firstChildWidth = node.children?.length ? getFlowSubtreeWidth(node.children[0], level + 1) : 0; const lastChildWidth = node.children?.length ? getFlowSubtreeWidth(node.children[node.children.length - 1], level + 1) : 0; const accentColor = isRoot ? 'var(--color-primary)' : isAgent ? '#3260b3' : isClose ? '#0f8f77' : 'var(--color-highlight)'; const nodeMessage = node.message_text || (isAgent ? '' : 'Sem mensagem configurada.'); return (
{nodeTypeLabel(node.node_type)} Nível {level + 1}
{node.title} {!isRoot && parentTitle ? ( abaixo de: {parentTitle} ) : null}
{!isAgent && !isClose && onAdd ? ( ) : null}
{isAgent ? ( Fila: {node.area_nome || areasById.get(Number(node.area_id))?.nome || 'não definida'} ) : isClose ? ( {isDeep && nodeMessage.length > 96 ? `${nodeMessage.slice(0, 96)}...` : nodeMessage} ) : ( {isDeep && nodeMessage.length > 96 ? `${nodeMessage.slice(0, 96)}...` : nodeMessage} )} {!isRoot && keywords.length ? (
Respostas que chegam aqui
{keywords.slice(0, visibleKeywordLimit).map((keyword) => ( {keyword} ))} {keywords.length > visibleKeywordLimit ? ( +{keywords.length - visibleKeywordLimit} ) : null}
) : null}
{onEdit ? ( ) : null} {!isRoot && onDelete ? ( ) : null}
{node.children?.length ? ( <>
{node.children.length > 1 ? ( ); } function NodeModal({ mode, node, parent, areas, draft, onDraftChange, onClose, onSave }) { if (!mode) return null; const isEdit = mode === 'edit'; const isRoot = node?.node_type === 'greeting'; const isAgent = draft.nodeType === 'agent' || node?.node_type === 'agent'; const isClose = draft.nodeType === 'close' || node?.node_type === 'close'; const canChooseType = !isEdit; function change(key, value) { onDraftChange((current) => ({ ...current, [key]: value })); } return (

{isEdit ? 'Editar nó' : `Adicionar filho em ${parent?.title}`}

Configure a mensagem, as palavras que ativam o caminho e o destino quando for terminal.

{canChooseType ? (
{[ ['question', 'Adicionar pergunta'], ['agent', 'Enviar para agente'], ['close', 'Encerrar pelo bot'], ].map(([type, label]) => ( ))}
) : null}
{!isRoot ? ( ) : null} {isAgent ? ( ) : isClose ? ( <>