const http = require("http"); const fs = require("fs"); const path = require("path"); const PORT = process.env.PORT || 3000; const DATA = path.join(__dirname, "data.json"); const UPLOADS = path.join(__dirname, "uploads"); if (!fs.existsSync(UPLOADS)) fs.mkdirSync(UPLOADS); function readData() { try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { presentations: [] }; } } function writeData(data) { fs.writeFileSync(DATA, JSON.stringify(data, null, 2)); } // Простой парсинг multipart function parseMultipart(req) { return new Promise((resolve, reject) => { const contentType = req.headers["content-type"] || ""; const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i); if (!boundaryMatch) { return reject(new Error("Invalid content-type: " + contentType)); } const boundary = "--" + (boundaryMatch[1] || boundaryMatch[2]).trim(); const chunks = []; req.on("data", chunk => chunks.push(chunk)); req.on("end", () => { const buffer = Buffer.concat(chunks); const parts = buffer.split(Buffer.from(boundary)); const result = { fields: {}, files: {} }; for (const part of parts) { if (part.length < 10) continue; const str = part.toString("binary"); const headerEnd = str.indexOf("\r\n\r\n"); if (headerEnd === -1) continue; const headers = str.slice(0, headerEnd); const content = part.slice(headerEnd + 4); const nameMatch = headers.match(/name="([^"]+)"/); const filenameMatch = headers.match(/filename="([^"]+)"/); if (!nameMatch) continue; const name = nameMatch[1]; // Файл if (filenameMatch) { const filename = filenameMatch[1]; const fileContent = content.slice(0, -2); // убираем \r\n в конце result.files[name] = { originalFilename: filename, filepath: path.join(UPLOADS, Date.now() + "_" + filename), size: fileContent.length }; fs.writeFileSync(result.files[name].filepath, fileContent); } else { // Поле result.fields[name] = content.toString("utf8").slice(0, -2).trim(); } } resolve(result); }); req.on("error", reject); }); } function readExcel(buffer) { const XLSX = require("xlsx"); const wb = XLSX.read(buffer, { type: "buffer" }); const sheet = wb.Sheets[wb.SheetNames[0]]; const json = XLSX.utils.sheet_to_json(sheet, { header: 1 }); return json.map(row => row.join(" ")).filter(t => t.trim()); } async function readWord(buffer) { const mammoth = require("mammoth"); const result = await mammoth.extractRawText({ buffer }); return result.value.split(/\n+/).filter(t => t.trim()); } async function readPdf(buffer) { const pdfParse = require("pdf-parse"); const result = await pdfParse(buffer); return result.text.split(/\n+/).filter(t => t.trim()); } function readText(buffer) { return buffer.toString("utf8").split(/\n+/).filter(t => t.trim()); } async function createPresentationFromText(text, title) { const AI_BASE_URL = process.env.AI_BASE_URL; const AI_API_KEY = process.env.AI_API_KEY; const AI_MODEL = process.env.AI_MODEL; if (!AI_BASE_URL || !AI_API_KEY) { const chunks = text.filter(t => t.length > 20); const slides = []; for (let i = 0; i < Math.min(chunks.length, 10); i++) { const chunk = chunks[i]; const lines = chunk.split(/[.!?]/).filter(l => l.trim()); slides.push({ title: lines[0]?.slice(0, 60) || "Слайд " + (i + 1), bullets: lines.slice(1, 5).map(l => l.trim().slice(0, 100)) }); } return slides.length > 0 ? slides : [{ title: "Презентация", bullets: text.slice(0, 5) }]; } try { const prompt = `Создай структуру презентации из этого текста. Верни ТОЛЬКО JSON массив слайдов в формате: [{"title": "Заголовок", "bullets": ["тезис 1", "тезис 2"]}, ...]. Максимум 8 слайдов. Текст: ${text.slice(0, 3000)}`; const resp = await fetch(AI_BASE_URL + "/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + AI_API_KEY }, body: JSON.stringify({ model: AI_MODEL || "gpt-3.5-turbo", messages: [{ role: "user", content: prompt }], temperature: 0.7 }) }); const json = await resp.json(); const content = json.choices?.[0]?.message?.content || ""; const match = content.match(/\[[\s\S]*\]/); if (match) { return JSON.parse(match[0]); } throw new Error("No JSON in response"); } catch (e) { console.error("AI error:", e); const chunks = text.filter(t => t.length > 20); const slides = []; for (let i = 0; i < Math.min(chunks.length, 8); i++) { const chunk = chunks[i]; const lines = chunk.split(/[.!?]/).filter(l => l.trim()); slides.push({ title: lines[0]?.slice(0, 60) || "Слайд " + (i + 1), bullets: lines.slice(1, 5).map(l => l.trim().slice(0, 100)) }); } return slides.length > 0 ? slides : [{ title: "Презентация", bullets: text.slice(0, 5) }]; } } http.createServer(async (req, res) => { const url = req.url.split("?")[0]; if (req.method === "GET" && url === "/api/presentations") { res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(readData().presentations)); } if (req.method === "POST" && url === "/api/presentations") { let s = ""; await new Promise(resolve => { req.on("data", c => s += c); req.on("end", resolve); }); const presentation = JSON.parse(s || "{}"); const data = readData(); presentation.id = Date.now(); presentation.slides = presentation.slides || [{ title: "Новый слайд", bullets: ["Тезис 1", "Тезис 2"] }]; data.presentations.push(presentation); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true, id: presentation.id })); } if (req.method === "GET" && url.startsWith("/api/presentations/")) { const id = parseInt(url.split("/").pop()); const data = readData(); const presentation = data.presentations.find(p => p.id === id); if (presentation) { res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(presentation)); } res.writeHead(404); return res.end("Not found"); } if (req.method === "PUT" && url.startsWith("/api/presentations/")) { let s = ""; await new Promise(resolve => { req.on("data", c => s += c); req.on("end", resolve); }); const presentation = JSON.parse(s || "{}"); const id = parseInt(url.split("/").pop()); const data = readData(); const index = data.presentations.findIndex(p => p.id === id); if (index !== -1) { data.presentations[index] = { ...data.presentations[index], ...presentation }; writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } res.writeHead(404); return res.end("Not found"); } if (req.method === "DELETE" && url.startsWith("/api/presentations/")) { const id = parseInt(url.split("/").pop()); const data = readData(); data.presentations = data.presentations.filter(p => p.id !== id); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } if (req.method === "POST" && url === "/api/create-from-file") { try { console.log("Received file upload request"); const multipart = await parseMultipart(req); console.log("Parsed multipart:", Object.keys(multipart.files)); const file = multipart.files.file; if (!file || !file.filepath) { console.error("No file found"); res.writeHead(400, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: false, error: "Нет файла" })); } console.log("File received:", file.originalFilename, file.size, "bytes"); const fileData = fs.readFileSync(file.filepath); const filename = file.originalFilename || "file"; try { fs.unlinkSync(file.filepath); } catch (_) {} let text = []; const ext = filename.split(".").pop().toLowerCase(); console.log("File extension:", ext); if (ext === "xlsx" || ext === "xls") { text = readExcel(fileData); } else if (ext === "docx") { text = await readWord(fileData); } else if (ext === "pdf") { text = await readPdf(fileData); } else { text = readText(fileData); } console.log("Extracted", text.length, "lines"); if (text.length === 0) { res.writeHead(400, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: false, error: "Пустой документ" })); } const title = filename.split(".")[0]; const slides = await createPresentationFromText(text.join(" "), title); console.log("Created", slides.length, "slides"); const data = readData(); const presentation = { id: Date.now(), title: title, author: "Авто-создано", slides: slides }; data.presentations.push(presentation); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true, slides: slides.length })); } catch (e) { console.error("Create from file error:", e.message, e.stack); res.writeHead(500, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: false, error: e.message })); } } let file = url === "/" ? "/index.html" : url.split("?")[0]; const full = path.join(__dirname, file); if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { const ext = path.extname(full); const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); return res.end(fs.readFileSync(full)); } res.writeHead(404); res.end("Not found"); }).listen(PORT, () => console.log("Сервер презентаций на порту " + PORT));