Добавлены функции для работы с AI настройками и улучшения текста

- Реализованы API для сохранения и получения AI настроек пользователя, включая OpenAI API ключ, базовый URL и модель.
- Добавлена возможность окончательного удаления всех архивных заметок с подтверждением пароля.
- Внедрена функция улучшения текста через AI, с обработкой запросов к OpenAI API.
- Обновлены интерфейсы для работы с AI настройками и добавлены уведомления для улучшения пользовательского опыта.
This commit is contained in:
Fovway 2025-10-26 14:45:02 +07:00
parent 323f96a502
commit d89a617264
10 changed files with 1978 additions and 110 deletions

View File

@ -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">&times;</span>
`;
// Создаем тело модального окна
const modalBody = document.createElement("div");
modalBody.className = "modal-body";
modalBody.innerHTML = `<p>${message}</p>`;
// Создаем футер с кнопками
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";
modalFooter.innerHTML = `
<button id="confirmBtn" class="${
options.confirmType === "danger" ? "btn-danger" : "btn-primary"
}" style="margin-right: 10px">
${options.confirmText || "OK"}
</button>
<button id="cancelBtn" class="btn-secondary">
${options.cancelText || "Отмена"}
</button>
`;
// Собираем модальное окно
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modalContent.appendChild(modalFooter);
modal.appendChild(modalContent);
// Добавляем на страницу
document.body.appendChild(modal);
// Функция закрытия
function closeModal() {
modal.style.display = "none";
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
// Обработчики событий
const closeBtn = modalHeader.querySelector(".modal-close");
const cancelBtn = modalFooter.querySelector("#cancelBtn");
const confirmBtn = modalFooter.querySelector("#confirmBtn");
closeBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
cancelBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
confirmBtn.addEventListener("click", () => {
closeModal();
resolve(true);
});
// Закрытие при клике вне модального окна
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeModal();
resolve(false);
}
});
// Закрытие по Escape
const handleEscape = (e) => {
if (e.key === "Escape") {
closeModal();
resolve(false);
document.removeEventListener("keydown", handleEscape);
}
};
document.addEventListener("keydown", handleEscape);
});
}
// Функция для кэширования аватарки // Функция для кэширования аватарки
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");
} }
} }
} }

View File

@ -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">&times;</span>
`;
// Создаем тело модального окна
const modalBody = document.createElement("div");
modalBody.className = "modal-body";
modalBody.innerHTML = `<p>${message}</p>`;
// Создаем футер с кнопками
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";
modalFooter.innerHTML = `
<button id="confirmBtn" class="${
options.confirmType === "danger" ? "btn-danger" : "btn-primary"
}" style="margin-right: 10px">
${options.confirmText || "OK"}
</button>
<button id="cancelBtn" class="btn-secondary">
${options.cancelText || "Отмена"}
</button>
`;
// Собираем модальное окно
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modalContent.appendChild(modalFooter);
modal.appendChild(modalContent);
// Добавляем на страницу
document.body.appendChild(modal);
// Функция закрытия
function closeModal() {
modal.style.display = "none";
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
// Обработчики событий
const closeBtn = modalHeader.querySelector(".modal-close");
const cancelBtn = modalFooter.querySelector("#cancelBtn");
const confirmBtn = modalFooter.querySelector("#confirmBtn");
closeBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
cancelBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
confirmBtn.addEventListener("click", () => {
closeModal();
resolve(true);
});
// Закрытие при клике вне модального окна
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeModal();
resolve(false);
}
});
// Закрытие по Escape
const handleEscape = (e) => {
if (e.key === "Escape") {
closeModal();
resolve(false);
document.removeEventListener("keydown", handleEscape);
}
};
document.addEventListener("keydown", handleEscape);
});
}
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();
} }

View File

@ -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>

View File

@ -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">

View File

@ -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">&times;</span>
`;
// Создаем тело модального окна
const modalBody = document.createElement("div");
modalBody.className = "modal-body";
modalBody.innerHTML = `<p>${message}</p>`;
// Создаем футер с кнопками
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";
modalFooter.innerHTML = `
<button id="confirmBtn" class="${
options.confirmType === "danger" ? "btn-danger" : "btn-primary"
}" style="margin-right: 10px">
${options.confirmText || "OK"}
</button>
<button id="cancelBtn" class="btn-secondary">
${options.cancelText || "Отмена"}
</button>
`;
// Собираем модальное окно
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modalContent.appendChild(modalFooter);
modal.appendChild(modalContent);
// Добавляем на страницу
document.body.appendChild(modal);
// Функция закрытия
function closeModal() {
modal.style.display = "none";
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
// Обработчики событий
const closeBtn = modalHeader.querySelector(".modal-close");
const cancelBtn = modalFooter.querySelector("#cancelBtn");
const confirmBtn = modalFooter.querySelector("#confirmBtn");
closeBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
cancelBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
confirmBtn.addEventListener("click", () => {
closeModal();
resolve(true);
});
// Закрытие при клике вне модального окна
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeModal();
resolve(false);
}
});
// Закрытие по Escape
const handleEscape = (e) => {
if (e.key === "Escape") {
closeModal();
resolve(false);
document.removeEventListener("keydown", handleEscape);
}
};
document.addEventListener("keydown", handleEscape);
});
}
// Логика переключения темы // Логика переключения темы
function 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");
} }
}); });

View File

@ -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">&times;</span>
`;
// Создаем тело модального окна
const modalBody = document.createElement("div");
modalBody.className = "modal-body";
modalBody.innerHTML = `<p>${message}</p>`;
// Создаем футер с кнопками
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";
modalFooter.innerHTML = `
<button id="confirmBtn" class="${
options.confirmType === "danger" ? "btn-danger" : "btn-primary"
}" style="margin-right: 10px">
${options.confirmText || "OK"}
</button>
<button id="cancelBtn" class="btn-secondary">
${options.cancelText || "Отмена"}
</button>
`;
// Собираем модальное окно
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modalContent.appendChild(modalFooter);
modal.appendChild(modalContent);
// Добавляем на страницу
document.body.appendChild(modal);
// Функция закрытия
function closeModal() {
modal.style.display = "none";
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
// Обработчики событий
const closeBtn = modalHeader.querySelector(".modal-close");
const cancelBtn = modalFooter.querySelector("#cancelBtn");
const confirmBtn = modalFooter.querySelector("#confirmBtn");
closeBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
cancelBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
confirmBtn.addEventListener("click", () => {
closeModal();
resolve(true);
});
// Закрытие при клике вне модального окна
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeModal();
resolve(false);
}
});
// Закрытие по Escape
const handleEscape = (e) => {
if (e.key === "Escape") {
closeModal();
resolve(false);
document.removeEventListener("keydown", handleEscape);
}
};
document.addEventListener("keydown", handleEscape);
});
}
// 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",

View File

@ -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"
>&times;</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">&times;</span> <span class="image-modal-close">&times;</span>

View File

@ -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">&times;</span>
`;
// Создаем тело модального окна
const modalBody = document.createElement("div");
modalBody.className = "modal-body";
modalBody.innerHTML = `<p>${message}</p>`;
// Создаем футер с кнопками
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";
modalFooter.innerHTML = `
<button id="confirmBtn" class="${
options.confirmType === "danger" ? "btn-danger" : "btn-primary"
}" style="margin-right: 10px">
${options.confirmText || "OK"}
</button>
<button id="cancelBtn" class="btn-secondary">
${options.cancelText || "Отмена"}
</button>
`;
// Собираем модальное окно
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modalContent.appendChild(modalFooter);
modal.appendChild(modalContent);
// Добавляем на страницу
document.body.appendChild(modal);
// Функция закрытия
function closeModal() {
modal.style.display = "none";
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
// Обработчики событий
const closeBtn = modalHeader.querySelector(".modal-close");
const cancelBtn = modalFooter.querySelector("#cancelBtn");
const confirmBtn = modalFooter.querySelector("#confirmBtn");
closeBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
cancelBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
confirmBtn.addEventListener("click", () => {
closeModal();
resolve(true);
});
// Закрытие при клике вне модального окна
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeModal();
resolve(false);
}
});
// Закрытие по Escape
const handleEscape = (e) => {
if (e.key === "Escape") {
closeModal();
resolve(false);
document.removeEventListener("keydown", handleEscape);
}
};
document.addEventListener("keydown", handleEscape);
});
} }
// Переключение табов // Переключение табов
@ -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);
}
}

View File

@ -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
View File

@ -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;