478 lines
23 KiB
TypeScript
478 lines
23 KiB
TypeScript
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>
|
||
);
|
||
};
|