feat(frontend): add advanced node components to FlowBuilder
Add visual components for all new Flow Engine node types including: - Basic nodes: Buttons, SetVariable - Control nodes: Switch, Delay, Random, Loop, GoTo - Validation nodes: Email, Phone, Number, Date, Regex, Options - Script nodes: JavaScript, HTTP Request - AI nodes: AI Response, AI Sentiment Reorganize toolbar into categorized dropdown menus for better UX. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import ReactFlow, {
|
|||||||
NodeTypes,
|
NodeTypes,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import { Button, message, Space } from 'antd';
|
import { Button, message, Space, Dropdown } from 'antd';
|
||||||
import { SaveOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
import { SaveOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiClient } from '../api/client';
|
import { apiClient } from '../api/client';
|
||||||
@@ -42,11 +42,140 @@ const WaitInputNode = ({ data }: { data: any }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ButtonsNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #13c2c2', borderRadius: 8, background: '#e6fffb' }}>
|
||||||
|
<strong>🔘 Botones</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>{data.config?.buttons?.length || 0} opciones</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SetVariableNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #eb2f96', borderRadius: 8, background: '#fff0f6' }}>
|
||||||
|
<strong>📝 Variable</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>{data.config?.variable || 'Sin nombre'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SwitchNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #fa8c16', borderRadius: 8, background: '#fff7e6' }}>
|
||||||
|
<strong>🔀 Switch</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>{data.config?.cases?.length || 0} casos</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DelayNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #a0d911', borderRadius: 8, background: '#fcffe6' }}>
|
||||||
|
<strong>⏱️ Delay</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>{data.config?.seconds || 0}s</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RandomNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #2f54eb', borderRadius: 8, background: '#f0f5ff' }}>
|
||||||
|
<strong>🎲 Random</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>A/B Test</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LoopNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #52c41a', borderRadius: 8, background: '#f6ffed' }}>
|
||||||
|
<strong>🔄 Loop</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>Max: {data.config?.max_iterations || 10}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const GoToNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #595959', borderRadius: 8, background: '#fafafa' }}>
|
||||||
|
<strong>➡️ GoTo</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>{data.config?.target_node || 'Sin destino'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ValidateEmailNode = () => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #1890ff', borderRadius: 8, background: '#e6f7ff' }}>
|
||||||
|
<strong>✉️ Validar Email</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ValidatePhoneNode = () => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #1890ff', borderRadius: 8, background: '#e6f7ff' }}>
|
||||||
|
<strong>📱 Validar Teléfono</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ValidateNumberNode = () => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #1890ff', borderRadius: 8, background: '#e6f7ff' }}>
|
||||||
|
<strong>🔢 Validar Número</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ValidateDateNode = () => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #1890ff', borderRadius: 8, background: '#e6f7ff' }}>
|
||||||
|
<strong>📅 Validar Fecha</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ValidateRegexNode = () => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #1890ff', borderRadius: 8, background: '#e6f7ff' }}>
|
||||||
|
<strong>🔍 Validar Regex</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ValidateOptionsNode = () => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #1890ff', borderRadius: 8, background: '#e6f7ff' }}>
|
||||||
|
<strong>📋 Validar Opciones</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const JavaScriptNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #fadb14', borderRadius: 8, background: '#fffb8f' }}>
|
||||||
|
<strong>📜 JavaScript</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>Script</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const HttpRequestNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #722ed1', borderRadius: 8, background: '#f9f0ff' }}>
|
||||||
|
<strong>🌐 HTTP Request</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>{data.config?.method || 'GET'} {data.config?.url?.slice(0, 20) || ''}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AIResponseNode = ({ data }: { data: any }) => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #eb2f96', borderRadius: 8, background: '#fff0f6' }}>
|
||||||
|
<strong>🤖 AI Response</strong>
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>GPT</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AISentimentNode = () => (
|
||||||
|
<div style={{ padding: 10, border: '2px solid #eb2f96', borderRadius: 8, background: '#fff0f6' }}>
|
||||||
|
<strong>💭 AI Sentiment</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const nodeTypes: NodeTypes = {
|
const nodeTypes: NodeTypes = {
|
||||||
trigger: TriggerNode,
|
trigger: TriggerNode,
|
||||||
message: MessageNode,
|
message: MessageNode,
|
||||||
condition: ConditionNode,
|
condition: ConditionNode,
|
||||||
wait_input: WaitInputNode,
|
wait_input: WaitInputNode,
|
||||||
|
buttons: ButtonsNode,
|
||||||
|
set_variable: SetVariableNode,
|
||||||
|
switch: SwitchNode,
|
||||||
|
delay: DelayNode,
|
||||||
|
random: RandomNode,
|
||||||
|
loop: LoopNode,
|
||||||
|
goto: GoToNode,
|
||||||
|
validate_email: ValidateEmailNode,
|
||||||
|
validate_phone: ValidatePhoneNode,
|
||||||
|
validate_number: ValidateNumberNode,
|
||||||
|
validate_date: ValidateDateNode,
|
||||||
|
validate_regex: ValidateRegexNode,
|
||||||
|
validate_options: ValidateOptionsNode,
|
||||||
|
javascript: JavaScriptNode,
|
||||||
|
http_request: HttpRequestNode,
|
||||||
|
ai_response: AIResponseNode,
|
||||||
|
ai_sentiment: AISentimentNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Flow {
|
interface Flow {
|
||||||
@@ -124,10 +253,59 @@ export default function FlowBuilder() {
|
|||||||
<span style={{ fontSize: 18, fontWeight: 'bold' }}>{flow?.name}</span>
|
<span style={{ fontSize: 18, fontWeight: 'bold' }}>{flow?.name}</span>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={() => addNode('trigger')}>+ Trigger</Button>
|
<Dropdown
|
||||||
<Button onClick={() => addNode('message')}>+ Mensaje</Button>
|
menu={{
|
||||||
<Button onClick={() => addNode('condition')}>+ Condición</Button>
|
items: [
|
||||||
<Button onClick={() => addNode('wait_input')}>+ Esperar Input</Button>
|
{ key: 'trigger', label: '🚀 Trigger', onClick: () => addNode('trigger') },
|
||||||
|
{ key: 'message', label: '💬 Mensaje', onClick: () => addNode('message') },
|
||||||
|
{ key: 'buttons', label: '🔘 Botones', onClick: () => addNode('buttons') },
|
||||||
|
{ key: 'wait_input', label: '⏳ Esperar Input', onClick: () => addNode('wait_input') },
|
||||||
|
{ key: 'set_variable', label: '📝 Variable', onClick: () => addNode('set_variable') },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button>+ Básicos</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{ key: 'condition', label: '❓ Condición', onClick: () => addNode('condition') },
|
||||||
|
{ key: 'switch', label: '🔀 Switch', onClick: () => addNode('switch') },
|
||||||
|
{ key: 'delay', label: '⏱️ Delay', onClick: () => addNode('delay') },
|
||||||
|
{ key: 'random', label: '🎲 Random', onClick: () => addNode('random') },
|
||||||
|
{ key: 'loop', label: '🔄 Loop', onClick: () => addNode('loop') },
|
||||||
|
{ key: 'goto', label: '➡️ GoTo', onClick: () => addNode('goto') },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button>+ Control</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{ key: 'validate_email', label: '✉️ Email', onClick: () => addNode('validate_email') },
|
||||||
|
{ key: 'validate_phone', label: '📱 Teléfono', onClick: () => addNode('validate_phone') },
|
||||||
|
{ key: 'validate_number', label: '🔢 Número', onClick: () => addNode('validate_number') },
|
||||||
|
{ key: 'validate_date', label: '📅 Fecha', onClick: () => addNode('validate_date') },
|
||||||
|
{ key: 'validate_regex', label: '🔍 Regex', onClick: () => addNode('validate_regex') },
|
||||||
|
{ key: 'validate_options', label: '📋 Opciones', onClick: () => addNode('validate_options') },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button>+ Validación</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{ key: 'javascript', label: '📜 JavaScript', onClick: () => addNode('javascript') },
|
||||||
|
{ key: 'http_request', label: '🌐 HTTP Request', onClick: () => addNode('http_request') },
|
||||||
|
{ key: 'ai_response', label: '🤖 AI Response', onClick: () => addNode('ai_response') },
|
||||||
|
{ key: 'ai_sentiment', label: '💭 AI Sentiment', onClick: () => addNode('ai_sentiment') },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button>+ Avanzados</Button>
|
||||||
|
</Dropdown>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
|
|||||||
Reference in New Issue
Block a user