247 lines
8.3 KiB
TypeScript
247 lines
8.3 KiB
TypeScript
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}>
|
||
×
|
||
</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>
|
||
);
|
||
};
|
||
|