document.addEventListener("DOMContentLoaded", function () { const API_BASE = "https://assistentzerocoder.ru"; // Базовый URL API const HEALTH_ENDPOINT = `${API_BASE}/healthz`; const chatIcon = document.getElementById("chat-icon"); const chatWidget = document.getElementById("chat-widget"); const chatOverlay = document.getElementById("chat-overlay"); const chatMessages = document.getElementById("chat-messages"); const userInput = document.getElementById("user-message"); const sendButton = document.getElementById("send-button"); const closeChatButton = document.getElementById("close-chat"); if (!chatIcon || !chatWidget || !chatOverlay || !chatMessages || !userInput || !sendButton || !closeChatButton) { console.error("Ошибка: Один или несколько элементов чата не найдены."); return; } let isProcessing = false; // Хелперы для надёжного управления стилями c !important function setDisplayImportant(element, value) { try { element.style.setProperty("display", value, "important"); } catch (_) {} } function setOpacityImportant(element, value) { try { element.style.setProperty("opacity", String(value), "important"); } catch (_) {} } function setVisibilityImportant(element, value) { try { element.style.setProperty("visibility", value, "important"); } catch (_) {} } // По умолчанию скрываем иконку: она появится только если сервер здоров setDisplayImportant(chatIcon, "none"); // На случай, если CSS не загрузился: принудительно скрываем виджет и оверлей try { setDisplayImportant(chatWidget, "none"); setOpacityImportant(chatWidget, 0); setDisplayImportant(chatOverlay, "none"); setOpacityImportant(chatOverlay, 0); } catch (_) {} // Динамически подключаем CSS, если он не был добавлен в try { const cssHref = `${API_BASE}/static/chat-style.css`; const alreadyLinked = Array.from(document.styleSheets || []).some(ss => { try { return ss.href && ss.href.indexOf(cssHref) !== -1; } catch (_) { return false; } }); if (!alreadyLinked) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = cssHref; link.crossOrigin = "anonymous"; document.head.appendChild(link); } } catch (_) {} // Грузим DOMPurify для санитизации Markdown, если не подключен try { if (!window.DOMPurify) { const s = document.createElement("script"); s.src = "https://cdn.jsdelivr.net/npm/dompurify@3.0.9/dist/purify.min.js"; s.defer = true; document.head.appendChild(s); } } catch (_) {} function shouldForceIcon() { try { if (window.ASSISTANT_SHOW_ICON === true) return true; const params = new URLSearchParams(window.location.search || ""); return params.get("showChatIcon") === "1"; } catch (_) { return false; } } // Функция открытия чата function openChat() { setDisplayImportant(chatWidget, "flex"); setDisplayImportant(chatOverlay, "block"); setVisibilityImportant(chatWidget, "visible"); setVisibilityImportant(chatOverlay, "visible"); setTimeout(() => { setOpacityImportant(chatWidget, 1); setOpacityImportant(chatOverlay, 0.5); }, 10); try { chatWidget.setAttribute("aria-hidden", "false"); } catch (_) {} try { chatOverlay.setAttribute("aria-hidden", "false"); } catch (_) {} } // Функция закрытия чата function closeChat() { setOpacityImportant(chatWidget, 0); setOpacityImportant(chatOverlay, 0); setTimeout(() => { setDisplayImportant(chatWidget, "none"); setDisplayImportant(chatOverlay, "none"); setVisibilityImportant(chatWidget, "hidden"); setVisibilityImportant(chatOverlay, "hidden"); }, 500); try { chatWidget.setAttribute("aria-hidden", "true"); } catch (_) {} try { chatOverlay.setAttribute("aria-hidden", "true"); } catch (_) {} } async function canOpenChat() { try { const res = await fetch(HEALTH_ENDPOINT, { method: "GET" }); if (!res.ok) return false; const data = await res.json(); return data.status === "ok"; } catch (e) { return false; } } // Открытие чата при клике по иконке - только если сервер здоров chatIcon.addEventListener("click", async function () { if (await canOpenChat()) { openChat(); } else { // Мягкое отключение: просто не открываем console.warn("Чат недоступен: сервер нездоров"); } }); // Закрытие по клику на затемнение вокруг виджета chatOverlay.addEventListener("click", function () { closeChat(); }); // Закрытие по Escape document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeChat(); }); // Показываем иконку при успешном health-check; если включён форс-показ — сразу показываем (async () => { if (shouldForceIcon()) { setDisplayImportant(chatIcon, "flex"); setVisibilityImportant(chatIcon, "visible"); setOpacityImportant(chatIcon, 1); } else if (await canOpenChat()) { setDisplayImportant(chatIcon, "flex"); setVisibilityImportant(chatIcon, "visible"); setOpacityImportant(chatIcon, 1); } else { setDisplayImportant(chatIcon, "none"); setOpacityImportant(chatIcon, 0); setVisibilityImportant(chatIcon, "hidden"); } })(); // Периодическая проверка здоровья раз в 30 секунд, // чтобы автоматически показать иконку, когда сервер оживёт try { setInterval(async () => { if (shouldForceIcon()) { setDisplayImportant(chatIcon, "flex"); setVisibilityImportant(chatIcon, "visible"); setOpacityImportant(chatIcon, 1); return; } if (await canOpenChat()) { setDisplayImportant(chatIcon, "flex"); setVisibilityImportant(chatIcon, "visible"); setOpacityImportant(chatIcon, 1); } else { setDisplayImportant(chatIcon, "none"); setOpacityImportant(chatIcon, 0); setVisibilityImportant(chatIcon, "hidden"); } }, 30000); } catch (_) {} // Автопоказ виджета через 45 секунд, один раз за сессию, только если сервер здоров try { if (!sessionStorage.getItem("chat_auto_opened")) { setTimeout(async () => { try { if (await canOpenChat()) { openChat(); sessionStorage.setItem("chat_auto_opened", "1"); } } catch (_) {} }, 45000); } } catch (_) {} // Закрытие чата closeChatButton.addEventListener("click", function () { closeChat(); }); // Убираем автопоказ, чтобы не ломать страницы при недоступности // Отправка сообщения: обработка клика по кнопке и нажатия Enter sendButton.addEventListener("click", sendMessage); userInput.addEventListener("keydown", function (event) { if (event.key === "Enter" && !isProcessing) { sendMessage(); } }); // Обработчик кнопок с быстрыми вопросами document.querySelectorAll(".quick-question").forEach(button => { button.addEventListener("click", function () { const quickMessage = this.getAttribute("data-message") || ""; if (!quickMessage.trim()) return; // Не отправляем пустое сообщение userInput.value = quickMessage; sendMessage(); }); }); // Функция отправки сообщения с обработкой Markdown function sendMessage() { const message = userInput.value.trim(); if (!message || isProcessing) return; // Проверка длины сообщения: если больше 700 символов, показываем предупреждение и не отправляем if (message.length > 700) { alert("Сообщение слишком большое. Пожалуйста, сократите сообщение до 700 символов."); return; } console.log("Отправляем сообщение:", message); // Добавляем сообщение пользователя в чат chatMessages.insertAdjacentHTML("beforeend", `
Вы: ${message}
`); userInput.value = ""; sendButton.disabled = true; isProcessing = true; // Анимация "Ульяна печатает..." const typingMessage = `
Ульяна: ...
`; chatMessages.insertAdjacentHTML("beforeend", typingMessage); chatMessages.scrollTop = chatMessages.scrollHeight; let dotCount = 0; const dotInterval = setInterval(() => { const dots = document.getElementById("dots"); if (dots) { dotCount = (dotCount + 1) % 4; dots.textContent = ".".repeat(dotCount); } }, 500); const threadId = sessionStorage.getItem("thread_id"); if (!threadId) { // Создаем новый thread, если его нет fetch(`${API_BASE}/start`, { method: "GET" }) .then(response => { if (!response.ok) throw new Error(`Ошибка HTTP: ${response.status}`); return response.json(); }) .then(data => { if (data.thread_id) { sessionStorage.setItem("thread_id", data.thread_id); sendChatMessage(data.thread_id, message); } else { throw new Error("Получен пустой thread_id"); } }) .catch(error => { console.error("Ошибка при создании thread:", error); clearInterval(dotInterval); document.getElementById("typing")?.remove(); sendButton.disabled = false; isProcessing = false; // Мягкое отключение UI userInput.disabled = true; sendButton.disabled = true; chatWidget.classList.add("disabled"); chatMessages.insertAdjacentHTML("beforeend", `
Ульяна: Извините, чат временно не работает. Пожалуйста, попробуйте позже.
`); }); } else { sendChatMessage(threadId, message); } // Функция отправки сообщения на эндпоинт /chat function sendChatMessage(threadId, msg) { const courseId = document.querySelector('meta[name="course-id"]')?.getAttribute("content") || "default_course"; fetch(`${API_BASE}/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ thread_id: threadId, message: msg, course_id: courseId }) }) .then(async response => { // Специальная обработка 409: запрос во время активного run if (response.status === 409) { try { const data = await response.json(); return { __special409: true, data }; } catch (_) { return { __special409: true, data: { response: "⏳ Пожалуйста, подождите — ассистент ещё формирует предыдущий ответ." } }; } } if (!response.ok) throw new Error(`Ошибка HTTP: ${response.status}`); const data = await response.json(); return { data }; }) .then(({ __special409, data }) => { clearInterval(dotInterval); document.getElementById("typing")?.remove(); if (__special409 === true) { // Мягко сообщаем пользователю и НЕ отключаем ввод chatMessages.insertAdjacentHTML("beforeend", `
Ульяна: ${data.response || "⏳ Пожалуйста, подождите — ассистент ещё формирует предыдущий ответ."}
`); userInput.disabled = false; sendButton.disabled = false; isProcessing = false; chatMessages.scrollTop = chatMessages.scrollHeight; return; } if (data.disable_input) { userInput.disabled = true; sendButton.disabled = true; chatWidget.classList.add("disabled"); chatMessages.insertAdjacentHTML("beforeend", `
Ульяна: ${data.response}
`); } else { let formattedResponse; try { const rawHtml = marked.parse(data.response); formattedResponse = (window.DOMPurify ? window.DOMPurify.sanitize(rawHtml) : rawHtml); } catch (e) { formattedResponse = data.response; } chatMessages.insertAdjacentHTML("beforeend", `
Ульяна:
${formattedResponse}
`); userInput.disabled = false; sendButton.disabled = false; } chatMessages.scrollTop = chatMessages.scrollHeight; isProcessing = false; }) .catch(error => { console.error("Ошибка:", error); clearInterval(dotInterval); document.getElementById("typing")?.remove(); // Любую сетевую/серверную ошибку трактуем как недоступность userInput.disabled = true; sendButton.disabled = true; chatWidget.classList.add("disabled"); chatMessages.insertAdjacentHTML("beforeend", `
Ульяна: Извините, чат временно не работает. Пожалуйста, попробуйте позже.
`); isProcessing = false; chatMessages.scrollTop = chatMessages.scrollHeight; }); } } });