noteJS-react/src/components/notes/MergeNotesModal.tsx

247 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useRef } from "react";
import { aiApi } from "../../api/aiApi";
import { offlineNotesApi } from "../../api/offlineNotesApi";
import { Note } from "../../types/note";
import { NotePreview } from "./NotePreview";
import { useNotification } from "../../hooks/useNotification";
interface MergeNotesModalProps {
isOpen: boolean;
onClose: () => void;
selectedNotes: Note[];
onSuccess: () => void;
}
export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
isOpen,
onClose,
selectedNotes,
onSuccess,
}) => {
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();
// Выполняем объединение при открытии модального окна
useEffect(() => {
if (isOpen && selectedNotes.length >= 2) {
isClosedRef.current = false;
handleMerge();
} else {
// Сбрасываем состояние при закрытии
setMergedContent("");
setIsLoading(false);
setIsSaving(false);
setDeleteOriginalNotes(false);
isClosedRef.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
const handleClose = () => {
isClosedRef.current = true;
setIsLoading(false);
setIsSaving(false);
setMergedContent("");
setDeleteOriginalNotes(false);
onClose();
};
const handleMerge = async () => {
setIsLoading(true);
setMergedContent("");
try {
const notesContent = selectedNotes.map((note) => note.content);
const merged = await aiApi.mergeNotes(notesContent);
// Проверяем, не была ли модалка закрыта во время запроса
if (!isClosedRef.current) {
setMergedContent(merged);
}
} catch (error) {
// Игнорируем ошибку, если модалка была закрыта
if (isClosedRef.current) {
return;
}
console.error("Ошибка объединения заметок:", error);
showNotification("Ошибка объединения заметок", "error");
handleClose();
} finally {
if (!isClosedRef.current) {
setIsLoading(false);
}
}
};
const handleSave = async () => {
if (!mergedContent.trim()) {
showNotification("Нет контента для сохранения", "warning");
return;
}
setIsSaving(true);
try {
const now = new Date();
const date = now.toLocaleDateString("ru-RU");
const time = now.toLocaleTimeString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
});
await offlineNotesApi.create({
content: mergedContent,
date,
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) {
console.error("Ошибка сохранения заметки:", error);
showNotification("Ошибка сохранения заметки", "error");
} finally {
setIsSaving(false);
}
};
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
handleClose();
}
};
if (isOpen) {
document.addEventListener("keydown", handleEscape);
}
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal" style={{ display: "block" }} onClick={handleClose}>
<div
className="modal-content"
style={{ maxWidth: "800px", maxHeight: "80vh", overflow: "auto" }}
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<h3>Объединение заметок</h3>
<span className="modal-close" onClick={handleClose}>
&times;
</span>
</div>
<div className="modal-body">
{isLoading ? (
<div style={{ textAlign: "center", padding: "40px 20px" }}>
<div className="loading-spinner" style={{ margin: "0 auto 20px" }}></div>
<p>Объединяю заметки через ИИ...</p>
<p style={{ fontSize: "14px", color: "#666", marginTop: "10px" }}>
Выбрано заметок: {selectedNotes.length}
</p>
</div>
) : (
<>
<div style={{ marginBottom: "15px", color: "#666" }}>
<p>
Результат объединения {selectedNotes.length}{" "}
{selectedNotes.length === 2
? "заметок"
: selectedNotes.length > 4
? "заметок"
: "заметок"}
:
</p>
</div>
<div
style={{
border: "1px solid var(--border-color)",
borderRadius: "8px",
padding: "15px",
backgroundColor: "var(--bg-secondary)",
maxHeight: "400px",
overflow: "auto",
}}
>
<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>
<div className="modal-footer">
<button
className="btn-primary"
onClick={handleSave}
disabled={isLoading || isSaving || !mergedContent}
>
{isSaving ? "Сохранение..." : "Сохранить"}
</button>
<button
className="btn-secondary"
onClick={handleClose}
disabled={isSaving}
>
{isLoading ? "Отменить" : "Отмена"}
</button>
</div>
</div>
</div>
);
};