krona-front/src/features/automation/AutomationModule.tsx

413 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};