refactor: remove dead Python bridge and unused packages

Removed abandoned SVG generation code that was never used in production:

**Deleted Files:**
- packages/core/src/bridge.py (302 lines) - Python-shell bridge for SVG generation
- packages/core/client/node/src/soroban-generator-bridge.ts - TypeScript wrapper
- packages/core/client/typescript/ - Entire unused @soroban/client package
- packages/core/client/browser/ - Empty package

**Dependencies Removed:**
- python-shell - Only used by abandoned bridge code
- @types/minimatch - Only needed by removed TypeScript packages
- @soroban/client from apps/web

**Code Cleanup:**
- Simplified packages/core/client/node/src/index.ts exports
- Removed SorobanGeneratorBridge, BridgeFlashcardConfig, BridgeFlashcardResult exports

**Impact:**
- ~800 lines of dead TypeScript code removed
- 302 lines of unused Python code removed
- 2 npm dependencies removed
- Build verified successful - no functionality affected

**What Remains Active:**
- generate.py - PDF generation via Typst CLI (actively used by /api/generate)
- soroban-generator.ts - CLI wrapper for PDF generation
- api.py - Optional FastAPI server
- generate_examples.py - Documentation image generator
- Web app uses @soroban/abacus-react for all SVG rendering

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-10-18 09:11:40 -05:00
parent cf997b9cbc
commit 22426f677f
34 changed files with 16 additions and 1550 deletions

View File

@ -92,7 +92,9 @@
"Bash(ls:*)",
"Bash(do if [ -f \"$file\" ])",
"Bash(! echo \"$file\")",
"Bash(then sed -i '' \"s|from ''''../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" sed -i '' \"s|from ''''../../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" fi done)"
"Bash(then sed -i '' \"s|from ''''../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" sed -i '' \"s|from ''''../../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" fi done)",
"Bash(pnpm install)",
"Bash(pnpm exec turbo build --filter=@soroban/web)"
],
"deny": [],
"ask": []

View File

@ -48,7 +48,6 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@react-spring/web": "^10.0.2",
"@soroban/abacus-react": "workspace:*",
"@soroban/client": "workspace:*",
"@soroban/core": "workspace:*",
"@soroban/templates": "workspace:*",
"@tanstack/react-form": "^0.19.0",

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi

View File

@ -1 +0,0 @@
../../../../../../node_modules/.pnpm/@myriaddreamin+typst-ts-renderer@0.6.0/node_modules/@myriaddreamin/typst-ts-renderer

View File

@ -1 +0,0 @@
../../../../../../node_modules/.pnpm/@myriaddreamin+typst-ts-web-compiler@0.6.0/node_modules/@myriaddreamin/typst-ts-web-compiler

View File

@ -1 +0,0 @@
../../../../../node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite

View File

@ -1,19 +0,0 @@
{
"name": "soroban-flashcards-browser",
"version": "1.0.0",
"description": "Browser-based Soroban Flashcard Generator using Typst.ts",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@myriaddreamin/typst-ts-web-compiler": "^0.6.0",
"@myriaddreamin/typst-ts-renderer": "^0.6.0"
},
"devDependencies": {
"vite": "^5.0.0",
"typescript": "^5.0.0"
}
}

View File

@ -2,12 +2,12 @@
* Node.js TypeScript wrapper for Soroban Flashcard Generator
* Calls Python functions via child_process
*/
interface FlashcardConfig$1 {
interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
orientation?: 'portrait' | 'landscape';
paperSize?: "us-letter" | "a4" | "a3" | "a5";
orientation?: "portrait" | "landscape";
margins?: {
top?: string;
bottom?: string;
@ -24,12 +24,12 @@ interface FlashcardConfig$1 {
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: 'diamond' | 'circle' | 'square';
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
beadShape?: "diamond" | "circle" | "square";
colorScheme?: "monochrome" | "place-value" | "heaven-earth" | "alternating";
coloredNumerals?: boolean;
scaleFactor?: number;
}
declare class SorobanGenerator$1 {
declare class SorobanGenerator {
private pythonPath;
private generatorPath;
private projectRoot;
@ -38,15 +38,15 @@ declare class SorobanGenerator$1 {
/**
* Generate flashcards and return PDF as Buffer
*/
generate(config: FlashcardConfig$1): Promise<Buffer>;
generate(config: FlashcardConfig): Promise<Buffer>;
/**
* Generate flashcards and save to file
*/
generateToFile(config: FlashcardConfig$1, outputPath: string): Promise<void>;
generateToFile(config: FlashcardConfig, outputPath: string): Promise<void>;
/**
* Generate flashcards and return as base64 string
*/
generateBase64(config: FlashcardConfig$1): Promise<string>;
generateBase64(config: FlashcardConfig): Promise<string>;
private executePython;
/**
* Check if all dependencies are installed
@ -59,65 +59,4 @@ declare class SorobanGenerator$1 {
}
declare function expressExample(): Promise<void>;
/**
* TypeScript wrapper using python-shell for clean function interface
* No CLI arguments - just function calls with objects
*/
interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
orientation?: 'portrait' | 'landscape';
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: 'diamond' | 'circle' | 'square';
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
coloredNumerals?: boolean;
scaleFactor?: number;
format?: 'pdf' | 'svg';
mode?: 'single-card' | 'flashcards';
number?: number;
}
interface FlashcardResult {
pdf: string;
count: number;
numbers: number[];
}
declare class SorobanGenerator {
private pythonShell;
private projectRoot;
constructor(projectRoot?: string);
/**
* Initialize persistent Python process for better performance
*/
initialize(): Promise<void>;
/**
* Generate flashcards - clean function interface
*/
generate(config: FlashcardConfig): Promise<FlashcardResult>;
/**
* Generate and return as Buffer
*/
generateBuffer(config: FlashcardConfig): Promise<Buffer>;
/**
* Clean up Python process
*/
close(): Promise<void>;
}
export { FlashcardConfig as BridgeFlashcardConfig, FlashcardResult as BridgeFlashcardResult, FlashcardConfig$1 as FlashcardConfig, SorobanGenerator$1 as SorobanGenerator, SorobanGenerator as SorobanGeneratorBridge, SorobanGenerator$1 as default, expressExample };
export { type FlashcardConfig, SorobanGenerator, SorobanGenerator as default, expressExample };

View File

@ -31,7 +31,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var src_exports = {};
__export(src_exports, {
SorobanGenerator: () => SorobanGenerator,
SorobanGeneratorBridge: () => SorobanGenerator2,
default: () => SorobanGenerator,
expressExample: () => expressExample
});
@ -194,96 +193,8 @@ var SorobanGenerator = class {
async function expressExample() {
const generator = new SorobanGenerator();
}
// src/soroban-generator-bridge.ts
var import_python_shell = require("python-shell");
var path2 = __toESM(require("path"));
var SorobanGenerator2 = class {
pythonShell = null;
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot || path2.join(__dirname, "../../");
}
/**
* Initialize persistent Python process for better performance
*/
async initialize() {
if (this.pythonShell)
return;
this.pythonShell = new import_python_shell.PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
pythonOptions: ["-u"],
// Unbuffered
scriptPath: this.projectRoot
});
}
/**
* Generate flashcards - clean function interface
*/
async generate(config) {
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
const shell = new import_python_shell.PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot
});
shell.on("message", (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
});
shell.on("error", (err) => {
reject(err);
});
shell.send(config);
shell.end((err, code, signal) => {
if (err)
reject(err);
});
});
}
return new Promise((resolve, reject) => {
if (!this.pythonShell) {
reject(new Error("Not initialized"));
return;
}
const handler = (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
this.pythonShell?.removeListener("message", handler);
};
this.pythonShell.on("message", handler);
this.pythonShell.send(config);
});
}
/**
* Generate and return as Buffer
*/
async generateBuffer(config) {
const result = await this.generate(config);
return Buffer.from(result.pdf, "base64");
}
/**
* Clean up Python process
*/
async close() {
if (this.pythonShell) {
this.pythonShell.end(() => {
});
this.pythonShell = null;
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SorobanGenerator,
SorobanGeneratorBridge,
expressExample
});

View File

@ -155,96 +155,8 @@ var SorobanGenerator = class {
async function expressExample() {
const generator = new SorobanGenerator();
}
// src/soroban-generator-bridge.ts
import { PythonShell } from "python-shell";
import * as path2 from "path";
var SorobanGenerator2 = class {
pythonShell = null;
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot || path2.join(__dirname, "../../");
}
/**
* Initialize persistent Python process for better performance
*/
async initialize() {
if (this.pythonShell)
return;
this.pythonShell = new PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
pythonOptions: ["-u"],
// Unbuffered
scriptPath: this.projectRoot
});
}
/**
* Generate flashcards - clean function interface
*/
async generate(config) {
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
const shell = new PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot
});
shell.on("message", (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
});
shell.on("error", (err) => {
reject(err);
});
shell.send(config);
shell.end((err, code, signal) => {
if (err)
reject(err);
});
});
}
return new Promise((resolve, reject) => {
if (!this.pythonShell) {
reject(new Error("Not initialized"));
return;
}
const handler = (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
this.pythonShell?.removeListener("message", handler);
};
this.pythonShell.on("message", handler);
this.pythonShell.send(config);
});
}
/**
* Generate and return as Buffer
*/
async generateBuffer(config) {
const result = await this.generate(config);
return Buffer.from(result.pdf, "base64");
}
/**
* Clean up Python process
*/
async close() {
if (this.pythonShell) {
this.pythonShell.end(() => {
});
this.pythonShell = null;
}
}
};
export {
SorobanGenerator,
SorobanGenerator2 as SorobanGeneratorBridge,
SorobanGenerator as default,
expressExample
};

View File

@ -6,9 +6,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
"$basedir/../../../../../../node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/esbuild" "$@"
"$basedir/../../../../../../node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/bin/esbuild" "$@"
exit $?

View File

@ -1 +0,0 @@
../../../../../node_modules/.pnpm/python-shell@5.0.0/node_modules/python-shell

View File

@ -18,11 +18,7 @@
"type-check": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"python-shell": "^5.0.0"
},
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@types/node": "^20.0.0",
"tsup": "^7.0.0",
"typescript": "^5.0.0",

View File

@ -5,12 +5,5 @@
export * from "./soroban-generator";
// Export bridge generator with different name to avoid conflicts
export {
SorobanGenerator as SorobanGeneratorBridge,
FlashcardConfig as BridgeFlashcardConfig,
FlashcardResult as BridgeFlashcardResult,
} from "./soroban-generator-bridge";
// Default export for convenience
export { SorobanGenerator as default } from "./soroban-generator";

View File

@ -1,190 +0,0 @@
/**
* TypeScript wrapper using python-shell for clean function interface
* No CLI arguments - just function calls with objects
*/
import { PythonShell } from "python-shell";
import * as path from "path";
export interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: "us-letter" | "a4" | "a3" | "a5";
orientation?: "portrait" | "landscape";
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: "diamond" | "circle" | "square";
colorScheme?: "monochrome" | "place-value" | "heaven-earth" | "alternating";
coloredNumerals?: boolean;
scaleFactor?: number;
format?: "pdf" | "svg";
mode?: "single-card" | "flashcards";
number?: number;
}
export interface FlashcardResult {
pdf: string; // base64 encoded PDF or SVG content (depending on format)
count: number;
numbers: number[];
}
export class SorobanGenerator {
private pythonShell: PythonShell | null = null;
private projectRoot: string;
constructor(projectRoot?: string) {
this.projectRoot = projectRoot || path.join(__dirname, "../../");
}
/**
* Initialize persistent Python process for better performance
*/
async initialize(): Promise<void> {
if (this.pythonShell) return;
this.pythonShell = new PythonShell(path.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
pythonOptions: ["-u"], // Unbuffered
scriptPath: this.projectRoot,
});
}
/**
* Generate flashcards - clean function interface
*/
async generate(config: FlashcardConfig): Promise<FlashcardResult> {
// One-shot mode if not initialized
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
const shell = new PythonShell(path.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot,
});
shell.on("message", (message: any) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message as FlashcardResult);
}
});
shell.on("error", (err: any) => {
reject(err);
});
shell.send(config);
shell.end((err: any, code: any, signal: any) => {
if (err) reject(err);
});
});
}
// Persistent mode
return new Promise((resolve, reject) => {
if (!this.pythonShell) {
reject(new Error("Not initialized"));
return;
}
const handler = (message: any) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message as FlashcardResult);
}
this.pythonShell?.removeListener("message", handler);
};
this.pythonShell.on("message", handler);
this.pythonShell.send(config);
});
}
/**
* Generate and return as Buffer
*/
async generateBuffer(config: FlashcardConfig): Promise<Buffer> {
const result = await this.generate(config);
return Buffer.from(result.pdf, "base64");
}
/**
* Clean up Python process
*/
async close(): Promise<void> {
if (this.pythonShell) {
this.pythonShell.end(() => {});
this.pythonShell = null;
}
}
}
// Example usage - just like calling a regular TypeScript function
async function example() {
const generator = new SorobanGenerator();
// Just call it like a function!
const result = await generator.generate({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true,
});
// You get back a clean result object
console.log(`Generated ${result.count} flashcards`);
// Convert to Buffer if needed
const pdfBuffer = Buffer.from(result.pdf, "base64");
// Or use persistent mode for better performance
await generator.initialize();
// Now calls are faster
const result2 = await generator.generate({ range: "0-9" });
const result3 = await generator.generate({ range: "10-19" });
await generator.close();
}
// Express example - clean function calls
export function expressRoute(app: any) {
const generator = new SorobanGenerator();
app.post("/api/flashcards", async (req: any, res: any) => {
try {
// Just pass the config object directly!
const result = await generator.generate(req.body);
// Send back JSON or PDF
if (req.query.format === "json") {
res.json(result);
} else {
const pdfBuffer = Buffer.from(result.pdf, "base64");
res.contentType("application/pdf");
res.send(pdfBuffer);
}
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
}

View File

@ -1,58 +0,0 @@
/**
* TypeScript client for Soroban Flashcard Generator API
*/
interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
orientation?: 'portrait' | 'landscape';
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: 'diamond' | 'circle' | 'square';
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
coloredNumerals?: boolean;
scaleFactor?: number;
}
interface FlashcardResponse {
pdf: string;
count: number;
numbers: number[];
}
declare class SorobanFlashcardClient {
private apiUrl;
constructor(apiUrl?: string);
/**
* Generate flashcards and return as base64 PDF
*/
generate(config: FlashcardConfig): Promise<FlashcardResponse>;
/**
* Generate flashcards and download as PDF file
*/
generateAndDownload(config: FlashcardConfig, filename?: string): Promise<void>;
/**
* Generate flashcards and open in new tab
*/
generateAndOpen(config: FlashcardConfig): Promise<void>;
/**
* Check API health
*/
health(): Promise<boolean>;
}
declare function example(): Promise<void>;
export { FlashcardConfig, FlashcardResponse, SorobanFlashcardClient, SorobanFlashcardClient as default, example };

View File

@ -1,148 +0,0 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
SorobanFlashcardClient: () => SorobanFlashcardClient,
default: () => SorobanFlashcardClient,
example: () => example
});
module.exports = __toCommonJS(src_exports);
// src/soroban-flashcards.ts
var SorobanFlashcardClient = class {
apiUrl;
constructor(apiUrl = "http://localhost:8000") {
this.apiUrl = apiUrl;
}
/**
* Generate flashcards and return as base64 PDF
*/
async generate(config) {
const response = await fetch(`${this.apiUrl}/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
range: config.range,
step: config.step ?? 1,
cards_per_page: config.cardsPerPage ?? 6,
paper_size: config.paperSize ?? "us-letter",
orientation: config.orientation ?? "portrait",
margins: config.margins ?? {
top: "0.5in",
bottom: "0.5in",
left: "0.5in",
right: "0.5in"
},
gutter: config.gutter ?? "5mm",
shuffle: config.shuffle ?? false,
seed: config.seed,
show_cut_marks: config.showCutMarks ?? false,
show_registration: config.showRegistration ?? false,
font_family: config.fontFamily ?? "DejaVu Sans",
font_size: config.fontSize ?? "48pt",
columns: config.columns ?? "auto",
show_empty_columns: config.showEmptyColumns ?? false,
hide_inactive_beads: config.hideInactiveBeads ?? false,
bead_shape: config.beadShape ?? "diamond",
color_scheme: config.colorScheme ?? "monochrome",
colored_numerals: config.coloredNumerals ?? false,
scale_factor: config.scaleFactor ?? 0.9,
format: "base64"
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to generate flashcards");
}
return response.json();
}
/**
* Generate flashcards and download as PDF file
*/
async generateAndDownload(config, filename = "flashcards.pdf") {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Generate flashcards and open in new tab
*/
async generateAndOpen(config) {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
}
/**
* Check API health
*/
async health() {
try {
const response = await fetch(`${this.apiUrl}/health`);
const data = await response.json();
return data.status === "healthy";
} catch {
return false;
}
}
};
async function example() {
const client = new SorobanFlashcardClient();
await client.generateAndDownload({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true
});
await client.generateAndDownload(
{
range: "0-100",
step: 5,
cardsPerPage: 6
},
"counting-by-5s.pdf"
);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SorobanFlashcardClient,
example
});

View File

@ -1,120 +0,0 @@
// src/soroban-flashcards.ts
var SorobanFlashcardClient = class {
apiUrl;
constructor(apiUrl = "http://localhost:8000") {
this.apiUrl = apiUrl;
}
/**
* Generate flashcards and return as base64 PDF
*/
async generate(config) {
const response = await fetch(`${this.apiUrl}/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
range: config.range,
step: config.step ?? 1,
cards_per_page: config.cardsPerPage ?? 6,
paper_size: config.paperSize ?? "us-letter",
orientation: config.orientation ?? "portrait",
margins: config.margins ?? {
top: "0.5in",
bottom: "0.5in",
left: "0.5in",
right: "0.5in"
},
gutter: config.gutter ?? "5mm",
shuffle: config.shuffle ?? false,
seed: config.seed,
show_cut_marks: config.showCutMarks ?? false,
show_registration: config.showRegistration ?? false,
font_family: config.fontFamily ?? "DejaVu Sans",
font_size: config.fontSize ?? "48pt",
columns: config.columns ?? "auto",
show_empty_columns: config.showEmptyColumns ?? false,
hide_inactive_beads: config.hideInactiveBeads ?? false,
bead_shape: config.beadShape ?? "diamond",
color_scheme: config.colorScheme ?? "monochrome",
colored_numerals: config.coloredNumerals ?? false,
scale_factor: config.scaleFactor ?? 0.9,
format: "base64"
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to generate flashcards");
}
return response.json();
}
/**
* Generate flashcards and download as PDF file
*/
async generateAndDownload(config, filename = "flashcards.pdf") {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Generate flashcards and open in new tab
*/
async generateAndOpen(config) {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
}
/**
* Check API health
*/
async health() {
try {
const response = await fetch(`${this.apiUrl}/health`);
const data = await response.json();
return data.status === "healthy";
} catch {
return false;
}
}
};
async function example() {
const client = new SorobanFlashcardClient();
await client.generateAndDownload({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true
});
await client.generateAndDownload(
{
range: "0-100",
step: 5,
cardsPerPage: 6
},
"counting-by-5s.pdf"
);
}
export {
SorobanFlashcardClient,
SorobanFlashcardClient as default,
example
};

View File

@ -1,14 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
"$basedir/../../../../../../node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/esbuild" "$@"
exit $?

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tsup/dist/cli-default.js" "$@"
else
exec node "$basedir/../tsup/dist/cli-default.js" "$@"
fi

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tsup/dist/cli-node.js" "$@"
else
exec node "$basedir/../tsup/dist/cli-node.js" "$@"
fi

View File

@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
else
exec node "$basedir/../vitest/vitest.mjs" "$@"
fi

View File

@ -1 +0,0 @@
../../../../../node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup

View File

@ -1 +0,0 @@
../../../../../node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules/vitest

View File

@ -1,38 +0,0 @@
{
"name": "@soroban/client",
"version": "1.0.0",
"description": "TypeScript client for Soroban Flashcard Generator",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.esm.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"test": "vitest",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist"
},
"keywords": [
"soroban",
"abacus",
"flashcards",
"education",
"math"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@types/node": "^20.0.0",
"tsup": "^7.0.0",
"typescript": "^5.0.0",
"vitest": "^1.0.0"
}
}

View File

@ -1,7 +0,0 @@
/**
* Soroban Flashcard Generator - TypeScript Client
* Re-export main client functionality
*/
export * from "./soroban-flashcards";
export { SorobanFlashcardClient as default } from "./soroban-flashcards";

View File

@ -1,176 +0,0 @@
/**
* TypeScript client for Soroban Flashcard Generator API
*/
export interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: "us-letter" | "a4" | "a3" | "a5";
orientation?: "portrait" | "landscape";
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: "diamond" | "circle" | "square";
colorScheme?: "monochrome" | "place-value" | "heaven-earth" | "alternating";
coloredNumerals?: boolean;
scaleFactor?: number;
}
export interface FlashcardResponse {
pdf: string; // base64 encoded PDF
count: number;
numbers: number[];
}
export class SorobanFlashcardClient {
private apiUrl: string;
constructor(apiUrl: string = "http://localhost:8000") {
this.apiUrl = apiUrl;
}
/**
* Generate flashcards and return as base64 PDF
*/
async generate(config: FlashcardConfig): Promise<FlashcardResponse> {
const response = await fetch(`${this.apiUrl}/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
range: config.range,
step: config.step ?? 1,
cards_per_page: config.cardsPerPage ?? 6,
paper_size: config.paperSize ?? "us-letter",
orientation: config.orientation ?? "portrait",
margins: config.margins ?? {
top: "0.5in",
bottom: "0.5in",
left: "0.5in",
right: "0.5in",
},
gutter: config.gutter ?? "5mm",
shuffle: config.shuffle ?? false,
seed: config.seed,
show_cut_marks: config.showCutMarks ?? false,
show_registration: config.showRegistration ?? false,
font_family: config.fontFamily ?? "DejaVu Sans",
font_size: config.fontSize ?? "48pt",
columns: config.columns ?? "auto",
show_empty_columns: config.showEmptyColumns ?? false,
hide_inactive_beads: config.hideInactiveBeads ?? false,
bead_shape: config.beadShape ?? "diamond",
color_scheme: config.colorScheme ?? "monochrome",
colored_numerals: config.coloredNumerals ?? false,
scale_factor: config.scaleFactor ?? 0.9,
format: "base64",
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to generate flashcards");
}
return response.json();
}
/**
* Generate flashcards and download as PDF file
*/
async generateAndDownload(
config: FlashcardConfig,
filename: string = "flashcards.pdf",
): Promise<void> {
const result = await this.generate(config);
// Convert base64 to blob
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
// Create download link
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Generate flashcards and open in new tab
*/
async generateAndOpen(config: FlashcardConfig): Promise<void> {
const result = await this.generate(config);
// Convert base64 to blob
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
// Open in new tab
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
}
/**
* Check API health
*/
async health(): Promise<boolean> {
try {
const response = await fetch(`${this.apiUrl}/health`);
const data = await response.json();
return data.status === "healthy";
} catch {
return false;
}
}
}
// Example usage function
export async function example() {
const client = new SorobanFlashcardClient();
// Generate and download flashcards for 0-99 with place-value coloring
await client.generateAndDownload({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true,
});
// Generate counting by 5s
await client.generateAndDownload(
{
range: "0-100",
step: 5,
cardsPerPage: 6,
},
"counting-by-5s.pdf",
);
}

View File

@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@ -1,303 +0,0 @@
#!/usr/bin/env python3
"""
Python bridge for Node.js integration
Provides a clean function interface instead of CLI
"""
import json
import sys
import base64
import tempfile
import os
import glob
from pathlib import Path
import subprocess
# Import our existing functions
from generate import parse_range, generate_typst_file, generate_single_card_typst
def generate_flashcards_json(config_json):
"""
Generate flashcards from JSON config
Returns base64 encoded PDF
"""
config = json.loads(config_json)
# Parse numbers
numbers = parse_range(
config.get('range', '0-9'),
config.get('step', 1)
)
# Handle shuffle
if config.get('shuffle', False):
import random
if 'seed' in config:
random.seed(config['seed'])
random.shuffle(numbers)
# Build Typst config
typst_config = {
'cards_per_page': config.get('cardsPerPage', 6),
'paper_size': config.get('paperSize', 'us-letter'),
'orientation': config.get('orientation', 'portrait'),
'margins': config.get('margins', {
'top': '0.5in',
'bottom': '0.5in',
'left': '0.5in',
'right': '0.5in'
}),
'gutter': config.get('gutter', '5mm'),
'show_cut_marks': config.get('showCutMarks', False),
'show_registration': config.get('showRegistration', False),
'font_family': config.get('fontFamily', 'DejaVu Sans'),
'font_size': config.get('fontSize', '48pt'),
'columns': config.get('columns', 'auto'),
'show_empty_columns': config.get('showEmptyColumns', False),
'hide_inactive_beads': config.get('hideInactiveBeads', False),
'bead_shape': config.get('beadShape', 'diamond'),
'color_scheme': config.get('colorScheme', 'monochrome'),
'colored_numerals': config.get('coloredNumerals', False),
'scale_factor': config.get('scaleFactor', 0.9),
}
# Generate in core package directory to match main generator behavior
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
# Generate Typst file - same setup as main generate.py
core_package_root = Path(__file__).parent.parent # packages/core directory
# Create temp files in core package root, not temp directory
temp_typst = core_package_root / f'temp_flashcards_{os.getpid()}.typ'
temp_pdf = core_package_root / f'temp_flashcards_{os.getpid()}.pdf'
# Convert Python list to Typst array syntax (same as main generate.py)
if numbers:
numbers_str = '(' + ', '.join(str(n) for n in numbers) + ',)'
else:
numbers_str = '()'
# Create temp Typst with relative imports (works when run from core package root)
typst_content = f'''
#import "templates/flashcards.typ": generate-flashcards
#generate-flashcards(
{numbers_str},
cards-per-page: {typst_config['cards_per_page']},
paper-size: "{typst_config['paper_size']}",
orientation: "{typst_config['orientation']}",
margins: (
top: {typst_config['margins'].get('top', '0.5in')},
bottom: {typst_config['margins'].get('bottom', '0.5in')},
left: {typst_config['margins'].get('left', '0.5in')},
right: {typst_config['margins'].get('right', '0.5in')}
),
gutter: {typst_config['gutter']},
show-cut-marks: {str(typst_config['show_cut_marks']).lower()},
show-registration: {str(typst_config['show_registration']).lower()},
font-family: "{typst_config['font_family']}",
font-size: {typst_config['font_size']},
columns: {typst_config['columns']},
show-empty-columns: {str(typst_config['show_empty_columns']).lower()},
hide-inactive-beads: {str(typst_config['hide_inactive_beads']).lower()},
bead-shape: "{typst_config['bead_shape']}",
color-scheme: "{typst_config['color_scheme']}",
colored-numerals: {str(typst_config['colored_numerals']).lower()},
scale-factor: {typst_config['scale_factor']}
)
'''
with open(temp_typst, 'w') as f:
f.write(typst_content)
# Get format preference
output_format = config.get('format', 'pdf').lower()
temp_svg = None
try:
if output_format == 'svg':
# Generate SVG using Typst with page template for multi-page support
temp_svg = core_package_root / f'temp_flashcards_{os.getpid()}_{{p}}.svg'
result = subprocess.run(
['typst', 'compile', str(temp_typst), str(temp_svg), '--format', 'svg'],
capture_output=True,
text=True,
cwd=str(core_package_root)
)
if result.returncode != 0:
return json.dumps({
'error': f'Typst SVG compilation failed: {result.stderr}'
})
# Read SVG content - find the first generated page
svg_pattern = core_package_root / f'temp_flashcards_{os.getpid()}_*.svg'
import glob
svg_files = glob.glob(str(svg_pattern))
if not svg_files:
return json.dumps({
'error': 'No SVG files were generated'
})
# Read the first SVG file (page 1)
svg_file = Path(svg_files[0])
with open(svg_file, 'r', encoding='utf-8') as f:
svg_content = f.read()
# Clean up all generated SVG files
for svg_path in svg_files:
Path(svg_path).unlink()
result_data = {
'pdf': svg_content, # Keep field name for compatibility
'count': len(numbers),
'numbers': numbers[:100]
}
else:
# Generate PDF (default)
result = subprocess.run(
['typst', 'compile', str(temp_typst), str(temp_pdf)],
capture_output=True,
text=True,
cwd=str(core_package_root)
)
if result.returncode != 0:
return json.dumps({
'error': f'Typst compilation failed: {result.stderr}'
})
# Read and encode PDF
with open(temp_pdf, 'rb') as f:
pdf_bytes = f.read()
result_data = {
'pdf': base64.b64encode(pdf_bytes).decode('utf-8'),
'count': len(numbers),
'numbers': numbers[:100] # Limit preview
}
finally:
# Clean up temp files
for temp_file in [temp_typst, temp_pdf, temp_svg if output_format == 'svg' else None]:
if temp_file and temp_file.exists():
temp_file.unlink()
return json.dumps(result_data)
def generate_single_card_json(config_json):
"""
Generate a single card SVG from JSON config
Specifically for preview - always returns front side (abacus)
"""
config = json.loads(config_json)
# Extract the single number
number = config.get('number')
if number is None:
return json.dumps({'error': 'Missing number parameter'})
# Build Typst config optimized for preview display
typst_config = {
'bead_shape': config.get('beadShape', 'diamond'),
'color_scheme': config.get('colorScheme', 'monochrome'),
'color_palette': config.get('colorPalette', 'default'),
'colored_numerals': config.get('coloredNumerals', False),
'hide_inactive_beads': config.get('hideInactiveBeads', False),
'show_empty_columns': config.get('showEmptyColumns', False),
'columns': config.get('columns', 'auto'),
'transparent': config.get('transparent', False),
'card_width': '120pt', # Smaller card for larger abacus
'card_height': '160pt', # Smaller card for larger abacus
'font_size': config.get('fontSize', '48pt'),
'font_family': config.get('fontFamily', 'DejaVu Sans'),
'scale_factor': config.get('scaleFactor', 4.0), # Much larger scale for preview visibility
}
# Generate in core package directory
core_package_root = Path(__file__).parent.parent
temp_typst = core_package_root / f'temp_single_{number}_{os.getpid()}.typ'
temp_svg = core_package_root / f'temp_single_{number}_{os.getpid()}.svg'
try:
# Create single card content directly with correct template path
typst_content = f'''
#import "templates/single-card.typ": generate-single-card
#generate-single-card(
{number},
side: "front",
bead-shape: "{typst_config['bead_shape']}",
color-scheme: "{typst_config['color_scheme']}",
color-palette: "{typst_config['color_palette']}",
colored-numerals: {str(typst_config['colored_numerals']).lower()},
hide-inactive-beads: {str(typst_config['hide_inactive_beads']).lower()},
show-empty-columns: {str(typst_config['show_empty_columns']).lower()},
columns: {typst_config['columns']},
transparent: {str(typst_config['transparent']).lower()},
width: {typst_config['card_width']},
height: {typst_config['card_height']},
font-size: {typst_config['font_size']},
font-family: "{typst_config['font_family']}",
scale-factor: {typst_config['scale_factor']}
)
'''
with open(temp_typst, 'w') as f:
f.write(typst_content)
# Generate SVG
result = subprocess.run(
['typst', 'compile', str(temp_typst), str(temp_svg), '--format', 'svg'],
capture_output=True,
text=True,
cwd=str(core_package_root)
)
if result.returncode != 0:
return json.dumps({
'error': f'Typst SVG compilation failed: {result.stderr}'
})
# Read SVG content
if not temp_svg.exists():
return json.dumps({
'error': 'SVG file was not generated'
})
with open(temp_svg, 'r', encoding='utf-8') as f:
svg_content = f.read()
return json.dumps({
'pdf': svg_content, # Keep field name for compatibility
'count': 1,
'numbers': [number]
})
except Exception as e:
return json.dumps({'error': f'Single card generation failed: {str(e)}'})
finally:
# Clean up temp files
for temp_file in [temp_typst, temp_svg]:
if temp_file and temp_file.exists():
temp_file.unlink()
if __name__ == '__main__':
# Read JSON from stdin, write JSON to stdout
# This allows clean function-like communication
for line in sys.stdin:
try:
config = json.loads(line.strip())
# Check if this is a single-card generation request
if config.get('mode') == 'single-card':
result = generate_single_card_json(line.strip())
else:
result = generate_flashcards_json(line.strip())
print(result)
sys.stdout.flush()
except Exception as e:
print(json.dumps({'error': str(e)}))
sys.stdout.flush()

View File

@ -119,9 +119,6 @@ importers:
'@soroban/abacus-react':
specifier: workspace:*
version: link:../../packages/abacus-react
'@soroban/client':
specifier: workspace:*
version: link:../../packages/core/client/typescript
'@soroban/core':
specifier: workspace:*
version: link:../../packages/core/client/node
@ -380,49 +377,8 @@ importers:
specifier: ^1.0.0
version: 1.6.1(@types/node@20.19.19)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)
packages/core/client/browser:
dependencies:
'@myriaddreamin/typst-ts-renderer':
specifier: ^0.6.0
version: 0.6.0
'@myriaddreamin/typst-ts-web-compiler':
specifier: ^0.6.0
version: 0.6.0
devDependencies:
typescript:
specifier: ^5.0.0
version: 5.9.3
vite:
specifier: ^5.0.0
version: 5.4.20(@types/node@20.19.19)(terser@5.44.0)
packages/core/client/node:
dependencies:
python-shell:
specifier: ^5.0.0
version: 5.0.0
devDependencies:
'@types/minimatch':
specifier: ^6.0.0
version: 6.0.0
'@types/node':
specifier: ^20.0.0
version: 20.19.19
tsup:
specifier: ^7.0.0
version: 7.3.0(postcss@8.5.6)(typescript@5.9.3)
typescript:
specifier: ^5.0.0
version: 5.9.3
vitest:
specifier: ^1.0.0
version: 1.6.1(@types/node@20.19.19)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)
packages/core/client/typescript:
devDependencies:
'@types/minimatch':
specifier: ^6.0.0
version: 6.0.0
'@types/node':
specifier: ^20.0.0
version: 20.19.19
@ -1957,12 +1913,6 @@ packages:
'@types/react': '>=16'
react: '>=16'
'@myriaddreamin/typst-ts-renderer@0.6.0':
resolution: {integrity: sha512-56Mids4E5Ob6LeEeXDedvmsVnEWnLmc1qeUOeUSruL/zI3S9QXleF/c3Os1FXwJmLuCFbWTEIq8Quh2cXlnxKw==}
'@myriaddreamin/typst-ts-web-compiler@0.6.0':
resolution: {integrity: sha512-P/eIJ5RnfElj0NYzn5PI296t/IwWtgqUyyTMi5Jm5X3V5kZfskkH+LI7mSQe8tEyxwgCvxbxvFe5adinA3K8Gg==}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@ -7773,10 +7723,6 @@ packages:
python-bridge@1.1.0:
resolution: {integrity: sha512-qjQ0QB8p9cn/XDeILQH0aP307hV58lrmv0Opjyub68Um7FHdF+ZXlTqyxNkKaXOFk2QSkScoPWwn7U9GGnrkeQ==}
python-shell@5.0.0:
resolution: {integrity: sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==}
engines: {node: '>=0.10'}
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@ -10814,10 +10760,6 @@ snapshots:
'@types/react': 18.3.26
react: 18.3.1
'@myriaddreamin/typst-ts-renderer@0.6.0': {}
'@myriaddreamin/typst-ts-web-compiler@0.6.0': {}
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.5.0
@ -17642,8 +17584,6 @@ snapshots:
dependencies:
bluebird: 3.7.2
python-shell@5.0.0: {}
qs@6.13.0:
dependencies:
side-channel: 1.1.0