// Кастомное расширение для скрытого текста (спойлеров) const spoilerExtension = { name: "spoiler", level: "inline", start(src) { return src.match(/\|\|/) ? src.indexOf("||") : -1; }, tokenizer(src, tokens) { const rule = /^\|\|(.*?)\|\|/; const match = rule.exec(src); if (match) { return { type: "spoiler", raw: match[0], text: match[1].trim(), }; } }, renderer(token) { return `${token.text}`; }, }; // Настройка marked.js для поддержки внешних ссылок function setupMarkedRenderer() { // Функция для определения внешних ссылок function isExternalLink(href) { try { const url = new URL(href); return url.origin !== window.location.origin; } catch (e) { // Если URL невалидный, считаем его внутренним return false; } } // Создаем renderer для marked const renderer = new marked.Renderer(); // Переопределяем рендеринг ссылок для открытия внешних ссылок в браузере const originalLink = renderer.link.bind(renderer); renderer.link = function (href, title, text) { const isExternal = isExternalLink(href); if (isExternal) { // Внешние ссылки открываем в браузере return `${text}`; } else { // Внутренние ссылки обрабатываем как обычно return originalLink(href, title, text); } }; // Регистрируем расширение спойлеров marked.use({ extensions: [spoilerExtension] }); // Настраиваем marked marked.setOptions({ gfm: true, breaks: true, renderer: renderer, html: true, }); } // Функция для добавления обработчиков спойлеров function addSpoilerEventListeners() { document.querySelectorAll(".spoiler").forEach((spoiler) => { // Проверяем, не добавлен ли уже обработчик if (spoiler._clickHandler) { return; // Пропускаем, если обработчик уже добавлен } // Создаем новый обработчик spoiler._clickHandler = function (event) { // Если уже раскрыт — не мешаем выделению текста if (this.classList.contains("revealed")) { return; } event.stopPropagation(); this.classList.add("revealed"); console.log("Спойлер кликнут:", this.textContent); }; spoiler.addEventListener("click", spoiler._clickHandler); }); } // Функция для добавления обработчиков внешних ссылок function addExternalLinkListeners() { // Обработчики для внешних ссылок document.querySelectorAll(".external-link").forEach((linkElement) => { // Проверяем, не добавлен ли уже обработчик if (linkElement._externalClickHandler) { return; // Пропускаем, если обработчик уже добавлен } // Создаем новый обработчик linkElement._externalClickHandler = function (event) { // Для PWA приложений открываем ссылку в браузере if ( window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true ) { event.preventDefault(); window.open(this.href, "_blank", "noopener,noreferrer"); } // В обычном браузере оставляем стандартное поведение (target="_blank") }; linkElement.addEventListener("click", linkElement._externalClickHandler); }); } // Логика переключения темы function initThemeToggle() { const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (!themeToggleBtn) return; // Загружаем сохраненную тему или используем системную const savedTheme = localStorage.getItem("theme"); const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; const currentTheme = savedTheme || systemTheme; // Применяем тему applyTheme(currentTheme); // Обработчик клика на переключатель themeToggleBtn.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme"); const newTheme = currentTheme === "dark" ? "light" : "dark"; applyTheme(newTheme); localStorage.setItem("theme", newTheme); }); // Слушаем изменения системной темы window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", (e) => { if (!localStorage.getItem("theme")) { applyTheme(e.matches ? "dark" : "light"); } }); } function applyTheme(theme) { document.documentElement.setAttribute("data-theme", theme); // Обновляем meta теги для PWA const themeColorMeta = document.querySelector('meta[name="theme-color"]'); if (themeColorMeta) { themeColorMeta.setAttribute( "content", theme === "dark" ? "#1a1a1a" : "#007bff" ); } // Обновляем иконку переключателя const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (themeToggleBtn) { const icon = themeToggleBtn.querySelector(".iconify"); if (icon) { icon.setAttribute( "data-icon", theme === "dark" ? "mdi:weather-sunny" : "mdi:weather-night" ); } } } // Инициализируем переключатель темы при загрузке страницы document.addEventListener("DOMContentLoaded", function () { initThemeToggle(); setupMarkedRenderer(); }); // Переменные для пагинации логов let logsOffset = 0; const logsLimit = 50; let hasMoreLogs = true; // DOM элементы для внешнего вида const settingsAccentColorInput = document.getElementById( "settings-accentColor" ); const updateAppearanceBtn = document.getElementById("updateAppearanceBtn"); // Проверка аутентификации async function checkAuthentication() { try { const response = await fetch("/api/auth/status"); if (!response.ok) { localStorage.removeItem("isAuthenticated"); localStorage.removeItem("username"); window.location.href = "/"; return false; } const authData = await response.json(); if (!authData.authenticated) { localStorage.removeItem("isAuthenticated"); localStorage.removeItem("username"); window.location.href = "/"; return false; } return true; } catch (error) { console.error("Ошибка проверки аутентификации:", error); return false; } } // Загрузка информации о пользователе для применения accent color и заполнения формы async function loadUserInfo() { try { const response = await fetch("/api/user"); if (response.ok) { const user = await response.json(); const accentColor = user.accent_color || "#007bff"; // Применяем цветовой акцент if ( getComputedStyle(document.documentElement) .getPropertyValue("--accent-color") .trim() !== accentColor ) { document.documentElement.style.setProperty( "--accent-color", accentColor ); } // Заполняем поле цветового акцента в настройках if (settingsAccentColorInput) { settingsAccentColorInput.value = accentColor; updateColorPickerSelection(accentColor); } } } catch (error) { console.error("Ошибка загрузки информации о пользователе:", error); } } // Функция для обновления выбора цвета в цветовой палитре function updateColorPickerSelection(selectedColor) { const colorOptions = document.querySelectorAll( "#appearance-tab .color-option" ); colorOptions.forEach((option) => { option.classList.remove("selected"); if (option.dataset.color === selectedColor) { option.classList.add("selected"); } }); } // Система стека уведомлений let notificationStack = []; // Универсальная функция для показа уведомлений function showNotification(message, type = "info") { // Создаем уведомление const notification = document.createElement("div"); notification.className = `notification notification-${type}`; notification.textContent = message; notification.id = `notification-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`; // Добавляем возможность закрытия по клику notification.style.cursor = "pointer"; notification.addEventListener("click", () => { removeNotification(notification); }); // Добавляем на страницу document.body.appendChild(notification); // Добавляем в стек notificationStack.push(notification); // Обновляем позиции всех уведомлений updateNotificationPositions(); // Анимация появления setTimeout(() => { if (window.innerWidth <= 768) { // На мобильных устройствах используем только translateY notification.style.transform = "translateY(0)"; } else { // На десктопе используем translateX(-50%) translateY(0) notification.style.transform = "translateX(-50%) translateY(0)"; } }, 100); // Автоматическое удаление через 4 секунды setTimeout(() => { removeNotification(notification); }, 4000); } // Функция для удаления уведомления function removeNotification(notification) { // Анимация исчезновения if (window.innerWidth <= 768) { // На мобильных устройствах используем только translateY notification.style.transform = "translateY(-100%)"; } else { // На десктопе используем translateX(-50%) translateY(-100%) notification.style.transform = "translateX(-50%) translateY(-100%)"; } setTimeout(() => { // Удаляем из DOM if (notification.parentNode) { notification.parentNode.removeChild(notification); } // Удаляем из стека const index = notificationStack.indexOf(notification); if (index > -1) { notificationStack.splice(index, 1); } // Обновляем позиции оставшихся уведомлений updateNotificationPositions(); }, 300); } // Функция для обновления позиций уведомлений function updateNotificationPositions() { notificationStack.forEach((notification, index) => { const offset = index * 70; // 70px между уведомлениями notification.style.top = `${20 + offset}px`; // Обновляем transform в зависимости от размера экрана if (window.innerWidth <= 768) { // На мобильных устройствах используем только translateY notification.style.transform = "translateY(0)"; } else { // На десктопе используем translateX(-50%) translateY(0) notification.style.transform = "translateX(-50%) translateY(0)"; } }); } // Обработчик изменения размера окна window.addEventListener("resize", () => { updateNotificationPositions(); }); // Универсальная функция для модальных окон подтверждения function showConfirmModal(title, message, options = {}) { return new Promise((resolve) => { // Создаем модальное окно const modal = document.createElement("div"); modal.className = "modal"; modal.style.display = "block"; // Создаем содержимое модального окна const modalContent = document.createElement("div"); modalContent.className = "modal-content"; // Создаем заголовок const modalHeader = document.createElement("div"); modalHeader.className = "modal-header"; modalHeader.innerHTML = `

${title}

× `; // Создаем тело модального окна const modalBody = document.createElement("div"); modalBody.className = "modal-body"; modalBody.innerHTML = `

${message}

`; // Создаем футер с кнопками const modalFooter = document.createElement("div"); modalFooter.className = "modal-footer"; modalFooter.innerHTML = ` `; // Собираем модальное окно modalContent.appendChild(modalHeader); modalContent.appendChild(modalBody); modalContent.appendChild(modalFooter); modal.appendChild(modalContent); // Добавляем на страницу document.body.appendChild(modal); // Функция закрытия function closeModal() { modal.style.display = "none"; if (modal.parentNode) { modal.parentNode.removeChild(modal); } } // Обработчики событий const closeBtn = modalHeader.querySelector(".modal-close"); const cancelBtn = modalFooter.querySelector("#cancelBtn"); const confirmBtn = modalFooter.querySelector("#confirmBtn"); closeBtn.addEventListener("click", () => { closeModal(); resolve(false); }); cancelBtn.addEventListener("click", () => { closeModal(); resolve(false); }); confirmBtn.addEventListener("click", () => { closeModal(); resolve(true); }); // Закрытие при клике вне модального окна modal.addEventListener("click", (e) => { if (e.target === modal) { closeModal(); resolve(false); } }); // Закрытие по Escape const handleEscape = (e) => { if (e.key === "Escape") { closeModal(); resolve(false); document.removeEventListener("keydown", handleEscape); } }; document.addEventListener("keydown", handleEscape); }); } // Переключение табов function initTabs() { const tabs = document.querySelectorAll(".settings-tab"); const contents = document.querySelectorAll(".tab-content"); tabs.forEach((tab) => { tab.addEventListener("click", () => { const tabName = tab.dataset.tab; // Убираем активный класс со всех табов и контентов tabs.forEach((t) => t.classList.remove("active")); contents.forEach((c) => c.classList.remove("active")); // Добавляем активный класс к выбранному табу и контенту tab.classList.add("active"); document.getElementById(`${tabName}-tab`).classList.add("active"); // Загружаем данные для таба if (tabName === "archive") { loadArchivedNotes(); } else if (tabName === "logs") { loadLogs(true); } else if (tabName === "appearance") { // Данные внешнего вида уже загружены в loadUserInfo() } else if (tabName === "ai") { loadAiSettings(); } }); }); } // Загрузка архивных заметок async function loadArchivedNotes() { const container = document.getElementById("archived-notes-container"); container.innerHTML = '

Загрузка...

'; try { const response = await fetch("/api/notes/archived"); if (!response.ok) { throw new Error("Ошибка загрузки архивных заметок"); } const notes = await response.json(); if (notes.length === 0) { container.innerHTML = '

Архив пуст

'; return; } container.innerHTML = ""; notes.forEach((note) => { const noteDiv = document.createElement("div"); noteDiv.className = "archived-note-item"; noteDiv.dataset.noteId = note.id; // Форматируем дату const created = new Date(note.created_at.replace(" ", "T") + "Z"); const dateStr = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }).format(created); // Преобразуем markdown в HTML для предпросмотра const htmlContent = marked.parse(note.content); const preview = htmlContent.substring(0, 200) + (htmlContent.length > 200 ? "..." : ""); // Изображения let imagesHtml = ""; if (note.images && note.images.length > 0) { imagesHtml = `
${note.images.length} изображений
`; } noteDiv.innerHTML = `
${dateStr}
${preview}
${imagesHtml} `; container.appendChild(noteDiv); }); // Добавляем обработчики событий addArchivedNotesEventListeners(); } catch (error) { console.error("Ошибка загрузки архивных заметок:", error); container.innerHTML = '

Ошибка загрузки архивных заметок

'; } } // Добавление обработчиков для архивных заметок function addArchivedNotesEventListeners() { // Добавляем обработчики для спойлеров addSpoilerEventListeners(); // Добавляем обработчики для внешних ссылок addExternalLinkListeners(); // Восстановление document.querySelectorAll(".btn-restore").forEach((btn) => { btn.addEventListener("click", async (e) => { const noteId = e.target.closest("button").dataset.id; const confirmed = await showConfirmModal( "Подтверждение восстановления", "Восстановить эту заметку из архива?", { confirmText: "Восстановить" } ); if (confirmed) { try { const response = await fetch(`/api/notes/${noteId}/unarchive`, { method: "PUT", }); if (!response.ok) { throw new Error("Ошибка восстановления заметки"); } // Удаляем элемент из списка document.querySelector(`[data-note-id="${noteId}"]`)?.remove(); // Проверяем, остались ли заметки const container = document.getElementById("archived-notes-container"); if (container.children.length === 0) { container.innerHTML = '

Архив пуст

'; } showNotification("Заметка восстановлена!", "success"); } catch (error) { console.error("Ошибка:", error); showNotification("Ошибка восстановления заметки", "error"); } } }); }); // Окончательное удаление document.querySelectorAll(".btn-delete-permanent").forEach((btn) => { btn.addEventListener("click", async (e) => { const noteId = e.target.closest("button").dataset.id; const confirmed = await showConfirmModal( "Подтверждение удаления", "Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!", { confirmType: "danger", confirmText: "Удалить навсегда" } ); if (confirmed) { try { const response = await fetch(`/api/notes/archived/${noteId}`, { method: "DELETE", }); if (!response.ok) { throw new Error("Ошибка удаления заметки"); } // Удаляем элемент из списка document.querySelector(`[data-note-id="${noteId}"]`)?.remove(); // Проверяем, остались ли заметки const container = document.getElementById("archived-notes-container"); if (container.children.length === 0) { container.innerHTML = '

Архив пуст

'; } showNotification("Заметка удалена окончательно", "success"); } catch (error) { console.error("Ошибка:", error); showNotification("Ошибка удаления заметки", "error"); } } }); }); } // Обработчик кнопки "Удалить все" архивные заметки function initDeleteAllArchivedHandler() { const deleteAllBtn = document.getElementById("delete-all-archived"); const modal = document.getElementById("deleteAllArchivedModal"); const closeBtn = document.getElementById("deleteAllArchivedModalClose"); const cancelBtn = document.getElementById("cancelDeleteAllArchived"); const confirmBtn = document.getElementById("confirmDeleteAllArchived"); const passwordInput = document.getElementById("deleteAllPassword"); // Открытие модального окна deleteAllBtn.addEventListener("click", () => { // Проверяем, есть ли архивные заметки const container = document.getElementById("archived-notes-container"); const noteItems = container.querySelectorAll(".archived-note-item"); if (noteItems.length === 0) { showNotification("Архив уже пуст", "info"); return; } // Очищаем поле пароля и открываем модальное окно passwordInput.value = ""; modal.style.display = "block"; passwordInput.focus(); }); // Закрытие модального окна function closeModal() { modal.style.display = "none"; passwordInput.value = ""; } closeBtn.addEventListener("click", closeModal); cancelBtn.addEventListener("click", closeModal); // Закрытие при клике вне модального окна window.addEventListener("click", (e) => { if (e.target === modal) { closeModal(); } }); // Подтверждение удаления confirmBtn.addEventListener("click", async () => { const password = passwordInput.value.trim(); if (!password) { showNotification("Введите пароль", "warning"); passwordInput.focus(); return; } // Блокируем кнопку во время выполнения confirmBtn.disabled = true; confirmBtn.innerHTML = ' Удаление...'; try { const response = await fetch("/api/notes/archived/all", { method: "DELETE", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ password }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Ошибка удаления"); } // Успешное удаление showNotification(data.message, "success"); closeModal(); // Перезагружаем архивные заметки loadArchivedNotes(); } catch (error) { console.error("Ошибка:", error); showNotification( error.message || "Ошибка удаления архивных заметок", "error" ); } finally { // Разблокируем кнопку confirmBtn.disabled = false; confirmBtn.innerHTML = ' Удалить все'; } }); // Обработка Enter в поле пароля passwordInput.addEventListener("keypress", (e) => { if (e.key === "Enter") { confirmBtn.click(); } }); } // Загрузка логов async function loadLogs(reset = false) { if (reset) { logsOffset = 0; hasMoreLogs = true; } const tbody = document.getElementById("logsTableBody"); const loadMoreContainer = document.getElementById("logsLoadMore"); const filterValue = document.getElementById("logTypeFilter").value; if (reset) { tbody.innerHTML = 'Загрузка...'; } try { let url = `/api/logs?limit=${logsLimit}&offset=${logsOffset}`; if (filterValue) { url += `&action_type=${filterValue}`; } const response = await fetch(url); if (!response.ok) { throw new Error("Ошибка загрузки логов"); } const logs = await response.json(); if (reset) { tbody.innerHTML = ""; } if (logs.length === 0 && logsOffset === 0) { tbody.innerHTML = 'Логов пока нет'; loadMoreContainer.style.display = "none"; return; } if (logs.length < logsLimit) { hasMoreLogs = false; } logs.forEach((log) => { const row = document.createElement("tr"); // Форматируем дату const created = new Date(log.created_at.replace(" ", "T") + "Z"); const dateStr = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", }).format(created); // Форматируем тип действия const actionTypes = { login: "Вход", logout: "Выход", register: "Регистрация", note_create: "Создание заметки", note_update: "Редактирование", note_delete: "Удаление", note_pin: "Закрепление", note_archive: "Архивирование", note_unarchive: "Восстановление", note_delete_permanent: "Окончательное удаление", profile_update: "Обновление профиля", ai_improve: "Улучшение через AI", }; const actionText = actionTypes[log.action_type] || log.action_type; row.innerHTML = ` ${dateStr} ${actionText} ${log.details || "-"} `; tbody.appendChild(row); }); logsOffset += logs.length; if (hasMoreLogs && logs.length > 0) { loadMoreContainer.style.display = "block"; } else { loadMoreContainer.style.display = "none"; } } catch (error) { console.error("Ошибка загрузки логов:", error); if (reset) { tbody.innerHTML = 'Ошибка загрузки логов'; } } } // Инициализация при загрузке страницы document.addEventListener("DOMContentLoaded", async function () { // Проверяем аутентификацию const isAuth = await checkAuthentication(); if (!isAuth) return; // Загружаем информацию о пользователе await loadUserInfo(); // Инициализируем табы initTabs(); // Инициализируем обработчик удаления всех архивных заметок initDeleteAllArchivedHandler(); // Обработчик фильтра логов document.getElementById("logTypeFilter").addEventListener("change", () => { loadLogs(true); }); // Обработчик кнопки обновления логов document.getElementById("refreshLogs").addEventListener("click", () => { loadLogs(true); }); // Обработчик кнопки "Загрузить еще" document.getElementById("loadMoreLogsBtn").addEventListener("click", () => { loadLogs(false); }); // Обработчик кнопки сохранения внешнего вида if (updateAppearanceBtn) { updateAppearanceBtn.addEventListener("click", async function () { const accentColor = settingsAccentColorInput.value; try { const response = await fetch("/api/user/profile", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ accent_color: accentColor, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Ошибка обновления цветового акцента"); } const result = await response.json(); // Применяем новый цветовой акцент document.documentElement.style.setProperty( "--accent-color", accentColor ); showNotification( result.message || "Цветовой акцент успешно обновлен", "success" ); } catch (error) { console.error("Ошибка обновления цветового акцента:", error); showNotification(error.message, "error"); } }); } // Обработчики для цветовой палитры function setupAppearanceColorPicker() { const colorOptions = document.querySelectorAll( "#appearance-tab .color-option" ); colorOptions.forEach((option) => { option.addEventListener("click", function () { const selectedColor = this.dataset.color; settingsAccentColorInput.value = selectedColor; updateColorPickerSelection(selectedColor); }); }); // Обработчик для input color if (settingsAccentColorInput) { settingsAccentColorInput.addEventListener("input", function () { updateColorPickerSelection(this.value); }); } } setupAppearanceColorPicker(); // Обработчик кнопки сохранения AI настроек const updateAiSettingsBtn = document.getElementById("updateAiSettingsBtn"); if (updateAiSettingsBtn) { updateAiSettingsBtn.addEventListener("click", async function () { const apiKey = document.getElementById("openai-api-key").value.trim(); const baseUrl = document.getElementById("openai-base-url").value.trim(); const model = document.getElementById("openai-model").value.trim(); if (!apiKey) { showNotification("API ключ обязателен", "error"); return; } if (!baseUrl) { showNotification("Base URL обязателен", "error"); return; } if (!model) { showNotification("Название модели обязательно", "error"); return; } try { const response = await fetch("/api/user/ai-settings", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ openai_api_key: apiKey, openai_base_url: baseUrl, openai_model: model, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Ошибка сохранения AI настроек"); } const result = await response.json(); showNotification( result.message || "AI настройки успешно сохранены", "success" ); // Обновляем состояние переключателя после сохранения updateAiToggleState(); } catch (error) { console.error("Ошибка сохранения AI настроек:", error); showNotification(error.message, "error"); } }); } // Обработчик переключателя AI const aiEnabledToggle = document.getElementById("ai-enabled-toggle"); if (aiEnabledToggle) { aiEnabledToggle.addEventListener("change", async function () { // Проверяем заполнены ли настройки перед включением if (aiEnabledToggle.checked && !checkAiSettingsFilled()) { aiEnabledToggle.checked = false; showNotification("Сначала заполните все AI настройки", "warning"); return; } const isEnabled = aiEnabledToggle.checked; try { const response = await fetch("/api/user/ai-settings", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ ai_enabled: isEnabled ? 1 : 0, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Ошибка сохранения настройки AI"); } const result = await response.json(); showNotification( isEnabled ? "Помощь ИИ включена" : "Помощь ИИ отключена", "success" ); // Сохраняем в localStorage для быстрого доступа в app.js localStorage.setItem("ai_enabled", isEnabled ? "1" : "0"); } catch (error) { console.error("Ошибка сохранения настройки AI:", error); showNotification(error.message, "error"); // Откатываем изменения при ошибке aiEnabledToggle.checked = !isEnabled; } }); } // Отслеживаем изменения в полях AI настроек const apiKeyInput = document.getElementById("openai-api-key"); const baseUrlInput = document.getElementById("openai-base-url"); const modelInput = document.getElementById("openai-model"); [apiKeyInput, baseUrlInput, modelInput].forEach((input) => { if (input) { input.addEventListener("input", updateAiToggleState); } }); }); // Проверка заполнения AI настроек function checkAiSettingsFilled() { const apiKey = document.getElementById("openai-api-key").value.trim(); const baseUrl = document.getElementById("openai-base-url").value.trim(); const model = document.getElementById("openai-model").value.trim(); return apiKey && baseUrl && model; } // Обновление состояния переключателя AI function updateAiToggleState() { const aiEnabledToggle = document.getElementById("ai-enabled-toggle"); const toggleLabel = document.querySelector(".ai-toggle-label"); const toggleDesc = document.querySelector(".toggle-text-desc"); if (!aiEnabledToggle) return; const isFilled = checkAiSettingsFilled(); if (isFilled) { aiEnabledToggle.disabled = false; toggleLabel.classList.remove("disabled"); toggleDesc.textContent = 'Показывать кнопку "Помощь ИИ" в редакторах заметок'; } else { aiEnabledToggle.disabled = true; aiEnabledToggle.checked = false; toggleLabel.classList.add("disabled"); toggleDesc.textContent = "Сначала заполните API Key, Base URL и Модель ниже"; } } // Загрузка AI настроек async function loadAiSettings() { try { const response = await fetch("/api/user/ai-settings"); if (response.ok) { const settings = await response.json(); const apiKeyInput = document.getElementById("openai-api-key"); const baseUrlInput = document.getElementById("openai-base-url"); const modelInput = document.getElementById("openai-model"); const aiEnabledToggle = document.getElementById("ai-enabled-toggle"); if (apiKeyInput) { apiKeyInput.value = settings.openai_api_key || ""; } if (baseUrlInput) { baseUrlInput.value = settings.openai_base_url || ""; } if (modelInput) { modelInput.value = settings.openai_model || ""; } if (aiEnabledToggle) { aiEnabledToggle.checked = settings.ai_enabled === 1; } // Проверяем и обновляем состояние переключателя updateAiToggleState(); // Сохраняем в localStorage для быстрого доступа в app.js localStorage.setItem("ai_enabled", settings.ai_enabled ? "1" : "0"); } } catch (error) { console.error("Ошибка загрузки AI настроек:", error); } }