import React, { useState, useEffect, useRef } from 'react'; import { NAV_ITEMS } from '../constants'; import { Bot, X, Menu, Send, MessageSquare } from 'lucide-react'; import { UserRole } from '../types'; import { apiClient, backendApi } from '../services/apiClient'; import { ROLE_ACCESS } from '../constants/roleAccess'; interface NavigationProps { activeTab: string; setActiveTab: (id: string) => void; currentUserRole: UserRole; /** Если передан, используется вместо ROLE_ACCESS по роли (из /auth/me) */ allowedSections?: string[]; } const PRIMARY_COUNT = 4; interface AIConversation { id: number; title: string | null; createdAt: string; } interface AIMessage { id?: number; role: string; content: string; toolCallsJson?: unknown; createdAt?: string; } export const Navigation: React.FC = ({ activeTab, setActiveTab, currentUserRole, allowedSections }) => { const [showAIModal, setShowAIModal] = useState(false); const [showMoreSheet, setShowMoreSheet] = useState(false); const [aiEnabled, setAiEnabled] = useState(false); const [aiConversations, setAiConversations] = useState([]); const [aiConversationId, setAiConversationId] = useState(null); const [aiMessages, setAiMessages] = useState([]); const [aiInput, setAiInput] = useState(''); const [aiLoading, setAiLoading] = useState(false); const [aiError, setAiError] = useState(null); const aiMessagesEndRef = useRef(null); const allowedTabs = allowedSections && allowedSections.length > 0 ? (allowedSections.includes('all') ? ['dashboard', 'objects', 'requests', 'pr', 'finance', 'legal', 'development', 'hr', 'office', 'admin'] : allowedSections) : (ROLE_ACCESS[currentUserRole] || []); const visibleItems = NAV_ITEMS.filter(item => { if (allowedTabs.includes('all')) return true; return allowedTabs.includes(item.id); }); const primaryItems = visibleItems.slice(0, PRIMARY_COUNT); const middleIndex = Math.ceil(visibleItems.length / 2); const leftItems = visibleItems.slice(0, middleIndex); const rightItems = visibleItems.slice(middleIndex); const closeSheetAndNavigate = (id: string) => { setActiveTab(id); setShowMoreSheet(false); }; const fetchAiStatus = () => { backendApi.getAIChatStatus().then((data) => setAiEnabled(data.enabled === true)).catch(() => setAiEnabled(false)); }; useEffect(() => { fetchAiStatus(); }, []); // Обновить нижнее меню (кнопка ИИ) только при смене настроек ИИ в Панели управления useEffect(() => { const onAiStatusChanged = () => fetchAiStatus(); window.addEventListener('mkd-ai-status-changed', onAiStatusChanged); return () => window.removeEventListener('mkd-ai-status-changed', onAiStatusChanged); }, []); useEffect(() => { if (!showAIModal || !aiEnabled) return; apiClient.get('/ai/conversations').then(setAiConversations).catch(() => setAiConversations([])); }, [showAIModal, aiEnabled]); useEffect(() => { if (!showAIModal || !aiEnabled || aiConversationId == null) { setAiMessages([]); return; } apiClient.get(`/ai/conversations/${aiConversationId}/messages`).then(setAiMessages).catch(() => setAiMessages([])); }, [showAIModal, aiEnabled, aiConversationId]); useEffect(() => { aiMessagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [aiMessages]); const startNewChat = () => { setAiConversationId(null); setAiMessages([]); setAiInput(''); setAiError(null); }; const sendAIMessage = async () => { const text = aiInput.trim(); if (!text || aiLoading) return; setAiError(null); const userMsg: AIMessage = { role: 'user', content: text }; setAiMessages((prev) => [...prev, userMsg]); setAiInput(''); setAiLoading(true); try { const res = await apiClient.post<{ conversationId: number; assistantMessage: string; toolResults?: { toolName: string; success: boolean; error?: string }[]; }>('/ai/chat', { conversationId: aiConversationId, message: text }); setAiConversationId(res.conversationId); setAiMessages((prev) => [...prev, { role: 'assistant', content: res.assistantMessage }]); setAiConversations((prev) => { if (prev.some((c) => c.id === res.conversationId)) return prev; return [{ id: res.conversationId, title: null, createdAt: new Date().toISOString() }, ...prev]; }); } catch (e: unknown) { const errMsg = e && typeof e === 'object' && 'message' in e ? String((e as { message: string }).message) : 'Ошибка отправки'; setAiError(errMsg); setAiMessages((prev) => prev.filter((m) => m !== userMsg)); } finally { setAiLoading(false); } }; return ( <> {/* ИИ-чат: только если ИИ включён в настройках */} {aiEnabled && showAIModal && (
setShowAIModal(false)}>
e.stopPropagation()}>

ИИ-помощник

{aiConversations.map((c) => ( ))}
{aiMessages.length === 0 && !aiLoading && (

Напишите, что нужно сделать — например: «Покажи список домов» или «Создай счёт на оплату».

)} {aiMessages.map((msg, i) => (
{msg.content}
))} {aiLoading && (
Думаю…
)}
{aiError &&
{aiError}
}
setAiInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAIMessage(); } }} placeholder="Сообщение..." className="flex-1 rounded-xl border border-slate-200 px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" disabled={aiLoading} />
)} {/* Mobile: 4 кнопки + Ещё, снизу выезжает панель (свайп вверх) */}
{primaryItems.map((item) => ( setActiveTab(item.id)} /> ))}
{/* Bottom sheet: все разделы + ИИ */} {showMoreSheet && ( <>
setShowMoreSheet(false)} aria-hidden="true" />
e.stopPropagation()} >
{visibleItems.map((item) => { const Icon = item.icon; return ( ); })} {aiEnabled && ( )}
)} {/* Desktop: полный бар (слева / центр ИИ при включённом ИИ / справа) */}
{leftItems.map((item) => ( setActiveTab(item.id)} /> ))}
{aiEnabled && (
)}
{rightItems.map((item) => ( setActiveTab(item.id)} /> ))}
); }; const NavButton = ({ item, isActive, onClick }: any) => { const Icon = item.icon; return ( ); };