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

View File

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

View File

@ -21,6 +21,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
const [mergedContent, setMergedContent] = useState<string>(""); const [mergedContent, setMergedContent] = useState<string>("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [deleteOriginalNotes, setDeleteOriginalNotes] = useState(false);
const isClosedRef = useRef(false); const isClosedRef = useRef(false);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
@ -34,6 +35,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
setMergedContent(""); setMergedContent("");
setIsLoading(false); setIsLoading(false);
setIsSaving(false); setIsSaving(false);
setDeleteOriginalNotes(false);
isClosedRef.current = false; isClosedRef.current = false;
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -44,6 +46,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
setIsLoading(false); setIsLoading(false);
setIsSaving(false); setIsSaving(false);
setMergedContent(""); setMergedContent("");
setDeleteOriginalNotes(false);
onClose(); onClose();
}; };
@ -95,7 +98,27 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
time, 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"); showNotification("Объединенная заметка сохранена!", "success");
}
onSuccess(); onSuccess();
handleClose(); handleClose();
} catch (error) { } catch (error) {
@ -169,6 +192,34 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
> >
<NotePreview content={mergedContent} /> <NotePreview content={mergedContent} />
</div> </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> </div>

View File

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

View File

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

View File

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

View File

@ -1154,6 +1154,58 @@ textarea:focus {
flex-wrap: wrap; 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 { .notesHeaderBtn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;