✨ Добавлена поддержка спойлеров и улучшена функциональность AI настроек
- Реализована возможность вставки спойлеров в заметки с помощью нового интерфейса и логики обработки. - Добавлен переключатель для включения/выключения помощи ИИ в настройках пользователя, с проверкой заполненности обязательных полей. - Обновлены API для получения и сохранения настроек AI, включая новую колонку `ai_enabled` в таблице пользователей. - Улучшены стили и обработчики событий для новых элементов интерфейса, включая спойлеры и переключатель AI.
This commit is contained in:
parent
155f4303d5
commit
1479205261
133
public/app.js
133
public/app.js
@ -305,6 +305,7 @@ const codeBtn = document.getElementById("codeBtn");
|
||||
const linkBtn = document.getElementById("linkBtn");
|
||||
const checkboxBtn = document.getElementById("checkboxBtn");
|
||||
const imageBtn = document.getElementById("imageBtn");
|
||||
const spoilerBtn = document.getElementById("spoilerBtn");
|
||||
const previewBtn = document.getElementById("previewBtn");
|
||||
const aiImproveBtn = document.getElementById("aiImproveBtn");
|
||||
|
||||
@ -669,6 +670,33 @@ function insertColorMarkdown(color) {
|
||||
}
|
||||
|
||||
// Функция для вставки 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) {
|
||||
const start = noteInput.selectionStart;
|
||||
const end = noteInput.selectionEnd;
|
||||
@ -1084,6 +1112,10 @@ checkboxBtn.addEventListener("click", function () {
|
||||
insertMarkdown("- [ ] ");
|
||||
});
|
||||
|
||||
spoilerBtn.addEventListener("click", function () {
|
||||
insertSpoiler();
|
||||
});
|
||||
|
||||
// Обработчик для кнопки загрузки изображений
|
||||
imageBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
@ -1721,6 +1753,32 @@ renderer.listitem = function (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({
|
||||
gfm: true, // GitHub Flavored Markdown (включает strikethrough)
|
||||
breaks: true,
|
||||
@ -1871,6 +1929,9 @@ async function renderNotes(notes) {
|
||||
// Добавляем обработчики для чекбоксов в заметках
|
||||
addCheckboxEventListeners();
|
||||
|
||||
// Добавляем обработчики для спойлеров
|
||||
addSpoilerEventListeners();
|
||||
|
||||
// Обрабатываем длинные заметки
|
||||
handleLongNotes();
|
||||
|
||||
@ -2390,6 +2451,12 @@ function addNoteEventListeners() {
|
||||
'<span class="iconify" data-icon="mdi:robot"></span> Помощь ИИ';
|
||||
aiImproveEditBtn.title = "Улучшить или создать текст через ИИ";
|
||||
|
||||
// Проверяем настройку AI и скрываем кнопку если отключено
|
||||
const aiEnabled = localStorage.getItem("ai_enabled");
|
||||
if (aiEnabled !== "1") {
|
||||
aiImproveEditBtn.style.display = "none";
|
||||
}
|
||||
|
||||
// Кнопка сохранить
|
||||
const saveEditBtn = document.createElement("button");
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
@ -295,6 +295,9 @@
|
||||
<button class="btnMarkdown" id="colorBtn" title="Цвет текста">
|
||||
<span class="iconify" data-icon="mdi:palette"></span>
|
||||
</button>
|
||||
<button class="btnMarkdown" id="spoilerBtn" title="Скрытый текст">
|
||||
<span class="iconify" data-icon="mdi:eye-off"></span>
|
||||
</button>
|
||||
<div class="header-dropdown">
|
||||
<button class="btnMarkdown" id="headerBtn" title="Заголовок">
|
||||
<span
|
||||
|
||||
@ -168,6 +168,25 @@
|
||||
<div class="tab-content" id="ai-tab">
|
||||
<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">
|
||||
<label for="openai-api-key">OpenAI API Key</label>
|
||||
<input
|
||||
|
||||
@ -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 для поддержки внешних ссылок
|
||||
function setupMarkedRenderer() {
|
||||
// Функция для определения внешних ссылок
|
||||
@ -30,6 +53,9 @@ function setupMarkedRenderer() {
|
||||
}
|
||||
};
|
||||
|
||||
// Регистрируем расширение спойлеров
|
||||
marked.use({ extensions: [spoilerExtension] });
|
||||
|
||||
// Настраиваем marked
|
||||
marked.setOptions({
|
||||
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() {
|
||||
// Обработчики для внешних ссылок
|
||||
@ -509,6 +554,9 @@ async function loadArchivedNotes() {
|
||||
|
||||
// Добавление обработчиков для архивных заметок
|
||||
function addArchivedNotesEventListeners() {
|
||||
// Добавляем обработчики для спойлеров
|
||||
addSpoilerEventListeners();
|
||||
|
||||
// Добавляем обработчики для внешних ссылок
|
||||
addExternalLinkListeners();
|
||||
|
||||
@ -932,14 +980,107 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
result.message || "AI настройки успешно сохранены",
|
||||
"success"
|
||||
);
|
||||
|
||||
// Обновляем состояние переключателя после сохранения
|
||||
updateAiToggleState();
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения AI настроек:", 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 настроек
|
||||
async function loadAiSettings() {
|
||||
try {
|
||||
@ -949,6 +1090,7 @@ async function loadAiSettings() {
|
||||
const apiKeyInput = document.getElementById("openai-api-key");
|
||||
const baseUrlInput = document.getElementById("openai-base-url");
|
||||
const modelInput = document.getElementById("openai-model");
|
||||
const aiEnabledToggle = document.getElementById("ai-enabled-toggle");
|
||||
|
||||
if (apiKeyInput) {
|
||||
apiKeyInput.value = settings.openai_api_key || "";
|
||||
@ -959,6 +1101,15 @@ async function loadAiSettings() {
|
||||
if (modelInput) {
|
||||
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) {
|
||||
console.error("Ошибка загрузки AI настроек:", error);
|
||||
|
||||
205
public/style.css
205
public/style.css
@ -549,6 +549,123 @@ header {
|
||||
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 {
|
||||
color: var(--icon-danger);
|
||||
margin-top: 10px;
|
||||
@ -1793,6 +1910,37 @@ textarea:focus {
|
||||
|
||||
/* Мобильная адаптация */
|
||||
@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 {
|
||||
display: flex;
|
||||
@ -3125,3 +3273,60 @@ textarea:focus {
|
||||
background: #d1ecf1;
|
||||
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);
|
||||
}
|
||||
|
||||
90
server.js
90
server.js
@ -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 и добавляем их если нужно
|
||||
@ -1907,7 +1922,7 @@ app.get("/api/user/ai-settings", requireApiAuth, (req, res) => {
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения AI настроек:", err.message);
|
||||
@ -1918,25 +1933,72 @@ app.get("/api/user/ai-settings", requireApiAuth, (req, res) => {
|
||||
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 настроек
|
||||
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;
|
||||
|
||||
if (!openai_api_key || !openai_base_url || !openai_model) {
|
||||
return res.status(400).json({ error: "Все поля обязательны" });
|
||||
}
|
||||
// ai_enabled может быть передан отдельно от остальных настроек
|
||||
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 =
|
||||
"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) {
|
||||
// Логируем обновление AI настроек
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(userId, "profile_update", "Изменен статус AI", clientIP);
|
||||
|
||||
res.json({ success: true, message: "Настройки AI успешно сохранены" });
|
||||
});
|
||||
} 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) {
|
||||
console.error("Ошибка сохранения AI настроек:", err.message);
|
||||
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);
|
||||
|
||||
res.json({ success: true, message: "AI настройки успешно сохранены" });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API для улучшения текста через AI
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user