const { useState, useCallback, useEffect, useMemo, useRef } = React; const { createRoot } = ReactDOM; const defaultCategories = [ { label: "по вопросу: найти таблицу в HR контуре", endpoint: "/api/hr/table-search", }, { label: "по вопросу: найти код в HR контуре", endpoint: "/api/hr/code-search", }, { label: "по введённым таблицам найти все связки", endpoint: "/api/hr/table-links", }, { label: "по запросу сгенерировать sql-vip", endpoint: "/api/hr/sql-vip", }, ]; const normalizeCategoryItem = (category) => { if (!category) return { label: "", endpoint: null }; if (typeof category === "string") { return { label: category, endpoint: null }; } return { label: category.label || "", endpoint: category.endpoint || null, }; }; const normalizeCategoryList = (categories) => (Array.isArray(categories) ? categories.map(normalizeCategoryItem) : []); const ratingOptions = [ { value: 1, label: "Плохо", color: "rgba(220, 38, 38, 0.5)" }, { value: 2, label: "Средне", color: "rgba(245, 158, 11, 0.5)" }, { value: 3, label: "Хорошо", color: "rgba(16, 185, 129, 0.5)" }, ]; function App() { const [token, setToken] = useState(null); const [username, setUsername] = useState(""); const [loginForm, setLoginForm] = useState({ username: "", password: "" }); const [authError, setAuthError] = useState(""); const [isAuthenticating, setIsAuthenticating] = useState(false); const [chatList, setChatList] = useState([]); const [activeChatId, setActiveChatId] = useState(null); const [input, setInput] = useState(""); const [isSending, setIsSending] = useState(false); const [isRatingOpen, setIsRatingOpen] = useState(false); const [availableCategories, setAvailableCategories] = useState(() => normalizeCategoryList(defaultCategories)); const [isSelectingCategories, setIsSelectingCategories] = useState(false); const [analytics, setAnalytics] = useState(null); const [view, setView] = useState("chat"); const [historyPage, setHistoryPage] = useState(0); const [historyPageSizeSelection, setHistoryPageSizeSelection] = useState(10); const [historyUsernameFilter, setHistoryUsernameFilter] = useState(""); const [historyChatTypeFilter, setHistoryChatTypeFilter] = useState(""); const messagesContainerRef = useRef(null); const overallMetrics = analytics?.overall ?? null; const ratingReport = overallMetrics?.ratingReport; const summaryRatingBreakdown = ratingReport?.breakdown ?? []; const overallMessageCount = Object.values(overallMetrics?.messageCounts ?? {}).reduce((sum, value) => sum + value, 0); const totalChats = overallMetrics?.totalChats ?? 0; const openChats = overallMetrics?.openChats ?? 0; const closedChats = overallMetrics?.closedChats ?? 0; const userSummaries = analytics?.userSummaries ?? {}; const availableUsers = analytics?.availableUsers ?? []; const availableChatTypes = (analytics?.availableChatTypes ?? []).filter(Boolean); const messageHistoryPayload = analytics?.messageHistory ?? {}; const historyItems = messageHistoryPayload.items ?? []; const historyPageIndex = messageHistoryPayload.page ?? 0; const historyPageCount = Math.max(messageHistoryPayload.pageCount ?? 1, 1); const historyTotal = messageHistoryPayload.total ?? historyItems.length; const historyPageSize = messageHistoryPayload.pageSize ?? historyPageSizeSelection; const historyStartIndex = historyTotal ? historyPageIndex * historyPageSize + 1 : 0; const historyEndIndex = historyTotal ? Math.min(historyTotal, historyStartIndex + historyItems.length - 1) : 0; const canHistoryPrev = historyPageIndex > 0; const canHistoryNext = historyPageIndex < historyPageCount - 1; const historyRangeLabel = historyTotal ? `${historyStartIndex}-${historyEndIndex} из ${historyTotal}` : "Нет записей"; const overallCategoryUsage = overallMetrics?.categoryUsage ?? []; const recentEvents = analytics?.recentEvents ?? []; const topChats = analytics?.topChats ?? []; const userSummaryEntries = Object.entries(userSummaries); const isAdmin = username === "vip-admin"; const pagedMessageHistory = historyItems; const showMessageHistory = historyItems.length > 0; const showHistoryControls = historyPageCount > 1; const headers = useMemo(() => (token ? { Authorization: `Bearer ${token}` } : {}), [token]); const activeChat = useMemo(() => chatList.find((chat) => chat.id === activeChatId) ?? chatList[0], [chatList, activeChatId]); const chatHistory = activeChat?.messages ?? []; const chatHistoryLength = chatHistory.length; const isChatWritable = Boolean(activeChat && !activeChat.closed); const fetchCategories = useCallback(async () => { if (!token) return; try { const response = await fetch("/api/categories", { headers }); if (!response.ok) throw new Error("Не удалось загрузить категории"); const data = await response.json(); const normalized = normalizeCategoryList(data.categories); setAvailableCategories(normalized.length ? normalized : normalizeCategoryList(defaultCategories)); } catch (error) { console.error(error); setAvailableCategories(normalizeCategoryList(defaultCategories)); } }, [headers, token]); const fetchChats = useCallback(async () => { if (!token) return; try { const response = await fetch("/api/chat/list", { headers }); if (!response.ok) throw new Error("Не удалось загрузить чаты"); const data = await response.json(); setChatList(data.chats); setActiveChatId(data.chats.find((chat) => !chat.closed)?.id ?? data.chats[0]?.id ?? null); } catch (error) { console.error(error); } }, [headers, token]); const fetchAnalytics = useCallback(async () => { if (!token || !isAdmin) return; try { const query = new URLSearchParams(); query.set("history_page", historyPage); query.set("history_page_size", historyPageSizeSelection); if (historyUsernameFilter) query.set("history_username", historyUsernameFilter); if (historyChatTypeFilter) query.set("history_chat_type", historyChatTypeFilter); const response = await fetch(`/api/analytics?${query.toString()}`, { headers }); if (!response.ok) throw new Error("Не удалось загрузить аналитику"); const data = await response.json(); setAnalytics(data); } catch (error) { console.error(error); setAnalytics(null); } }, [headers, historyPage, historyPageSizeSelection, historyUsernameFilter, historyChatTypeFilter, isAdmin, token]); useEffect(() => { if (!token) return; fetchCategories(); fetchChats(); if (isAdmin) { fetchAnalytics(); } }, [token, fetchCategories, fetchChats, fetchAnalytics, isAdmin]); useEffect(() => { if (!isAdmin && view === "analytics") { setView("chat"); } }, [isAdmin, view]); useEffect(() => { setHistoryPage(0); }, [historyPageSizeSelection, historyUsernameFilter, historyChatTypeFilter]); useEffect(() => { if (historyPage !== historyPageIndex) { setHistoryPage(historyPageIndex); } }, [historyPageIndex, historyPage]); useEffect(() => { setHistoryPage((prev) => Math.min(prev, historyPageCount - 1)); }, [historyPageCount]); useEffect(() => { if (!isAdmin) { setAnalytics(null); } }, [isAdmin]); useEffect(() => { if (view !== "chat") return; const container = messagesContainerRef.current; if (!container) return; window.requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; }); }, [chatHistoryLength, isSending, activeChatId, view]); const handleLogin = useCallback( async (event) => { event.preventDefault(); setIsAuthenticating(true); setAuthError(""); try { const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(loginForm), }); if (!response.ok) { const data = await response.json(); throw new Error(data.detail || "Неверные данные"); } const data = await response.json(); setToken(data.token); setUsername(data.username); setLoginForm({ username: "", password: "" }); } catch (error) { setAuthError(error.message); console.error(error); } finally { setIsAuthenticating(false); } }, [loginForm], ); const handleLogout = useCallback(() => { setToken(null); setUsername(""); setChatList([]); setActiveChatId(null); setAnalytics(null); }, []); const handleCreateChat = useCallback( async (category) => { const selected = normalizeCategoryItem(category); if (!selected.label || !token) return; setIsSelectingCategories(false); const resolvedEndpoint = selected.endpoint || defaultCategories[0]?.endpoint || null; const payload = { chat_id: Date.now(), title: `Сеанс ${new Date().toLocaleTimeString()}`, categories: [selected.label], endpoint: resolvedEndpoint, }; try { const response = await fetch("/api/chat/create", { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify(payload), }); if (!response.ok) throw new Error("Не удалось создать чат"); const data = await response.json(); setChatList((prev) => [data.chat, ...(prev.filter((chat) => chat.id !== data.chat.id))]); setActiveChatId(data.chat.id); fetchAnalytics(); } catch (error) { console.error(error); } }, [headers, token, fetchAnalytics], ); const handleSend = useCallback( async (event) => { event.preventDefault(); if (!input.trim() || !activeChat || !isChatWritable) return; const messageContent = input.trim(); setInput(""); setIsSending(true); try { const response = await fetch("/api/chat/respond", { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ chat_id: activeChat.id, message: messageContent, categories: activeChat.categories, endpoint: activeChat.endpoint, }), }); if (!response.ok) throw new Error("Не удалось отправить сообщение"); await response.json(); fetchChats(); fetchAnalytics(); } catch (error) { console.error(error); } finally { setIsSending(false); } }, [activeChat, fetchChats, fetchAnalytics, headers, input, isChatWritable], ); const handleRateChat = useCallback( async (value) => { if (!activeChat) return; try { const response = await fetch("/api/chat/rate", { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ chat_id: activeChat.id, rating: value }), }); if (!response.ok) throw new Error("Не удалось оценить чат"); await response.json(); setIsRatingOpen(false); fetchChats(); fetchAnalytics(); } catch (error) { console.error(error); } }, [activeChat, fetchChats, fetchAnalytics, headers], ); if (!token) { return (

VIP AI (experimental)

{authError &&

{authError}

}
); } const ratingBadgeColor = activeChat?.rating ? ratingOptions.find((option) => option.value === activeChat.rating)?.color : "rgba(37, 99, 235, 0.15)"; return (

{isAdmin && ( )}
{view === "analytics" && isAdmin ? (

Обзор по данным

{analytics ? ( <>

Всего сеансов

{totalChats}

Открытые

{openChats}

Завершённые

{closedChats}

Сообщения

{overallMessageCount}

Категории

{overallCategoryUsage.length ? (
    {overallCategoryUsage.map((entry) => (
  • {entry.category} {entry.count}
  • ))}
) : (

Категории пока не используются

)}

Недавние события

{recentEvents.length ? (
    {recentEvents.map((event) => (
  • {event.type} {event.username ? ` · ${event.username}` : ""} {new Date(event.timestamp).toLocaleString()}
  • ))}
) : (

Событий пока нет

)}

Лучшие чаты

{topChats.length ? (
    {topChats.map((chat) => (
  • {chat.title} · {chat.username} {chat.messages} сообщений
  • ))}
) : (

Пока нет активных чатов

)}

Отчёт по оценке

{ratingReport && summaryRatingBreakdown.length ? (

Средняя оценка: {ratingReport.average?.toFixed(1) ?? "—"}

    {summaryRatingBreakdown.map((entry) => (
  • Оценка {entry.rating}/3 {entry.count} раз · {entry.share}%
  • ))}
) : (

Оценок пока нет

)}

Данные по каждому пользователю

{userSummaryEntries.length ? (
    {userSummaryEntries.map(([user, summary]) => (
  • {user}

    Сеансов: {summary.totalChats} · Открытых: {summary.openChats} · Завершённых: {summary.closedChats}

    Сообщений: {Object.values(summary.messageCounts || {}).reduce((sum, value) => sum + value, 0)}

  • ))}
) : (

Нет данных по пользователям

)}
) : (

Данные ещё не получены

)}
) : ( <>
{chatHistory.map((message) => (
{message.role === "assistant" ? "AI" : "Вы"}

{message.content.split('\n').map((line, i) => ( {line}
))}

))} {isSending && (
AI
)}