Добавлена поддержка спойлеров и улучшена функциональность AI настроек

- Реализована возможность вставки спойлеров в заметки с помощью нового интерфейса и логики обработки.
- Добавлен переключатель для включения/выключения помощи ИИ в настройках пользователя, с проверкой заполненности обязательных полей.
- Обновлены API для получения и сохранения настроек AI, включая новую колонку `ai_enabled` в таблице пользователей.
- Улучшены стили и обработчики событий для новых элементов интерфейса, включая спойлеры и переключатель AI.
This commit is contained in:
Fovway 2025-10-28 06:22:37 +07:00
parent 155f4303d5
commit 1479205261
6 changed files with 586 additions and 15 deletions

View File

@ -305,6 +305,7 @@ const codeBtn = document.getElementById("codeBtn");
const linkBtn = document.getElementById("linkBtn"); 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 spoilerBtn = document.getElementById("spoilerBtn");
const previewBtn = document.getElementById("previewBtn"); const previewBtn = document.getElementById("previewBtn");
const aiImproveBtn = document.getElementById("aiImproveBtn"); const aiImproveBtn = document.getElementById("aiImproveBtn");
@ -669,6 +670,33 @@ function insertColorMarkdown(color) {
} }
// Функция для вставки markdown // Функция для вставки markdown
function insertSpoiler() {
const start = noteInput.selectionStart;
const end = noteInput.selectionEnd;
const text = noteInput.value;
const before = text.substring(0, start);
const selected = text.substring(start, end);
const after = text.substring(end);
let newText;
let newCursorPos;
if (selected) {
// Если есть выделенный текст, оборачиваем его в спойлер
newText = before + "||" + selected + "||" + after;
newCursorPos = start + selected.length + 4; // После выделенного текста
} else {
// Если нет выделенного текста, вставляем пустой спойлер
newText = before + "||скрытый текст||" + after;
newCursorPos = start + 2; // Внутри спойлера для редактирования
}
noteInput.value = newText;
noteInput.setSelectionRange(newCursorPos, newCursorPos);
noteInput.focus();
}
function insertMarkdown(tag) { function insertMarkdown(tag) {
const start = noteInput.selectionStart; const start = noteInput.selectionStart;
const end = noteInput.selectionEnd; const end = noteInput.selectionEnd;
@ -1084,6 +1112,10 @@ checkboxBtn.addEventListener("click", function () {
insertMarkdown("- [ ] "); insertMarkdown("- [ ] ");
}); });
spoilerBtn.addEventListener("click", function () {
insertSpoiler();
});
// Обработчик для кнопки загрузки изображений // Обработчик для кнопки загрузки изображений
imageBtn.addEventListener("click", function (event) { imageBtn.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
@ -1721,6 +1753,32 @@ renderer.listitem = function (text, task, checked) {
return originalListItem(text, task, checked); return originalListItem(text, task, checked);
}; };
// Кастомное расширение для скрытого текста (спойлеров)
const spoilerExtension = {
name: "spoiler",
level: "inline",
start(src) {
return src.match(/\|\|/) ? src.indexOf("||") : -1;
},
tokenizer(src, tokens) {
const rule = /^\|\|(.*?)\|\|/;
const match = rule.exec(src);
if (match) {
return {
type: "spoiler",
raw: match[0],
text: match[1].trim(),
};
}
},
renderer(token) {
return `<span class="spoiler" title="Нажмите, чтобы показать">${token.text}</span>`;
},
};
// Регистрируем расширение через marked.use()
marked.use({ extensions: [spoilerExtension] });
marked.setOptions({ marked.setOptions({
gfm: true, // GitHub Flavored Markdown (включает strikethrough) gfm: true, // GitHub Flavored Markdown (включает strikethrough)
breaks: true, breaks: true,
@ -1871,6 +1929,9 @@ async function renderNotes(notes) {
// Добавляем обработчики для чекбоксов в заметках // Добавляем обработчики для чекбоксов в заметках
addCheckboxEventListeners(); addCheckboxEventListeners();
// Добавляем обработчики для спойлеров
addSpoilerEventListeners();
// Обрабатываем длинные заметки // Обрабатываем длинные заметки
handleLongNotes(); handleLongNotes();
@ -2390,6 +2451,12 @@ function addNoteEventListeners() {
'<span class="iconify" data-icon="mdi:robot"></span> Помощь ИИ'; '<span class="iconify" data-icon="mdi:robot"></span> Помощь ИИ';
aiImproveEditBtn.title = "Улучшить или создать текст через ИИ"; aiImproveEditBtn.title = "Улучшить или создать текст через ИИ";
// Проверяем настройку AI и скрываем кнопку если отключено
const aiEnabled = localStorage.getItem("ai_enabled");
if (aiEnabled !== "1") {
aiImproveEditBtn.style.display = "none";
}
// Кнопка сохранить // Кнопка сохранить
const saveEditBtn = document.createElement("button"); const saveEditBtn = document.createElement("button");
saveEditBtn.textContent = "Сохранить"; saveEditBtn.textContent = "Сохранить";
@ -2969,6 +3036,24 @@ function addCheckboxEventListeners() {
}); });
} }
function addSpoilerEventListeners() {
document.querySelectorAll(".spoiler").forEach((spoiler) => {
// Проверяем, не добавлен ли уже обработчик
if (spoiler._clickHandler) {
return; // Пропускаем, если обработчик уже добавлен
}
// Создаем новый обработчик
spoiler._clickHandler = function (event) {
event.stopPropagation();
this.classList.toggle("revealed");
console.log("Спойлер кликнут:", this.textContent);
};
spoiler.addEventListener("click", spoiler._clickHandler);
});
}
// Функция сохранения заметки (вынесена отдельно для повторного использования) // Функция сохранения заметки (вынесена отдельно для повторного использования)
async function saveNote() { async function saveNote() {
if (noteInput.value.trim() !== "" || selectedImages.length > 0) { if (noteInput.value.trim() !== "" || selectedImages.length > 0) {
@ -4176,5 +4261,51 @@ function applyTheme(theme) {
} }
} }
// Функция для проверки и применения видимости кнопок AI
async function updateAiButtonsVisibility() {
// Проверяем localStorage сначала для быстрого ответа
let aiEnabled = localStorage.getItem("ai_enabled");
// Если нет в localStorage, загружаем с сервера
if (aiEnabled === null) {
try {
const response = await fetch("/api/user/ai-settings");
if (response.ok) {
const settings = await response.json();
aiEnabled = settings.ai_enabled ? "1" : "0";
localStorage.setItem("ai_enabled", aiEnabled);
} else {
aiEnabled = "0"; // По умолчанию выключено
}
} catch (error) {
console.error("Ошибка загрузки настроек AI:", error);
aiEnabled = "0"; // По умолчанию выключено
}
}
const isEnabled = aiEnabled === "1";
// Показываем/скрываем кнопку AI в основном редакторе
const mainAiBtn = document.getElementById("aiImproveBtn");
if (mainAiBtn) {
mainAiBtn.style.display = isEnabled ? "" : "none";
}
// Показываем/скрываем кнопки AI в редакторах заметок
document.querySelectorAll(".btnAI").forEach((btn) => {
btn.style.display = isEnabled ? "" : "none";
});
}
// Инициализируем переключатель темы при загрузке страницы // Инициализируем переключатель темы при загрузке страницы
document.addEventListener("DOMContentLoaded", initThemeToggle); document.addEventListener("DOMContentLoaded", () => {
initThemeToggle();
updateAiButtonsVisibility();
});
// Обновляем видимость кнопок AI когда пользователь возвращается на страницу
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
updateAiButtonsVisibility();
}
});

View File

@ -295,6 +295,9 @@
<button class="btnMarkdown" id="colorBtn" title="Цвет текста"> <button class="btnMarkdown" id="colorBtn" title="Цвет текста">
<span class="iconify" data-icon="mdi:palette"></span> <span class="iconify" data-icon="mdi:palette"></span>
</button> </button>
<button class="btnMarkdown" id="spoilerBtn" title="Скрытый текст">
<span class="iconify" data-icon="mdi:eye-off"></span>
</button>
<div class="header-dropdown"> <div class="header-dropdown">
<button class="btnMarkdown" id="headerBtn" title="Заголовок"> <button class="btnMarkdown" id="headerBtn" title="Заголовок">
<span <span

View File

@ -168,6 +168,25 @@
<div class="tab-content" id="ai-tab"> <div class="tab-content" id="ai-tab">
<h3>Настройки AI</h3> <h3>Настройки AI</h3>
<div class="form-group ai-toggle-group">
<label class="ai-toggle-label">
<div class="toggle-label-content">
<span class="toggle-text-main">Включить помощь ИИ</span>
<span class="toggle-text-desc"
>Показывать кнопку "Помощь ИИ" в редакторах заметок</span
>
</div>
<div class="toggle-switch-wrapper">
<input
type="checkbox"
id="ai-enabled-toggle"
class="toggle-checkbox"
/>
<span class="toggle-slider"></span>
</div>
</label>
</div>
<div class="form-group"> <div class="form-group">
<label for="openai-api-key">OpenAI API Key</label> <label for="openai-api-key">OpenAI API Key</label>
<input <input

View File

@ -1,3 +1,26 @@
// Кастомное расширение для скрытого текста (спойлеров)
const spoilerExtension = {
name: "spoiler",
level: "inline",
start(src) {
return src.match(/\|\|/) ? src.indexOf("||") : -1;
},
tokenizer(src, tokens) {
const rule = /^\|\|(.*?)\|\|/;
const match = rule.exec(src);
if (match) {
return {
type: "spoiler",
raw: match[0],
text: match[1].trim(),
};
}
},
renderer(token) {
return `<span class="spoiler" title="Нажмите, чтобы показать">${token.text}</span>`;
},
};
// Настройка marked.js для поддержки внешних ссылок // Настройка marked.js для поддержки внешних ссылок
function setupMarkedRenderer() { function setupMarkedRenderer() {
// Функция для определения внешних ссылок // Функция для определения внешних ссылок
@ -30,6 +53,9 @@ function setupMarkedRenderer() {
} }
}; };
// Регистрируем расширение спойлеров
marked.use({ extensions: [spoilerExtension] });
// Настраиваем marked // Настраиваем marked
marked.setOptions({ marked.setOptions({
gfm: true, gfm: true,
@ -39,6 +65,25 @@ function setupMarkedRenderer() {
}); });
} }
// Функция для добавления обработчиков спойлеров
function addSpoilerEventListeners() {
document.querySelectorAll(".spoiler").forEach((spoiler) => {
// Проверяем, не добавлен ли уже обработчик
if (spoiler._clickHandler) {
return; // Пропускаем, если обработчик уже добавлен
}
// Создаем новый обработчик
spoiler._clickHandler = function (event) {
event.stopPropagation();
this.classList.toggle("revealed");
console.log("Спойлер кликнут:", this.textContent);
};
spoiler.addEventListener("click", spoiler._clickHandler);
});
}
// Функция для добавления обработчиков внешних ссылок // Функция для добавления обработчиков внешних ссылок
function addExternalLinkListeners() { function addExternalLinkListeners() {
// Обработчики для внешних ссылок // Обработчики для внешних ссылок
@ -509,6 +554,9 @@ async function loadArchivedNotes() {
// Добавление обработчиков для архивных заметок // Добавление обработчиков для архивных заметок
function addArchivedNotesEventListeners() { function addArchivedNotesEventListeners() {
// Добавляем обработчики для спойлеров
addSpoilerEventListeners();
// Добавляем обработчики для внешних ссылок // Добавляем обработчики для внешних ссылок
addExternalLinkListeners(); addExternalLinkListeners();
@ -932,14 +980,107 @@ document.addEventListener("DOMContentLoaded", async function () {
result.message || "AI настройки успешно сохранены", result.message || "AI настройки успешно сохранены",
"success" "success"
); );
// Обновляем состояние переключателя после сохранения
updateAiToggleState();
} catch (error) { } catch (error) {
console.error("Ошибка сохранения AI настроек:", error); console.error("Ошибка сохранения AI настроек:", error);
showNotification(error.message, "error"); showNotification(error.message, "error");
} }
}); });
} }
// Обработчик переключателя AI
const aiEnabledToggle = document.getElementById("ai-enabled-toggle");
if (aiEnabledToggle) {
aiEnabledToggle.addEventListener("change", async function () {
// Проверяем заполнены ли настройки перед включением
if (aiEnabledToggle.checked && !checkAiSettingsFilled()) {
aiEnabledToggle.checked = false;
showNotification("Сначала заполните все AI настройки", "warning");
return;
}
const isEnabled = aiEnabledToggle.checked;
try {
const response = await fetch("/api/user/ai-settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
ai_enabled: isEnabled ? 1 : 0,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Ошибка сохранения настройки AI");
}
const result = await response.json();
showNotification(
isEnabled ? "Помощь ИИ включена" : "Помощь ИИ отключена",
"success"
);
// Сохраняем в localStorage для быстрого доступа в app.js
localStorage.setItem("ai_enabled", isEnabled ? "1" : "0");
} catch (error) {
console.error("Ошибка сохранения настройки AI:", error);
showNotification(error.message, "error");
// Откатываем изменения при ошибке
aiEnabledToggle.checked = !isEnabled;
}
});
}
// Отслеживаем изменения в полях AI настроек
const apiKeyInput = document.getElementById("openai-api-key");
const baseUrlInput = document.getElementById("openai-base-url");
const modelInput = document.getElementById("openai-model");
[apiKeyInput, baseUrlInput, modelInput].forEach((input) => {
if (input) {
input.addEventListener("input", updateAiToggleState);
}
});
}); });
// Проверка заполнения AI настроек
function checkAiSettingsFilled() {
const apiKey = document.getElementById("openai-api-key").value.trim();
const baseUrl = document.getElementById("openai-base-url").value.trim();
const model = document.getElementById("openai-model").value.trim();
return apiKey && baseUrl && model;
}
// Обновление состояния переключателя AI
function updateAiToggleState() {
const aiEnabledToggle = document.getElementById("ai-enabled-toggle");
const toggleLabel = document.querySelector(".ai-toggle-label");
const toggleDesc = document.querySelector(".toggle-text-desc");
if (!aiEnabledToggle) return;
const isFilled = checkAiSettingsFilled();
if (isFilled) {
aiEnabledToggle.disabled = false;
toggleLabel.classList.remove("disabled");
toggleDesc.textContent =
'Показывать кнопку "Помощь ИИ" в редакторах заметок';
} else {
aiEnabledToggle.disabled = true;
aiEnabledToggle.checked = false;
toggleLabel.classList.add("disabled");
toggleDesc.textContent =
"Сначала заполните API Key, Base URL и Модель ниже";
}
}
// Загрузка AI настроек // Загрузка AI настроек
async function loadAiSettings() { async function loadAiSettings() {
try { try {
@ -949,6 +1090,7 @@ async function loadAiSettings() {
const apiKeyInput = document.getElementById("openai-api-key"); const apiKeyInput = document.getElementById("openai-api-key");
const baseUrlInput = document.getElementById("openai-base-url"); const baseUrlInput = document.getElementById("openai-base-url");
const modelInput = document.getElementById("openai-model"); const modelInput = document.getElementById("openai-model");
const aiEnabledToggle = document.getElementById("ai-enabled-toggle");
if (apiKeyInput) { if (apiKeyInput) {
apiKeyInput.value = settings.openai_api_key || ""; apiKeyInput.value = settings.openai_api_key || "";
@ -959,6 +1101,15 @@ async function loadAiSettings() {
if (modelInput) { if (modelInput) {
modelInput.value = settings.openai_model || ""; modelInput.value = settings.openai_model || "";
} }
if (aiEnabledToggle) {
aiEnabledToggle.checked = settings.ai_enabled === 1;
}
// Проверяем и обновляем состояние переключателя
updateAiToggleState();
// Сохраняем в localStorage для быстрого доступа в app.js
localStorage.setItem("ai_enabled", settings.ai_enabled ? "1" : "0");
} }
} catch (error) { } catch (error) {
console.error("Ошибка загрузки AI настроек:", error); console.error("Ошибка загрузки AI настроек:", error);

View File

@ -549,6 +549,123 @@ header {
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
} }
/* Toggle Switch Styles */
.ai-toggle-group {
margin-bottom: 20px;
}
.ai-toggle-label {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
font-weight: normal !important;
margin-bottom: 0 !important;
padding: 16px 20px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-secondary);
border-radius: 8px;
transition: all 0.3s ease;
}
.ai-toggle-label:hover {
background-color: var(--bg-secondary);
border-color: var(--accent-color);
}
.ai-toggle-label.disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: var(--bg-secondary);
}
.ai-toggle-label.disabled:hover {
background-color: var(--bg-secondary);
border-color: var(--border-secondary);
}
.ai-toggle-label.disabled .toggle-text-main {
color: var(--text-secondary);
}
.ai-toggle-label.disabled .toggle-text-desc {
color: #e67e22;
font-weight: 500;
}
.ai-toggle-label.disabled .toggle-slider {
background-color: #999;
cursor: not-allowed;
}
.ai-toggle-label.disabled .toggle-slider::before {
background-color: #ddd;
}
.toggle-label-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
min-width: 0;
}
.toggle-text-main {
color: var(--text-primary);
font-size: 15px;
font-weight: 500;
}
.toggle-text-desc {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.4;
}
.toggle-switch-wrapper {
flex-shrink: 0;
margin-left: 20px;
position: relative;
}
.toggle-checkbox {
display: none;
}
.toggle-slider {
position: relative;
display: block;
width: 50px;
height: 26px;
background-color: #ccc;
border-radius: 26px;
transition: background-color 0.3s ease;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.toggle-slider::before {
content: "";
position: absolute;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: white;
top: 2px;
left: 2px;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle-checkbox:checked + .toggle-slider {
background-color: var(--accent-color, #007bff);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
}
.toggle-checkbox:checked + .toggle-slider::before {
transform: translateX(24px);
}
.error-message { .error-message {
color: var(--icon-danger); color: var(--icon-danger);
margin-top: 10px; margin-top: 10px;
@ -1793,6 +1910,37 @@ textarea:focus {
/* Мобильная адаптация */ /* Мобильная адаптация */
@media (max-width: 768px) { @media (max-width: 768px) {
/* Адаптация переключателя AI */
.ai-toggle-label {
padding: 12px 16px;
}
.toggle-text-main {
font-size: 14px;
}
.toggle-text-desc {
font-size: 11px;
}
.toggle-switch-wrapper {
margin-left: 12px;
}
.toggle-slider {
width: 44px;
height: 24px;
}
.toggle-slider::before {
width: 20px;
height: 20px;
}
.toggle-checkbox:checked + .toggle-slider::before {
transform: translateX(20px);
}
/* Показываем мобильное меню */ /* Показываем мобильное меню */
.mobile-menu-btn { .mobile-menu-btn {
display: flex; display: flex;
@ -3125,3 +3273,60 @@ textarea:focus {
background: #d1ecf1; background: #d1ecf1;
color: #0c5460; color: #0c5460;
} }
/* Стили для скрытого текста (спойлеров) */
.spoiler {
background: linear-gradient(45deg, #f0f0f0, #e8e8e8);
color: transparent;
cursor: pointer;
border-radius: 4px;
padding: 3px 6px;
user-select: none;
transition: all 0.3s ease;
position: relative;
border: 1px solid #ddd;
font-weight: 500;
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
text-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
}
.spoiler::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
border-radius: 4px;
filter: blur(1px);
z-index: -1;
}
.spoiler:hover {
background: linear-gradient(45deg, #e8e8e8, #d8d8d8);
transform: scale(1.02);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.spoiler:hover::before {
background: rgba(255, 255, 255, 0.9);
}
.spoiler.revealed {
background: linear-gradient(45deg, #e8f5e8, #d4edda);
color: #155724;
border-color: #c3e6cb;
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
text-shadow: none;
}
.spoiler.revealed::before {
display: none;
}
.spoiler.revealed:hover {
background: linear-gradient(45deg, #d4edda, #c3e6cb);
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.35);
}

View File

@ -483,6 +483,21 @@ function runMigrations() {
} }
}); });
} }
// Проверяем существование колонки ai_enabled
const hasAiEnabled = columns.some((col) => col.name === "ai_enabled");
if (!hasAiEnabled) {
db.run(
"ALTER TABLE users ADD COLUMN ai_enabled INTEGER DEFAULT 0",
(err) => {
if (err) {
console.error("Ошибка добавления колонки ai_enabled:", err.message);
} else {
console.log("Колонка ai_enabled добавлена в таблицу users");
}
}
);
}
}); });
// Проверяем существование колонок в таблице notes и добавляем их если нужно // Проверяем существование колонок в таблице notes и добавляем их если нужно
@ -1907,7 +1922,7 @@ app.get("/api/user/ai-settings", requireApiAuth, (req, res) => {
} }
const sql = const sql =
"SELECT openai_api_key, openai_base_url, openai_model FROM users WHERE id = ?"; "SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, settings) => { db.get(sql, [req.session.userId], (err, settings) => {
if (err) { if (err) {
console.error("Ошибка получения AI настроек:", err.message); console.error("Ошибка получения AI настроек:", err.message);
@ -1918,25 +1933,72 @@ app.get("/api/user/ai-settings", requireApiAuth, (req, res) => {
return res.status(404).json({ error: "Настройки не найдены" }); return res.status(404).json({ error: "Настройки не найдены" });
} }
res.json(settings); res.json({
openai_api_key: settings.openai_api_key || "",
openai_base_url: settings.openai_base_url || "",
openai_model: settings.openai_model || "",
ai_enabled: settings.ai_enabled || 0,
});
}); });
}); });
// API для сохранения AI настроек // API для сохранения AI настроек
app.put("/api/user/ai-settings", requireApiAuth, (req, res) => { app.put("/api/user/ai-settings", requireApiAuth, (req, res) => {
const { openai_api_key, openai_base_url, openai_model } = req.body; const { openai_api_key, openai_base_url, openai_model, ai_enabled } =
req.body;
const userId = req.session.userId; const userId = req.session.userId;
if (!openai_api_key || !openai_base_url || !openai_model) { // ai_enabled может быть передан отдельно от остальных настроек
return res.status(400).json({ error: "Все поля обязательны" }); if (
} ai_enabled !== undefined &&
!openai_api_key &&
!openai_base_url &&
!openai_model
) {
const updateSql = "UPDATE users SET ai_enabled = ? WHERE id = ?";
db.run(updateSql, [ai_enabled ? 1 : 0, userId], function (err) {
if (err) {
console.error("Ошибка сохранения статуса AI:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
const updateSql = // Логируем обновление AI настроек
"UPDATE users SET openai_api_key = ?, openai_base_url = ?, openai_model = ? WHERE id = ?"; const clientIP = getClientIP(req);
db.run( logAction(userId, "profile_update", "Изменен статус AI", clientIP);
updateSql,
[openai_api_key, openai_base_url, openai_model, userId], res.json({ success: true, message: "Настройки AI успешно сохранены" });
function (err) { });
} else {
// Если сохраняем полные настройки
if (!openai_api_key || !openai_base_url || !openai_model) {
return res.status(400).json({ error: "Все поля обязательны" });
}
// При сохранении полных настроек также проверяем ai_enabled
// Если все поля заполнены и ai_enabled не передан, оставляем текущее значение
// Если передан ai_enabled, используем его
const finalAiEnabled =
ai_enabled !== undefined ? (ai_enabled ? 1 : 0) : undefined;
let updateSql, updateParams;
if (finalAiEnabled !== undefined) {
updateSql =
"UPDATE users SET openai_api_key = ?, openai_base_url = ?, openai_model = ?, ai_enabled = ? WHERE id = ?";
updateParams = [
openai_api_key,
openai_base_url,
openai_model,
finalAiEnabled,
userId,
];
} else {
updateSql =
"UPDATE users SET openai_api_key = ?, openai_base_url = ?, openai_model = ? WHERE id = ?";
updateParams = [openai_api_key, openai_base_url, openai_model, userId];
}
db.run(updateSql, updateParams, function (err) {
if (err) { if (err) {
console.error("Ошибка сохранения AI настроек:", err.message); console.error("Ошибка сохранения AI настроек:", err.message);
return res.status(500).json({ error: "Ошибка сервера" }); return res.status(500).json({ error: "Ошибка сервера" });
@ -1947,8 +2009,8 @@ app.put("/api/user/ai-settings", requireApiAuth, (req, res) => {
logAction(userId, "profile_update", "Обновлены AI настройки", clientIP); logAction(userId, "profile_update", "Обновлены AI настройки", clientIP);
res.json({ success: true, message: "AI настройки успешно сохранены" }); res.json({ success: true, message: "AI настройки успешно сохранены" });
} });
); }
}); });
// API для улучшения текста через AI // API для улучшения текста через AI