FEAT: Incrementa gestao do fluxo do bot
This commit is contained in:
parent
dcad70b708
commit
751038be0f
@ -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 }}>
|
||||||
|
<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' }}>
|
<span style={{ color: 'var(--color-primary)', fontSize: '0.74rem', fontWeight: 900, textTransform: 'uppercase' }}>
|
||||||
{nodeTypeLabel(node.node_type)}
|
{nodeTypeLabel(node.node_type)}
|
||||||
</span>
|
</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,17 +238,21 @@ 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: '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' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
|
||||||
{keywords.slice(0, 8).map((keyword) => (
|
{keywords.slice(0, visibleKeywordLimit).map((keyword) => (
|
||||||
<span
|
<span
|
||||||
key={keyword}
|
key={keyword}
|
||||||
style={{
|
style={{
|
||||||
@ -191,6 +266,21 @@ function FlowNode({ node, areasById, onAdd, onEdit, onDelete }) {
|
|||||||
{keyword}
|
{keyword}
|
||||||
</span>
|
</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
|
||||||
|
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}
|
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}
|
node={child}
|
||||||
areasById={areasById}
|
areasById={areasById}
|
||||||
onAdd={onAdd}
|
onAdd={onAdd}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
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',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user