NoteJS/public/settings.js
Fovway 1479205261 Добавлена поддержка спойлеров и улучшена функциональность AI настроек
- Реализована возможность вставки спойлеров в заметки с помощью нового интерфейса и логики обработки.
- Добавлен переключатель для включения/выключения помощи ИИ в настройках пользователя, с проверкой заполненности обязательных полей.
- Обновлены API для получения и сохранения настроек AI, включая новую колонку `ai_enabled` в таблице пользователей.
- Улучшены стили и обработчики событий для новых элементов интерфейса, включая спойлеры и переключатель AI.
2025-10-28 06:22:37 +07:00

1118 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Кастомное расширение для скрытого текста (спойлеров)
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 `<span class="spoiler" title="Нажмите, чтобы показать">${token.text}</span>`;
},
};
// Настройка 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 `<a href="${href}" title="${
title || ""
}" target="_blank" rel="noopener noreferrer" class="external-link">${text}</a>`;
} 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) {
event.stopPropagation();
this.classList.toggle("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 = `
<h3>${title}</h3>
<span class="modal-close">&times;</span>
`;
// Создаем тело модального окна
const modalBody = document.createElement("div");
modalBody.className = "modal-body";
modalBody.innerHTML = `<p>${message}</p>`;
// Создаем футер с кнопками
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";
modalFooter.innerHTML = `
<button id="confirmBtn" class="${
options.confirmType === "danger" ? "btn-danger" : "btn-primary"
}" style="margin-right: 10px">
${options.confirmText || "OK"}
</button>
<button id="cancelBtn" class="btn-secondary">
${options.cancelText || "Отмена"}
</button>
`;
// Собираем модальное окно
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 =
'<p style="text-align: center; color: #999;">Загрузка...</p>';
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 =
'<p style="text-align: center; color: #999;">Архив пуст</p>';
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 = `<div class="archived-note-images">${note.images.length} изображений</div>`;
}
noteDiv.innerHTML = `
<div class="archived-note-header">
<span class="archived-note-date">${dateStr}</span>
<div class="archived-note-actions">
<button class="btn-restore" data-id="${note.id}" title="Восстановить">
<span class="iconify" data-icon="mdi:restore"></span> Восстановить
</button>
<button class="btn-delete-permanent" data-id="${note.id}" title="Удалить навсегда">
<span class="iconify" data-icon="mdi:delete-forever"></span> Удалить
</button>
</div>
</div>
<div class="archived-note-content">${preview}</div>
${imagesHtml}
`;
container.appendChild(noteDiv);
});
// Добавляем обработчики событий
addArchivedNotesEventListeners();
} catch (error) {
console.error("Ошибка загрузки архивных заметок:", error);
container.innerHTML =
'<p style="text-align: center; color: #dc3545;">Ошибка загрузки архивных заметок</p>';
}
}
// Добавление обработчиков для архивных заметок
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 =
'<p style="text-align: center; color: #999;">Архив пуст</p>';
}
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 =
'<p style="text-align: center; color: #999;">Архив пуст</p>';
}
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 =
'<span class="iconify" data-icon="mdi:loading"></span> Удаление...';
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 =
'<span class="iconify" data-icon="mdi:delete-forever"></span> Удалить все';
}
});
// Обработка 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 =
'<tr><td colspan="4" style="text-align: center;">Загрузка...</td></tr>';
}
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 =
'<tr><td colspan="4" style="text-align: center; color: #999;">Логов пока нет</td></tr>';
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 = `
<td>${dateStr}</td>
<td><span class="log-action-badge log-action-${
log.action_type
}">${actionText}</span></td>
<td>${log.details || "-"}</td>
<td>${log.ip_address || "-"}</td>
`;
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 =
'<tr><td colspan="4" style="text-align: center; color: #dc3545;">Ошибка загрузки логов</td></tr>';
}
}
}
// Инициализация при загрузке страницы
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);
}
}