286 lines
10 KiB
JavaScript
286 lines
10 KiB
JavaScript
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));
|