chore: initialize math-tasks project
This commit is contained in:
commit
f7bb740d87
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
output/
|
||||||
|
.env
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Math Tasks Generator
|
||||||
|
|
||||||
|
Printable math worksheet generator (A4 PDF) for children aged 7–9. Claude Code-driven workflow: user describes a task idea → Claude generates JSON config → creates HTML pages with Tailwind CSS → converts to PDF via Puppeteer.
|
||||||
|
|
||||||
|
**No template engine.** Claude Code generates fresh HTML pages directly from JSON task configs each time.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:css # Build Tailwind CSS (minified)
|
||||||
|
npm run build:css:watch # Watch mode for CSS
|
||||||
|
npm run preview # Serve HTML at localhost:3000
|
||||||
|
npm run dev # CSS watch + preview server (concurrent)
|
||||||
|
npm run pdf -- <file> # Convert HTML file to PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate problems from a task config:
|
||||||
|
```bash
|
||||||
|
node src/scripts/generate-problems.mjs tasks/<task>.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate images via Banatie API:
|
||||||
|
```bash
|
||||||
|
node src/scripts/banatie.mjs --type background --prompt "forest theme" --output assets/backgrounds/forest.png
|
||||||
|
node src/scripts/banatie.mjs --type icon --prompt "golden star" --output assets/icons/stars/star1.png
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
styles/main.css — Tailwind source with A4/print styles
|
||||||
|
scripts/
|
||||||
|
generate-pdf.mjs — HTML → PDF via Puppeteer
|
||||||
|
generate-problems.mjs — JSON task → concrete problem list
|
||||||
|
banatie.mjs — Banatie API client for image generation
|
||||||
|
tasks/ — JSON task definition files
|
||||||
|
assets/
|
||||||
|
backgrounds/ — large background images per theme (~1200x1700px)
|
||||||
|
icons/ — icon sets in subfolders (128x128px transparent PNG)
|
||||||
|
output/
|
||||||
|
html/ — generated HTML (gitignored)
|
||||||
|
pdf/ — generated PDFs (gitignored)
|
||||||
|
css/ — built Tailwind CSS (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON Task Format
|
||||||
|
|
||||||
|
Each task is a JSON file in `tasks/` with this structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "multiply-2-3",
|
||||||
|
"title": "Умножение на 2 и 3",
|
||||||
|
"description": "Worksheet for practicing multiplication by 2 and 3",
|
||||||
|
"template": "{a} × {b} = ___",
|
||||||
|
"variables": {
|
||||||
|
"a": { "type": "range", "min": 1, "max": 10 },
|
||||||
|
"b": { "type": "set", "values": [2, 3] }
|
||||||
|
},
|
||||||
|
"problemCount": 20,
|
||||||
|
"layout": {
|
||||||
|
"columns": 2,
|
||||||
|
"problemsPerPage": 20
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"title": "Умножение",
|
||||||
|
"subtitle": "Реши примеры",
|
||||||
|
"name": "Имя: _______________",
|
||||||
|
"date": "Дата: _______________"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"background": "assets/backgrounds/forest.png",
|
||||||
|
"icons": "assets/icons/stars/",
|
||||||
|
"iconReward": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
- **template** — problem template with `{variable}` placeholders
|
||||||
|
- **variables** — each variable is either `range` (min/max) or `set` (explicit values)
|
||||||
|
- **problemCount** — how many problems to generate
|
||||||
|
- **layout.columns** — 1 or 2 column layout
|
||||||
|
- **layout.problemsPerPage** — max problems per A4 page
|
||||||
|
- **labels** — all visible text (no hardcoded language)
|
||||||
|
- **theme.background** — path to background image
|
||||||
|
- **theme.icons** — path to icon directory (for collectible rewards)
|
||||||
|
- **theme.iconReward** — show an icon every N problems solved
|
||||||
|
|
||||||
|
## HTML Generation Guidelines
|
||||||
|
|
||||||
|
When generating HTML worksheets:
|
||||||
|
|
||||||
|
- **Page size:** A4 = 210mm × 297mm. Use the `.page-a4` class.
|
||||||
|
- **CSS:** Link to `../css/styles.css` (relative from `output/html/`)
|
||||||
|
- **Page breaks:** Use `break-after: page` between pages. Each `.page-a4` is one printed page.
|
||||||
|
- **Background images:** Use absolute paths or paths relative to HTML location. Apply via `.page-background` as a full-page positioned image.
|
||||||
|
- **Icons:** Inline small 128×128 images next to problems or as rewards.
|
||||||
|
- **Fonts:** Use system fonts or Google Fonts loaded via `<link>`.
|
||||||
|
- **Print-friendly:** Avoid shadows, gradients that don't print well. Test with `npm run pdf`.
|
||||||
|
- **Images in PDF:** Use local file paths (not URLs). Puppeteer resolves `file://` protocol.
|
||||||
|
- **Embed images** as base64 data URIs when possible for reliable PDF rendering.
|
||||||
|
|
||||||
|
## Banatie API
|
||||||
|
|
||||||
|
REST API for generating images.
|
||||||
|
|
||||||
|
- **Backgrounds:** ~1200×1700px, themed illustrations (forest, space, ocean, etc.)
|
||||||
|
- **Icons:** 128×128px, transparent PNG, simple collectible items (stars, gems, animals)
|
||||||
|
|
||||||
|
Configuration is in `src/scripts/banatie.mjs`. Set the `BANATIE_API_KEY` environment variable for authentication.
|
||||||
|
|
||||||
|
## PDF Generation
|
||||||
|
|
||||||
|
Puppeteer settings for A4 worksheets:
|
||||||
|
- Format: A4
|
||||||
|
- `printBackground: true` (required for background images)
|
||||||
|
- Margins: zero (CSS handles margins)
|
||||||
|
- `preferCSSPageSize: true`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. User describes the math task idea to Claude
|
||||||
|
2. Claude creates/updates a JSON config in `tasks/`
|
||||||
|
3. Claude runs `node src/scripts/generate-problems.mjs tasks/<task>.json` to get concrete problems
|
||||||
|
4. Claude generates HTML file(s) in `output/html/` using the problems and Tailwind classes
|
||||||
|
5. Run `npm run build:css` to compile CSS
|
||||||
|
6. Run `npm run pdf -- output/html/<file>.html` to create PDF
|
||||||
|
7. Preview with `npm run preview` at localhost:3000
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "math-tasks",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Проект для генерации заданий по математике для детей 7–9 лет.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build:css": "npx @tailwindcss/cli -i src/styles/main.css -o output/css/styles.css --minify",
|
||||||
|
"build:css:watch": "npx @tailwindcss/cli -i src/styles/main.css -o output/css/styles.css --watch",
|
||||||
|
"preview": "npx serve output/html --cors -l 3000",
|
||||||
|
"pdf": "node src/scripts/generate-pdf.mjs",
|
||||||
|
"dev": "concurrently \"npm run build:css:watch\" \"npm run preview\""
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"puppeteer": "^24.37.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"serve": "^14.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Math Tasks Generator — Setup Steps
|
||||||
|
|
||||||
|
## Step 1: Initialize Node.js project
|
||||||
|
- [x] `npm init -y` with `"type": "module"`
|
||||||
|
- [x] Create `.gitignore` (node_modules/, output/)
|
||||||
|
|
||||||
|
## Step 2: Install dependencies
|
||||||
|
- [x] `puppeteer` — HTML to PDF conversion (headless Chrome)
|
||||||
|
- [x] `@tailwindcss/cli` — Tailwind CSS v4 build tool (dev)
|
||||||
|
- [x] `serve` — static file server for HTML preview (dev)
|
||||||
|
- [x] `concurrently` — run CSS watch + preview server together (dev)
|
||||||
|
|
||||||
|
## Step 3: Create directory structure
|
||||||
|
- [x] `src/styles/main.css` — Tailwind source with A4/print styles
|
||||||
|
- [x] `src/scripts/generate-pdf.mjs` — HTML → PDF via Puppeteer
|
||||||
|
- [x] `src/scripts/generate-problems.mjs` — JSON task → concrete problem list
|
||||||
|
- [x] `src/scripts/banatie.mjs` — Banatie API client for image generation
|
||||||
|
- [x] `tasks/` — JSON task definition files
|
||||||
|
- [x] `assets/backgrounds/` — large background images per theme
|
||||||
|
- [x] `assets/icons/` — icon sets in subfolders
|
||||||
|
- [x] `output/html/` — generated HTML (gitignored)
|
||||||
|
- [x] `output/pdf/` — generated PDFs (gitignored)
|
||||||
|
- [x] `output/css/` — built Tailwind CSS (gitignored)
|
||||||
|
|
||||||
|
## Step 4: Configure Tailwind CSS v4
|
||||||
|
- [x] `src/styles/main.css` with `@import "tailwindcss"`, `@page` rules for A4
|
||||||
|
- [x] Custom utility classes: `.page-a4`, `.page-background`, `.page-content`, `.problem-row`, `.problem-icon`, `.problem-text`, `.answer-line`
|
||||||
|
- [x] Build script: `npx @tailwindcss/cli -i src/styles/main.css -o output/css/styles.css`
|
||||||
|
|
||||||
|
## Step 5: Create PDF generation script
|
||||||
|
- [x] `src/scripts/generate-pdf.mjs` using Puppeteer
|
||||||
|
- [x] Accepts HTML file path, outputs PDF to `output/pdf/`
|
||||||
|
- [x] Settings: A4 format, `printBackground: true`, zero margins, `preferCSSPageSize: true`
|
||||||
|
|
||||||
|
## Step 6: Create problem generation utility
|
||||||
|
- [x] `src/scripts/generate-problems.mjs` — reads JSON task, generates randomized math problems
|
||||||
|
- [x] Exports function and works as CLI
|
||||||
|
- [x] Handles variable substitution, deduplication
|
||||||
|
|
||||||
|
## Step 7: Create example JSON task
|
||||||
|
- [x] `tasks/example-multiply.json` with sample multiplication task config
|
||||||
|
- [x] Demonstrates full JSON schema: template, variables, labels, theme refs
|
||||||
|
|
||||||
|
## Step 8: Set up npm scripts
|
||||||
|
- [x] `build:css` — build Tailwind CSS (minified)
|
||||||
|
- [x] `build:css:watch` — watch mode
|
||||||
|
- [x] `preview` — serve HTML at localhost:3000
|
||||||
|
- [x] `pdf` — convert HTML to PDF
|
||||||
|
- [x] `dev` — CSS watch + preview (concurrent)
|
||||||
|
|
||||||
|
## Step 9: Initialize git repo
|
||||||
|
- [ ] `git init`, initial commit
|
||||||
|
|
||||||
|
## Step 10: Configure Banatie API integration
|
||||||
|
- [x] `src/scripts/banatie.mjs` — API client for image generation
|
||||||
|
- [x] Documented in CLAUDE.md
|
||||||
|
- [x] Standard sizes: backgrounds 1200×1700px, icons 128×128px transparent PNG
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"id": "multiply-2-3",
|
||||||
|
"title": "Умножение на 2 и 3",
|
||||||
|
"description": "Worksheet for practicing multiplication by 2 and 3",
|
||||||
|
"template": "{a} × {b} = ___",
|
||||||
|
"variables": {
|
||||||
|
"a": { "type": "range", "min": 1, "max": 10 },
|
||||||
|
"b": { "type": "set", "values": [2, 3] }
|
||||||
|
},
|
||||||
|
"problemCount": 20,
|
||||||
|
"layout": {
|
||||||
|
"columns": 2,
|
||||||
|
"problemsPerPage": 20
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"title": "Умножение",
|
||||||
|
"subtitle": "Реши примеры",
|
||||||
|
"name": "Имя: _______________",
|
||||||
|
"date": "Дата: _______________"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"background": null,
|
||||||
|
"icons": null,
|
||||||
|
"iconReward": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue