Add source upload deck starter
This commit is contained in:
parent
eb0a17f75e
commit
2501e86a80
6 changed files with 741 additions and 69 deletions
19
README.md
19
README.md
|
|
@ -6,6 +6,11 @@ The first build is intentionally local-first: a CLI, an API, and a small web UI
|
||||||
share the same deck spec, style packs, and PPTX renderer. The same core contract
|
share the same deck spec, style packs, and PPTX renderer. The same core contract
|
||||||
can later run behind `dev.slides.scottfelten.com` and `slides.scottfelten.com`.
|
can later run behind `dev.slides.scottfelten.com` and `slides.scottfelten.com`.
|
||||||
|
|
||||||
|
The web UI is source-first: paste notes, drop a file, choose a file, or paste an
|
||||||
|
image into the source area. The MVP extracts text from Markdown/text, PDF, and
|
||||||
|
PPTX files. Image upload starts a deck from image metadata today; vision/OCR is
|
||||||
|
reserved for the Model Service Gateway integration.
|
||||||
|
|
||||||
## MVP Contract
|
## MVP Contract
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -47,6 +52,19 @@ Run the web shell:
|
||||||
npm run dev:web
|
npm run dev:web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then open:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:5185
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported starter sources:
|
||||||
|
|
||||||
|
- `.md`, `.markdown`, `.txt`
|
||||||
|
- `.pdf`
|
||||||
|
- `.pptx`
|
||||||
|
- image files or pasted clipboard images
|
||||||
|
|
||||||
## AgentPlane Notes
|
## AgentPlane Notes
|
||||||
|
|
||||||
- Implementation truth should live in Forgejo.
|
- Implementation truth should live in Forgejo.
|
||||||
|
|
@ -55,4 +73,3 @@ npm run dev:web
|
||||||
- Google Slides generation should reuse the existing AgentPlane guidance:
|
- Google Slides generation should reuse the existing AgentPlane guidance:
|
||||||
copy templates, inventory native containers, fill existing object IDs, and
|
copy templates, inventory native containers, fill existing object IDs, and
|
||||||
render thumbnails/contact sheets before handoff.
|
render thumbnails/contact sheets before handoff.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,19 @@
|
||||||
"@slide-factory/render-pptx": "0.1.0",
|
"@slide-factory/render-pptx": "0.1.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"fast-xml-parser": "^4.5.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"multer": "^2.1.1",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
"tsx": "^4.16.2",
|
"tsx": "^4.16.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/multer": "^1.4.12",
|
||||||
|
"@types/pdf-parse": "^1.1.4",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import { access, mkdir, writeFile } from "node:fs/promises";
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { inflateSync } from "node:zlib";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import { XMLParser } from "fast-xml-parser";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import multer from "multer";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import pdfParse from "pdf-parse";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { loadStylePack, planDeckFromSource } from "@slide-factory/core";
|
import { loadStylePack, planDeckFromSource, SourceDocument } from "@slide-factory/core";
|
||||||
import { renderPptx } from "@slide-factory/render-pptx";
|
import { renderPptx } from "@slide-factory/render-pptx";
|
||||||
|
|
||||||
const CreateDeckRequestSchema = z.object({
|
const CreateDeckRequestSchema = z.object({
|
||||||
|
|
@ -22,6 +27,13 @@ const CreateDeckRequestSchema = z.object({
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = Number(process.env.SLIDE_FACTORY_PORT || 3025);
|
const port = Number(process.env.SLIDE_FACTORY_PORT || 3025);
|
||||||
const outputDir = path.resolve(process.env.SLIDE_FACTORY_OUTPUT_DIR || "outputs");
|
const outputDir = path.resolve(process.env.SLIDE_FACTORY_OUTPUT_DIR || "outputs");
|
||||||
|
const upload = multer({
|
||||||
|
storage: multer.memoryStorage(),
|
||||||
|
limits: {
|
||||||
|
fileSize: 30 * 1024 * 1024,
|
||||||
|
files: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json({ limit: "5mb" }));
|
app.use(express.json({ limit: "5mb" }));
|
||||||
|
|
@ -34,29 +46,38 @@ app.get("/api/health", (_req, res) => {
|
||||||
app.post("/api/decks", async (req, res, next) => {
|
app.post("/api/decks", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = CreateDeckRequestSchema.parse(req.body);
|
const body = CreateDeckRequestSchema.parse(req.body);
|
||||||
const id = nanoid(10);
|
const result = await createDeckArtifacts({
|
||||||
const stylePath = await resolveStylePath(body.style);
|
|
||||||
const style = await loadStylePack(stylePath);
|
|
||||||
const deck = planDeckFromSource({
|
|
||||||
source: body.input,
|
source: body.input,
|
||||||
style,
|
|
||||||
instructions: body.instructions,
|
instructions: body.instructions,
|
||||||
audience: body.audience
|
audience: body.audience,
|
||||||
|
styleId: body.style
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobDir = path.join(outputDir, id);
|
res.status(201).json(result);
|
||||||
const specPath = path.join(jobDir, "deck.json");
|
} catch (error) {
|
||||||
const pptxPath = path.join(jobDir, "deck.pptx");
|
next(error);
|
||||||
await mkdir(jobDir, { recursive: true });
|
}
|
||||||
await writeFile(specPath, `${JSON.stringify(deck, null, 2)}\n`);
|
});
|
||||||
await renderPptx({ deck, style, outputPath: pptxPath });
|
|
||||||
|
app.post("/api/decks/from-source", upload.single("source"), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const file = req.file;
|
||||||
|
const pastedText = typeof req.body.content === "string" ? req.body.content : "";
|
||||||
|
const source = file ? await sourceFromUpload(file) : sourceFromText(pastedText);
|
||||||
|
const result = await createDeckArtifacts({
|
||||||
|
source,
|
||||||
|
instructions: stringField(req.body.instructions),
|
||||||
|
audience: stringField(req.body.audience) || "executives",
|
||||||
|
styleId: stringField(req.body.style) || "incorta"
|
||||||
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
id,
|
...result,
|
||||||
title: deck.title,
|
source: {
|
||||||
slides: deck.slides.length,
|
title: source.title,
|
||||||
specUrl: `/outputs/${id}/deck.json`,
|
type: source.type,
|
||||||
pptxUrl: `/outputs/${id}/deck.pptx`
|
sourceName: source.sourceName
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|
@ -86,6 +107,251 @@ async function resolveStylePath(styleId: string): Promise<string> {
|
||||||
return direct;
|
return direct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createDeckArtifacts(options: {
|
||||||
|
source: SourceDocument;
|
||||||
|
instructions?: string;
|
||||||
|
audience: string;
|
||||||
|
styleId: string;
|
||||||
|
}) {
|
||||||
|
const id = nanoid(10);
|
||||||
|
const stylePath = await resolveStylePath(options.styleId);
|
||||||
|
const style = await loadStylePack(stylePath);
|
||||||
|
const deck = planDeckFromSource({
|
||||||
|
source: options.source,
|
||||||
|
style,
|
||||||
|
instructions: options.instructions,
|
||||||
|
audience: options.audience
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobDir = path.join(outputDir, id);
|
||||||
|
const specPath = path.join(jobDir, "deck.json");
|
||||||
|
const pptxPath = path.join(jobDir, "deck.pptx");
|
||||||
|
await mkdir(jobDir, { recursive: true });
|
||||||
|
await writeFile(specPath, `${JSON.stringify(deck, null, 2)}\n`);
|
||||||
|
await renderPptx({ deck, style, outputPath: pptxPath });
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title: deck.title,
|
||||||
|
slides: deck.slides.length,
|
||||||
|
specUrl: `/outputs/${id}/deck.json`,
|
||||||
|
pptxUrl: `/outputs/${id}/deck.pptx`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceFromText(content: string): SourceDocument {
|
||||||
|
const trimmed = content.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error("Provide source text or upload a file.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "markdown",
|
||||||
|
title: deriveTitle(trimmed),
|
||||||
|
content: trimmed,
|
||||||
|
sourceName: "pasted-source.md"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sourceFromUpload(file: Express.Multer.File): Promise<SourceDocument> {
|
||||||
|
const extension = path.extname(file.originalname).toLowerCase();
|
||||||
|
const mime = file.mimetype;
|
||||||
|
|
||||||
|
if (extension === ".md" || extension === ".markdown") {
|
||||||
|
const content = file.buffer.toString("utf8");
|
||||||
|
return {
|
||||||
|
type: "markdown",
|
||||||
|
title: deriveTitle(content) || baseName(file.originalname),
|
||||||
|
content,
|
||||||
|
sourceName: file.originalname
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension === ".txt" || mime.startsWith("text/")) {
|
||||||
|
const content = file.buffer.toString("utf8");
|
||||||
|
return {
|
||||||
|
type: "text",
|
||||||
|
title: deriveTitle(content) || baseName(file.originalname),
|
||||||
|
content,
|
||||||
|
sourceName: file.originalname
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension === ".pdf" || mime === "application/pdf") {
|
||||||
|
const content = await extractPdfText(file.buffer);
|
||||||
|
if (!content) throw new Error("No text could be extracted from the PDF.");
|
||||||
|
return {
|
||||||
|
type: "pdf",
|
||||||
|
title: baseName(file.originalname),
|
||||||
|
content,
|
||||||
|
sourceName: file.originalname
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension === ".pptx" || mime.includes("presentation")) {
|
||||||
|
const content = await extractPptxText(file.buffer);
|
||||||
|
if (!content.trim()) throw new Error("No text could be extracted from the slide deck.");
|
||||||
|
return {
|
||||||
|
type: "pptx",
|
||||||
|
title: baseName(file.originalname),
|
||||||
|
content,
|
||||||
|
sourceName: file.originalname
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mime.startsWith("image/")) {
|
||||||
|
return {
|
||||||
|
type: "text",
|
||||||
|
title: baseName(file.originalname),
|
||||||
|
sourceName: file.originalname,
|
||||||
|
content: [
|
||||||
|
`# ${baseName(file.originalname)}`,
|
||||||
|
"",
|
||||||
|
"Image source uploaded.",
|
||||||
|
"",
|
||||||
|
`File name: ${file.originalname}`,
|
||||||
|
`Image type: ${mime}`,
|
||||||
|
`File size: ${Math.round(file.size / 1024)} KB`,
|
||||||
|
"",
|
||||||
|
"Vision/OCR is not enabled in this local MVP yet. Use the instructions field to describe the slide goal, audience, and what the image should support."
|
||||||
|
].join("\n")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported source type: ${file.originalname || mime}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractPdfText(buffer: Buffer): Promise<string> {
|
||||||
|
try {
|
||||||
|
const parsed = await pdfParse(buffer);
|
||||||
|
const content = parsed.text.trim();
|
||||||
|
if (content) return content;
|
||||||
|
} catch {
|
||||||
|
// Fall through to a lightweight stream extractor for simple PDFs.
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractPdfTextFromStreams(buffer).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPdfTextFromStreams(buffer: Buffer): string {
|
||||||
|
const pdf = buffer.toString("latin1");
|
||||||
|
const chunks: string[] = [];
|
||||||
|
const streamPattern = /<<(.*?)>>\s*stream\r?\n([\s\S]*?)\r?\nendstream/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = streamPattern.exec(pdf))) {
|
||||||
|
const dictionary = match[1] || "";
|
||||||
|
const raw = Buffer.from(match[2] || "", "latin1");
|
||||||
|
let stream = raw;
|
||||||
|
if (dictionary.includes("/FlateDecode")) {
|
||||||
|
try {
|
||||||
|
stream = inflateSync(raw);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const text = extractPdfStrings(stream.toString("latin1")).join(" ");
|
||||||
|
if (text) chunks.push(text);
|
||||||
|
}
|
||||||
|
return chunks.join("\n\n").replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPdfStrings(stream: string): string[] {
|
||||||
|
const values: string[] = [];
|
||||||
|
const literalPattern = /\((?:\\.|[^\\)])*\)/g;
|
||||||
|
for (const match of stream.matchAll(literalPattern)) {
|
||||||
|
const value = decodePdfLiteral(match[0].slice(1, -1));
|
||||||
|
if (value.trim()) values.push(value.trim());
|
||||||
|
}
|
||||||
|
const hexPattern = /<([0-9A-Fa-f\s]{4,})>/g;
|
||||||
|
for (const match of stream.matchAll(hexPattern)) {
|
||||||
|
const value = decodePdfHex(match[1] || "");
|
||||||
|
if (value.trim()) values.push(value.trim());
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodePdfLiteral(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/\\n/g, "\n")
|
||||||
|
.replace(/\\r/g, "\r")
|
||||||
|
.replace(/\\t/g, "\t")
|
||||||
|
.replace(/\\b/g, "\b")
|
||||||
|
.replace(/\\f/g, "\f")
|
||||||
|
.replace(/\\([()\\])/g, "$1")
|
||||||
|
.replace(/\\([0-7]{1,3})/g, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodePdfHex(value: string): string {
|
||||||
|
const normalized = value.replace(/\s+/g, "");
|
||||||
|
const bytes = (normalized.match(/.{1,2}/g) || []).map((byte) => Number.parseInt(byte.padEnd(2, "0"), 16));
|
||||||
|
if (bytes[0] === 0xfe && bytes[1] === 0xff) {
|
||||||
|
const chars: string[] = [];
|
||||||
|
for (let index = 2; index + 1 < bytes.length; index += 2) {
|
||||||
|
chars.push(String.fromCharCode((bytes[index] << 8) + bytes[index + 1]));
|
||||||
|
}
|
||||||
|
return chars.join("");
|
||||||
|
}
|
||||||
|
return Buffer.from(bytes).toString("latin1");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractPptxText(buffer: Buffer): Promise<string> {
|
||||||
|
const zip = await JSZip.loadAsync(buffer);
|
||||||
|
const parser = new XMLParser({
|
||||||
|
ignoreAttributes: true,
|
||||||
|
textNodeName: "#text"
|
||||||
|
});
|
||||||
|
const slideFiles = Object.keys(zip.files)
|
||||||
|
.filter((name) => /^ppt\/slides\/slide\d+\.xml$/.test(name))
|
||||||
|
.sort((a, b) => slideNumber(a) - slideNumber(b));
|
||||||
|
|
||||||
|
const slides: string[] = [];
|
||||||
|
for (const fileName of slideFiles) {
|
||||||
|
const xml = await zip.file(fileName)?.async("string");
|
||||||
|
if (!xml) continue;
|
||||||
|
const parsed = parser.parse(xml);
|
||||||
|
const text = collectText(parsed)
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
if (text) {
|
||||||
|
slides.push(`## Slide ${slideNumber(fileName)}\n\n${text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slides.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectText(value: unknown): string[] {
|
||||||
|
if (typeof value === "string") return [value];
|
||||||
|
if (Array.isArray(value)) return value.flatMap(collectText);
|
||||||
|
if (!value || typeof value !== "object") return [];
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
return Object.entries(record).flatMap(([key, child]) => {
|
||||||
|
if (key === "a:t" || key === "#text") return collectText(child);
|
||||||
|
return collectText(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function slideNumber(fileName: string): number {
|
||||||
|
return Number(/slide(\d+)\.xml$/.exec(fileName)?.[1] || "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveTitle(content: string): string | undefined {
|
||||||
|
return content
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find((line) => line.startsWith("# "))
|
||||||
|
?.replace(/^#\s+/, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseName(fileName: string): string {
|
||||||
|
return path.basename(fileName, path.extname(fileName)).replace(/[-_]+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringField(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function exists(targetPath: string): Promise<boolean> {
|
async function exists(targetPath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await access(targetPath);
|
await access(targetPath);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { DragEvent, useMemo, useRef, useState } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { Download, FileText, WandSparkles } from "lucide-react";
|
import { Download, FileText, Image as ImageIcon, Loader2, Paperclip, Upload, WandSparkles, X } from "lucide-react";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
type DeckResult = {
|
type DeckResult = {
|
||||||
|
|
@ -9,29 +9,71 @@ type DeckResult = {
|
||||||
slides: number;
|
slides: number;
|
||||||
specUrl: string;
|
specUrl: string;
|
||||||
pptxUrl: string;
|
pptxUrl: string;
|
||||||
|
source?: {
|
||||||
|
title?: string;
|
||||||
|
type?: string;
|
||||||
|
sourceName?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initialContent = `# Customer Discovery Notes
|
||||||
|
|
||||||
|
Paste notes, transcript excerpts, or brief content here.
|
||||||
|
|
||||||
|
- Confirm the top decisions.
|
||||||
|
- Build an executive narrative.
|
||||||
|
- Render a PPTX for review.`;
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [content, setContent] = useState(`# Customer Discovery Notes\n\nPaste notes, transcript excerpts, or brief content here.\n\n- Confirm the top decisions.\n- Build an executive narrative.\n- Render a PPTX for review.`);
|
const [content, setContent] = useState(initialContent);
|
||||||
const [instructions, setInstructions] = useState("Create a concise executive deck.");
|
const [instructions, setInstructions] = useState("Create a concise executive deck.");
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
const [result, setResult] = useState<DeckResult | null>(null);
|
const [result, setResult] = useState<DeckResult | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const sourceLabel = useMemo(() => {
|
||||||
|
if (file) return `${file.name} · ${formatBytes(file.size)}`;
|
||||||
|
const chars = content.trim().length;
|
||||||
|
return chars ? `${chars.toLocaleString()} characters pasted` : "No source selected";
|
||||||
|
}, [content, file]);
|
||||||
|
|
||||||
|
function setSourceFile(nextFile: File | null) {
|
||||||
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||||
|
setFile(nextFile);
|
||||||
|
setResult(null);
|
||||||
|
setError(null);
|
||||||
|
if (nextFile?.type.startsWith("image/")) {
|
||||||
|
setPreviewUrl(URL.createObjectURL(nextFile));
|
||||||
|
} else {
|
||||||
|
setPreviewUrl(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(event: DragEvent<HTMLLabelElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
const dropped = event.dataTransfer.files?.[0];
|
||||||
|
if (dropped) setSourceFile(dropped);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onPaste(event: React.ClipboardEvent<HTMLTextAreaElement>) {
|
||||||
|
const image = Array.from(event.clipboardData.files).find((item) => item.type.startsWith("image/"));
|
||||||
|
if (image) {
|
||||||
|
event.preventDefault();
|
||||||
|
setSourceFile(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createDeck() {
|
async function createDeck() {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/decks", {
|
const response = file ? await createDeckFromFile(file) : await createDeckFromText();
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
style: "incorta",
|
|
||||||
instructions,
|
|
||||||
input: { type: "markdown", content }
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (!response.ok) throw new Error(json.error || "Deck generation failed");
|
if (!response.ok) throw new Error(json.error || "Deck generation failed");
|
||||||
setResult(json);
|
setResult(json);
|
||||||
|
|
@ -42,42 +84,114 @@ function App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDeckFromFile(sourceFile: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.set("source", sourceFile);
|
||||||
|
formData.set("style", "incorta");
|
||||||
|
formData.set("audience", "executives");
|
||||||
|
formData.set("instructions", instructions);
|
||||||
|
return fetch("/api/decks/from-source", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeckFromText() {
|
||||||
|
return fetch("/api/decks", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
style: "incorta",
|
||||||
|
instructions,
|
||||||
|
input: { type: "markdown", content }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="app-shell">
|
<main className="app-shell">
|
||||||
<section className="workspace">
|
<section className="workspace">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Slide Factory</p>
|
<p className="eyebrow">Slide Factory</p>
|
||||||
<h1>Deck builder</h1>
|
<h1>Source to deck</h1>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={createDeck} disabled={busy}>
|
<button className="primary-action" onClick={createDeck} disabled={busy}>
|
||||||
<WandSparkles size={18} />
|
{busy ? <Loader2 className="spin" size={18} /> : <WandSparkles size={18} />}
|
||||||
{busy ? "Building" : "Build"}
|
{busy ? "Building" : "Build deck"}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid">
|
<div className="main-grid">
|
||||||
|
<section className="source-pane">
|
||||||
|
<label
|
||||||
|
className={`drop-zone ${dragging ? "is-dragging" : ""}`}
|
||||||
|
onDragOver={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setDragging(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setDragging(false)}
|
||||||
|
onDrop={onDrop}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".md,.markdown,.txt,.pdf,.pptx,image/*"
|
||||||
|
onChange={(event) => setSourceFile(event.target.files?.[0] || null)}
|
||||||
|
/>
|
||||||
|
<Upload size={20} />
|
||||||
|
<span>Drop source</span>
|
||||||
|
<button type="button" className="secondary-action" onClick={() => fileInputRef.current?.click()}>
|
||||||
|
<Paperclip size={16} />
|
||||||
|
Choose file
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="source-status">
|
||||||
|
<span>{sourceLabel}</span>
|
||||||
|
{file && (
|
||||||
|
<button type="button" className="icon-action" onClick={() => setSourceFile(null)} aria-label="Clear file">
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{previewUrl && (
|
||||||
|
<div className="image-preview">
|
||||||
|
<img src={previewUrl} alt="" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>Instructions</span>
|
<span>Instructions</span>
|
||||||
<input value={instructions} onChange={(event) => setInstructions(event.target.value)} />
|
<input value={instructions} onChange={(event) => setInstructions(event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="field content">
|
<label className="field content">
|
||||||
<span>Source Content</span>
|
<span>Paste source</span>
|
||||||
<textarea value={content} onChange={(event) => setContent(event.target.value)} />
|
<textarea
|
||||||
|
value={content}
|
||||||
|
onPaste={onPaste}
|
||||||
|
onChange={(event) => {
|
||||||
|
setContent(event.target.value);
|
||||||
|
if (file) setSourceFile(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
<aside className="result-panel">
|
<aside className="result-panel">
|
||||||
<div className="result-title">
|
<div className="result-title">
|
||||||
<FileText size={18} />
|
{file?.type.startsWith("image/") ? <ImageIcon size={18} /> : <FileText size={18} />}
|
||||||
<span>Output</span>
|
<span>Output</span>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
{!result && !error && <p className="muted">Build a deck to get download links.</p>}
|
{!result && !error && <p className="muted">Ready for a source.</p>}
|
||||||
{result && (
|
{result && (
|
||||||
<div className="result">
|
<div className="result">
|
||||||
<strong>{result.title}</strong>
|
<strong>{result.title}</strong>
|
||||||
<span>{result.slides} slides</span>
|
<span>{result.slides} slides</span>
|
||||||
|
{result.source?.sourceName && <span className="source-chip">{result.source.sourceName}</span>}
|
||||||
<a href={result.pptxUrl}>
|
<a href={result.pptxUrl}>
|
||||||
<Download size={16} />
|
<Download size={16} />
|
||||||
PPTX
|
PPTX
|
||||||
|
|
@ -92,9 +206,14 @@ function App() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(value: number) {
|
||||||
|
if (value < 1024) return `${value} B`;
|
||||||
|
if (value < 1024 * 1024) return `${Math.round(value / 1024)} KB`;
|
||||||
|
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
:root {
|
:root {
|
||||||
color: #172033;
|
color: #172033;
|
||||||
background: #f5f7fb;
|
background: #f4f6fa;
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,13 +18,17 @@ textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
max-width: 1180px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +37,7 @@ textarea {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
|
|
@ -49,30 +53,87 @@ h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.primary-action,
|
||||||
|
.secondary-action,
|
||||||
|
.icon-action {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-height: 40px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-action {
|
||||||
|
min-height: 40px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #172033;
|
background: #172033;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
.primary-action:disabled {
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.secondary-action {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #172033;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d8dee9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-action {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
color: #596579;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d8dee9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-pane {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 68px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px dashed #aab4c4;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.is-dragging {
|
||||||
|
border-color: #f05a28;
|
||||||
|
background: #fff7f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 4px;
|
||||||
|
color: #596579;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -87,7 +148,8 @@ button:disabled {
|
||||||
|
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
.result-panel {
|
.result-panel,
|
||||||
|
.image-preview {
|
||||||
border: 1px solid #d8dee9;
|
border: 1px solid #d8dee9;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
@ -98,21 +160,31 @@ input {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
grid-column: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
min-height: 520px;
|
min-height: 390px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 340px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.result-panel {
|
.result-panel {
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1 / span 2;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
min-height: 420px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-title {
|
.result-title {
|
||||||
|
|
@ -135,6 +207,18 @@ textarea {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #596579;
|
||||||
|
background: #f4f6fa;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.result a {
|
.result a {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -144,13 +228,31 @@ textarea {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.grid {
|
.app-shell {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-panel {
|
.drop-zone {
|
||||||
grid-column: 1;
|
grid-template-columns: auto 1fr;
|
||||||
grid-row: auto;
|
}
|
||||||
|
|
||||||
|
.drop-zone .secondary-action {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-self: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
163
package-lock.json
generated
163
package-lock.json
generated
|
|
@ -30,13 +30,19 @@
|
||||||
"@slide-factory/render-pptx": "0.1.0",
|
"@slide-factory/render-pptx": "0.1.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"fast-xml-parser": "^4.5.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"multer": "^2.1.1",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
"tsx": "^4.16.2",
|
"tsx": "^4.16.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/multer": "^1.4.12",
|
||||||
|
"@types/pdf-parse": "^1.1.4",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1264,6 +1270,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/multer": {
|
||||||
|
"version": "1.4.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
|
||||||
|
"integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.42",
|
"version": "20.19.42",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz",
|
||||||
|
|
@ -1274,6 +1290,16 @@
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pdf-parse": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
|
|
@ -1382,6 +1408,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/append-field": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
|
@ -1472,6 +1504,23 @@
|
||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/busboy": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
|
"dependencies": {
|
||||||
|
"streamsearch": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
|
@ -1539,6 +1588,35 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concat-stream/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
|
@ -1838,6 +1916,24 @@
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-xml-parser": {
|
||||||
|
"version": "4.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz",
|
||||||
|
"integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"strnum": "^1.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"fxparser": "src/cli/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
|
|
@ -2230,6 +2326,25 @@
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/multer": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"append-field": "^1.0.0",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"type-is": "^1.6.18"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "5.1.11",
|
"version": "5.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
|
||||||
|
|
@ -2257,6 +2372,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-ensure": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.47",
|
"version": "2.0.47",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
|
||||||
|
|
@ -2320,6 +2441,22 @@
|
||||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdf-parse": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-ensure": "^0.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
@ -2765,6 +2902,14 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamsearch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
|
@ -2780,6 +2925,18 @@
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/strnum": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
@ -2820,6 +2977,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue