From d89a6172641f85bde8080a3d4b2556c2aac9147f Mon Sep 17 00:00:00 2001 From: Fovway Date: Sun, 26 Oct 2025 14:45:02 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20AI=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализованы API для сохранения и получения AI настроек пользователя, включая OpenAI API ключ, базовый URL и модель. - Добавлена возможность окончательного удаления всех архивных заметок с подтверждением пароля. - Внедрена функция улучшения текста через AI, с обработкой запросов к OpenAI API. - Обновлены интерфейсы для работы с AI настройками и добавлены уведомления для улучшения пользовательского опыта. --- public/app.js | 361 ++++++++++++++++++++++++++++++++-- public/index.html | 107 +++++++++- public/notes.html | 12 +- public/profile.html | 2 - public/profile.js | 223 ++++++++++++++++++--- public/pwa.js | 105 +++++++++- public/settings.html | 128 +++++++++++- public/settings.js | 385 ++++++++++++++++++++++++++++++++++-- public/style.css | 310 ++++++++++++++++++++++++++--- server.js | 455 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1978 insertions(+), 110 deletions(-) diff --git a/public/app.js b/public/app.js index f4ac5a3..f730957 100644 --- a/public/app.js +++ b/public/app.js @@ -2,6 +2,198 @@ const AVATAR_CACHE_KEY = "avatar_cache"; const AVATAR_TIMESTAMP_KEY = "avatar_timestamp"; +// Система стека уведомлений +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(() => { + notification.style.transform = "translateX(-50%) translateY(0)"; + }, 100); + + // Автоматическое удаление через 4 секунды + setTimeout(() => { + removeNotification(notification); + }, 4000); +} + +// Функция для удаления уведомления +function removeNotification(notification) { + // Анимация исчезновения + 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`; + }); +} + +// Тестовая функция для демонстрации стека уведомлений (можно удалить в продакшене) +function testNotificationStack() { + showNotification("Первое уведомление", "success"); + setTimeout(() => showNotification("Второе уведомление", "info"), 500); + setTimeout(() => showNotification("Третье уведомление", "warning"), 1000); + setTimeout(() => showNotification("Четвертое уведомление", "error"), 1500); +} + +// Тестовая функция для демонстрации модальных окон (можно удалить в продакшене) +async function testModalWindows() { + // Тест обычного модального окна + const result1 = await showConfirmModal( + "Тест обычного окна", + "Это тестовое модальное окно с обычной кнопкой", + { confirmText: "Подтвердить" } + ); + console.log("Результат обычного окна:", result1); + + // Тест опасного модального окна + const result2 = await showConfirmModal( + "Тест опасного окна", + "Это тестовое модальное окно с красной кнопкой", + { confirmType: "danger", confirmText: "Удалить" } + ); + console.log("Результат опасного окна:", result2); +} + +// Универсальная функция для модальных окон подтверждения +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); + }); +} + // Функция для кэширования аватарки async function cacheAvatar(avatarUrl) { try { @@ -88,6 +280,7 @@ const linkBtn = document.getElementById("linkBtn"); const checkboxBtn = document.getElementById("checkboxBtn"); const imageBtn = document.getElementById("imageBtn"); const previewBtn = document.getElementById("previewBtn"); +const aiImproveBtn = document.getElementById("aiImproveBtn"); // Кнопка настроек const settingsBtn = document.getElementById("settings-btn"); @@ -884,6 +1077,56 @@ previewBtn.addEventListener("click", function () { togglePreview(); }); +// Обработчик для кнопки улучшения через AI +aiImproveBtn.addEventListener("click", async function () { + const content = noteInput.value.trim(); + + if (!content) { + showNotification("Введите текст для улучшения", "warning"); + return; + } + + // Показываем индикатор загрузки + const originalTitle = aiImproveBtn.title; + aiImproveBtn.disabled = true; + aiImproveBtn.innerHTML = + ''; + aiImproveBtn.title = "Обработка..."; + + try { + const response = await fetch("/api/ai/improve", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ text: content }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Ошибка улучшения текста"); + } + + // Заменяем текст на улучшенный + noteInput.value = data.improvedText; + + // Авторасширяем textarea + autoExpandTextarea(noteInput); + + showNotification("Текст успешно улучшен!", "success"); + } catch (error) { + console.error("Ошибка улучшения текста:", error); + showNotification(error.message || "Ошибка улучшения текста", "error"); + } finally { + // Восстанавливаем кнопку + aiImproveBtn.disabled = false; + aiImproveBtn.innerHTML = + ''; + aiImproveBtn.title = originalTitle; + } +}); + // Функция переключения режима предпросмотра function togglePreview() { isPreviewMode = !isPreviewMode; @@ -934,7 +1177,10 @@ imageInput.addEventListener("change", function (event) { if (file.type.startsWith("image/")) { // Проверяем размер файла (максимум 10MB) if (file.size > 10 * 1024 * 1024) { - alert(`Файл "${file.name}" слишком большой. Максимальный размер: 10MB`); + showNotification( + `Файл "${file.name}" слишком большой. Максимальный размер: 10MB`, + "error" + ); return; } @@ -949,7 +1195,7 @@ imageInput.addEventListener("change", function (event) { addedCount++; } } else { - alert(`Файл "${file.name}" не является изображением`); + showNotification(`Файл "${file.name}" не является изображением`, "error"); } }); @@ -1041,7 +1287,7 @@ function updateImagePreview() { reader.onerror = function () { console.error("Ошибка чтения файла:", file.name); - alert(`Ошибка чтения файла: ${file.name}`); + showNotification(`Ошибка чтения файла: ${file.name}`, "error"); }; reader.readAsDataURL(file); @@ -1132,7 +1378,7 @@ async function uploadImages(noteId) { } // Показываем ошибку пользователю - alert(`Ошибка загрузки изображений: ${error.message}`); + showNotification(`Ошибка загрузки изображений: ${error.message}`, "error"); return []; } } @@ -1546,7 +1792,7 @@ function addNoteEventListeners() { await loadNotes(true); } catch (error) { console.error("Ошибка:", error); - alert("Ошибка изменения закрепления"); + showNotification("Ошибка изменения закрепления", "error"); } }); }); @@ -1555,11 +1801,12 @@ function addNoteEventListeners() { document.querySelectorAll("#archiveBtn").forEach((btn) => { btn.addEventListener("click", async function (event) { const noteId = event.target.closest("#archiveBtn").dataset.id; - if ( - confirm( - "Архивировать эту заметку? Её можно будет восстановить из настроек." - ) - ) { + const confirmed = await showConfirmModal( + "Подтверждение архивирования", + "Архивировать эту заметку? Её можно будет восстановить из настроек.", + { confirmText: "Архивировать" } + ); + if (confirmed) { try { const response = await fetch(`/api/notes/${noteId}/archive`, { method: "PUT", @@ -1573,7 +1820,7 @@ function addNoteEventListeners() { await loadNotes(true); } catch (error) { console.error("Ошибка:", error); - alert("Ошибка архивирования заметки"); + showNotification("Ошибка архивирования заметки", "error"); } } }); @@ -1583,7 +1830,12 @@ function addNoteEventListeners() { document.querySelectorAll("#deleteBtn").forEach((btn) => { btn.addEventListener("click", async function (event) { const noteId = event.target.dataset.id; - if (confirm("Вы уверены, что хотите удалить эту заметку?")) { + const confirmed = await showConfirmModal( + "Подтверждение удаления", + "Вы уверены, что хотите удалить эту заметку?", + { confirmType: "danger", confirmText: "Удалить" } + ); + if (confirmed) { try { const response = await fetch(`/api/notes/${noteId}`, { method: "DELETE", @@ -1597,7 +1849,7 @@ function addNoteEventListeners() { await loadNotes(true); } catch (error) { console.error("Ошибка:", error); - alert("Ошибка удаления заметки"); + showNotification("Ошибка удаления заметки", "error"); } } }); @@ -1976,6 +2228,17 @@ function addNoteEventListeners() { const saveButtonContainer = document.createElement("div"); saveButtonContainer.classList.add("save-button-container"); + // Контейнер для кнопок действий + const actionButtons = document.createElement("div"); + actionButtons.classList.add("action-buttons"); + + // Кнопка ИИ помощи + const aiImproveEditBtn = document.createElement("button"); + aiImproveEditBtn.classList.add("btnSave", "btnAI"); + aiImproveEditBtn.innerHTML = + ' Помощь ИИ'; + aiImproveEditBtn.title = "Улучшить или создать текст через ИИ"; + // Кнопка сохранить const saveEditBtn = document.createElement("button"); saveEditBtn.textContent = "Сохранить"; @@ -1985,15 +2248,18 @@ function addNoteEventListeners() { const cancelEditBtn = document.createElement("button"); cancelEditBtn.textContent = "Отмена"; cancelEditBtn.classList.add("btnSave"); - cancelEditBtn.style.marginLeft = "8px"; // Подсказка о горячей клавише const saveHint = document.createElement("span"); saveHint.classList.add("save-hint"); saveHint.textContent = "или нажмите Alt + Enter"; - saveButtonContainer.appendChild(saveEditBtn); - saveButtonContainer.appendChild(cancelEditBtn); + // Добавляем кнопки в контейнер действий + actionButtons.appendChild(aiImproveEditBtn); + actionButtons.appendChild(saveEditBtn); + actionButtons.appendChild(cancelEditBtn); + + saveButtonContainer.appendChild(actionButtons); saveButtonContainer.appendChild(saveHint); // Функция обновления превью изображений для режима редактирования @@ -2103,7 +2369,7 @@ function addNoteEventListeners() { await loadNotes(true); } catch (error) { console.error("Ошибка:", error); - alert("Ошибка сохранения заметки"); + showNotification("Ошибка сохранения заметки", "error"); } } }; @@ -2115,7 +2381,11 @@ function addNoteEventListeners() { const hasNewImages = editSelectedImages.length > 0; if (hasTextChanges || hasNewImages) { - const ok = confirm("Отменить изменения?"); + const ok = await showConfirmModal( + "Подтверждение отмены", + "Отменить изменения?", + { confirmText: "Отменить" } + ); if (!ok) return; } @@ -2269,6 +2539,57 @@ function addNoteEventListeners() { }); }); + // Обработчик кнопки ИИ для редактирования + aiImproveEditBtn.addEventListener("click", async function () { + const textarea = noteElement.querySelector(".edit-note-textarea"); + const content = textarea.value.trim(); + + if (!content) { + showNotification("Введите текст для улучшения", "warning"); + return; + } + + // Показываем индикатор загрузки + const originalHTML = aiImproveEditBtn.innerHTML; + const originalTitle = aiImproveEditBtn.title; + aiImproveEditBtn.disabled = true; + aiImproveEditBtn.innerHTML = + ' Обработка...'; + aiImproveEditBtn.title = "Обработка..."; + + try { + const response = await fetch("/api/ai/improve", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ text: content }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Ошибка улучшения текста"); + } + + // Заменяем текст на улучшенный + textarea.value = data.improvedText; + + // Авторасширяем textarea + autoExpandTextarea(textarea); + + showNotification("Текст успешно улучшен!", "success"); + } catch (error) { + console.error("Ошибка улучшения текста:", error); + showNotification(error.message || "Ошибка улучшения текста", "error"); + } finally { + // Восстанавливаем кнопку + aiImproveEditBtn.disabled = false; + aiImproveEditBtn.innerHTML = originalHTML; + aiImproveEditBtn.title = originalTitle; + } + }); + // Обработчик сохранения редактирования saveEditBtn.addEventListener("click", saveEditNote); @@ -2456,7 +2777,7 @@ function addCheckboxEventListeners() { console.error("Ошибка автосохранения чекбокса:", error); // Если не удалось сохранить, возвращаем чекбокс в прежнее состояние this.checked = !this.checked; - alert("Ошибка сохранения изменений"); + showNotification("Ошибка сохранения изменений", "error"); } } }; @@ -2588,7 +2909,7 @@ async function saveNote() { savingIndicator.remove(); } - alert("Ошибка сохранения заметки"); + showNotification("Ошибка сохранения заметки", "error"); } } } diff --git a/public/index.html b/public/index.html index 569a722..166f92f 100644 --- a/public/index.html +++ b/public/index.html @@ -172,6 +172,102 @@