fix: resolve SorobanGeneratorBridge path issues for SVG generation

- Fixed path duplication bug in bridge constructor
- Updated bridge to use event-based PythonShell approach
- Updated web preview route to use correct Python script path
- Web app now generates real SVGs from Python/Typst instead of mock SVGs

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-09-14 18:01:43 -05:00
parent a02285289a
commit 845a4ffc48
7 changed files with 129 additions and 108 deletions

View File

@ -85,7 +85,8 @@
"Bash(pnpm --filter @soroban/web type-check)",
"Bash(sed:*)",
"Bash(pnpm type-check:*)",
"Bash(git bisect:*)"
"Bash(git bisect:*)",
"Bash(pnpm --filter @soroban/web dev)"
],
"deny": [],
"ask": []

View File

@ -2,8 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'
import { SorobanGeneratorBridge } from '@soroban/core'
import path from 'path'
// Initialize generator (let it figure out its own path)
const generator = new SorobanGeneratorBridge()
// Initialize generator with correct path to Python scripts
const projectRoot = path.join(process.cwd(), '../../packages/core')
const generator = new SorobanGeneratorBridge(projectRoot)
export async function POST(request: NextRequest) {
try {

View File

@ -2,6 +2,67 @@
* Node.js TypeScript wrapper for Soroban Flashcard Generator
* Calls Python functions via child_process
*/
interface FlashcardConfig$1 {
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;
}
declare class SorobanGenerator$1 {
private pythonPath;
private generatorPath;
private projectRoot;
constructor(projectRoot?: string);
private findPython;
/**
* Generate flashcards and return PDF as Buffer
*/
generate(config: FlashcardConfig$1): Promise<Buffer>;
/**
* Generate flashcards and save to file
*/
generateToFile(config: FlashcardConfig$1, outputPath: string): Promise<void>;
/**
* Generate flashcards and return as base64 string
*/
generateBase64(config: FlashcardConfig$1): Promise<string>;
private executePython;
/**
* Check if all dependencies are installed
*/
checkDependencies(): Promise<{
python: boolean;
typst: boolean;
qpdf: boolean;
}>;
}
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;
@ -28,35 +89,35 @@ interface FlashcardConfig {
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 pythonPath;
private generatorPath;
private pythonShell;
private projectRoot;
constructor(projectRoot?: string);
private findPython;
/**
* Generate flashcards and return PDF as Buffer
* Initialize persistent Python process for better performance
*/
generate(config: FlashcardConfig): Promise<Buffer>;
initialize(): Promise<void>;
/**
* Generate flashcards and save to file
* Generate flashcards - clean function interface
*/
generateToFile(config: FlashcardConfig, outputPath: string): Promise<void>;
generate(config: FlashcardConfig): Promise<FlashcardResult>;
/**
* Generate flashcards and return as base64 string
* Generate and return as Buffer
*/
generateBase64(config: FlashcardConfig): Promise<string>;
private executePython;
generateBuffer(config: FlashcardConfig): Promise<Buffer>;
/**
* Check if all dependencies are installed
* Clean up Python process
*/
checkDependencies(): Promise<{
python: boolean;
typst: boolean;
qpdf: boolean;
}>;
close(): Promise<void>;
}
declare function expressExample(): Promise<void>;
export { FlashcardConfig, SorobanGenerator, SorobanGenerator as default, expressExample };
export { FlashcardConfig as BridgeFlashcardConfig, FlashcardResult as BridgeFlashcardResult, FlashcardConfig$1 as FlashcardConfig, SorobanGenerator$1 as SorobanGenerator, SorobanGenerator as SorobanGeneratorBridge, SorobanGenerator$1 as default, expressExample };

View File

@ -209,7 +209,7 @@ var SorobanGenerator2 = class {
if (this.pythonShell)
return;
this.pythonShell = new import_python_shell.PythonShell(
path2.join(this.projectRoot, "src", "bridge.py"),
path2.join("src", "bridge.py"),
{
mode: "json",
pythonPath: "python3",
@ -225,42 +225,28 @@ var SorobanGenerator2 = class {
async generate(config) {
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
import_python_shell.PythonShell.run(
path2.join(this.projectRoot, "src", "bridge.py"),
{
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot,
args: []
},
(err, results) => {
if (err) {
reject(err);
} else if (results && results[0]) {
const result = results[0];
if (result.error) {
reject(new Error(result.error));
} else {
resolve(result);
}
} else {
reject(new Error("No result from Python"));
}
}
);
import_python_shell.PythonShell.defaultOptions = {};
const shell = new import_python_shell.PythonShell(
path2.join(this.projectRoot, "src", "bridge.py"),
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)
console.error(err);
reject(err);
});
});
}

View File

@ -170,7 +170,7 @@ var SorobanGenerator2 = class {
if (this.pythonShell)
return;
this.pythonShell = new PythonShell(
path2.join(this.projectRoot, "src", "bridge.py"),
path2.join("src", "bridge.py"),
{
mode: "json",
pythonPath: "python3",
@ -186,42 +186,28 @@ var SorobanGenerator2 = class {
async generate(config) {
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
PythonShell.run(
path2.join(this.projectRoot, "src", "bridge.py"),
{
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot,
args: []
},
(err, results) => {
if (err) {
reject(err);
} else if (results && results[0]) {
const result = results[0];
if (result.error) {
reject(new Error(result.error));
} else {
resolve(result);
}
} else {
reject(new Error("No result from Python"));
}
}
);
PythonShell.defaultOptions = {};
const shell = new PythonShell(
path2.join(this.projectRoot, "src", "bridge.py"),
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)
console.error(err);
reject(err);
});
});
}

View File

@ -58,7 +58,7 @@ export class SorobanGenerator {
if (this.pythonShell) return;
this.pythonShell = new PythonShell(
path.join(this.projectRoot, 'src', 'bridge.py'),
path.join('src', 'bridge.py'),
{
mode: 'json',
pythonPath: 'python3',
@ -75,44 +75,30 @@ export class SorobanGenerator {
// One-shot mode if not initialized
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
PythonShell.run(
path.join(this.projectRoot, 'src', 'bridge.py'),
const shell = new PythonShell(
path.join('src', 'bridge.py'),
{
mode: 'json',
pythonPath: 'python3',
scriptPath: this.projectRoot,
args: [],
},
(err, results) => {
if (err) {
reject(err);
} else if (results && results[0]) {
const result = results[0] as any;
if (result.error) {
reject(new Error(result.error));
} else {
resolve(result as FlashcardResult);
}
} else {
reject(new Error('No result from Python'));
}
}
);
// Send config as JSON
PythonShell.defaultOptions = {};
const shell = new PythonShell(
path.join(this.projectRoot, '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, code, signal) => {
if (err) console.error(err);
shell.end((err: any, code: any, signal: any) => {
if (err) reject(err);
});
});
}
@ -189,8 +175,8 @@ async function example() {
// Express example - clean function calls
export function expressRoute(app: any) {
const generator = new SorobanGenerator();
app.post('/api/flashcards', async (req, res) => {
app.post('/api/flashcards', async (req: any, res: any) => {
try {
// Just pass the config object directly!
const result = await generator.generate(req.body);
@ -204,7 +190,7 @@ export function expressRoute(app: any) {
res.send(pdfBuffer);
}
} catch (error) {
res.status(500).json({ error: error.message });
res.status(500).json({ error: (error as Error).message });
}
});
}

Binary file not shown.