327 lines
15 KiB
HTML
327 lines
15 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Конструктор презентаций</title>
|
||
<style>
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #0f1218; color: #f4f4f5; }
|
||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||
header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 1px solid #27272a; margin-bottom: 30px; }
|
||
h1 { font-size: 28px; }
|
||
button { background: #00b8cc; color: #0f1218; border: none; border-radius: 8px; padding: 10px 20px; cursor: pointer; font-size: 14px; font-weight: 600; }
|
||
button:hover { background: #00d9e8; }
|
||
button.secondary { background: #27272a; color: #f4f4f5; }
|
||
button.danger { background: #ef4444; color: white; }
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
|
||
.card { background: #18181b; border-radius: 12px; padding: 20px; cursor: pointer; transition: transform 0.2s; }
|
||
.card:hover { transform: translateY(-4px); background: #27272a; }
|
||
.card h3 { margin-bottom: 10px; font-size: 18px; }
|
||
.card p { color: #a1a1aa; font-size: 14px; }
|
||
.card-actions { margin-top: 15px; display: flex; gap: 8px; }
|
||
.card-actions button { padding: 6px 12px; font-size: 12px; }
|
||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100; }
|
||
.modal.on { display: flex; justify-content: center; align-items: center; }
|
||
.modal-content { background: #18181b; border-radius: 12px; padding: 30px; width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; }
|
||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||
.modal-header h2 { font-size: 24px; }
|
||
input[type="text"], textarea { width: 100%; padding: 12px; background: #27272a; border: 1px solid #3f3f46; border-radius: 8px; color: #f4f4f5; font-size: 14px; margin-bottom: 15px; }
|
||
textarea { min-height: 100px; resize: vertical; }
|
||
.slide-editor { background: #27272a; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
|
||
.slide-editor h4 { margin-bottom: 10px; color: #00b8cc; }
|
||
.actions { display: flex; gap: 10px; margin-top: 20px; }
|
||
.empty { text-align: center; padding: 60px 20px; color: #a1a1aa; }
|
||
.preview-slide { background: #0f1218; border: 1px solid #3f3f46; border-radius: 8px; padding: 30px; margin-bottom: 20px; }
|
||
.preview-slide h1 { font-size: 32px; margin-bottom: 20px; color: #f4f4f5; }
|
||
.preview-slide li { font-size: 18px; line-height: 1.6; margin-bottom: 8px; list-style: none; padding-left: 20px; position: relative; }
|
||
.preview-slide li::before { content: ""; position: absolute; left: 0; top: 8px; width: 8px; height: 8px; border-radius: 2px; background: #00b8cc; }
|
||
.nav { display: flex; justify-content: space-between; margin-top: 20px; }
|
||
.upload-zone { border: 2px dashed #3f3f46; border-radius: 12px; padding: 40px; text-align: center; margin-bottom: 20px; cursor: pointer; transition: all 0.2s; }
|
||
.upload-zone:hover { border-color: #00b8cc; background: #1f1f23; }
|
||
.upload-zone.dragover { border-color: #00b8cc; background: #1f1f23; }
|
||
.upload-zone p { color: #a1a1aa; margin-bottom: 10px; }
|
||
.tabs { display: flex; gap: 10px; margin-bottom: 20px; }
|
||
.tab { padding: 10px 20px; background: #27272a; border-radius: 8px; cursor: pointer; }
|
||
.tab.active { background: #00b8cc; color: #0f1218; }
|
||
.loading { text-align: center; padding: 40px; color: #a1a1aa; }
|
||
.spinner { display: inline-block; width: 30px; height: 30px; border: 3px solid #27272a; border-top-color: #00b8cc; border-radius: 50%; animation: spin 1s linear infinite; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>📊 Конструктор презентаций</h1>
|
||
<button onclick="showCreateModal()">+ Новая презентация</button>
|
||
</header>
|
||
<div id="presentations" class="grid"></div>
|
||
</div>
|
||
|
||
<!-- Модальное окно создания -->
|
||
<div id="createModal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Новая презентация</h2>
|
||
<button class="secondary" onclick="closeModal('createModal')">✕</button>
|
||
</div>
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="switchTab('manual')">Вручную</div>
|
||
<div class="tab" onclick="switchTab('auto')">Из документа (ИИ)</div>
|
||
</div>
|
||
|
||
<div id="manualTab">
|
||
<input type="text" id="newTitle" placeholder="Название презентации">
|
||
<input type="text" id="newAuthor" placeholder="Автор (необязательно)">
|
||
<div class="actions">
|
||
<button onclick="createPresentation()">Создать</button>
|
||
<button class="secondary" onclick="closeModal('createModal')">Отмена</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="autoTab" style="display: none;">
|
||
<div class="upload-zone" id="uploadZone" onclick="document.getElementById('fileInput').click()">
|
||
<p>📎 Перетащите файл сюда или кликните для выбора</p>
|
||
<p style="font-size: 12px;">Поддерживаются: .xlsx, .docx, .pdf, .txt</p>
|
||
<input type="file" id="fileInput" accept=".xlsx,.xls,.docx,.pdf,.txt,.csv" style="display: none;" onchange="handleFile(this.files[0])">
|
||
</div>
|
||
<div id="uploadStatus" class="loading" style="display: none;">
|
||
<div class="spinner"></div>
|
||
<p>Анализирую документ и создаю презентацию...</p>
|
||
</div>
|
||
<div id="fileInfo" style="margin-bottom: 15px; color: #a1a1aa; display: none;"></div>
|
||
<div class="actions">
|
||
<button onclick="createFromDocument()">🤖 Создать презентацию из файла</button>
|
||
<button class="secondary" onclick="closeModal('createModal')">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно редактора -->
|
||
<div id="editorModal" class="modal">
|
||
<div class="modal-content" style="max-width: 1000px;">
|
||
<div class="modal-header">
|
||
<h2 id="editTitle">Редактирование</h2>
|
||
<button class="secondary" onclick="closeModal('editorModal')">✕</button>
|
||
</div>
|
||
<input type="text" id="presTitle" placeholder="Название">
|
||
<div id="slidesContainer"></div>
|
||
<button class="secondary" onclick="addSlide()">+ Добавить слайд</button>
|
||
<div class="actions">
|
||
<button onclick="savePresentation()">Сохранить</button>
|
||
<button class="secondary" onclick="showPreview()">Предпросмотр</button>
|
||
<button class="danger" onclick="deletePresentation()">Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно предпросмотра -->
|
||
<div id="previewModal" class="modal">
|
||
<div class="modal-content" style="max-width: 900px;">
|
||
<div class="modal-header">
|
||
<h2 id="previewTitle">Предпросмотр</h2>
|
||
<button class="secondary" onclick="closeModal('previewModal')">✕</button>
|
||
</div>
|
||
<div id="previewContainer"></div>
|
||
<div class="nav">
|
||
<button class="secondary" onclick="prevSlide()">← Назад</button>
|
||
<span id="slideNum" style="color: #a1a1aa; align-self: center;"></span>
|
||
<button class="secondary" onclick="nextSlide()">Вперёд →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let presentations = [];
|
||
let currentPres = null;
|
||
let previewIndex = 0;
|
||
let uploadedFile = null;
|
||
|
||
async function load() {
|
||
presentations = await fetch("/api/presentations").then(r => r.json());
|
||
render();
|
||
}
|
||
|
||
function render() {
|
||
const container = document.getElementById("presentations");
|
||
if (presentations.length === 0) {
|
||
container.innerHTML = '<div class="empty"><h2>Нет презентаций</h2><p>Создайте первую презентацию!</p></div>';
|
||
return;
|
||
}
|
||
container.innerHTML = presentations.map(p => `
|
||
<div class="card" onclick="openEditor(${p.id})">
|
||
<h3>${escapeHtml(p.title)}</h3>
|
||
<p>${p.author || "Без автора"} • ${p.slides ? p.slides.length : 0} слайд(ов)</p>
|
||
<div class="card-actions">
|
||
<button onclick="event.stopPropagation(); openEditor(${p.id})">Открыть</button>
|
||
</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function switchTab(tab) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
document.getElementById('manualTab').style.display = tab === 'manual' ? 'block' : 'none';
|
||
document.getElementById('autoTab').style.display = tab === 'auto' ? 'block' : 'none';
|
||
}
|
||
|
||
function showCreateModal() {
|
||
document.getElementById("newTitle").value = "";
|
||
document.getElementById("newAuthor").value = "";
|
||
uploadedFile = null;
|
||
document.getElementById("fileInfo").style.display = "none";
|
||
document.getElementById("uploadStatus").style.display = "none";
|
||
document.getElementById("createModal").classList.add("on");
|
||
}
|
||
|
||
async function createPresentation() {
|
||
const title = document.getElementById("newTitle").value.trim();
|
||
if (!title) return alert("Введите название");
|
||
await fetch("/api/presentations", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ title, author: document.getElementById("newAuthor").value.trim() })
|
||
});
|
||
closeModal("createModal");
|
||
load();
|
||
}
|
||
|
||
function handleFile(file) {
|
||
if (!file) return;
|
||
uploadedFile = file;
|
||
document.getElementById("fileInfo").style.display = "block";
|
||
document.getElementById("fileInfo").textContent = "📄 " + file.name + " (" + Math.round(file.size / 1024) + " КБ)";
|
||
}
|
||
|
||
// Drag-and-drop
|
||
const uploadZone = document.getElementById("uploadZone");
|
||
if (uploadZone) {
|
||
uploadZone.addEventListener("dragover", (e) => {
|
||
e.preventDefault();
|
||
uploadZone.classList.add("dragover");
|
||
});
|
||
uploadZone.addEventListener("dragleave", () => {
|
||
uploadZone.classList.remove("dragover");
|
||
});
|
||
uploadZone.addEventListener("drop", (e) => {
|
||
e.preventDefault();
|
||
uploadZone.classList.remove("dragover");
|
||
const file = e.dataTransfer.files[0];
|
||
if (file) handleFile(file);
|
||
});
|
||
}
|
||
|
||
async function createFromDocument() {
|
||
if (!uploadedFile) return alert("Выберите файл");
|
||
document.getElementById("uploadStatus").style.display = "block";
|
||
|
||
const formData = new FormData();
|
||
formData.append("file", uploadedFile);
|
||
|
||
try {
|
||
const resp = await fetch("/api/create-from-file", {
|
||
method: "POST",
|
||
body: formData
|
||
});
|
||
const result = await resp.json();
|
||
document.getElementById("uploadStatus").style.display = "none";
|
||
|
||
if (result.ok) {
|
||
alert("Презентация создана! " + result.slides + " слайд(ов)");
|
||
closeModal("createModal");
|
||
load();
|
||
} else {
|
||
alert("Ошибка: " + (result.error || "Не удалось создать презентацию"));
|
||
}
|
||
} catch (e) {
|
||
document.getElementById("uploadStatus").style.display = "none";
|
||
alert("Ошибка загрузки: " + e.message);
|
||
}
|
||
}
|
||
|
||
async function openEditor(id) {
|
||
currentPres = await fetch("/api/presentations/" + id).then(r => r.json());
|
||
document.getElementById("presTitle").value = currentPres.title;
|
||
document.getElementById("editTitle").textContent = currentPres.title;
|
||
renderSlides();
|
||
document.getElementById("editorModal").classList.add("on");
|
||
}
|
||
|
||
function renderSlides() {
|
||
const container = document.getElementById("slidesContainer");
|
||
container.innerHTML = currentPres.slides.map((s, i) => `
|
||
<div class="slide-editor">
|
||
<h4>Слайд ${i + 1}</h4>
|
||
<input type="text" value="${escapeHtml(s.title)}" onchange="updateSlide(${i}, 'title', this.value)" placeholder="Заголовок слайда">
|
||
<textarea onchange="updateSlide(${i}, 'bullets', this.value)" placeholder="Тезисы (каждый с новой строки)">${(s.bullets || []).join("\n")}</textarea>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function updateSlide(index, field, value) {
|
||
if (field === "bullets") {
|
||
currentPres.slides[index][field] = value.split("\n").filter(l => l.trim());
|
||
} else {
|
||
currentPres.slides[index][field] = value;
|
||
}
|
||
}
|
||
|
||
function addSlide() {
|
||
currentPres.slides.push({ title: "Новый слайд", bullets: ["Тезис"] });
|
||
renderSlides();
|
||
}
|
||
|
||
async function savePresentation() {
|
||
currentPres.title = document.getElementById("presTitle").value;
|
||
await fetch("/api/presentations/" + currentPres.id, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(currentPres)
|
||
});
|
||
alert("Сохранено!");
|
||
load();
|
||
}
|
||
|
||
async function deletePresentation() {
|
||
if (!confirm("Удалить презентацию?")) return;
|
||
await fetch("/api/presentations/" + currentPres.id, { method: "DELETE" });
|
||
closeModal("editorModal");
|
||
load();
|
||
}
|
||
|
||
function showPreview() {
|
||
previewIndex = 0;
|
||
document.getElementById("previewTitle").textContent = currentPres.title;
|
||
renderPreview();
|
||
document.getElementById("previewModal").classList.add("on");
|
||
}
|
||
|
||
function renderPreview() {
|
||
const container = document.getElementById("previewContainer");
|
||
const slide = currentPres.slides[previewIndex];
|
||
container.innerHTML = `
|
||
<div class="preview-slide">
|
||
<h1>${escapeHtml(slide.title)}</h1>
|
||
<ul>${(slide.bullets || []).map(b => "<li>" + escapeHtml(b) + "</li>").join("")}</ul>
|
||
</div>
|
||
`;
|
||
document.getElementById("slideNum").textContent = (previewIndex + 1) + " / " + currentPres.slides.length;
|
||
}
|
||
|
||
function prevSlide() { if (previewIndex > 0) { previewIndex--; renderPreview(); } }
|
||
function nextSlide() { if (previewIndex < currentPres.slides.length - 1) { previewIndex++; renderPreview(); } }
|
||
|
||
function closeModal(id) { document.getElementById(id).classList.remove("on"); }
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return "";
|
||
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|