413 lines
17 KiB
TypeScript
413 lines
17 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { Badge, Button, MaturityBadge, Modal, useToast } from '../../shared/ui';
|
||
import {
|
||
ArrowDown,
|
||
Play,
|
||
Plus,
|
||
CheckCircle2,
|
||
Check,
|
||
Clock,
|
||
Terminal,
|
||
Power,
|
||
} from 'lucide-react';
|
||
|
||
interface WorkflowNode {
|
||
id: string;
|
||
type: 'trigger' | 'condition' | 'ai' | 'action';
|
||
title: string;
|
||
description: string;
|
||
badge: string;
|
||
badgeVariant: 'green' | 'gold' | 'info' | 'neutral';
|
||
latencyMs: number;
|
||
}
|
||
|
||
interface TestLogStep {
|
||
stepNumber: number;
|
||
nodeTitle: string;
|
||
status: 'pending' | 'running' | 'success' | 'failed';
|
||
output: string;
|
||
latencyMs: number;
|
||
}
|
||
|
||
const INITIAL_NODES: WorkflowNode[] = [
|
||
{
|
||
id: 'n1',
|
||
type: 'trigger',
|
||
title: 'Триггер: Новое входящее сообщение в Telegram',
|
||
description: 'Срабатывает мгновенно при обращении гостя в бота заведения',
|
||
badge: 'Событие',
|
||
badgeVariant: 'gold',
|
||
latencyMs: 14,
|
||
},
|
||
{
|
||
id: 'n2',
|
||
type: 'condition',
|
||
title: 'Условие: Поиск клиента в базе CRM платформы',
|
||
description: 'Проверка номера телефона и истории заказов: клиент найден в базе',
|
||
badge: 'CRM Match',
|
||
badgeVariant: 'neutral',
|
||
latencyMs: 42,
|
||
},
|
||
{
|
||
id: 'n3',
|
||
type: 'ai',
|
||
title: '✦ Krona AI: Определение намерения гостя (Intent Analysis)',
|
||
description: 'Классификация сообщения: запрос бронирования столика (уверенность 98%)',
|
||
badge: 'ИИ-анализ',
|
||
badgeVariant: 'gold',
|
||
latencyMs: 175,
|
||
},
|
||
{
|
||
id: 'n4',
|
||
type: 'action',
|
||
title: 'Действие: Создание сделки и назначение менеджера',
|
||
description: 'Открытие карточки сделки в воронке со статусом «Бронирование стола»',
|
||
badge: 'CRM Сделка',
|
||
badgeVariant: 'green',
|
||
latencyMs: 65,
|
||
},
|
||
{
|
||
id: 'n5',
|
||
type: 'action',
|
||
title: 'Действие: Отправка подтверждения гостю в чат',
|
||
description: 'Формирование персонализированного ответа с подтверждением даты и времени',
|
||
badge: 'Telegram Reply',
|
||
badgeVariant: 'info',
|
||
latencyMs: 88,
|
||
},
|
||
];
|
||
|
||
export const AutomationModule: React.FC = () => {
|
||
const { showToast } = useToast();
|
||
const [nodes, setNodes] = useState<WorkflowNode[]>(INITIAL_NODES);
|
||
const [isWorkflowActive, setIsWorkflowActive] = useState(true);
|
||
|
||
// Test Run execution state
|
||
const [isRunningTest, setIsRunningTest] = useState(false);
|
||
const [testLogs, setTestLogs] = useState<TestLogStep[]>([]);
|
||
const [testComplete, setTestComplete] = useState(false);
|
||
|
||
// New Node Modal
|
||
const [isNewNodeModalOpen, setIsNewNodeModalOpen] = useState(false);
|
||
const [nodeType, setNodeType] = useState<'trigger' | 'condition' | 'ai' | 'action'>('action');
|
||
const [nodeTitle, setNodeTitle] = useState('');
|
||
const [nodeDesc, setNodeDesc] = useState('');
|
||
|
||
const handleRunTest = () => {
|
||
setIsRunningTest(true);
|
||
setTestComplete(false);
|
||
setTestLogs([]);
|
||
|
||
const logSteps: TestLogStep[] = [
|
||
{ stepNumber: 1, nodeTitle: nodes[0].title, status: 'running', output: 'Получен вебхук Telegram: "Добрый вечер, хотим столик на четверых на 19:30"', latencyMs: 14 },
|
||
];
|
||
setTestLogs([...logSteps]);
|
||
|
||
setTimeout(() => {
|
||
logSteps[0].status = 'success';
|
||
logSteps.push({
|
||
stepNumber: 2,
|
||
nodeTitle: nodes[1]?.title || 'Поиск клиента',
|
||
status: 'running',
|
||
output: 'Поиск по номеру +7 (913) 482-19-02: Найден контакт «Анна Воронина» (VIP, 14 заказов)',
|
||
latencyMs: 42,
|
||
});
|
||
setTestLogs([...logSteps]);
|
||
}, 400);
|
||
|
||
setTimeout(() => {
|
||
logSteps[1].status = 'success';
|
||
logSteps.push({
|
||
stepNumber: 3,
|
||
nodeTitle: nodes[2]?.title || 'AI Анализ',
|
||
status: 'running',
|
||
output: 'Модель Krona NLP v1.8: Класс = table_booking, Персоны = 4, Время = 19:30, Confidence = 98.4%',
|
||
latencyMs: 175,
|
||
});
|
||
setTestLogs([...logSteps]);
|
||
}, 900);
|
||
|
||
setTimeout(() => {
|
||
logSteps[2].status = 'success';
|
||
logSteps.push({
|
||
stepNumber: 4,
|
||
nodeTitle: nodes[3]?.title || 'Создание сделки',
|
||
status: 'running',
|
||
output: 'Сделка #DL-9428 создана в воронке «Бронирование». Назначен менеджер: Алина Соколова',
|
||
latencyMs: 65,
|
||
});
|
||
setTestLogs([...logSteps]);
|
||
}, 1400);
|
||
|
||
setTimeout(() => {
|
||
logSteps[3].status = 'success';
|
||
logSteps.push({
|
||
stepNumber: 5,
|
||
nodeTitle: nodes[4]?.title || 'Отправка ответа',
|
||
status: 'success',
|
||
output: 'Сообщение доставлено пользователю: "Анна, здравствуйте! Забронировали Стол 5 на 19:30."',
|
||
latencyMs: 88,
|
||
});
|
||
setTestLogs([...logSteps]);
|
||
setIsRunningTest(false);
|
||
setTestComplete(true);
|
||
|
||
showToast({
|
||
title: 'Тестовый запуск успешен!',
|
||
message: 'Все 5 узлов цепочки выполнены без ошибок за 384 ms',
|
||
type: 'success',
|
||
});
|
||
}, 1900);
|
||
};
|
||
|
||
const handleAddNode = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!nodeTitle.trim()) return;
|
||
|
||
const badgeVariants: Record<string, 'green' | 'gold' | 'info' | 'neutral'> = {
|
||
trigger: 'gold',
|
||
condition: 'neutral',
|
||
ai: 'gold',
|
||
action: 'green',
|
||
};
|
||
|
||
const newNode: WorkflowNode = {
|
||
id: `n-${Date.now()}`,
|
||
type: nodeType,
|
||
title: nodeTitle.trim(),
|
||
description: nodeDesc.trim() || 'Пользовательский шаг автоматизации',
|
||
badge: nodeType.toUpperCase(),
|
||
badgeVariant: badgeVariants[nodeType],
|
||
latencyMs: Math.floor(20 + Math.random() * 80),
|
||
};
|
||
|
||
setNodes((prev) => [...prev, newNode]);
|
||
setIsNewNodeModalOpen(false);
|
||
setNodeTitle('');
|
||
setNodeDesc('');
|
||
showToast({
|
||
title: 'Узел добавлен в цепочку',
|
||
message: `Блок «${newNode.title}» готов к выполнению`,
|
||
type: 'success',
|
||
});
|
||
};
|
||
|
||
const totalLatency = nodes.reduce((sum, n) => sum + n.latencyMs, 0);
|
||
|
||
return (
|
||
<div className="space-y-4 animate-fade-in">
|
||
{/* Header with MaturityBadge */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-krona-surface p-3.5 rounded-xl border border-krona-border shadow-krona-soft">
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="text-sm font-bold text-krona-primary">
|
||
Автоматизация & Visual Workflow Engine
|
||
</h2>
|
||
<MaturityBadge level="target" size="sm" />
|
||
</div>
|
||
<p className="text-xs text-krona-secondary mt-0.5">
|
||
Визуальный конструктор бизнес-процессов: триггеры, условия, AI-агенты и автоматические действия
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant={isWorkflowActive ? 'secondary' : 'outline'}
|
||
size="sm"
|
||
onClick={() => {
|
||
setIsWorkflowActive(!isWorkflowActive);
|
||
showToast({
|
||
title: isWorkflowActive ? 'Сценарий деактивирован' : 'Сценарий активирован',
|
||
message: isWorkflowActive ? 'Цепочка приостановлена' : 'Сценарий слушает входящие события',
|
||
type: 'info',
|
||
});
|
||
}}
|
||
>
|
||
<Power className="w-3.5 h-3.5 mr-1" />
|
||
{isWorkflowActive ? 'Активен (Вкл)' : 'Отключен (Пауза)'}
|
||
</Button>
|
||
<Button variant="primary" size="sm" onClick={() => setIsNewNodeModalOpen(true)}>
|
||
<Plus className="w-3.5 h-3.5 mr-1" />
|
||
Добавить узел
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4">
|
||
{/* Left: Visual Workflow Chain */}
|
||
<div className="lg:col-span-7 bg-krona-surface border border-krona-border rounded-xl p-5 shadow-krona-soft space-y-4">
|
||
<div className="flex items-center justify-between pb-3 border-b border-krona-border">
|
||
<div>
|
||
<h4 className="text-xs font-bold text-krona-primary">
|
||
Сценарий: «Автоматическая обработка брони стола из Telegram»
|
||
</h4>
|
||
<span className="text-[11px] text-krona-secondary">
|
||
Всего шагов: {nodes.length} • Среднее время сквозного выполнения: ~{totalLatency} ms
|
||
</span>
|
||
</div>
|
||
<Badge variant={isWorkflowActive ? 'green' : 'neutral'} size="sm">
|
||
{isWorkflowActive ? 'Работает в фоне' : 'На паузе'}
|
||
</Badge>
|
||
</div>
|
||
|
||
{/* Flow Nodes List */}
|
||
<div className="max-w-xl mx-auto py-2 flex flex-col items-center space-y-2">
|
||
{nodes.map((node, i) => (
|
||
<React.Fragment key={node.id}>
|
||
<div className="w-full p-3.5 rounded-xl border border-krona-border bg-krona-workspace shadow-krona-soft flex items-start justify-between gap-3 hover:border-krona-border-strong transition-all">
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className="text-xs font-bold text-krona-primary">{node.title}</span>
|
||
</div>
|
||
<p className="text-[11px] text-krona-secondary leading-snug">{node.description}</p>
|
||
<span className="text-[10px] text-krona-secondary font-mono mt-1 block">
|
||
Ожидаемая задержка: ~{node.latencyMs} ms
|
||
</span>
|
||
</div>
|
||
<Badge variant={node.badgeVariant} size="sm">{node.badge}</Badge>
|
||
</div>
|
||
|
||
{i < nodes.length - 1 && (
|
||
<div className="flex flex-col items-center my-0.5 text-krona-gold">
|
||
<ArrowDown className="w-4 h-4" />
|
||
</div>
|
||
)}
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Test Run Execution Console */}
|
||
<div className="lg:col-span-5 bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft flex flex-col justify-between space-y-3 text-xs">
|
||
<div>
|
||
<div className="flex items-center justify-between pb-2 border-b border-krona-border">
|
||
<div className="flex items-center gap-1.5 font-bold text-krona-primary">
|
||
<Terminal className="w-4 h-4 text-krona-gold" />
|
||
<span>Консоль тестового прогона (Test Run)</span>
|
||
</div>
|
||
<Button
|
||
variant="gold"
|
||
size="sm"
|
||
disabled={isRunningTest}
|
||
onClick={handleRunTest}
|
||
className="text-xs"
|
||
>
|
||
<Play className="w-3.5 h-3.5 mr-1" />
|
||
{isRunningTest ? 'Выполнение...' : 'Тестовый запуск'}
|
||
</Button>
|
||
</div>
|
||
|
||
<p className="text-[11px] text-krona-secondary pt-2">
|
||
Симуляция сквозного прохождения входящего события по всем шагам сценария с замером задержки каждого узла:
|
||
</p>
|
||
|
||
{/* Execution logs output */}
|
||
<div className="mt-3 bg-zinc-950 text-zinc-200 font-mono text-[11px] p-3 rounded-xl min-h-[320px] max-h-[460px] overflow-y-auto space-y-2.5 border border-zinc-800">
|
||
{testLogs.length === 0 ? (
|
||
<div className="text-zinc-500 text-center py-20">
|
||
Нажмите «Тестовый запуск» для симуляции выполнения сценария
|
||
</div>
|
||
) : (
|
||
testLogs.map((step) => (
|
||
<div key={step.stepNumber} className="space-y-0.5 animate-fade-in">
|
||
<div className="flex items-center justify-between text-[10px]">
|
||
<span className="text-krona-gold font-bold">
|
||
[ШАГ {step.stepNumber}] {step.nodeTitle}
|
||
</span>
|
||
<span className="text-zinc-400">{step.latencyMs} ms</span>
|
||
</div>
|
||
<div className="text-zinc-300 pl-2 border-l border-zinc-700">
|
||
{step.output}
|
||
</div>
|
||
<div className="text-[10px] pl-2 font-bold">
|
||
{step.status === 'success' && (
|
||
<span className="text-emerald-400 flex items-center gap-1">
|
||
<Check className="w-3 h-3" /> УСПЕШНО
|
||
</span>
|
||
)}
|
||
{step.status === 'running' && (
|
||
<span className="text-amber-400 flex items-center gap-1">
|
||
<Clock className="w-3 h-3 animate-spin" /> ВЫПОЛНЕНИЕ...
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
|
||
{testComplete && (
|
||
<div className="pt-2 border-t border-zinc-800 text-emerald-400 font-bold text-center">
|
||
✓ ВСЕ ШАГИ ВЫПОЛНЕНЫ УСПЕШНО (ИТОГО: 384 ms)
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-2.5 bg-krona-workspace rounded-lg border border-krona-border text-[11px] text-krona-secondary flex items-center gap-2">
|
||
<CheckCircle2 className="w-4 h-4 text-krona-green shrink-0" />
|
||
<span>Логи тестового запуска записываются в журнал аудита безопасности.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Modal: New Node */}
|
||
{isNewNodeModalOpen && (
|
||
<Modal
|
||
isOpen={isNewNodeModalOpen}
|
||
onClose={() => setIsNewNodeModalOpen(false)}
|
||
title="Добавить узел автоматизации"
|
||
subtitle="Выбор типа операции в цепочке"
|
||
maxWidth="sm"
|
||
footer={
|
||
<div className="flex gap-2 w-full justify-end">
|
||
<Button variant="secondary" onClick={() => setIsNewNodeModalOpen(false)}>
|
||
Отмена
|
||
</Button>
|
||
<Button variant="primary" onClick={handleAddNode}>
|
||
Добавить в сценарий
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<div className="space-y-3 text-xs">
|
||
<div className="space-y-1">
|
||
<label className="font-semibold text-krona-primary">Тип узла:</label>
|
||
<select
|
||
value={nodeType}
|
||
onChange={(e) => setNodeType(e.target.value as any)}
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green"
|
||
>
|
||
<option value="action">Действие (Action)</option>
|
||
<option value="condition">Условие (Condition)</option>
|
||
<option value="ai">ИИ-агент (Krona AI)</option>
|
||
<option value="trigger">Триггер (Trigger)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="font-semibold text-krona-primary">Название шага:</label>
|
||
<input
|
||
type="text"
|
||
value={nodeTitle}
|
||
onChange={(e) => setNodeTitle(e.target.value)}
|
||
placeholder="Например: Отправить уведомление в Telegram администратору"
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="font-semibold text-krona-primary">Описание / Логика:</label>
|
||
<textarea
|
||
rows={2}
|
||
value={nodeDesc}
|
||
onChange={(e) => setNodeDesc(e.target.value)}
|
||
placeholder="Укажите условия или параметры вызова..."
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green resize-none"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|