FEAT: Incrementa gestao do fluxo do bot

This commit is contained in:
Rafael Alves Lopes 2026-05-26 11:35:17 -03:00
parent dcad70b708
commit 751038be0f

View File

@ -78,6 +78,32 @@ function collectPublishWarnings(node, 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 (
<div
@ -108,18 +134,44 @@ function WhatsAppPreview({ message }) {
);
}
function FlowNode({ node, areasById, onAdd, onEdit, onDelete }) {
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 (
<div style={{ display: 'grid', justifyItems: 'center', gap: '0.8rem', minWidth: 260 }}>
<div
style={{
display: 'grid',
justifyItems: 'center',
gap: '0.95rem',
minWidth: subtreeWidth,
width: subtreeWidth,
}}
>
<article
style={{
width: 280,
width: nodeWidth,
border: '1px solid var(--color-border)',
borderTop: `5px solid ${accentColor}`,
borderRadius: 18,
background: isRoot
? 'linear-gradient(180deg, #fff, rgba(0,164,183,0.09))'
@ -129,17 +181,36 @@ function FlowNode({ node, areasById, onAdd, onEdit, onDelete }) {
? 'linear-gradient(180deg, #fff, rgba(0,164,183,0.1))'
: '#fff',
boxShadow: '0 12px 28px rgba(0, 49, 80, 0.08)',
padding: '0.95rem',
padding: isDeep ? '0.8rem' : '0.95rem',
display: 'grid',
gap: '0.7rem',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.7rem', alignItems: 'start' }}>
<div style={{ minWidth: 0 }}>
<span style={{ color: 'var(--color-primary)', fontSize: '0.74rem', fontWeight: 900, textTransform: 'uppercase' }}>
{nodeTypeLabel(node.node_type)}
</span>
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', flexWrap: 'wrap', marginBottom: '0.15rem' }}>
<span style={{ color: 'var(--color-primary)', fontSize: '0.74rem', fontWeight: 900, textTransform: 'uppercase' }}>
{nodeTypeLabel(node.node_type)}
</span>
<span
style={{
borderRadius: 999,
padding: '0.12rem 0.42rem',
background: 'rgba(0,49,80,0.06)',
color: 'var(--color-text-soft)',
fontSize: '0.68rem',
fontWeight: 900,
}}
>
Nível {level + 1}
</span>
</div>
<strong style={{ display: 'block', lineHeight: 1.25 }}>{node.title}</strong>
{!isRoot && parentTitle ? (
<span style={{ display: 'block', marginTop: '0.22rem', color: 'var(--color-text-soft)', fontSize: '0.78rem', fontWeight: 700 }}>
abaixo de: {parentTitle}
</span>
) : null}
</div>
{!isAgent && !isClose && onAdd ? (
<button
@ -167,30 +238,49 @@ function FlowNode({ node, areasById, onAdd, onEdit, onDelete }) {
</span>
) : isClose ? (
<span style={{ color: 'var(--color-text-soft)', lineHeight: 1.35, whiteSpace: 'pre-wrap' }}>
{node.message_text || 'Fecha o atendimento sem enviar para agente.'}
{isDeep && nodeMessage.length > 96 ? `${nodeMessage.slice(0, 96)}...` : nodeMessage}
</span>
) : (
<span style={{ color: 'var(--color-text-soft)', whiteSpace: 'pre-wrap', lineHeight: 1.35 }}>
{node.message_text || 'Sem mensagem configurada.'}
<span style={{ color: 'var(--color-text-soft)', whiteSpace: isDeep ? 'normal' : 'pre-wrap', lineHeight: 1.35 }}>
{isDeep && nodeMessage.length > 96 ? `${nodeMessage.slice(0, 96)}...` : nodeMessage}
</span>
)}
{!isRoot && keywords.length ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
{keywords.slice(0, 8).map((keyword) => (
<span
key={keyword}
style={{
borderRadius: 999,
background: 'rgba(0,49,80,0.07)',
padding: '0.22rem 0.5rem',
fontSize: '0.75rem',
fontWeight: 800,
}}
>
{keyword}
</span>
))}
<div style={{ display: 'grid', gap: '0.35rem' }}>
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.72rem', fontWeight: 900 }}>
Respostas que chegam aqui
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
{keywords.slice(0, visibleKeywordLimit).map((keyword) => (
<span
key={keyword}
style={{
borderRadius: 999,
background: 'rgba(0,49,80,0.07)',
padding: '0.22rem 0.5rem',
fontSize: '0.75rem',
fontWeight: 800,
}}
>
{keyword}
</span>
))}
{keywords.length > visibleKeywordLimit ? (
<span
style={{
borderRadius: 999,
background: 'rgba(0,49,80,0.04)',
padding: '0.22rem 0.5rem',
fontSize: '0.75rem',
fontWeight: 800,
color: 'var(--color-text-soft)',
}}
>
+{keywords.length - visibleKeywordLimit}
</span>
) : null}
</div>
</div>
) : null}
@ -218,27 +308,76 @@ function FlowNode({ node, areasById, onAdd, onEdit, onDelete }) {
{node.children?.length ? (
<>
<div style={{ width: 2, height: 20, background: 'rgba(0,49,80,0.18)' }} />
<div
style={{
width: 2,
height: 38,
background: 'linear-gradient(180deg, rgba(0,49,80,0.28), rgba(0,49,80,0.1))',
}}
/>
<div
style={{
display: 'flex',
gap: '1rem',
gap: childGap,
alignItems: 'start',
justifyContent: 'center',
flexWrap: 'nowrap',
paddingTop: '0.2rem',
paddingTop: 34,
position: 'relative',
width: '100%',
}}
>
{node.children.map((child) => (
<FlowNode
key={child.id}
node={child}
areasById={areasById}
onAdd={onAdd}
onEdit={onEdit}
onDelete={onDelete}
{node.children.length > 1 ? (
<div
aria-hidden="true"
style={{
position: 'absolute',
top: 0,
left: firstChildWidth / 2,
right: lastChildWidth / 2,
height: 2,
background: 'rgba(0,49,80,0.16)',
}}
/>
))}
) : null}
{node.children.map((child) => {
const childWidth = getFlowSubtreeWidth(child, level + 1);
return (
<div
key={child.id}
style={{
position: 'relative',
display: 'grid',
justifyItems: 'center',
minWidth: childWidth,
width: childWidth,
}}
>
<div
aria-hidden="true"
style={{
position: 'absolute',
top: -34,
left: '50%',
width: 2,
height: 34,
background: 'rgba(0,49,80,0.2)',
transform: 'translateX(-50%)',
}}
/>
<FlowNode
node={child}
areasById={areasById}
onAdd={onAdd}
onEdit={onEdit}
onDelete={onDelete}
level={level + 1}
parentTitle={node.title}
/>
</div>
);
})}
</div>
</>
) : null}
@ -550,6 +689,7 @@ export function KnowledgeBasePanel({ areas, mode = 'admin', isMobile = false })
const root = flow?.tree;
const hasPublished = Boolean(flow?.latestPublished);
const publishWarnings = useMemo(() => collectPublishWarnings(root).slice(0, 5), [root]);
const treeMinWidth = useMemo(() => Math.max(1100, getFlowSubtreeWidth(root)), [root]);
return (
<div style={{ display: 'grid', gap: '1rem' }}>
@ -624,7 +764,9 @@ export function KnowledgeBasePanel({ areas, mode = 'admin', isMobile = false })
style={{
border: '1px solid var(--color-border)',
borderRadius: 22,
background: 'linear-gradient(180deg, #fff, rgba(0,49,80,0.03))',
background:
'linear-gradient(180deg, #fff, rgba(0,49,80,0.03)), radial-gradient(circle at 1px 1px, rgba(0,49,80,0.08) 1px, transparent 0)',
backgroundSize: 'auto, 22px 22px',
overflow: 'auto',
minHeight: 520,
padding: '1.25rem',
@ -634,7 +776,7 @@ export function KnowledgeBasePanel({ areas, mode = 'admin', isMobile = false })
style={{
transform: `scale(${zoom})`,
transformOrigin: 'top center',
minWidth: 900,
minWidth: treeMinWidth,
minHeight: 480,
display: 'grid',
justifyContent: 'center',