diff --git a/backend/server.js b/backend/server.js index 642cb2e..f1575dd 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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); diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 4cb0939..753490f 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -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$/] }); diff --git a/src/components/notes/MergeNotesModal.tsx b/src/components/notes/MergeNotesModal.tsx index fafcca0..7fff726 100644 --- a/src/components/notes/MergeNotesModal.tsx +++ b/src/components/notes/MergeNotesModal.tsx @@ -21,6 +21,7 @@ export const MergeNotesModal: React.FC = ({ const [mergedContent, setMergedContent] = useState(""); 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 = ({ 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 = ({ setIsLoading(false); setIsSaving(false); setMergedContent(""); + setDeleteOriginalNotes(false); onClose(); }; @@ -95,7 +98,27 @@ export const MergeNotesModal: React.FC = ({ time, }); - showNotification("Объединенная заметка сохранена!", "success"); + // Удаляем исходные заметки, если тумблер включен + 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 = ({ > +
+ +
)} diff --git a/src/components/notes/NoteItem.tsx b/src/components/notes/NoteItem.tsx index 9c4664b..b0e51b0 100644 --- a/src/components/notes/NoteItem.tsx +++ b/src/components/notes/NoteItem.tsx @@ -1136,19 +1136,6 @@ export const NoteItem: React.FC = ({ >
- onSelect && onSelect(note.id)} - onClick={(e) => e.stopPropagation()} - style={{ - width: "18px", - height: "18px", - cursor: "pointer", - marginRight: "10px", - verticalAlign: "middle", - }} - /> {formatDate()} {note.is_pinned ? ( @@ -1182,13 +1169,19 @@ export const NoteItem: React.FC = ({ >
-
onSelect && onSelect(note.id)} + onClick={(e) => e.stopPropagation()} + /> + {/*
-
+
*/} diff --git a/src/pages/NotesPage.tsx b/src/pages/NotesPage.tsx index 7f27514..6a1dba8 100644 --- a/src/pages/NotesPage.tsx +++ b/src/pages/NotesPage.tsx @@ -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 && ( )} @@ -289,22 +290,22 @@ const NotesPage: React.FC = () => { isOpen={isDeleteModalOpen} onClose={() => setIsDeleteModalOpen(false)} onConfirm={handleDeleteConfirm} - title="Удаление заметок" + title="Архивирование заметок" message={

- Вы уверены, что хотите удалить{" "} + Вы уверены, что хотите архивировать{" "} {selectedNoteIds.length}{" "} {selectedNoteIds.length === 1 ? "заметку" : selectedNoteIds.length > 4 ? "заметок" : "заметки"} - ? Это действие нельзя отменить. + ? Заметки можно будет восстановить из архива в настройках.

} - confirmText={isDeleting ? "Удаление..." : "Удалить"} + confirmText={isDeleting ? "Архивирование..." : "Архивировать"} cancelText="Отмена" - confirmType="danger" + confirmType="primary" /> ); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 36d9e2c..503e724 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -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 = () => { }`} >
- Включить помощь ИИ + Включить функции ИИ - {checkAiSettingsFilled() - ? 'Показывать кнопку "Помощь ИИ" в редакторах заметок' - : "Сначала заполните API Key, Base URL и Модель ниже"} + {checkAiSettingsFilled() ? ( +
    +
  • Улучшение текста заметок
  • +
  • Объединение заметок
  • +
+ ) : ( + "Сначала заполните API Key, Base URL и Модель ниже" + )}
diff --git a/src/styles/style.css b/src/styles/style.css index 66b8597..ccc408c 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -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;