krona-front/src/features/ai/AiCenterModule.tsx

478 lines
23 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 {
Sparkles,
Bot,
Send,
CheckCircle2,
AlertTriangle,
FileText,
ExternalLink,
Terminal,
} from 'lucide-react';
import { ulid } from '../../shared/lib/ulid';
interface AiAgent {
id: string;
name: string;
role: string;
description: string;
tools: string[];
status: 'active' | 'busy' | 'idle';
tasksCompleted: number;
}
const AI_AGENTS: AiAgent[] = [
{
id: 'orch',
name: 'Главный Оркестратор',
role: 'Orchestrator',
description: 'Декомпозирует бизнес-цели и распределяет задачи между агентами-специалистами',
tools: ['Task Delegator', 'Cross-module Context', 'Guardian Policy'],
status: 'active',
tasksCompleted: 342,
},
{
id: 'analyst',
name: 'Бизнес-аналитик',
role: 'BI & Financial Analyst',
description: 'Анализирует чеки, food cost, маржинальность, динамику продаж и выявляет аномалии',
tools: ['SQL Query', 'Revenue Forecast', 'ABC Matrix', 'Shift Audit'],
status: 'active',
tasksCompleted: 819,
},
{
id: 'marketer',
name: 'Бизнес-маркетолог',
role: 'Growth & Retention',
description: 'Генерирует контент-планы, офферы для рассылок, анализирует сегменты клиентов в зоне риска',
tools: ['Promo Engine', 'Telegram Bot Sender', 'Copywriter', 'Cohort Analysis'],
status: 'active',
tasksCompleted: 420,
},
{
id: 'warehouse_agent',
name: 'Складской су-шеф',
role: 'Supply Chain Agent',
description: 'Мониторит критические остатки, сроки годности и формирует авто-заказы поставщикам',
tools: ['Supplier Auto-Order', 'Waste Tracker', 'Inventory Audit'],
status: 'active',
tasksCompleted: 612,
},
];
const PRECONFIGURED_PROMPTS = [
{
title: 'Food Cost и маржа за сегодня',
agentId: 'analyst',
prompt: 'Проанализируй Food Cost и маржинальность блюд текущей смены. Есть ли отклонения от нормы?',
tools: ['ReadPosOrders()', 'CalculateFoodCostVariance()', 'AuditStopList()'],
result: 'Анализ завершен: фактический Food Cost составил 29.4% (норма 30.0%). Позиция «Том Ям» показала маржу 68.5% из-за стабильных цен на тигровые креветки. Отклонений по граммовкам не зафиксировано.',
artifactTitle: 'Отчет маржинальности смены #84',
},
{
title: 'Кампания возврата спящих гостей',
agentId: 'marketer',
prompt: 'Сформируй триггерную рассылку для гостей, не заказывавших более 30 дней. Предложи оффер с высокой конверсией.',
tools: ['ScanInactiveCustomers()', 'DraftPromoOffer()', 'EstimateBudget()'],
result: 'Сегмент «Спящие клиенты» насчитывает 248 контактов. Сформирован персонализированный оффер: «Сет Осенний со скидкой 20% + ролл Калифорния в подарок». Ожидаемый отклик: 18-22%.',
artifactTitle: 'Проект кампании «Осенний возврат 2026»',
},
{
title: 'Контроль дефицита на складе',
agentId: 'warehouse_agent',
prompt: 'Проверь физические остатки ингредиентов с учетом броней на вечер. Какие позиции нужно заказать?',
tools: ['AuditStockBalances()', 'CheckReservationBOM()', 'CreatePurchaseDraft()'],
result: 'Критический остаток: «Сливочный сыр Cre действия» остался 4.2 кг при вечернем прогнозе 6.5 кг. Сформирован черновик закупки #PO-109 на 15 кг у поставщика «СибМолоко» со скидкой 5%.',
artifactTitle: 'Черновик заявки поставщику #PO-109',
},
{
title: 'Аудит графика и переработок',
agentId: 'orch',
prompt: 'Проверь расписание смен сотрудников на текущую неделю. Есть ли нарушения нормативов ТК РФ?',
tools: ['ReadEmployeeShifts()', 'CheckLaborRegulations()', 'SuggestScheduleOptimization()'],
result: 'Выявлен 1 конфликт: сотрудник Артем Васильев назначен на смену 14 часов в пятницу (норма до 12 ч). Рекомендовано разделить смену с подменным официантом или сместить начало на 11:00.',
artifactTitle: 'Акт проверки графиков ТК РФ',
},
];
interface ChatMessage {
id: string;
sender: 'user' | 'ai';
text: string;
time: string;
toolsUsed?: string[];
artifact?: { title: string; type: string };
}
export const AiCenterModule: React.FC = () => {
const { showToast } = useToast();
const [activeAgentId, setActiveAgentId] = useState<string>('orch');
const [promptInput, setPromptInput] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [activeToolIndex, setActiveToolIndex] = useState<number>(-1);
const [currentToolsStep, setCurrentToolsStep] = useState<string[]>([]);
const [artifactModalData, setArtifactModalData] = useState<{ title: string; content: string } | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: 'm-1',
sender: 'ai',
text: 'Здравствуйте! Я Главный Оркестратор Кроны. Моя команда специализированных агентов (Аналитик, Маркетолог, Складской су-шеф) готова помочь вам оптимизировать операционные процессы, найти точки роста выручки и предотвратить кассовые разрывы.',
time: '12:00',
},
]);
const activeAgent = AI_AGENTS.find((a) => a.id === activeAgentId) || AI_AGENTS[0];
const handleSelectPreconfigured = (p: typeof PRECONFIGURED_PROMPTS[0]) => {
setActiveAgentId(p.agentId);
runPromptFlow(p.prompt, p.tools, p.result, p.artifactTitle);
};
const handleSendCustomPrompt = (e: React.FormEvent) => {
e.preventDefault();
if (!promptInput.trim() || isProcessing) return;
const userText = promptInput.trim();
setPromptInput('');
runPromptFlow(
userText,
['ReadWorkspaceContext()', 'QueryDatabaseEngine()', 'SynthesizeBusinessStrategy()'],
`Агент «${activeAgent.name}» проанализировал ваш запрос «${userText}». Все релевантные данные сверены с текущим состоянием касс, складов и CRM платформы. Предложен проект решения.`,
`Аналитическая записка: ${userText.slice(0, 30)}...`
);
};
const runPromptFlow = (
userText: string,
tools: string[],
resultText: string,
artifactTitle: string
) => {
const userMsg: ChatMessage = {
id: `u-${ulid()}`,
sender: 'user',
text: userText,
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }),
};
setMessages((prev) => [...prev, userMsg]);
setIsProcessing(true);
setCurrentToolsStep(tools);
setActiveToolIndex(0);
setTimeout(() => {
setActiveToolIndex(1);
}, 450);
setTimeout(() => {
setActiveToolIndex(2);
}, 900);
setTimeout(() => {
setIsProcessing(false);
setActiveToolIndex(-1);
const aiMsg: ChatMessage = {
id: `ai-${Date.now()}`,
sender: 'ai',
text: resultText,
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }),
toolsUsed: tools,
artifact: { title: artifactTitle, type: 'report' },
};
setMessages((prev) => [...prev, aiMsg]);
showToast({
title: 'ИИ-задача выполнена',
message: `Агент «${activeAgent.name}» завершил выполнение задачи`,
type: 'success',
});
}, 1350);
};
return (
<div className="space-y-4 animate-fade-in">
{/* ⚠️ DEMO ONLY & SIMULATED AI BANNER */}
<div className="p-3.5 rounded-xl border border-purple-400/50 bg-purple-500/10 text-purple-900 dark:text-purple-200 flex flex-col sm:flex-row sm:items-center justify-between gap-2 shadow-xs">
<div className="flex items-center gap-2.5">
<AlertTriangle className="w-5 h-5 text-purple-600 shrink-0" />
<div className="text-xs">
<span className="font-bold block">
Демонстрационный контур ИИ (Simulated AI Engine)
</span>
<span className="text-purple-800 dark:text-purple-300">
ИИ-агенты симулируют автономное выполнение бизнес-сценариев, цепочки вызова инструментов (tool calls) и генерацию артефактов без реальных LLM API.
</span>
</div>
</div>
<MaturityBadge level="demo" size="sm" className="shrink-0" />
</div>
{/* Header */}
<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">
✦ Krona AI Center & Автономная бизнес-команда
</h2>
<MaturityBadge level="demo" size="sm" />
<Badge variant="gold" size="sm">Крона Модель v1.8 Flash</Badge>
</div>
<p className="text-xs text-krona-secondary mt-0.5">
Специализированные агенты с прямым доступом к инструментам платформы, кассам и складским журналам
</p>
</div>
<div className="flex items-center gap-2 text-xs">
<Badge variant="green" size="md">4 агента активны</Badge>
<Badge variant="neutral" size="md">Задач за день: 2 195</Badge>
</div>
</div>
{/* Pre-configured Prompts Row */}
<div className="space-y-2">
<span className="text-[11px] font-bold text-krona-secondary uppercase tracking-wider block">
Быстрые бизнес-запросы для руководства:
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2.5">
{PRECONFIGURED_PROMPTS.map((p, idx) => (
<button
key={idx}
disabled={isProcessing}
onClick={() => handleSelectPreconfigured(p)}
className="p-3 text-left rounded-xl border border-krona-border bg-krona-surface hover:border-krona-gold hover:bg-krona-gold/5 transition-all text-xs space-y-1 group disabled:opacity-50"
>
<div className="font-bold text-krona-primary group-hover:text-krona-gold transition-colors flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5 text-krona-gold" />
<span>{p.title}</span>
</div>
<p className="text-[11px] text-krona-secondary line-clamp-2">
{p.prompt}
</p>
</button>
))}
</div>
</div>
{/* Main AI Workspace Grid */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4">
{/* Left: AI Agents Roster */}
<div className="lg:col-span-4 bg-krona-surface border border-krona-border rounded-xl p-3.5 space-y-3 shadow-krona-soft">
<span className="text-xs font-bold text-krona-primary uppercase tracking-wider block pb-2 border-b border-krona-border">
Команда ИИ-агентов
</span>
<div className="space-y-2">
{AI_AGENTS.map((agent) => {
const isSelected = agent.id === activeAgentId;
return (
<div
key={agent.id}
onClick={() => setActiveAgentId(agent.id)}
className={`p-3 rounded-xl border transition-all cursor-pointer select-none space-y-2 ${
isSelected
? 'border-krona-gold bg-krona-gold/5 shadow-xs'
: 'border-krona-border bg-krona-workspace hover:border-krona-border-strong'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-krona-surface border border-krona-border text-krona-gold">
<Bot className="w-4 h-4" />
</div>
<div>
<h4 className="text-xs font-bold text-krona-primary">{agent.name}</h4>
<span className="text-[10px] text-krona-secondary">{agent.role}</span>
</div>
</div>
<Badge variant="green" size="sm">Online</Badge>
</div>
<p className="text-[11px] text-krona-secondary leading-snug">
{agent.description}
</p>
<div className="flex items-center justify-between pt-1 border-t border-krona-border/50 text-[10px] text-krona-secondary">
<span>Инструментов: {agent.tools.length}</span>
<span className="font-mono font-bold text-krona-primary">{agent.tasksCompleted} задач</span>
</div>
</div>
);
})}
</div>
</div>
{/* Right: Interactive Chat & Tool Calls Progress */}
<div className="lg:col-span-8 bg-krona-surface border border-krona-border rounded-xl shadow-krona-soft flex flex-col h-[540px] overflow-hidden">
{/* Active Agent Banner */}
<div className="p-3 border-b border-krona-border bg-krona-workspace flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
<span className="font-bold text-krona-primary">
Чат с агентом: {activeAgent.name} ({activeAgent.role})
</span>
</div>
<span className="text-[11px] text-krona-secondary">
Память контекста: Активна
</span>
</div>
{/* Messages Thread */}
<div className="flex-1 p-4 overflow-y-auto space-y-3.5 text-xs">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex flex-col ${msg.sender === 'user' ? 'items-end' : 'items-start'}`}
>
<div
className={`p-3.5 rounded-xl max-w-[82%] leading-relaxed shadow-xs space-y-2 ${
msg.sender === 'user'
? 'bg-[#2F684C] text-[#F8F6F0] rounded-br-none'
: 'bg-krona-workspace border border-krona-border text-krona-primary rounded-bl-none'
}`}
>
<p>{msg.text}</p>
{/* Simulated tool calls badges */}
{msg.toolsUsed && msg.toolsUsed.length > 0 && (
<div className="pt-2 border-t border-krona-border/50 space-y-1">
<span className="text-[10px] text-krona-secondary font-semibold uppercase block">
Выполненные шаги инструментов (Tool Calls):
</span>
<div className="flex flex-wrap gap-1">
{msg.toolsUsed.map((tool, idx) => (
<span
key={idx}
className="inline-flex items-center gap-1 font-mono text-[10px] bg-krona-surface border border-krona-border px-2 py-0.5 rounded text-krona-primary"
>
<CheckCircle2 className="w-2.5 h-2.5 text-emerald-600" />
{tool}
</span>
))}
</div>
</div>
)}
{/* Clickable Artifact Output */}
{msg.artifact && (
<div className="pt-2 border-t border-krona-border/50">
<button
type="button"
onClick={() =>
setArtifactModalData({
title: msg.artifact!.title,
content: msg.text,
})
}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-krona-gold/15 text-krona-gold-text border border-krona-gold/30 hover:bg-krona-gold/25 transition-colors font-bold text-xs"
>
<FileText className="w-3.5 h-3.5 text-krona-gold" />
<span>Открыть сформированный артефакт: «{msg.artifact.title}»</span>
<ExternalLink className="w-3 h-3 ml-1 opacity-70" />
</button>
</div>
)}
</div>
<span className="text-[10px] text-krona-secondary mt-1 px-1">{msg.time}</span>
</div>
))}
{/* Simulated Live Tool Call Progress */}
{isProcessing && (
<div className="p-3 bg-krona-workspace border border-krona-gold/40 rounded-xl space-y-2 max-w-[80%] animate-pulse">
<div className="flex items-center gap-2 font-bold text-xs text-krona-gold">
<Terminal className="w-4 h-4" />
<span>Агент «{activeAgent.name}» выполняет цепочку вызовов инструментов...</span>
</div>
<div className="space-y-1.5 font-mono text-[11px] text-krona-secondary">
{currentToolsStep.map((tool, idx) => (
<div key={idx} className="flex items-center gap-2">
{idx < activeToolIndex ? (
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 shrink-0" />
) : idx === activeToolIndex ? (
<span className="w-3.5 h-3.5 rounded-full border-2 border-krona-gold border-t-transparent animate-spin shrink-0"></span>
) : (
<span className="w-3.5 h-3.5 rounded-full bg-krona-border/60 shrink-0"></span>
)}
<span className={idx === activeToolIndex ? 'text-krona-primary font-bold' : ''}>
{idx + 1}. {tool}
</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Prompt input form */}
<div className="p-3 border-t border-krona-border bg-krona-workspace/50">
<form onSubmit={handleSendCustomPrompt} className="flex items-center gap-2">
<input
type="text"
value={promptInput}
disabled={isProcessing}
onChange={(e) => setPromptInput(e.target.value)}
placeholder={`Спросите агента «${activeAgent.name}» (например: "Рассчитай Food Cost за вчера")...`}
className="flex-1 bg-krona-surface border border-krona-border rounded-lg px-3 py-2 text-xs text-krona-primary placeholder:text-krona-secondary focus:outline-none focus:border-krona-green disabled:opacity-50"
/>
<Button
type="submit"
variant="gold"
size="sm"
disabled={isProcessing || !promptInput.trim()}
className="px-3 text-xs"
>
<Send className="w-3.5 h-3.5 mr-1" />
Спросить AI
</Button>
</form>
</div>
</div>
</div>
{/* Artifact Details Modal */}
{artifactModalData && (
<Modal
isOpen={Boolean(artifactModalData)}
onClose={() => setArtifactModalData(null)}
title={artifactModalData.title}
subtitle="Сгенерированный артефакт ИИ-агента Кроны"
maxWidth="md"
footer={
<div className="flex gap-2 w-full justify-between">
<Button
variant="secondary"
size="sm"
onClick={() => {
showToast({
title: 'Рекомендации применены',
message: 'Настройки применены к операционному контуру заведения',
type: 'success',
});
setArtifactModalData(null);
}}
>
Применить рекомендации в системе
</Button>
<Button variant="primary" size="sm" onClick={() => setArtifactModalData(null)}>
Закрыть
</Button>
</div>
}
>
<div className="space-y-3 text-xs">
<div className="p-3.5 bg-krona-workspace rounded-xl border border-krona-border font-mono text-xs leading-relaxed space-y-2">
<div className="font-bold text-krona-primary pb-1 border-b border-krona-border/60 flex items-center justify-between">
<span>{artifactModalData.title}</span>
<Badge variant="gold" size="sm">Verified AI Plan</Badge>
</div>
<p className="text-krona-primary">{artifactModalData.content}</p>
</div>
</div>
</Modal>
)}
</div>
);
};