✨ Добавлены функции для работы с 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_CACHE_KEY = "avatar_cache";
|
||||||
const AVATAR_TIMESTAMP_KEY = "avatar_timestamp";
|
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) {
|
async function cacheAvatar(avatarUrl) {
|
||||||
try {
|
try {
|
||||||
@ -88,6 +280,7 @@ const linkBtn = document.getElementById("linkBtn");
|
|||||||
const checkboxBtn = document.getElementById("checkboxBtn");
|
const checkboxBtn = document.getElementById("checkboxBtn");
|
||||||
const imageBtn = document.getElementById("imageBtn");
|
const imageBtn = document.getElementById("imageBtn");
|
||||||
const previewBtn = document.getElementById("previewBtn");
|
const previewBtn = document.getElementById("previewBtn");
|
||||||
|
const aiImproveBtn = document.getElementById("aiImproveBtn");
|
||||||
|
|
||||||
// Кнопка настроек
|
// Кнопка настроек
|
||||||
const settingsBtn = document.getElementById("settings-btn");
|
const settingsBtn = document.getElementById("settings-btn");
|
||||||
@ -884,6 +1077,56 @@ previewBtn.addEventListener("click", function () {
|
|||||||
togglePreview();
|
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() {
|
function togglePreview() {
|
||||||
isPreviewMode = !isPreviewMode;
|
isPreviewMode = !isPreviewMode;
|
||||||
@ -934,7 +1177,10 @@ imageInput.addEventListener("change", function (event) {
|
|||||||
if (file.type.startsWith("image/")) {
|
if (file.type.startsWith("image/")) {
|
||||||
// Проверяем размер файла (максимум 10MB)
|
// Проверяем размер файла (максимум 10MB)
|
||||||
if (file.size > 10 * 1024 * 1024) {
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
alert(`Файл "${file.name}" слишком большой. Максимальный размер: 10MB`);
|
showNotification(
|
||||||
|
`Файл "${file.name}" слишком большой. Максимальный размер: 10MB`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -949,7 +1195,7 @@ imageInput.addEventListener("change", function (event) {
|
|||||||
addedCount++;
|
addedCount++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert(`Файл "${file.name}" не является изображением`);
|
showNotification(`Файл "${file.name}" не является изображением`, "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1041,7 +1287,7 @@ function updateImagePreview() {
|
|||||||
|
|
||||||
reader.onerror = function () {
|
reader.onerror = function () {
|
||||||
console.error("Ошибка чтения файла:", file.name);
|
console.error("Ошибка чтения файла:", file.name);
|
||||||
alert(`Ошибка чтения файла: ${file.name}`);
|
showNotification(`Ошибка чтения файла: ${file.name}`, "error");
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@ -1132,7 +1378,7 @@ async function uploadImages(noteId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Показываем ошибку пользователю
|
// Показываем ошибку пользователю
|
||||||
alert(`Ошибка загрузки изображений: ${error.message}`);
|
showNotification(`Ошибка загрузки изображений: ${error.message}`, "error");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1546,7 +1792,7 @@ function addNoteEventListeners() {
|
|||||||
await loadNotes(true);
|
await loadNotes(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
alert("Ошибка изменения закрепления");
|
showNotification("Ошибка изменения закрепления", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1555,11 +1801,12 @@ function addNoteEventListeners() {
|
|||||||
document.querySelectorAll("#archiveBtn").forEach((btn) => {
|
document.querySelectorAll("#archiveBtn").forEach((btn) => {
|
||||||
btn.addEventListener("click", async function (event) {
|
btn.addEventListener("click", async function (event) {
|
||||||
const noteId = event.target.closest("#archiveBtn").dataset.id;
|
const noteId = event.target.closest("#archiveBtn").dataset.id;
|
||||||
if (
|
const confirmed = await showConfirmModal(
|
||||||
confirm(
|
"Подтверждение архивирования",
|
||||||
"Архивировать эту заметку? Её можно будет восстановить из настроек."
|
"Архивировать эту заметку? Её можно будет восстановить из настроек.",
|
||||||
)
|
{ confirmText: "Архивировать" }
|
||||||
) {
|
);
|
||||||
|
if (confirmed) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/notes/${noteId}/archive`, {
|
const response = await fetch(`/api/notes/${noteId}/archive`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@ -1573,7 +1820,7 @@ function addNoteEventListeners() {
|
|||||||
await loadNotes(true);
|
await loadNotes(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
alert("Ошибка архивирования заметки");
|
showNotification("Ошибка архивирования заметки", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1583,7 +1830,12 @@ function addNoteEventListeners() {
|
|||||||
document.querySelectorAll("#deleteBtn").forEach((btn) => {
|
document.querySelectorAll("#deleteBtn").forEach((btn) => {
|
||||||
btn.addEventListener("click", async function (event) {
|
btn.addEventListener("click", async function (event) {
|
||||||
const noteId = event.target.dataset.id;
|
const noteId = event.target.dataset.id;
|
||||||
if (confirm("Вы уверены, что хотите удалить эту заметку?")) {
|
const confirmed = await showConfirmModal(
|
||||||
|
"Подтверждение удаления",
|
||||||
|
"Вы уверены, что хотите удалить эту заметку?",
|
||||||
|
{ confirmType: "danger", confirmText: "Удалить" }
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/notes/${noteId}`, {
|
const response = await fetch(`/api/notes/${noteId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@ -1597,7 +1849,7 @@ function addNoteEventListeners() {
|
|||||||
await loadNotes(true);
|
await loadNotes(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
alert("Ошибка удаления заметки");
|
showNotification("Ошибка удаления заметки", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1976,6 +2228,17 @@ function addNoteEventListeners() {
|
|||||||
const saveButtonContainer = document.createElement("div");
|
const saveButtonContainer = document.createElement("div");
|
||||||
saveButtonContainer.classList.add("save-button-container");
|
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");
|
const saveEditBtn = document.createElement("button");
|
||||||
saveEditBtn.textContent = "Сохранить";
|
saveEditBtn.textContent = "Сохранить";
|
||||||
@ -1985,15 +2248,18 @@ function addNoteEventListeners() {
|
|||||||
const cancelEditBtn = document.createElement("button");
|
const cancelEditBtn = document.createElement("button");
|
||||||
cancelEditBtn.textContent = "Отмена";
|
cancelEditBtn.textContent = "Отмена";
|
||||||
cancelEditBtn.classList.add("btnSave");
|
cancelEditBtn.classList.add("btnSave");
|
||||||
cancelEditBtn.style.marginLeft = "8px";
|
|
||||||
|
|
||||||
// Подсказка о горячей клавише
|
// Подсказка о горячей клавише
|
||||||
const saveHint = document.createElement("span");
|
const saveHint = document.createElement("span");
|
||||||
saveHint.classList.add("save-hint");
|
saveHint.classList.add("save-hint");
|
||||||
saveHint.textContent = "или нажмите Alt + Enter";
|
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);
|
saveButtonContainer.appendChild(saveHint);
|
||||||
|
|
||||||
// Функция обновления превью изображений для режима редактирования
|
// Функция обновления превью изображений для режима редактирования
|
||||||
@ -2103,7 +2369,7 @@ function addNoteEventListeners() {
|
|||||||
await loadNotes(true);
|
await loadNotes(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
alert("Ошибка сохранения заметки");
|
showNotification("Ошибка сохранения заметки", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -2115,7 +2381,11 @@ function addNoteEventListeners() {
|
|||||||
const hasNewImages = editSelectedImages.length > 0;
|
const hasNewImages = editSelectedImages.length > 0;
|
||||||
|
|
||||||
if (hasTextChanges || hasNewImages) {
|
if (hasTextChanges || hasNewImages) {
|
||||||
const ok = confirm("Отменить изменения?");
|
const ok = await showConfirmModal(
|
||||||
|
"Подтверждение отмены",
|
||||||
|
"Отменить изменения?",
|
||||||
|
{ confirmText: "Отменить" }
|
||||||
|
);
|
||||||
if (!ok) return;
|
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);
|
saveEditBtn.addEventListener("click", saveEditNote);
|
||||||
|
|
||||||
@ -2456,7 +2777,7 @@ function addCheckboxEventListeners() {
|
|||||||
console.error("Ошибка автосохранения чекбокса:", error);
|
console.error("Ошибка автосохранения чекбокса:", error);
|
||||||
// Если не удалось сохранить, возвращаем чекбокс в прежнее состояние
|
// Если не удалось сохранить, возвращаем чекбокс в прежнее состояние
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
alert("Ошибка сохранения изменений");
|
showNotification("Ошибка сохранения изменений", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -2588,7 +2909,7 @@ async function saveNote() {
|
|||||||
savingIndicator.remove();
|
savingIndicator.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
alert("Ошибка сохранения заметки");
|
showNotification("Ошибка сохранения заметки", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -172,6 +172,102 @@
|
|||||||
|
|
||||||
<!-- PWA Service Worker Registration -->
|
<!-- PWA Service Worker Registration -->
|
||||||
<script>
|
<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) {
|
if ("serviceWorker" in navigator) {
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
@ -182,15 +278,18 @@
|
|||||||
// Проверяем обновления
|
// Проверяем обновления
|
||||||
registration.addEventListener("updatefound", () => {
|
registration.addEventListener("updatefound", () => {
|
||||||
const newWorker = registration.installing;
|
const newWorker = registration.installing;
|
||||||
newWorker.addEventListener("statechange", () => {
|
newWorker.addEventListener("statechange", async () => {
|
||||||
if (
|
if (
|
||||||
newWorker.state === "installed" &&
|
newWorker.state === "installed" &&
|
||||||
navigator.serviceWorker.controller
|
navigator.serviceWorker.controller
|
||||||
) {
|
) {
|
||||||
// Новый контент доступен, можно показать уведомление
|
// Новый контент доступен, можно показать уведомление
|
||||||
if (
|
const confirmed = await showConfirmModal(
|
||||||
confirm("Доступна новая версия приложения. Обновить?")
|
"Обновление приложения",
|
||||||
) {
|
"Доступна новая версия приложения. Обновить?",
|
||||||
|
{ confirmText: "Обновить" }
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
newWorker.postMessage({ type: "SKIP_WAITING" });
|
newWorker.postMessage({ type: "SKIP_WAITING" });
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -400,7 +400,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="save-button-container">
|
<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>
|
<span class="save-hint">или нажмите Alt + Enter</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -191,8 +191,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="messageContainer" class="message-container"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<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() {
|
function initThemeToggle() {
|
||||||
const themeToggleBtn = document.getElementById("theme-toggle-btn");
|
const themeToggleBtn = document.getElementById("theme-toggle-btn");
|
||||||
@ -72,7 +237,6 @@ const currentPasswordInput = document.getElementById("currentPassword");
|
|||||||
const newPasswordInput = document.getElementById("newPassword");
|
const newPasswordInput = document.getElementById("newPassword");
|
||||||
const confirmPasswordInput = document.getElementById("confirmPassword");
|
const confirmPasswordInput = document.getElementById("confirmPassword");
|
||||||
const changePasswordBtn = document.getElementById("changePasswordBtn");
|
const changePasswordBtn = document.getElementById("changePasswordBtn");
|
||||||
const messageContainer = document.getElementById("messageContainer");
|
|
||||||
|
|
||||||
// Кэширование аватарки
|
// Кэширование аватарки
|
||||||
const AVATAR_CACHE_KEY = "avatar_cache";
|
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() {
|
async function loadProfile() {
|
||||||
try {
|
try {
|
||||||
@ -246,7 +399,7 @@ async function loadProfile() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка загрузки профиля:", error);
|
console.error("Ошибка загрузки профиля:", error);
|
||||||
showMessage("Ошибка загрузки данных профиля", "error");
|
showNotification("Ошибка загрузки данных профиля", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,14 +410,17 @@ avatarInput.addEventListener("change", async function (event) {
|
|||||||
|
|
||||||
// Проверка размера файла (5MB)
|
// Проверка размера файла (5MB)
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
showMessage("Файл слишком большой. Максимальный размер: 5 МБ", "error");
|
showNotification(
|
||||||
|
"Файл слишком большой. Максимальный размер: 5 МБ",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка типа файла
|
// Проверка типа файла
|
||||||
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"];
|
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"];
|
||||||
if (!allowedTypes.includes(file.type)) {
|
if (!allowedTypes.includes(file.type)) {
|
||||||
showMessage(
|
showNotification(
|
||||||
"Недопустимый формат файла. Используйте JPG, PNG или GIF",
|
"Недопустимый формат файла. Используйте JPG, PNG или GIF",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
@ -300,10 +456,10 @@ avatarInput.addEventListener("change", async function (event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
showMessage("Аватарка успешно загружена", "success");
|
showNotification("Аватарка успешно загружена", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка загрузки аватарки:", error);
|
console.error("Ошибка загрузки аватарки:", error);
|
||||||
showMessage(error.message, "error");
|
showNotification(error.message, "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем input для возможности повторной загрузки того же файла
|
// Сбрасываем input для возможности повторной загрузки того же файла
|
||||||
@ -312,7 +468,12 @@ avatarInput.addEventListener("change", async function (event) {
|
|||||||
|
|
||||||
// Обработчик удаления аватарки
|
// Обработчик удаления аватарки
|
||||||
deleteAvatarBtn.addEventListener("click", async function () {
|
deleteAvatarBtn.addEventListener("click", async function () {
|
||||||
if (!confirm("Вы уверены, что хотите удалить аватарку?")) {
|
const confirmed = await showConfirmModal(
|
||||||
|
"Подтверждение удаления",
|
||||||
|
"Вы уверены, что хотите удалить аватарку?",
|
||||||
|
{ confirmType: "danger", confirmText: "Удалить" }
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,10 +495,10 @@ deleteAvatarBtn.addEventListener("click", async function () {
|
|||||||
// Очищаем кэш аватарки
|
// Очищаем кэш аватарки
|
||||||
clearAvatarCache();
|
clearAvatarCache();
|
||||||
|
|
||||||
showMessage("Аватарка успешно удалена", "success");
|
showNotification("Аватарка успешно удалена", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка удаления аватарки:", error);
|
console.error("Ошибка удаления аватарки:", error);
|
||||||
showMessage(error.message, "error");
|
showNotification(error.message, "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -348,17 +509,17 @@ updateProfileBtn.addEventListener("click", async function () {
|
|||||||
|
|
||||||
// Валидация
|
// Валидация
|
||||||
if (!username) {
|
if (!username) {
|
||||||
showMessage("Логин не может быть пустым", "error");
|
showNotification("Логин не может быть пустым", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
showMessage("Логин должен быть не менее 3 символов", "error");
|
showNotification("Логин должен быть не менее 3 символов", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (email && !isValidEmail(email)) {
|
if (email && !isValidEmail(email)) {
|
||||||
showMessage("Некорректный email адрес", "error");
|
showNotification("Некорректный email адрес", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,10 +542,10 @@ updateProfileBtn.addEventListener("click", async function () {
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
showMessage(result.message || "Профиль успешно обновлен", "success");
|
showNotification(result.message || "Профиль успешно обновлен", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка обновления профиля:", error);
|
console.error("Ошибка обновления профиля:", error);
|
||||||
showMessage(error.message, "error");
|
showNotification(error.message, "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -396,22 +557,22 @@ changePasswordBtn.addEventListener("click", async function () {
|
|||||||
|
|
||||||
// Валидация
|
// Валидация
|
||||||
if (!currentPassword) {
|
if (!currentPassword) {
|
||||||
showMessage("Введите текущий пароль", "error");
|
showNotification("Введите текущий пароль", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPassword) {
|
if (!newPassword) {
|
||||||
showMessage("Введите новый пароль", "error");
|
showNotification("Введите новый пароль", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
if (newPassword.length < 6) {
|
||||||
showMessage("Новый пароль должен быть не менее 6 символов", "error");
|
showNotification("Новый пароль должен быть не менее 6 символов", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
showMessage("Новый пароль и подтверждение не совпадают", "error");
|
showNotification("Новый пароль и подтверждение не совпадают", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,10 +600,10 @@ changePasswordBtn.addEventListener("click", async function () {
|
|||||||
newPasswordInput.value = "";
|
newPasswordInput.value = "";
|
||||||
confirmPasswordInput.value = "";
|
confirmPasswordInput.value = "";
|
||||||
|
|
||||||
showMessage(result.message || "Пароль успешно изменен", "success");
|
showNotification(result.message || "Пароль успешно изменен", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка изменения пароля:", 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
|
// PWA Service Worker Registration
|
||||||
class PWAManager {
|
class PWAManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -61,8 +157,13 @@ class PWAManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Показ уведомления об обновлении
|
// Показ уведомления об обновлении
|
||||||
showUpdateNotification() {
|
async showUpdateNotification() {
|
||||||
if (confirm("Доступна новая версия приложения. Обновить?")) {
|
const confirmed = await showConfirmModal(
|
||||||
|
"Обновление приложения",
|
||||||
|
"Доступна новая версия приложения. Обновить?",
|
||||||
|
{ confirmText: "Обновить" }
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
navigator.serviceWorker.controller.postMessage({
|
navigator.serviceWorker.controller.postMessage({
|
||||||
type: "SKIP_WAITING",
|
type: "SKIP_WAITING",
|
||||||
|
|||||||
@ -94,6 +94,9 @@
|
|||||||
<button class="settings-tab active" data-tab="appearance">
|
<button class="settings-tab active" data-tab="appearance">
|
||||||
<span class="iconify" data-icon="mdi:palette"></span> Внешний вид
|
<span class="iconify" data-icon="mdi:palette"></span> Внешний вид
|
||||||
</button>
|
</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">
|
<button class="settings-tab" data-tab="archive">
|
||||||
<span class="iconify" data-icon="mdi:archive"></span> Архив заметок
|
<span class="iconify" data-icon="mdi:archive"></span> Архив заметок
|
||||||
</button>
|
</button>
|
||||||
@ -161,9 +164,81 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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 style="color: #666; font-size: 14px; margin-bottom: 20px">
|
||||||
Архивированные заметки можно восстановить или удалить окончательно
|
Архивированные заметки можно восстановить или удалить окончательно
|
||||||
</p>
|
</p>
|
||||||
@ -193,6 +268,7 @@
|
|||||||
Окончательное удаление
|
Окончательное удаление
|
||||||
</option>
|
</option>
|
||||||
<option value="profile_update">Обновление профиля</option>
|
<option value="profile_update">Обновление профиля</option>
|
||||||
|
<option value="ai_improve">Улучшение через AI</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="refreshLogs" class="btnSave">
|
<button id="refreshLogs" class="btnSave">
|
||||||
<span class="iconify" data-icon="mdi:refresh"></span> Обновить
|
<span class="iconify" data-icon="mdi:refresh"></span> Обновить
|
||||||
@ -227,12 +303,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="settings-message-container" class="message-container"></div>
|
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>Создатель: <span>Fovway</span></p>
|
<p>Создатель: <span>Fovway</span></p>
|
||||||
</div>
|
</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">
|
<div id="imageModal" class="image-modal">
|
||||||
<span class="image-modal-close">×</span>
|
<span class="image-modal-close">×</span>
|
||||||
|
|||||||
@ -139,18 +139,169 @@ function updateColorPickerSelection(selectedColor) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для показа сообщений
|
// Система стека уведомлений
|
||||||
function showSettingsMessage(message, type = "success") {
|
let notificationStack = [];
|
||||||
const container = document.getElementById("settings-message-container");
|
|
||||||
if (container) {
|
|
||||||
container.textContent = message;
|
|
||||||
container.className = `message-container ${type}`;
|
|
||||||
container.style.display = "block";
|
|
||||||
|
|
||||||
setTimeout(() => {
|
// Универсальная функция для показа уведомлений
|
||||||
container.style.display = "none";
|
function showNotification(message, type = "info") {
|
||||||
}, 5000);
|
// Создаем уведомление
|
||||||
}
|
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);
|
loadLogs(true);
|
||||||
} else if (tabName === "appearance") {
|
} else if (tabName === "appearance") {
|
||||||
// Данные внешнего вида уже загружены в loadUserInfo()
|
// Данные внешнего вида уже загружены в loadUserInfo()
|
||||||
|
} else if (tabName === "ai") {
|
||||||
|
loadAiSettings();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -264,7 +417,12 @@ function addArchivedNotesEventListeners() {
|
|||||||
document.querySelectorAll(".btn-restore").forEach((btn) => {
|
document.querySelectorAll(".btn-restore").forEach((btn) => {
|
||||||
btn.addEventListener("click", async (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
const noteId = e.target.closest("button").dataset.id;
|
const noteId = e.target.closest("button").dataset.id;
|
||||||
if (confirm("Восстановить эту заметку из архива?")) {
|
const confirmed = await showConfirmModal(
|
||||||
|
"Подтверждение восстановления",
|
||||||
|
"Восстановить эту заметку из архива?",
|
||||||
|
{ confirmText: "Восстановить" }
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/notes/${noteId}/unarchive`, {
|
const response = await fetch(`/api/notes/${noteId}/unarchive`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@ -284,10 +442,10 @@ function addArchivedNotesEventListeners() {
|
|||||||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
alert("Заметка восстановлена!");
|
showNotification("Заметка восстановлена!", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
alert("Ошибка восстановления заметки");
|
showNotification("Ошибка восстановления заметки", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -297,9 +455,12 @@ function addArchivedNotesEventListeners() {
|
|||||||
document.querySelectorAll(".btn-delete-permanent").forEach((btn) => {
|
document.querySelectorAll(".btn-delete-permanent").forEach((btn) => {
|
||||||
btn.addEventListener("click", async (e) => {
|
btn.addEventListener("click", async (e) => {
|
||||||
const noteId = e.target.closest("button").dataset.id;
|
const noteId = e.target.closest("button").dataset.id;
|
||||||
if (
|
const confirmed = await showConfirmModal(
|
||||||
confirm("Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!")
|
"Подтверждение удаления",
|
||||||
) {
|
"Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!",
|
||||||
|
{ confirmType: "danger", confirmText: "Удалить навсегда" }
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/notes/archived/${noteId}`, {
|
const response = await fetch(`/api/notes/archived/${noteId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@ -319,16 +480,116 @@ function addArchivedNotesEventListeners() {
|
|||||||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
alert("Заметка удалена окончательно");
|
showNotification("Заметка удалена окончательно", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", 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) {
|
async function loadLogs(reset = false) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
@ -400,6 +661,7 @@ async function loadLogs(reset = false) {
|
|||||||
note_unarchive: "Восстановление",
|
note_unarchive: "Восстановление",
|
||||||
note_delete_permanent: "Окончательное удаление",
|
note_delete_permanent: "Окончательное удаление",
|
||||||
profile_update: "Обновление профиля",
|
profile_update: "Обновление профиля",
|
||||||
|
ai_improve: "Улучшение через AI",
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionText = actionTypes[log.action_type] || log.action_type;
|
const actionText = actionTypes[log.action_type] || log.action_type;
|
||||||
@ -444,6 +706,9 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|||||||
// Инициализируем табы
|
// Инициализируем табы
|
||||||
initTabs();
|
initTabs();
|
||||||
|
|
||||||
|
// Инициализируем обработчик удаления всех архивных заметок
|
||||||
|
initDeleteAllArchivedHandler();
|
||||||
|
|
||||||
// Обработчик фильтра логов
|
// Обработчик фильтра логов
|
||||||
document.getElementById("logTypeFilter").addEventListener("change", () => {
|
document.getElementById("logTypeFilter").addEventListener("change", () => {
|
||||||
loadLogs(true);
|
loadLogs(true);
|
||||||
@ -488,13 +753,13 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|||||||
accentColor
|
accentColor
|
||||||
);
|
);
|
||||||
|
|
||||||
showSettingsMessage(
|
showNotification(
|
||||||
result.message || "Цветовой акцент успешно обновлен",
|
result.message || "Цветовой акцент успешно обновлен",
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка обновления цветового акцента:", error);
|
console.error("Ошибка обновления цветового акцента:", error);
|
||||||
showSettingsMessage(error.message, "error");
|
showNotification(error.message, "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -521,4 +786,82 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupAppearanceColorPicker();
|
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 {
|
.save-button-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
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 {
|
.save-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
@ -1120,43 +1147,54 @@ textarea:focus {
|
|||||||
border-top: 1px solid var(--border-primary);
|
border-top: 1px solid var(--border-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-container {
|
/* Стили для уведомлений */
|
||||||
|
.notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%) translateY(-100%);
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
text-align: center;
|
color: white;
|
||||||
display: none;
|
|
||||||
z-index: 10000;
|
|
||||||
min-width: 300px;
|
|
||||||
max-width: 500px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
font-weight: 500;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.message-container {
|
.notification {
|
||||||
min-width: 90vw;
|
left: 10px;
|
||||||
max-width: 90vw;
|
right: 10px;
|
||||||
left: 5vw;
|
max-width: none;
|
||||||
transform: none;
|
transform: translateY(-100%);
|
||||||
padding: 10px 15px;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.message-container.success {
|
.notification.show {
|
||||||
background-color: #d4edda;
|
transform: translateY(0);
|
||||||
color: #155724;
|
}
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-container.error {
|
|
||||||
background-color: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
@ -1855,8 +1893,16 @@ textarea:focus {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btnSave {
|
.action-buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSave,
|
||||||
|
.btnAI {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптируем footer */
|
/* Адаптируем footer */
|
||||||
@ -2159,6 +2205,135 @@ textarea:focus {
|
|||||||
color: #bbb;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.image-preview-list {
|
.image-preview-list {
|
||||||
@ -2373,6 +2548,74 @@ textarea:focus {
|
|||||||
color: white;
|
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 {
|
.archived-note-content {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@ -2569,3 +2812,18 @@ textarea:focus {
|
|||||||
width: 100%;
|
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 bodyParser = require("body-parser");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
const app = express();
|
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 и добавляем их если нужно
|
// Проверяем существование колонок в таблице 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 для окончательного удаления архивной заметки
|
// API для окончательного удаления архивной заметки
|
||||||
app.delete("/api/notes/archived/:id", requireAuth, (req, res) => {
|
app.delete("/api/notes/archived/:id", requireAuth, (req, res) => {
|
||||||
const { id } = req.params;
|
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 для получения логов пользователя
|
// API для получения логов пользователя
|
||||||
app.get("/api/logs", requireAuth, (req, res) => {
|
app.get("/api/logs", requireAuth, (req, res) => {
|
||||||
const { action_type, limit = 100, offset = 0 } = req.query;
|
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) => {
|
app.post("/logout", (req, res) => {
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user