Добавлено новое поле ai_enabled в запросы к базе данных для получения настроек AI пользователя. Реализована проверка включения функций ИИ в API для улучшения обработки запросов. Обновлены компоненты MergeNotesModal и NotesPage для поддержки удаления оригинальных заметок. Изменены уведомления и стили для улучшения пользовательского интерфейса.

This commit is contained in:
Fovway 2025-11-07 15:51:35 +07:00
parent 300e881245
commit 772f5b1955
7 changed files with 160 additions and 44 deletions

View File

@ -2423,7 +2423,7 @@ app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
try {
// Получаем AI настройки пользователя
const getSettingsSql =
"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(getSettingsSql, [req.session.userId], async (err, settings) => {
if (err) {
console.error("Ошибка получения AI настроек:", err.message);
@ -2441,6 +2441,13 @@ app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
.json({ error: "Настройте AI настройки в параметрах" });
}
// Проверяем, включены ли функции ИИ
if (!settings.ai_enabled || settings.ai_enabled === 0) {
return res
.status(403)
.json({ error: "Функции ИИ отключены в настройках" });
}
try {
// Парсим URL
const url = new URL(settings.openai_base_url);
@ -2559,7 +2566,7 @@ app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
try {
// Получаем AI настройки пользователя
const getSettingsSql =
"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(getSettingsSql, [req.session.userId], async (err, settings) => {
if (err) {
console.error("Ошибка получения AI настроек:", err.message);
@ -2577,6 +2584,13 @@ app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
.json({ error: "Настройте AI настройки в параметрах" });
}
// Проверяем, включены ли функции ИИ
if (!settings.ai_enabled || settings.ai_enabled === 0) {
return res
.status(403)
.json({ error: "Функции ИИ отключены в настройках" });
}
try {
// Парсим URL
const url = new URL(settings.openai_base_url);

View File

@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "/index.html",
"revision": "0.u6qfhq29adg"
"revision": "0.pkl0gk07ge8"
}], {
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
});

View File

@ -21,6 +21,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
const [mergedContent, setMergedContent] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [deleteOriginalNotes, setDeleteOriginalNotes] = useState(false);
const isClosedRef = useRef(false);
const { showNotification } = useNotification();
@ -34,6 +35,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
setMergedContent("");
setIsLoading(false);
setIsSaving(false);
setDeleteOriginalNotes(false);
isClosedRef.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -44,6 +46,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
setIsLoading(false);
setIsSaving(false);
setMergedContent("");
setDeleteOriginalNotes(false);
onClose();
};
@ -95,7 +98,27 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
time,
});
// Удаляем исходные заметки, если тумблер включен
if (deleteOriginalNotes) {
try {
await Promise.all(
selectedNotes.map((note) => offlineNotesApi.delete(note.id))
);
showNotification(
`Объединенная заметка сохранена! Удалено ${selectedNotes.length} исходных заметок.`,
"success"
);
} catch (deleteError) {
console.error("Ошибка удаления исходных заметок:", deleteError);
showNotification(
"Объединенная заметка сохранена, но произошла ошибка при удалении исходных заметок",
"warning"
);
}
} else {
showNotification("Объединенная заметка сохранена!", "success");
}
onSuccess();
handleClose();
} catch (error) {
@ -169,6 +192,34 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
>
<NotePreview content={mergedContent} />
</div>
<div
className="form-group ai-toggle-group"
style={{ marginTop: "20px", marginBottom: "10px" }}
>
<label className="ai-toggle-label">
<div className="toggle-label-content">
<span className="toggle-text-main">
Удалить исходные заметки
</span>
<span className="toggle-text-desc">
{deleteOriginalNotes
? "Исходные заметки будут удалены после сохранения объединенной заметки"
: "Исходные заметки останутся в списке после сохранения объединенной заметки"}
</span>
</div>
<div className="toggle-switch-wrapper">
<input
type="checkbox"
id="delete-original-notes-toggle"
className="toggle-checkbox"
checked={deleteOriginalNotes}
onChange={(e) => setDeleteOriginalNotes(e.target.checked)}
disabled={isSaving}
/>
<span className="toggle-slider"></span>
</div>
</label>
</div>
</>
)}
</div>

View File

@ -1136,19 +1136,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
>
<div className="date">
<span className="date-text">
<input
type="checkbox"
checked={isSelected}
onChange={() => onSelect && onSelect(note.id)}
onClick={(e) => e.stopPropagation()}
style={{
width: "18px",
height: "18px",
cursor: "pointer",
marginRight: "10px",
verticalAlign: "middle",
}}
/>
{formatDate()}
{note.is_pinned ? (
<span className="pin-indicator">
@ -1182,13 +1169,19 @@ export const NoteItem: React.FC<NoteItemProps> = ({
>
<Icon icon="mdi:pencil" />
</div>
<div
<input
type="checkbox"
checked={isSelected}
onChange={() => onSelect && onSelect(note.id)}
onClick={(e) => e.stopPropagation()}
/>
{/* <div
className="notesHeaderBtn"
onClick={handleArchiveClick}
title="В архив"
>
<Icon icon="mdi:delete" />
</div>
</div> */}
</div>
</div>

View File

@ -33,6 +33,7 @@ const NotesPage: React.FC = () => {
const selectedDate = useAppSelector((state) => state.notes.selectedDate);
const selectedTag = useAppSelector((state) => state.notes.selectedTag);
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
const hasFilters = !!(selectedDate || selectedTag || searchQuery);
@ -115,13 +116,13 @@ const NotesPage: React.FC = () => {
setIsDeleting(true);
try {
// Удаляем все выбранные заметки
// Архивируем все выбранные заметки
await Promise.all(
selectedNoteIds.map((id) => offlineNotesApi.delete(id))
selectedNoteIds.map((id) => offlineNotesApi.archive(id))
);
showNotification(
`Удалено заметок: ${selectedNoteIds.length}`,
`Архивировано заметок: ${selectedNoteIds.length}`,
"success"
);
setSelectedNoteIds([]);
@ -131,8 +132,8 @@ const NotesPage: React.FC = () => {
notesListRef.current.reloadNotes();
}
} catch (error) {
console.error("Ошибка удаления заметок:", error);
showNotification("Ошибка удаления заметок", "error");
console.error("Ошибка архивирования заметок:", error);
showNotification("Ошибка архивирования заметок", "error");
} finally {
setIsDeleting(false);
}
@ -193,7 +194,7 @@ const NotesPage: React.FC = () => {
zIndex: 1000,
}}
>
{selectedNoteIds.length >= 2 && (
{selectedNoteIds.length >= 2 && aiEnabled && (
<button
onClick={handleMergeNotes}
style={{
@ -241,15 +242,15 @@ const NotesPage: React.FC = () => {
width: "56px",
height: "56px",
borderRadius: "50%",
backgroundColor: theme === "dark" ? "#F44336" : "#E53935",
backgroundColor: theme === "dark" ? "#FF9800" : "#FF9800",
color: "white",
border: "none",
cursor: isDeleting ? "not-allowed" : "pointer",
opacity: isDeleting ? 0.6 : 1,
boxShadow:
theme === "dark"
? "0 4px 12px rgba(244, 67, 54, 0.4)"
: "0 4px 12px rgba(229, 57, 53, 0.4)",
? "0 4px 12px rgba(255, 152, 0, 0.4)"
: "0 4px 12px rgba(255, 152, 0, 0.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
@ -261,20 +262,20 @@ const NotesPage: React.FC = () => {
e.currentTarget.style.transform = "scale(1.1)";
e.currentTarget.style.boxShadow =
theme === "dark"
? "0 6px 16px rgba(244, 67, 54, 0.6)"
: "0 6px 16px rgba(229, 57, 53, 0.6)";
? "0 6px 16px rgba(255, 152, 0, 0.6)"
: "0 6px 16px rgba(255, 152, 0, 0.6)";
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "scale(1)";
e.currentTarget.style.boxShadow =
theme === "dark"
? "0 4px 12px rgba(244, 67, 54, 0.4)"
: "0 4px 12px rgba(229, 57, 53, 0.4)";
? "0 4px 12px rgba(255, 152, 0, 0.4)"
: "0 4px 12px rgba(255, 152, 0, 0.4)";
}}
title={`Удалить ${selectedNoteIds.length} ${selectedNoteIds.length === 1 ? "заметку" : selectedNoteIds.length > 4 ? "заметок" : "заметки"}`}
title={`Архивировать ${selectedNoteIds.length} ${selectedNoteIds.length === 1 ? "заметку" : selectedNoteIds.length > 4 ? "заметок" : "заметки"}`}
>
<Icon icon="mdi:delete" />
<Icon icon="mdi:archive" />
</button>
</div>
)}
@ -289,22 +290,22 @@ const NotesPage: React.FC = () => {
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
onConfirm={handleDeleteConfirm}
title="Удаление заметок"
title="Архивирование заметок"
message={
<p>
Вы уверены, что хотите удалить{" "}
Вы уверены, что хотите архивировать{" "}
<strong>{selectedNoteIds.length}</strong>{" "}
{selectedNoteIds.length === 1
? "заметку"
: selectedNoteIds.length > 4
? "заметок"
: "заметки"}
? Это действие нельзя отменить.
? Заметки можно будет восстановить из архива в настройках.
</p>
}
confirmText={isDeleting ? "Удаление..." : "Удалить"}
confirmText={isDeleting ? "Архивирование..." : "Архивировать"}
cancelText="Отмена"
confirmType="danger"
confirmType="primary"
/>
</>
);

View File

@ -245,7 +245,7 @@ const SettingsPage: React.FC = () => {
setAiEnabled(checked);
localStorage.setItem("ai_enabled", checked ? "1" : "0");
showNotification(
checked ? "Помощь ИИ включена" : "Помощь ИИ отключена",
checked ? "Функции ИИ включены" : "Функции ИИ отключены",
"success"
);
} catch (error: any) {
@ -726,11 +726,16 @@ const SettingsPage: React.FC = () => {
}`}
>
<div className="toggle-label-content">
<span className="toggle-text-main">Включить помощь ИИ</span>
<span className="toggle-text-main">Включить функции ИИ</span>
<span className="toggle-text-desc">
{checkAiSettingsFilled()
? 'Показывать кнопку "Помощь ИИ" в редакторах заметок'
: "Сначала заполните API Key, Base URL и Модель ниже"}
{checkAiSettingsFilled() ? (
<ul style={{ margin: "8px 0 0 20px", padding: 0 }}>
<li>Улучшение текста заметок</li>
<li>Объединение заметок</li>
</ul>
) : (
"Сначала заполните API Key, Base URL и Модель ниже"
)}
</span>
</div>
<div className="toggle-switch-wrapper">

View File

@ -1154,6 +1154,58 @@ textarea:focus {
flex-wrap: wrap;
}
.note-actions input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
margin: 0;
vertical-align: middle;
flex-shrink: 0;
border: 2px solid var(--border-primary);
border-radius: 4px;
background-color: var(--bg-secondary);
transition: all 0.2s ease;
accent-color: var(--accent-color);
appearance: none;
-webkit-appearance: none;
position: relative;
}
.note-actions input[type="checkbox"]:hover {
border-color: var(--accent-color);
background-color: var(--bg-quaternary);
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.1);
}
.note-actions input[type="checkbox"]:checked {
background-color: var(--accent-color);
border-color: var(--accent-color);
}
.note-actions input[type="checkbox"]:checked::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -60%) rotate(45deg);
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
}
.note-actions input[type="checkbox"]:checked:hover {
background-color: var(--accent-color);
border-color: var(--accent-color);
opacity: 0.9;
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.2);
}
.note-actions input[type="checkbox"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.15);
}
.notesHeaderBtn {
display: inline-flex;
align-items: center;