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; 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 }) { function WhatsAppPreview({ message }) {
return ( return (
<div <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 keywords = splitKeywords(node.keywords);
const isRoot = node.node_type === 'greeting'; const isRoot = node.node_type === 'greeting';
const isAgent = node.node_type === 'agent'; const isAgent = node.node_type === 'agent';
const isClose = node.node_type === 'close'; 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 ( 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 <article
style={{ style={{
width: 280, width: nodeWidth,
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderTop: `5px solid ${accentColor}`,
borderRadius: 18, borderRadius: 18,
background: isRoot background: isRoot
? 'linear-gradient(180deg, #fff, rgba(0,164,183,0.09))' ? '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))' ? 'linear-gradient(180deg, #fff, rgba(0,164,183,0.1))'
: '#fff', : '#fff',
boxShadow: '0 12px 28px rgba(0, 49, 80, 0.08)', boxShadow: '0 12px 28px rgba(0, 49, 80, 0.08)',
padding: '0.95rem', padding: isDeep ? '0.8rem' : '0.95rem',
display: 'grid', display: 'grid',
gap: '0.7rem', gap: '0.7rem',
}} }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.7rem', alignItems: 'start' }}> <div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.7rem', alignItems: 'start' }}>
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
<span style={{ color: 'var(--color-primary)', fontSize: '0.74rem', fontWeight: 900, textTransform: 'uppercase' }}> <div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', flexWrap: 'wrap', marginBottom: '0.15rem' }}>
{nodeTypeLabel(node.node_type)} <span style={{ color: 'var(--color-primary)', fontSize: '0.74rem', fontWeight: 900, textTransform: 'uppercase' }}>
</span> {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> <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> </div>
{!isAgent && !isClose && onAdd ? ( {!isAgent && !isClose && onAdd ? (
<button <button
@ -167,30 +238,49 @@ function FlowNode({ node, areasById, onAdd, onEdit, onDelete }) {
</span> </span>
) : isClose ? ( ) : isClose ? (
<span style={{ color: 'var(--color-text-soft)', lineHeight: 1.35, whiteSpace: 'pre-wrap' }}> <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>
) : ( ) : (
<span style={{ color: 'var(--color-text-soft)', whiteSpace: 'pre-wrap', lineHeight: 1.35 }}> <span style={{ color: 'var(--color-text-soft)', whiteSpace: isDeep ? 'normal' : 'pre-wrap', lineHeight: 1.35 }}>
{node.message_text || 'Sem mensagem configurada.'} {isDeep && nodeMessage.length > 96 ? `${nodeMessage.slice(0, 96)}...` : nodeMessage}
</span> </span>
)} )}
{!isRoot && keywords.length ? ( {!isRoot && keywords.length ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}> <div style={{ display: 'grid', gap: '0.35rem' }}>
{keywords.slice(0, 8).map((keyword) => ( <span style={{ color: 'var(--color-text-soft)', fontSize: '0.72rem', fontWeight: 900 }}>
<span Respostas que chegam aqui
key={keyword} </span>
style={{ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
borderRadius: 999, {keywords.slice(0, visibleKeywordLimit).map((keyword) => (
background: 'rgba(0,49,80,0.07)', <span
padding: '0.22rem 0.5rem', key={keyword}
fontSize: '0.75rem', style={{
fontWeight: 800, borderRadius: 999,
}} background: 'rgba(0,49,80,0.07)',
> padding: '0.22rem 0.5rem',
{keyword} fontSize: '0.75rem',
</span> 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> </div>
) : null} ) : null}
@ -218,27 +308,76 @@ function FlowNode({ node, areasById, onAdd, onEdit, onDelete }) {
{node.children?.length ? ( {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 <div
style={{ style={{
display: 'flex', display: 'flex',
gap: '1rem', gap: childGap,
alignItems: 'start', alignItems: 'start',
justifyContent: 'center', justifyContent: 'center',
flexWrap: 'nowrap', flexWrap: 'nowrap',
paddingTop: '0.2rem', paddingTop: 34,
position: 'relative',
width: '100%',
}} }}
> >
{node.children.map((child) => ( {node.children.length > 1 ? (
<FlowNode <div
key={child.id} aria-hidden="true"
node={child} style={{
areasById={areasById} position: 'absolute',
onAdd={onAdd} top: 0,
onEdit={onEdit} left: firstChildWidth / 2,
onDelete={onDelete} 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> </div>
</> </>
) : null} ) : null}
@ -550,6 +689,7 @@ export function KnowledgeBasePanel({ areas, mode = 'admin', isMobile = false })
const root = flow?.tree; const root = flow?.tree;
const hasPublished = Boolean(flow?.latestPublished); const hasPublished = Boolean(flow?.latestPublished);
const publishWarnings = useMemo(() => collectPublishWarnings(root).slice(0, 5), [root]); const publishWarnings = useMemo(() => collectPublishWarnings(root).slice(0, 5), [root]);
const treeMinWidth = useMemo(() => Math.max(1100, getFlowSubtreeWidth(root)), [root]);
return ( return (
<div style={{ display: 'grid', gap: '1rem' }}> <div style={{ display: 'grid', gap: '1rem' }}>
@ -624,7 +764,9 @@ export function KnowledgeBasePanel({ areas, mode = 'admin', isMobile = false })
style={{ style={{
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderRadius: 22, 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', overflow: 'auto',
minHeight: 520, minHeight: 520,
padding: '1.25rem', padding: '1.25rem',
@ -634,7 +776,7 @@ export function KnowledgeBasePanel({ areas, mode = 'admin', isMobile = false })
style={{ style={{
transform: `scale(${zoom})`, transform: `scale(${zoom})`,
transformOrigin: 'top center', transformOrigin: 'top center',
minWidth: 900, minWidth: treeMinWidth,
minHeight: 480, minHeight: 480,
display: 'grid', display: 'grid',
justifyContent: 'center', justifyContent: 'center',