chore: initialize math-tasks project
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@utility page-a4 {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@utility page-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@utility page-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 15mm;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@utility problem-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
@utility problem-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@utility problem-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@utility answer-line {
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
border-bottom: 2px solid currentColor;
|
||||
text-align: center;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-a4 {
|
||||
break-after: page;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.page-a4:last-child {
|
||||
break-after: auto;
|
||||
page-break-after: auto;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user