chore: initialize math-tasks project

This commit is contained in:
2026-02-17 17:47:10 +07:00
commit f7bb740d87
10 changed files with 3775 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import { writeFileSync, mkdirSync } from 'fs';
import { resolve, dirname } from 'path';
const API_ENDPOINT = process.env.BANATIE_API_URL || 'https://api.banatie.com/v1/generate';
const API_KEY = process.env.BANATIE_API_KEY || '';
const PRESETS = {
background: { width: 1200, height: 1700, format: 'png' },
icon: { width: 128, height: 128, format: 'png', transparent: true },
};
export async function generateImage({ type = 'icon', prompt, output }) {
if (!API_KEY) {
console.error('BANATIE_API_KEY environment variable is not set');
process.exit(1);
}
const preset = PRESETS[type];
if (!preset) {
console.error(`Unknown type: ${type}. Use "background" or "icon".`);
process.exit(1);
}
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify({
prompt,
...preset,
}),
});
if (!response.ok) {
const body = await response.text();
console.error(`API error ${response.status}: ${body}`);
process.exit(1);
}
const buffer = Buffer.from(await response.arrayBuffer());
const outputPath = resolve(output);
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, buffer);
console.log(`Image saved: ${outputPath}`);
return outputPath;
}
function parseArgs(args) {
const result = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--type') result.type = args[++i];
else if (args[i] === '--prompt') result.prompt = args[++i];
else if (args[i] === '--output') result.output = args[++i];
}
return result;
}
const args = parseArgs(process.argv.slice(2));
if (args.prompt && args.output) {
generateImage(args);
} else if (process.argv.length > 2) {
console.error('Usage: node banatie.mjs --type <background|icon> --prompt "<description>" --output <path>');
process.exit(1);
}
+44
View File
@@ -0,0 +1,44 @@
import puppeteer from 'puppeteer';
import { resolve, basename } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
const OUTPUT_DIR = resolve(fileURLToPath(import.meta.url), '../../../output/pdf');
async function generatePdf(htmlPath) {
const absolutePath = resolve(htmlPath);
if (!existsSync(absolutePath)) {
console.error(`File not found: ${absolutePath}`);
process.exit(1);
}
mkdirSync(OUTPUT_DIR, { recursive: true });
const pdfName = basename(absolutePath, '.html') + '.pdf';
const pdfPath = resolve(OUTPUT_DIR, pdfName);
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(`file://${absolutePath}`, { waitUntil: 'networkidle0' });
await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
preferCSSPageSize: true,
});
await browser.close();
console.log(`PDF generated: ${pdfPath}`);
return pdfPath;
}
const htmlFile = process.argv[2];
if (!htmlFile) {
console.error('Usage: node generate-pdf.mjs <html-file>');
process.exit(1);
}
generatePdf(htmlFile);
+87
View File
@@ -0,0 +1,87 @@
import { readFileSync, readdirSync } from 'fs';
import { resolve } from 'path';
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function pickRandom(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function generateValue(varDef) {
if (varDef.type === 'range') {
return randomInt(varDef.min, varDef.max);
}
if (varDef.type === 'set') {
return pickRandom(varDef.values);
}
throw new Error(`Unknown variable type: ${varDef.type}`);
}
function formatProblem(template, values) {
return template.replace(/\{(\w+)\}/g, (_, key) => {
if (key in values) return values[key];
return `{${key}}`;
});
}
function getIcons(iconDir) {
if (!iconDir) return [];
try {
const resolved = resolve(iconDir);
return readdirSync(resolved)
.filter(f => /\.(png|jpg|jpeg|svg|webp)$/i.test(f))
.map(f => resolve(resolved, f));
} catch {
return [];
}
}
export function generateProblems(taskConfig) {
const { template, variables, problemCount = 20, theme = {} } = taskConfig;
const icons = getIcons(theme.icons);
const backgroundPath = theme.background ? resolve(theme.background) : null;
const iconReward = theme.iconReward || 1;
const problems = [];
const seen = new Set();
let attempts = 0;
const maxAttempts = problemCount * 10;
while (problems.length < problemCount && attempts < maxAttempts) {
attempts++;
const values = {};
for (const [name, def] of Object.entries(variables)) {
values[name] = generateValue(def);
}
const key = Object.values(values).join(',');
if (seen.has(key)) continue;
seen.add(key);
const text = formatProblem(template, values);
const problemIndex = problems.length;
const icon = icons.length > 0 && (problemIndex + 1) % iconReward === 0
? pickRandom(icons)
: null;
problems.push({ text, values, icon });
}
return {
id: taskConfig.id,
title: taskConfig.title,
labels: taskConfig.labels || {},
layout: taskConfig.layout || { columns: 2, problemsPerPage: 20 },
background: backgroundPath,
problems,
};
}
const taskFile = process.argv[2];
if (taskFile) {
const config = JSON.parse(readFileSync(resolve(taskFile), 'utf-8'));
const result = generateProblems(config);
console.log(JSON.stringify(result, null, 2));
}