✨ Добавлены функции для работы с AI настройками и улучшения текста
- Реализованы API для сохранения и получения AI настроек пользователя, включая OpenAI API ключ, базовый URL и модель. - Добавлена возможность окончательного удаления всех архивных заметок с подтверждением пароля. - Внедрена функция улучшения текста через AI, с обработкой запросов к OpenAI API. - Обновлены интерфейсы для работы с AI настройками и добавлены уведомления для улучшения пользовательского опыта.
This commit is contained in:
parent
323f96a502
commit
d89a617264
361
public/app.js
361
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 = `
|
||||
<h3>${title}</h3>
|
||||
<span class="modal-close">×</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);
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для кэширования аватарки
|
||||
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 =
|
||||
'<span class="iconify" data-icon="mdi:loading" style="animation: spin 1s linear infinite;"></span>';
|
||||
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 =
|
||||
'<span class="iconify" data-icon="mdi:robot"></span>';
|
||||
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 =
|
||||
'<span class="iconify" data-icon="mdi:robot"></span> Помощь ИИ';
|
||||
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 =
|
||||
'<span class="iconify" data-icon="mdi:loading" style="animation: spin 1s linear infinite;"></span> Обработка...';
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +172,102 @@
|
||||
|
||||
<!-- PWA Service Worker Registration -->
|
||||
<script>
|
||||
// Универсальная функция для модальных окон подтверждения
|
||||
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">×</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);
|
||||
});
|
||||
}
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
@ -182,15 +278,18 @@
|
||||
// Проверяем обновления
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener("statechange", () => {
|
||||
newWorker.addEventListener("statechange", async () => {
|
||||
if (
|
||||
newWorker.state === "installed" &&
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
// Новый контент доступен, можно показать уведомление
|
||||
if (
|
||||
confirm("Доступна новая версия приложения. Обновить?")
|
||||
) {
|
||||
const confirmed = await showConfirmModal(
|
||||
"Обновление приложения",
|
||||
"Доступна новая версия приложения. Обновить?",
|
||||
{ confirmText: "Обновить" }
|
||||
);
|
||||
if (confirmed) {
|
||||
newWorker.postMessage({ type: "SKIP_WAITING" });
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
@ -400,7 +400,17 @@
|
||||
</div>
|
||||
|
||||
<div class="save-button-container">
|
||||
<button class="btnSave" id="saveBtn">Сохранить</button>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
class="btnSave btnAI"
|
||||
id="aiImproveBtn"
|
||||
title="Улучшить или создать текст через ИИ"
|
||||
>
|
||||
<span class="iconify" data-icon="mdi:robot"></span>
|
||||
Помощь ИИ
|
||||
</button>
|
||||
<button class="btnSave" id="saveBtn">Сохранить</button>
|
||||
</div>
|
||||
<span class="save-hint">или нажмите Alt + Enter</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -191,8 +191,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="messageContainer" class="message-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
|
||||
@ -1,3 +1,168 @@
|
||||
// Система стека уведомлений
|
||||
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 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">×</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 initThemeToggle() {
|
||||
const themeToggleBtn = document.getElementById("theme-toggle-btn");
|
||||
@ -72,7 +237,6 @@ const currentPasswordInput = document.getElementById("currentPassword");
|
||||
const newPasswordInput = document.getElementById("newPassword");
|
||||
const confirmPasswordInput = document.getElementById("confirmPassword");
|
||||
const changePasswordBtn = document.getElementById("changePasswordBtn");
|
||||
const messageContainer = document.getElementById("messageContainer");
|
||||
|
||||
// Кэширование аватарки
|
||||
const AVATAR_CACHE_KEY = "avatar_cache";
|
||||
@ -185,17 +349,6 @@ function initLazyLoading() {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для показа сообщений
|
||||
function showMessage(message, type = "success") {
|
||||
messageContainer.textContent = message;
|
||||
messageContainer.className = `message-container ${type}`;
|
||||
messageContainer.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
messageContainer.style.display = "none";
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Загрузка данных профиля
|
||||
async function loadProfile() {
|
||||
try {
|
||||
@ -246,7 +399,7 @@ async function loadProfile() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Ошибка загрузки профиля:", error);
|
||||
showMessage("Ошибка загрузки данных профиля", "error");
|
||||
showNotification("Ошибка загрузки данных профиля", "error");
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,14 +410,17 @@ avatarInput.addEventListener("change", async function (event) {
|
||||
|
||||
// Проверка размера файла (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
showMessage("Файл слишком большой. Максимальный размер: 5 МБ", "error");
|
||||
showNotification(
|
||||
"Файл слишком большой. Максимальный размер: 5 МБ",
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка типа файла
|
||||
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
showMessage(
|
||||
showNotification(
|
||||
"Недопустимый формат файла. Используйте JPG, PNG или GIF",
|
||||
"error"
|
||||
);
|
||||
@ -300,10 +456,10 @@ avatarInput.addEventListener("change", async function (event) {
|
||||
}
|
||||
});
|
||||
|
||||
showMessage("Аватарка успешно загружена", "success");
|
||||
showNotification("Аватарка успешно загружена", "success");
|
||||
} catch (error) {
|
||||
console.error("Ошибка загрузки аватарки:", error);
|
||||
showMessage(error.message, "error");
|
||||
showNotification(error.message, "error");
|
||||
}
|
||||
|
||||
// Сбрасываем input для возможности повторной загрузки того же файла
|
||||
@ -312,7 +468,12 @@ avatarInput.addEventListener("change", async function (event) {
|
||||
|
||||
// Обработчик удаления аватарки
|
||||
deleteAvatarBtn.addEventListener("click", async function () {
|
||||
if (!confirm("Вы уверены, что хотите удалить аватарку?")) {
|
||||
const confirmed = await showConfirmModal(
|
||||
"Подтверждение удаления",
|
||||
"Вы уверены, что хотите удалить аватарку?",
|
||||
{ confirmType: "danger", confirmText: "Удалить" }
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -334,10 +495,10 @@ deleteAvatarBtn.addEventListener("click", async function () {
|
||||
// Очищаем кэш аватарки
|
||||
clearAvatarCache();
|
||||
|
||||
showMessage("Аватарка успешно удалена", "success");
|
||||
showNotification("Аватарка успешно удалена", "success");
|
||||
} catch (error) {
|
||||
console.error("Ошибка удаления аватарки:", error);
|
||||
showMessage(error.message, "error");
|
||||
showNotification(error.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@ -348,17 +509,17 @@ updateProfileBtn.addEventListener("click", async function () {
|
||||
|
||||
// Валидация
|
||||
if (!username) {
|
||||
showMessage("Логин не может быть пустым", "error");
|
||||
showNotification("Логин не может быть пустым", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (username.length < 3) {
|
||||
showMessage("Логин должен быть не менее 3 символов", "error");
|
||||
showNotification("Логин должен быть не менее 3 символов", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (email && !isValidEmail(email)) {
|
||||
showMessage("Некорректный email адрес", "error");
|
||||
showNotification("Некорректный email адрес", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -381,10 +542,10 @@ updateProfileBtn.addEventListener("click", async function () {
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
showMessage(result.message || "Профиль успешно обновлен", "success");
|
||||
showNotification(result.message || "Профиль успешно обновлен", "success");
|
||||
} catch (error) {
|
||||
console.error("Ошибка обновления профиля:", error);
|
||||
showMessage(error.message, "error");
|
||||
showNotification(error.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@ -396,22 +557,22 @@ changePasswordBtn.addEventListener("click", async function () {
|
||||
|
||||
// Валидация
|
||||
if (!currentPassword) {
|
||||
showMessage("Введите текущий пароль", "error");
|
||||
showNotification("Введите текущий пароль", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newPassword) {
|
||||
showMessage("Введите новый пароль", "error");
|
||||
showNotification("Введите новый пароль", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
showMessage("Новый пароль должен быть не менее 6 символов", "error");
|
||||
showNotification("Новый пароль должен быть не менее 6 символов", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showMessage("Новый пароль и подтверждение не совпадают", "error");
|
||||
showNotification("Новый пароль и подтверждение не совпадают", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -439,10 +600,10 @@ changePasswordBtn.addEventListener("click", async function () {
|
||||
newPasswordInput.value = "";
|
||||
confirmPasswordInput.value = "";
|
||||
|
||||
showMessage(result.message || "Пароль успешно изменен", "success");
|
||||
showNotification(result.message || "Пароль успешно изменен", "success");
|
||||
} catch (error) {
|
||||
console.error("Ошибка изменения пароля:", error);
|
||||
showMessage(error.message, "error");
|
||||
showNotification(error.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
105
public/pwa.js
105
public/pwa.js
@ -1,3 +1,99 @@
|
||||
// Универсальная функция для модальных окон подтверждения
|
||||
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">×</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);
|
||||
});
|
||||
}
|
||||
|
||||
// PWA Service Worker Registration
|
||||
class PWAManager {
|
||||
constructor() {
|
||||
@ -61,8 +157,13 @@ class PWAManager {
|
||||
}
|
||||
|
||||
// Показ уведомления об обновлении
|
||||
showUpdateNotification() {
|
||||
if (confirm("Доступна новая версия приложения. Обновить?")) {
|
||||
async showUpdateNotification() {
|
||||
const confirmed = await showConfirmModal(
|
||||
"Обновление приложения",
|
||||
"Доступна новая версия приложения. Обновить?",
|
||||
{ confirmText: "Обновить" }
|
||||
);
|
||||
if (confirmed) {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: "SKIP_WAITING",
|
||||
|
||||
@ -94,6 +94,9 @@
|
||||
<button class="settings-tab active" data-tab="appearance">
|
||||
<span class="iconify" data-icon="mdi:palette"></span> Внешний вид
|
||||
</button>
|
||||
<button class="settings-tab" data-tab="ai">
|
||||
<span class="iconify" data-icon="mdi:robot"></span> AI настройки
|
||||
</button>
|
||||
<button class="settings-tab" data-tab="archive">
|
||||
<span class="iconify" data-icon="mdi:archive"></span> Архив заметок
|
||||
</button>
|
||||
@ -161,9 +164,81 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AI настройки -->
|
||||
<div class="tab-content" id="ai-tab">
|
||||
<h3>Настройки AI</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="openai-api-key">OpenAI API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
id="openai-api-key"
|
||||
placeholder="sk-..."
|
||||
class="form-input"
|
||||
/>
|
||||
<p style="color: #666; font-size: 12px; margin-top: 5px">
|
||||
Введите ваш OpenAI API ключ
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="openai-base-url">OpenAI Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
id="openai-base-url"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
class="form-input"
|
||||
/>
|
||||
<p style="color: #666; font-size: 12px; margin-top: 5px">
|
||||
URL для API запросов (например, https://api.openai.com/v1)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="openai-model">Модель</label>
|
||||
<input
|
||||
type="text"
|
||||
id="openai-model"
|
||||
placeholder="gpt-3.5-turbo"
|
||||
class="form-input"
|
||||
/>
|
||||
<p style="color: #666; font-size: 12px; margin-top: 5px">
|
||||
Название модели (например, gpt-4, deepseek/deepseek-chat).
|
||||
<a
|
||||
href="https://openrouter.ai/models"
|
||||
target="_blank"
|
||||
style="color: var(--accent-color)"
|
||||
>
|
||||
Список доступных моделей
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button id="updateAiSettingsBtn" class="btnSave">
|
||||
Сохранить AI настройки
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Архив заметок -->
|
||||
<div class="tab-content" id="archive-tab">
|
||||
<h3>Архивные заметки</h3>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
"
|
||||
>
|
||||
<h3>Архивные заметки</h3>
|
||||
<button
|
||||
id="delete-all-archived"
|
||||
class="btn-danger"
|
||||
style="font-size: 14px; padding: 8px 16px"
|
||||
>
|
||||
<span class="iconify" data-icon="mdi:delete-sweep"></span> Удалить
|
||||
все
|
||||
</button>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px; margin-bottom: 20px">
|
||||
Архивированные заметки можно восстановить или удалить окончательно
|
||||
</p>
|
||||
@ -193,6 +268,7 @@
|
||||
Окончательное удаление
|
||||
</option>
|
||||
<option value="profile_update">Обновление профиля</option>
|
||||
<option value="ai_improve">Улучшение через AI</option>
|
||||
</select>
|
||||
<button id="refreshLogs" class="btnSave">
|
||||
<span class="iconify" data-icon="mdi:refresh"></span> Обновить
|
||||
@ -227,12 +303,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-message-container" class="message-container"></div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Создатель: <span>Fovway</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно подтверждения удаления всех архивных заметок -->
|
||||
<div id="deleteAllArchivedModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Подтверждение удаления</h3>
|
||||
<span class="modal-close" id="deleteAllArchivedModalClose"
|
||||
>×</span
|
||||
>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="color: #dc3545; font-weight: bold; margin-bottom: 15px">
|
||||
⚠️ ВНИМАНИЕ: Это действие нельзя отменить!
|
||||
</p>
|
||||
<p style="margin-bottom: 20px">
|
||||
Вы действительно хотите удалить ВСЕ архивные заметки? Все заметки и
|
||||
их изображения будут удалены навсегда.
|
||||
</p>
|
||||
<div style="margin-bottom: 15px">
|
||||
<label
|
||||
for="deleteAllPassword"
|
||||
style="display: block; margin-bottom: 5px; font-weight: bold"
|
||||
>
|
||||
Введите пароль для подтверждения:
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="deleteAllPassword"
|
||||
placeholder="Пароль от аккаунта"
|
||||
class="modal-password-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="confirmDeleteAllArchived"
|
||||
class="btn-danger"
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
<span class="iconify" data-icon="mdi:delete-forever"></span> Удалить
|
||||
все
|
||||
</button>
|
||||
<button id="cancelDeleteAllArchived" class="btn-secondary">
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для просмотра изображений -->
|
||||
<div id="imageModal" class="image-modal">
|
||||
<span class="image-modal-close">×</span>
|
||||
|
||||
@ -139,18 +139,169 @@ function updateColorPickerSelection(selectedColor) {
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для показа сообщений
|
||||
function showSettingsMessage(message, type = "success") {
|
||||
const container = document.getElementById("settings-message-container");
|
||||
if (container) {
|
||||
container.textContent = message;
|
||||
container.className = `message-container ${type}`;
|
||||
container.style.display = "block";
|
||||
// Система стека уведомлений
|
||||
let notificationStack = [];
|
||||
|
||||
setTimeout(() => {
|
||||
container.style.display = "none";
|
||||
}, 5000);
|
||||
}
|
||||
// Универсальная функция для показа уведомлений
|
||||
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 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">×</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);
|
||||
});
|
||||
}
|
||||
|
||||
// Переключение табов
|
||||
@ -177,6 +328,8 @@ function initTabs() {
|
||||
loadLogs(true);
|
||||
} else if (tabName === "appearance") {
|
||||
// Данные внешнего вида уже загружены в loadUserInfo()
|
||||
} else if (tabName === "ai") {
|
||||
loadAiSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -264,7 +417,12 @@ function addArchivedNotesEventListeners() {
|
||||
document.querySelectorAll(".btn-restore").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
const noteId = e.target.closest("button").dataset.id;
|
||||
if (confirm("Восстановить эту заметку из архива?")) {
|
||||
const confirmed = await showConfirmModal(
|
||||
"Подтверждение восстановления",
|
||||
"Восстановить эту заметку из архива?",
|
||||
{ confirmText: "Восстановить" }
|
||||
);
|
||||
if (confirmed) {
|
||||
try {
|
||||
const response = await fetch(`/api/notes/${noteId}/unarchive`, {
|
||||
method: "PUT",
|
||||
@ -284,10 +442,10 @@ function addArchivedNotesEventListeners() {
|
||||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||||
}
|
||||
|
||||
alert("Заметка восстановлена!");
|
||||
showNotification("Заметка восстановлена!", "success");
|
||||
} catch (error) {
|
||||
console.error("Ошибка:", error);
|
||||
alert("Ошибка восстановления заметки");
|
||||
showNotification("Ошибка восстановления заметки", "error");
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -297,9 +455,12 @@ function addArchivedNotesEventListeners() {
|
||||
document.querySelectorAll(".btn-delete-permanent").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
const noteId = e.target.closest("button").dataset.id;
|
||||
if (
|
||||
confirm("Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!")
|
||||
) {
|
||||
const confirmed = await showConfirmModal(
|
||||
"Подтверждение удаления",
|
||||
"Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!",
|
||||
{ confirmType: "danger", confirmText: "Удалить навсегда" }
|
||||
);
|
||||
if (confirmed) {
|
||||
try {
|
||||
const response = await fetch(`/api/notes/archived/${noteId}`, {
|
||||
method: "DELETE",
|
||||
@ -319,16 +480,116 @@ function addArchivedNotesEventListeners() {
|
||||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||||
}
|
||||
|
||||
alert("Заметка удалена окончательно");
|
||||
showNotification("Заметка удалена окончательно", "success");
|
||||
} catch (error) {
|
||||
console.error("Ошибка:", error);
|
||||
alert("Ошибка удаления заметки");
|
||||
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) {
|
||||
@ -400,6 +661,7 @@ async function loadLogs(reset = false) {
|
||||
note_unarchive: "Восстановление",
|
||||
note_delete_permanent: "Окончательное удаление",
|
||||
profile_update: "Обновление профиля",
|
||||
ai_improve: "Улучшение через AI",
|
||||
};
|
||||
|
||||
const actionText = actionTypes[log.action_type] || log.action_type;
|
||||
@ -444,6 +706,9 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
// Инициализируем табы
|
||||
initTabs();
|
||||
|
||||
// Инициализируем обработчик удаления всех архивных заметок
|
||||
initDeleteAllArchivedHandler();
|
||||
|
||||
// Обработчик фильтра логов
|
||||
document.getElementById("logTypeFilter").addEventListener("change", () => {
|
||||
loadLogs(true);
|
||||
@ -488,13 +753,13 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
accentColor
|
||||
);
|
||||
|
||||
showSettingsMessage(
|
||||
showNotification(
|
||||
result.message || "Цветовой акцент успешно обновлен",
|
||||
"success"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Ошибка обновления цветового акцента:", error);
|
||||
showSettingsMessage(error.message, "error");
|
||||
showNotification(error.message, "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -521,4 +786,82 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения AI настроек:", error);
|
||||
showNotification(error.message, "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Загрузка 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");
|
||||
|
||||
if (apiKeyInput) {
|
||||
apiKeyInput.value = settings.openai_api_key || "";
|
||||
}
|
||||
if (baseUrlInput) {
|
||||
baseUrlInput.value = settings.openai_base_url || "";
|
||||
}
|
||||
if (modelInput) {
|
||||
modelInput.value = settings.openai_model || "";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Ошибка загрузки AI настроек:", error);
|
||||
}
|
||||
}
|
||||
|
||||
310
public/style.css
310
public/style.css
@ -575,11 +575,38 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.save-button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btnAI {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btnAI:hover {
|
||||
background: linear-gradient(135deg, #5568d3 0%, #5a3f7d 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btnAI:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btnAI .iconify {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.save-hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
@ -1120,43 +1147,54 @@ textarea:focus {
|
||||
border-top: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.message-container {
|
||||
/* Стили для уведомлений */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transform: translateX(-50%) translateY(-100%);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
z-index: 10000;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
max-width: 350px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.3s ease, top 0.3s ease;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background-color: #007bff;
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@media (max-width: 768px) {
|
||||
.message-container {
|
||||
min-width: 90vw;
|
||||
max-width: 90vw;
|
||||
left: 5vw;
|
||||
transform: none;
|
||||
padding: 10px 15px;
|
||||
.notification {
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
max-width: none;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.message-container.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message-container.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
.notification.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@ -1855,8 +1893,16 @@ textarea:focus {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btnSave {
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btnSave,
|
||||
.btnAI {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Адаптируем footer */
|
||||
@ -2159,6 +2205,135 @@ textarea:focus {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* Модальное окно подтверждения */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fff;
|
||||
margin: 10% auto;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
animation: modalFadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
color: #aaa;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 25px;
|
||||
color: #555;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 25px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Темная тема для модального окна */
|
||||
[data-theme="dark"] .modal-content {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-header {
|
||||
border-bottom-color: var(--border-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-header h3 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-close {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-body {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-footer {
|
||||
border-top-color: var(--border-primary);
|
||||
}
|
||||
|
||||
/* Стили для поля пароля в модальном окне */
|
||||
.modal-password-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-password-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
background-color: var(--bg-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.25);
|
||||
}
|
||||
|
||||
.modal-password-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Адаптивность для изображений */
|
||||
@media (max-width: 768px) {
|
||||
.image-preview-list {
|
||||
@ -2373,6 +2548,74 @@ textarea:focus {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Стили для новых кнопок */
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: 1px solid #007bff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #004085;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: 1px solid #dc3545;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
|
||||
.btn-danger:disabled {
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--bg-quaternary);
|
||||
}
|
||||
|
||||
.archived-note-content {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
@ -2569,3 +2812,18 @@ textarea:focus {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Анимация загрузки для AI кнопки */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.log-action-ai_improve {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
455
server.js
455
server.js
@ -9,6 +9,8 @@ const rateLimit = require("express-rate-limit");
|
||||
const bodyParser = require("body-parser");
|
||||
const multer = require("multer");
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
require("dotenv").config();
|
||||
|
||||
const app = express();
|
||||
@ -398,6 +400,51 @@ function runMigrations() {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем существование колонок для AI настроек
|
||||
const hasOpenaiApiKey = columns.some(
|
||||
(col) => col.name === "openai_api_key"
|
||||
);
|
||||
const hasOpenaiBaseUrl = columns.some(
|
||||
(col) => col.name === "openai_base_url"
|
||||
);
|
||||
const hasOpenaiModel = columns.some((col) => col.name === "openai_model");
|
||||
|
||||
if (!hasOpenaiApiKey) {
|
||||
db.run("ALTER TABLE users ADD COLUMN openai_api_key TEXT", (err) => {
|
||||
if (err) {
|
||||
console.error(
|
||||
"Ошибка добавления колонки openai_api_key:",
|
||||
err.message
|
||||
);
|
||||
} else {
|
||||
console.log("Колонка openai_api_key добавлена в таблицу users");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasOpenaiBaseUrl) {
|
||||
db.run("ALTER TABLE users ADD COLUMN openai_base_url TEXT", (err) => {
|
||||
if (err) {
|
||||
console.error(
|
||||
"Ошибка добавления колонки openai_base_url:",
|
||||
err.message
|
||||
);
|
||||
} else {
|
||||
console.log("Колонка openai_base_url добавлена в таблицу users");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasOpenaiModel) {
|
||||
db.run("ALTER TABLE users ADD COLUMN openai_model TEXT", (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка добавления колонки openai_model:", err.message);
|
||||
} else {
|
||||
console.log("Колонка openai_model добавлена в таблицу users");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Проверяем существование колонок в таблице notes и добавляем их если нужно
|
||||
@ -1537,6 +1584,115 @@ app.get("/api/notes/archived", requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// API для окончательного удаления всех архивных заметок пользователя
|
||||
app.delete("/api/notes/archived/all", requireAuth, async (req, res) => {
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).json({ error: "Пароль обязателен" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем хеш пароля пользователя
|
||||
const userSql = "SELECT password FROM users WHERE id = ?";
|
||||
db.get(userSql, [req.session.userId], async (err, user) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения пользователя:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: "Пользователь не найден" });
|
||||
}
|
||||
|
||||
// Проверяем пароль
|
||||
const validPassword = await bcrypt.compare(password, user.password);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ error: "Неверный пароль" });
|
||||
}
|
||||
|
||||
// Получаем все архивные заметки пользователя
|
||||
const getNotesSql =
|
||||
"SELECT id FROM notes WHERE user_id = ? AND is_archived = 1";
|
||||
db.all(getNotesSql, [req.session.userId], (err, notes) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения архивных заметок:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Архив уже пуст",
|
||||
deletedCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем изображения для всех заметок
|
||||
const noteIds = notes.map((note) => note.id);
|
||||
const placeholders = noteIds.map(() => "?").join(",");
|
||||
|
||||
const getImagesSql = `SELECT file_path FROM note_images WHERE note_id IN (${placeholders})`;
|
||||
db.all(getImagesSql, noteIds, (err, images) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения изображений:", err.message);
|
||||
} else {
|
||||
// Удаляем файлы изображений
|
||||
images.forEach((image) => {
|
||||
const imagePath = path.join(__dirname, "public", image.file_path);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
try {
|
||||
fs.unlinkSync(imagePath);
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления файла изображения:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем записи об изображениях
|
||||
const deleteImagesSql = `DELETE FROM note_images WHERE note_id IN (${placeholders})`;
|
||||
db.run(deleteImagesSql, noteIds, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
|
||||
// Удаляем сами заметки
|
||||
const deleteNotesSql =
|
||||
"DELETE FROM notes WHERE user_id = ? AND is_archived = 1";
|
||||
db.run(deleteNotesSql, [req.session.userId], function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметок:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
const deletedCount = this.changes;
|
||||
|
||||
// Логируем действие
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete_permanent",
|
||||
`Удалены все архивные заметки (${deletedCount} шт.)`,
|
||||
clientIP
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Удалено ${deletedCount} архивных заметок`,
|
||||
deletedCount,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка при проверке пароля:", error);
|
||||
res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
});
|
||||
|
||||
// API для окончательного удаления архивной заметки
|
||||
app.delete("/api/notes/archived/:id", requireAuth, (req, res) => {
|
||||
const { id } = req.params;
|
||||
@ -1606,6 +1762,114 @@ app.delete("/api/notes/archived/:id", requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// API для окончательного удаления всех архивных заметок пользователя
|
||||
app.delete("/api/notes/archived/all", requireAuth, async (req, res) => {
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).json({ error: "Пароль обязателен" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем хеш пароля пользователя
|
||||
const userSql = "SELECT password FROM users WHERE id = ?";
|
||||
db.get(userSql, [req.session.userId], async (err, user) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения пользователя:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: "Пользователь не найден" });
|
||||
}
|
||||
|
||||
// Проверяем пароль
|
||||
const validPassword = await bcrypt.compare(password, user.password);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ error: "Неверный пароль" });
|
||||
}
|
||||
|
||||
// Получаем все архивные заметки пользователя
|
||||
const getNotesSql =
|
||||
"SELECT id FROM notes WHERE user_id = ? AND is_archived = 1";
|
||||
db.all(getNotesSql, [req.session.userId], (err, notes) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения архивных заметок:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Архив уже пуст",
|
||||
deletedCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем изображения для всех заметок
|
||||
const noteIds = notes.map((note) => note.id);
|
||||
const placeholders = noteIds.map(() => "?").join(",");
|
||||
|
||||
const getImagesSql = `SELECT file_path FROM note_images WHERE note_id IN (${placeholders})`;
|
||||
db.all(getImagesSql, noteIds, (err, images) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения изображений:", err.message);
|
||||
} else {
|
||||
// Удаляем файлы изображений
|
||||
images.forEach((image) => {
|
||||
const imagePath = path.join(__dirname, "public", image.file_path);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
try {
|
||||
fs.unlinkSync(imagePath);
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления файла изображения:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем записи об изображениях
|
||||
const deleteImagesSql = `DELETE FROM note_images WHERE note_id IN (${placeholders})`;
|
||||
db.run(deleteImagesSql, noteIds, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
|
||||
// Удаляем сами заметки
|
||||
const deleteNotesSql = `DELETE FROM notes WHERE user_id = ? AND is_archived = 1`;
|
||||
db.run(deleteNotesSql, [req.session.userId], function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметок:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
const deletedCount = this.changes;
|
||||
|
||||
// Логируем действие
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete_permanent",
|
||||
`Удалены все архивные заметки (${deletedCount} шт.)`,
|
||||
clientIP
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Удалено ${deletedCount} архивных заметок`,
|
||||
deletedCount,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка при проверке пароля:", error);
|
||||
res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
});
|
||||
|
||||
// API для получения логов пользователя
|
||||
app.get("/api/logs", requireAuth, (req, res) => {
|
||||
const { action_type, limit = 100, offset = 0 } = req.query;
|
||||
@ -1674,6 +1938,197 @@ app.get("/settings", requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения AI настроек
|
||||
app.get("/api/user/ai-settings", requireAuth, (req, res) => {
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({ error: "Не аутентифицирован" });
|
||||
}
|
||||
|
||||
const sql =
|
||||
"SELECT openai_api_key, openai_base_url, openai_model FROM users WHERE id = ?";
|
||||
db.get(sql, [req.session.userId], (err, settings) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения AI настроек:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!settings) {
|
||||
return res.status(404).json({ error: "Настройки не найдены" });
|
||||
}
|
||||
|
||||
res.json(settings);
|
||||
});
|
||||
});
|
||||
|
||||
// API для сохранения AI настроек
|
||||
app.put("/api/user/ai-settings", requireAuth, (req, res) => {
|
||||
const { openai_api_key, openai_base_url, openai_model } = req.body;
|
||||
const userId = req.session.userId;
|
||||
|
||||
if (!openai_api_key || !openai_base_url || !openai_model) {
|
||||
return res.status(400).json({ error: "Все поля обязательны" });
|
||||
}
|
||||
|
||||
const updateSql =
|
||||
"UPDATE users SET openai_api_key = ?, openai_base_url = ?, openai_model = ? WHERE id = ?";
|
||||
db.run(
|
||||
updateSql,
|
||||
[openai_api_key, openai_base_url, openai_model, userId],
|
||||
function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка сохранения AI настроек:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
// Логируем обновление AI настроек
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(userId, "profile_update", "Обновлены AI настройки", clientIP);
|
||||
|
||||
res.json({ success: true, message: "AI настройки успешно сохранены" });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// API для улучшения текста через AI
|
||||
app.post("/api/ai/improve", requireAuth, async (req, res) => {
|
||||
const { text } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: "Текст обязателен" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем AI настройки пользователя
|
||||
const getSettingsSql =
|
||||
"SELECT openai_api_key, openai_base_url, openai_model FROM users WHERE id = ?";
|
||||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения AI настроек:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (
|
||||
!settings ||
|
||||
!settings.openai_api_key ||
|
||||
!settings.openai_base_url ||
|
||||
!settings.openai_model
|
||||
) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Настройте AI настройки в параметрах" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Парсим URL
|
||||
const url = new URL(settings.openai_base_url);
|
||||
const isHttps = url.protocol === "https:";
|
||||
const hostname = url.hostname;
|
||||
const port = url.port || (isHttps ? 443 : 80);
|
||||
// Формируем путь, избегая дублирования
|
||||
let path = url.pathname || "";
|
||||
if (!path.endsWith("/chat/completions")) {
|
||||
path = path.endsWith("/")
|
||||
? path + "chat/completions"
|
||||
: path + "/chat/completions";
|
||||
}
|
||||
|
||||
// Подготавливаем данные для запроса
|
||||
const requestData = JSON.stringify({
|
||||
model: settings.openai_model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Ты универсальный помощник для работы с текстом. Твоя задача:\n1. Если пользователь просит что-то придумать или сгенерировать (например, поздравление, письмо, пост) - создай качественный текст на заданную тему\n2. Если пользователь предоставил готовый текст - улучши его: исправь ошибки, улучши стиль и грамотность, сделай текст более понятным и выразительным, сохрани основную мысль\n3. Если пользователь просит создать список, используй markdown формат:\n - Для маркированных списков используй: - или *\n - Для нумерованных: 1. 2. 3.\n - Для заголовков: # ## ###\n4. Используй markdown форматирование (жирный **текст**, курсив *текст*, списки, заголовки) когда это уместно\n5. Обращай внимание на контекст и намерение пользователя\nВерни только готовый текст без дополнительных пояснений.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
temperature: 0.5,
|
||||
max_tokens: 4000,
|
||||
});
|
||||
|
||||
// Выполняем HTTP запрос
|
||||
const improvedText = await new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
path: path,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${settings.openai_api_key}`,
|
||||
"Content-Length": Buffer.byteLength(requestData),
|
||||
},
|
||||
};
|
||||
|
||||
const client = isHttps ? https : http;
|
||||
const req = client.request(options, (res) => {
|
||||
let data = "";
|
||||
|
||||
res.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on("end", () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
const responseData = JSON.parse(data);
|
||||
const improvedText =
|
||||
responseData.choices[0]?.message?.content || text;
|
||||
resolve(improvedText);
|
||||
} catch (err) {
|
||||
console.error("Ошибка парсинга ответа:", err);
|
||||
reject(new Error("Ошибка обработки ответа от AI"));
|
||||
}
|
||||
} else {
|
||||
console.error("Ошибка OpenAI API:", res.statusCode, data);
|
||||
reject(
|
||||
new Error(
|
||||
`Ошибка OpenAI API: ${res.statusCode} - ${data.substring(
|
||||
0,
|
||||
100
|
||||
)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", (error) => {
|
||||
console.error("Ошибка запроса к OpenAI:", error);
|
||||
reject(new Error("Ошибка подключения к OpenAI API"));
|
||||
});
|
||||
|
||||
req.write(requestData);
|
||||
req.end();
|
||||
});
|
||||
|
||||
// Логируем использование AI
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"ai_improve",
|
||||
"Улучшен текст через AI",
|
||||
clientIP
|
||||
);
|
||||
|
||||
res.json({ success: true, improvedText });
|
||||
} catch (error) {
|
||||
console.error("Ошибка вызова OpenAI API:", error);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: error.message || "Ошибка подключения к OpenAI API" });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка улучшения текста:", error);
|
||||
res.status(500).json({ error: "Ошибка улучшения текста" });
|
||||
}
|
||||
});
|
||||
|
||||
// Выход
|
||||
app.post("/logout", (req, res) => {
|
||||
const userId = req.session.userId;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user